synthesisui 0.16.102 → 0.16.104

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.
@@ -636,6 +636,21 @@ export async function takeCensus(root, opts) {
636
636
  }
637
637
  usageSeen.add(rel);
638
638
  usageProgress?.step(rel);
639
+ /**
640
+ * THE THEME IS SWITCHED WHERE THE APP IS, and this loop was walking right
641
+ * past the file that says so. Measured on a real monorepo (dono, 02/08):
642
+ * `packages/ui` declares 346 dark utilities across 58 files and NONE of the
643
+ * four switch fields; `apps/web-dashboard` declares `data-theme`,
644
+ * `defaultTheme="dark"`, `enableSystem={false}` and an `<html>` carrying both
645
+ * - so "which face does it open in" was unanswerable from the census and cost
646
+ * a hand read of a file this loop already had open.
647
+ *
648
+ * The scope is read first and wins on every field, so this only fills what a
649
+ * library legitimately does not have - and the label is recorded, because the
650
+ * system being measured here and the theme being switched there are two facts,
651
+ * not one average.
652
+ */
653
+ readSignalsInto(signals, importTally, rel, src, u.label);
639
654
  scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
640
655
  // WHERE THE PAIRS LIVE. A library composes almost none of itself inside
641
656
  // itself - `Button` goes inside `Modal` in the APP, not in the library
@@ -1645,8 +1660,21 @@ function rootPackage(tag, sketch) {
1645
1660
  : {};
1646
1661
  }
1647
1662
  async function resolveReadParts(census, root) {
1648
- const read = census.reading?.components;
1649
- if (!read)
1663
+ /**
1664
+ * A READING IS OPTIONAL HERE. THE SKETCH IS NOT.
1665
+ *
1666
+ * This returned on line one when nobody had authored a reading, and a DERIVED
1667
+ * anatomy needs no reading at all - so the deriver shipped and never ran on the
1668
+ * path the owner actually uses. Measured on his own re-import (02/08): the census
1669
+ * carried `rootFrom` on 13 looks and `from` on 162 sketch nodes - the new fields
1670
+ * arrived - and `tree` on 0 of 36. Two components then read as empty contracts
1671
+ * and came back below the floor.
1672
+ *
1673
+ * "Medido 100%, convertido 0%", one week after the law that names it. The guard
1674
+ * is now about having something to convert, not about who asked.
1675
+ */
1676
+ const read = census.reading?.components ?? {};
1677
+ if (!census.looks || Object.keys(census.looks).length === 0)
1650
1678
  return;
1651
1679
  const declared = new Map(Object.entries(census.declared));
1652
1680
  // The manifest is read ONCE for the whole run: the version belongs in a rule's
@@ -1774,10 +1802,14 @@ async function resolveReadParts(census, root) {
1774
1802
  for (const n of resolved.notes)
1775
1803
  notes.add(n);
1776
1804
  }
1777
- const total = Object.keys(read).length;
1805
+ const authored = Object.keys(read).length;
1806
+ const total = authored || Object.keys(census.looks ?? {}).length;
1778
1807
  if (named > 0 || shaped > 0) {
1779
1808
  console.log("");
1780
- 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`));
1809
+ console.log(body(
1810
+ // "Your reading" only when there WAS one - otherwise the names came off
1811
+ // their own markup, and claiming otherwise credits somebody's absent work.
1812
+ `${named} part${named === 1 ? "" : "s"} named across ${total} component${total === 1 ? "" : "s"}${authored > 0 ? " - your reading and their markup" : " - read off their own markup"}, so each previews as what it IS rather than as a text box`));
1781
1813
  }
1782
1814
  if (shaped > 0) {
1783
1815
  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`));
@@ -1889,6 +1921,20 @@ export async function runImport(opts) {
1889
1921
  if (scope)
1890
1922
  census.scope = scope;
1891
1923
  }
1924
+ /**
1925
+ * DERIVE THE ANATOMY ON THE MEASURING PATH TOO.
1926
+ *
1927
+ * The send path resolves the reading above; a plain `--dry` measure never
1928
+ * reached that function, so the census on disk - the file the skill reads to
1929
+ * author its reading, and the file every one of our real-repo specs reads -
1930
+ * carried a sketch for every component and a tree for none. The person's own
1931
+ * re-import proved it: 36 sketches, 162 stamped nodes, 0 trees (dono, 02/08).
1932
+ *
1933
+ * Written into the census means the skill sees the shape it would otherwise
1934
+ * have to author from scratch, and can spend its judgement on the names.
1935
+ */
1936
+ if (!opts.census)
1937
+ await resolveReadParts(census, root);
1892
1938
  summarize(census);
1893
1939
  // Only when nobody has scoped yet. Telling someone to scope to the folder
1894
1940
  // they just scoped to reads as the tool not having noticed.
@@ -75,6 +75,7 @@ export function emptySignals() {
75
75
  enableSystem: null,
76
76
  prefersColorScheme: 0,
77
77
  htmlCarries: [],
78
+ switchedIn: null,
78
79
  page: [],
79
80
  },
80
81
  libraries: [],
@@ -113,7 +114,13 @@ export function libraryOf(specifier) {
113
114
  * Fold one file into the signals. Called from the walk the census already does, so nothing
114
115
  * here costs a second pass over the disk.
115
116
  */
116
- export function readSignalsInto(into, imports, file, source) {
117
+ export function readSignalsInto(into, imports, file, source,
118
+ /**
119
+ * WHERE THIS FILE IS, when it is not the system's own folder - the `--usage`
120
+ * label. Present means "read for evidence", and the only thing it changes is
121
+ * that a switch found here is RECORDED AS FOUND HERE. See `switchedIn`.
122
+ */
123
+ from) {
117
124
  const isCode = /\.(tsx|jsx|ts|js|mjs|cjs|vue|svelte)$/i.test(file);
118
125
  const isStyle = /\.(css|scss|sass|less)$/i.test(file);
119
126
  if (isStyle) {
@@ -146,6 +153,16 @@ export function readSignalsInto(into, imports, file, source) {
146
153
  }
147
154
  }
148
155
  }
156
+ /**
157
+ * THE SCOPE WINS, ALWAYS - `??=` is the whole rule. The system's own folder is
158
+ * read first, so a switch it declares is never overwritten by an app's; an app
159
+ * only fills what was null, which is the four fields a library legitimately does
160
+ * not have.
161
+ */
162
+ const before = into.theme.attribute === null &&
163
+ into.theme.defaultTheme === null &&
164
+ into.theme.enableSystem === null &&
165
+ into.theme.htmlCarries.length === 0;
149
166
  into.theme.attribute ??= THEME_ATTRIBUTE.exec(source)?.[1] ?? null;
150
167
  into.theme.defaultTheme ??= DEFAULT_THEME.exec(source)?.[1] ?? null;
151
168
  if (into.theme.enableSystem === null) {
@@ -160,12 +177,42 @@ export function readSignalsInto(into, imports, file, source) {
160
177
  */
161
178
  const html = HTML_ATTRS.exec(source);
162
179
  if (html) {
163
- for (const m of html[1].matchAll(/([a-zA-Z-]+)\s*=\s*(?:["']([^"']*)["']|\{["']([^"']*)["']\})/g)) {
164
- const attr = `${m[1]}="${m[2] ?? m[3] ?? ""}"`;
180
+ const carry = (attr) => {
165
181
  if (!into.theme.htmlCarries.includes(attr)) {
166
182
  into.theme.htmlCarries.push(attr);
167
183
  }
184
+ };
185
+ for (const m of html[1].matchAll(/([a-zA-Z-]+)\s*=\s*(?:["']([^"']*)["']|\{["']([^"']*)["']\})/g)) {
186
+ carry(`${m[1]}="${m[2] ?? m[3] ?? ""}"`);
168
187
  }
188
+ /**
189
+ * `style={{ colorScheme: "dark" }}` - the SECOND thing that decides what a dark
190
+ * rule matches, and the attribute pass above cannot see it: its value is not a
191
+ * quoted string sitting after an `=`, it is a property inside an object.
192
+ *
193
+ * It is on the owner's own root layout, right beside the `data-theme` the pass
194
+ * DID read, and it is what tells the browser which face to paint its own
195
+ * widgets - a scrollbar and a form control follow `color-scheme`, not a class
196
+ * (dono, 02/08).
197
+ */
198
+ const scheme = /\bcolorScheme\s*:\s*["']([^"']+)["']/.exec(html[1]);
199
+ if (scheme)
200
+ carry(`colorScheme="${scheme[1]}"`);
201
+ }
202
+ /**
203
+ * WHOSE FILE ANSWERED IT. Stamped only when the switch was null before this file
204
+ * and is not null after, so the label names the file's ROOT that actually
205
+ * supplied it - and only for a `--usage` root, because the scope needs no
206
+ * attribution: it is the system by definition.
207
+ */
208
+ if (from &&
209
+ into.theme.switchedIn === null &&
210
+ before &&
211
+ (into.theme.attribute !== null ||
212
+ into.theme.defaultTheme !== null ||
213
+ into.theme.enableSystem !== null ||
214
+ into.theme.htmlCarries.length > 0)) {
215
+ into.theme.switchedIn = from;
169
216
  }
170
217
  /**
171
218
  * THE PAGE PAINTS, from the markup. A `min-h-screen` container with a background is
@@ -310,6 +357,15 @@ export function describeSignals(s) {
310
357
  bits.push("with the OS allowed a vote");
311
358
  out.push(`${bits.join(", ")}.`);
312
359
  }
360
+ /**
361
+ * TWO PLACES, TWO FACTS. A library that declares 346 dark utilities and no
362
+ * switch is not a library missing something - the switch belongs to the app that
363
+ * mounts it. Saying WHERE keeps the sentence true and stops the reader from
364
+ * concluding the system declares a provider it does not have.
365
+ */
366
+ if (t.switchedIn) {
367
+ out.push(`the system is measured where you scoped it; the theme is switched in \`${t.switchedIn}\` - that is where the attribute, the default and the \`<html>\` came from.`);
368
+ }
313
369
  if (t.htmlCarries.length > 0) {
314
370
  out.push(`\`<html>\` carries ${t.htmlCarries.map((a) => `\`${a}\``).join(" ")} - which is the ancestor every dark rule of theirs matches, and therefore the one ours must match.`);
315
371
  }
@@ -23,6 +23,50 @@ import { importMap, packageOfTag } from "./imports.js";
23
23
  import { definitionSpan } from "./transcribe.js";
24
24
  /** `{children}` / `{props.children}` - the caller's content, spelled either way. */
25
25
  const CHILDREN = /\{\s*(?:props\.)?children\s*\}/;
26
+ /**
27
+ * THE TEXT THAT SITS DIRECTLY INSIDE ONE ELEMENT - its own gaps, not its
28
+ * children's contents.
29
+ *
30
+ * The first version of the slot check read only the span between an element's
31
+ * open tag and the next tag of any kind, so it saw `{children}` only when it was
32
+ * the FIRST thing inside. A real panel renders a header and THEN the caller's
33
+ * content, which is the mixed case the reader has a form for - and it was
34
+ * invisible. Found by driving `runImport` over a Base-UI dialog whose `{children}`
35
+ * follows its close button (02/08).
36
+ *
37
+ * Attribution matters as much as detection: `{children}` inside a CHILD is that
38
+ * child's slot, so the walk collects the gaps at this element's own level and
39
+ * skips whole subtrees.
40
+ */
41
+ function directText(source, events, index) {
42
+ const open = events[index];
43
+ const openEnd = source.indexOf(">", open.at);
44
+ if (openEnd === -1)
45
+ return "";
46
+ let from = openEnd + 1;
47
+ const gaps = [];
48
+ let rel = 0;
49
+ for (let j = index + 1; j < events.length; j++) {
50
+ const event = events[j];
51
+ if (event.kind === "open") {
52
+ if (rel === 0)
53
+ gaps.push(source.slice(from, event.at));
54
+ rel += 1;
55
+ continue;
56
+ }
57
+ rel -= 1;
58
+ // Our own close: the last gap runs up to it, and the element ends here.
59
+ if (rel < 0) {
60
+ gaps.push(source.slice(from, event.at));
61
+ break;
62
+ }
63
+ if (rel === 0) {
64
+ const closeEnd = source.indexOf(">", event.at);
65
+ from = closeEnd === -1 ? event.at : closeEnd + 1;
66
+ }
67
+ }
68
+ return gaps.join(" ");
69
+ }
26
70
  /**
27
71
  * Past this a component is a page in disguise, and the gate already said no.
28
72
  *
@@ -177,10 +221,12 @@ export function sketchOf(source, name) {
177
221
  !between.includes("<")) {
178
222
  node.text = between;
179
223
  }
180
- // The caller's content, as a fact - see `SketchNode.slot`.
181
- if (CHILDREN.test(between))
182
- node.slot = true;
183
224
  }
225
+ // The caller's content, as a fact - see `SketchNode.slot`. Read from the
226
+ // element's DIRECT text, so a `{children}` after a header still counts and one
227
+ // inside a child belongs to that child.
228
+ if (CHILDREN.test(directText(source, events, i)))
229
+ node.slot = true;
184
230
  out.push(node);
185
231
  depth += 1;
186
232
  }
@@ -148,11 +148,11 @@ file to pick between. Nothing is sent by \`--dry\`. Read the file.
148
148
  ### 2. Walk them through the decisions - ALL of them, before any reading
149
149
 
150
150
  **Every question this skill asks is answerable from the census you just took.** Source, name,
151
- primary, theme, canvas, our-kit-or-theirs, architecture - the numbers behind all seven are in
152
- the file on their disk, and none of them needs the anatomy.
151
+ primary, theme, canvas, architecture - the numbers behind all six are in the file on their disk,
152
+ and none of them needs the anatomy.
153
153
 
154
154
  They used to be asked AFTER the reading, and that put six hundred lines of work between the
155
- person's first answer and their second (dono, 01/08). A person who answers seven questions in
155
+ person's first answer and their second (dono, 01/08). A person who answers six questions in
156
156
  ninety seconds and then watches a bar has had a good four minutes; the same person interrupted
157
157
  twice in the middle of a silence has had a bad twenty.
158
158
 
@@ -160,7 +160,7 @@ So: ask everything here, in one pass, and **do not come back**. If something you
160
160
  the reading contradicts an answer, you do not reopen the question - you say so in the report at
161
161
  the end and let them change it there.
162
162
 
163
- Seven decisions are theirs. **Ask them as separate questions with selectable options** - use your
163
+ Six decisions are theirs. **Ask them as separate questions with selectable options** - use your
164
164
  question tool, one call per decision, so they pick instead of reading a wall and composing a
165
165
  reply. A single block containing everything is a report, and a report gets read, not answered.
166
166
 
@@ -244,35 +244,37 @@ canvas #222326 darkgray-500 - what DashboardLayout paints on <main>
244
244
  Say it and let them object. Offering five near-blacks to pick between is asking someone to
245
245
  re-decide something their code already decided.
246
246
 
247
- **2f. Our kit, or theirs alone** - ask this ONLY when they already have a component library,
248
- which is when \`--scope\` pointed at one and the CLI said so. An app with no components of its own
249
- has nothing to weigh and should never see the question.
247
+ **2f. Our kit** - do not ask. A project that already has a component library gets **theirs
248
+ alone**, and you say so in one line:
250
249
 
251
250
  \`\`\`
252
- Every system born here comes with 45 components of ours - button, card, input,
253
- table, the rest of a working toolkit. You already have 37.
254
-
255
- add ours to yours (recommended) 68 components, and the 23 governed by your
256
- own rules are the 23 you wrote
257
- mine alone 37 components, all of them yours. Our
258
- layouts that need a component you do not
259
- have stay out too. Add any of ours later
260
- from the library, one at a time
251
+ This system is yours alone - the 37 components you wrote, and none of ours.
252
+ Add any of ours later from the library, one at a time.
261
253
  \`\`\`
262
254
 
263
- Send it as \`"standards": "none"\` in the reading when they choose the second, and leave the field
264
- out when they choose the first. **Recommend adding ours**, and say why in one line: a component of
265
- ours costs nothing until something composes it, and having a \`table\` the day somebody needs a
266
- table is worth more than a shorter list.
255
+ Send it as \`"standards": "none"\` in the reading. Every time, with one exception below.
256
+
257
+ **Why this stopped being a question.** Somebody who points us at their own library is telling us
258
+ what their system is, and 45 components of ours landing beside 37 of theirs makes a system of 82
259
+ where only 37 are governed by anything they wrote - the number on their own screen stops being
260
+ about them. The kit is one click away in the library afterwards, so choosing wrong here costs a
261
+ click and choosing to ask costs a pause in the middle of their import.
262
+
263
+ **The exception, and it is not a preference.** An app with NO components of its own - \`--scope\`
264
+ pointed at an app rather than a library, and the CLI reported no component library - must NOT get
265
+ \`"standards": "none"\`: theirs alone plus nothing of theirs is a system with zero components. Leave
266
+ the field out there, exactly as before.
267
267
 
268
268
  Two things to be straight about, because both are true and neither is obvious:
269
269
 
270
- - **the foundations come from the seed either way.** Their palette, their scales, their type - all
270
+ - **the foundations still come from the seed.** Their palette, their scales, their type - all
271
271
  theirs - but the seven type slots, the easing and the neutral floor arrive from ours, because
272
272
  the alternative to a type scale is no components at all rather than their components.
273
273
  - **our layouts are written in our recipes.** A landing that composes \`button\`, \`card\` and
274
- \`badge\` needs those to exist. Choosing *mine alone* keeps the layouts whose recipes they
275
- happen to have and drops the rest - a layout pointing at a recipe nobody has renders as holes.
274
+ \`badge\` needs those to exist. Theirs alone keeps the layouts whose recipes they happen to have
275
+ and drops the rest - a layout pointing at a recipe nobody has renders as holes. Say that number
276
+ when the CLI reports it, so a shorter list of layouts is a fact they were told rather than one
277
+ they discover.
276
278
 
277
279
  **2g. Which architecture has priority** - ask only when the CLI reported more than
278
280
  one, which it does under "How this project is organised".
@@ -329,6 +331,13 @@ census carries is the failure this pipeline keeps paying for. The sketch now run
329
331
  so a big component arrives WHOLE: \`TextEditor\` used to lose its editable region at node 60 and
330
332
  that single truncation is what forced a hand-read of the file (dono, 01/08).
331
333
 
334
+ \`signals.theme\` includes the SWITCH - \`attribute\`, \`defaultTheme\`, \`enableSystem\` and what
335
+ \`<html>\` carries, \`colorScheme\` included - read from the \`--usage\` roots as well as the scope,
336
+ because a component library declares the dark utilities and the APP declares the switch. When it
337
+ came from an app, \`switchedIn\` names which one: say "the system is measured in \`packages/ui\`, the
338
+ theme is switched in \`apps/web-dashboard\`" rather than reporting that the library declares a
339
+ provider it does not have. Answering 2d never needs a layout file opened.
340
+
332
341
  **Validate in ONE call, at the end.** Build every recipe in memory first, then send them all to
333
342
  \`validate_recipes\` - one round trip for the whole reading instead of one per component. A reader
334
343
  with 35 anatomies spent 35 turns asking the same question, and none of that was thinking. Only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.102",
3
+ "version": "0.16.104",
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": {