synthesisui 0.16.79 → 0.16.81

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.
@@ -30,7 +30,7 @@
30
30
  * of the three outcomes this pipeline has paid for twice.
31
31
  */
32
32
  import { transcribe, } from "./doctor/transcribe.js";
33
- /** The nine forms, closed. A renderer that accepts any tag draws anything. */
33
+ /** The ten forms, closed. A renderer that accepts any tag draws anything. */
34
34
  const FORMS = new Set([
35
35
  "image",
36
36
  "heading",
@@ -42,6 +42,7 @@ const FORMS = new Set([
42
42
  "stack",
43
43
  "component",
44
44
  "external",
45
+ "slot",
45
46
  ]);
46
47
  /** The two that arrange, and the only two that take children. */
47
48
  const CONTAINERS = new Set(["row", "stack"]);
@@ -198,6 +199,19 @@ root) {
198
199
  const text = typeof node.text === "string" ? node.text.trim() : "";
199
200
  if (text)
200
201
  node_.text = text.slice(0, 120);
202
+ /**
203
+ * A SLOT KEEPS ITS STYLES AND BORROWS ITS CONTENT.
204
+ *
205
+ * Unlike the other two frontiers it is not somebody else's element - it IS
206
+ * theirs, and its border and hover belong on it. Only what goes inside is the
207
+ * caller's, so `expects` says what that is in their words instead of the
208
+ * preview drawing a blank.
209
+ */
210
+ if (as === "slot") {
211
+ const expects = typeof node.expects === "string" ? node.expects.trim() : "";
212
+ if (expects)
213
+ node_.expects = expects.slice(0, 80);
214
+ }
201
215
  // Only the two arrangers descend. A leaf with children is a leaf that was
202
216
  // labelled wrongly, and its children would render nowhere.
203
217
  if (CONTAINERS.has(as) && Array.isArray(node.children)) {
@@ -1,12 +1,15 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, join, relative } from "node:path";
3
3
  import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
4
4
  import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
5
+ import { describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
5
6
  import { findBrokenRefs } from "../doctor/broken-refs.js";
7
+ import { nestingRules, propRules, readDefinitionProps, readNesting, } from "../doctor/call-sites.js";
6
8
  import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
7
9
  import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
10
+ import { describeGate, gateComponent, groupSkips, } from "../doctor/component-gate.js";
8
11
  import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
9
- import { crosswalk, observedRules } from "../doctor/crosswalk.js";
12
+ import { crosswalk, isLibrary, observedRules } from "../doctor/crosswalk.js";
10
13
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
11
14
  import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
12
15
  import { diagnose, scanSource } from "../doctor/scan.js";
@@ -14,6 +17,7 @@ import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
14
17
  import { buildTable } from "../doctor/tokens.js";
15
18
  import { rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
16
19
  import { body, paint, section } from "../output.js";
20
+ import { startProgress } from "../progress.js";
17
21
  import { detectStack, resolveDeps } from "../stack.js";
18
22
  import { walk, walkAll } from "./doctor.js";
19
23
  /**
@@ -207,7 +211,8 @@ function distinctValues(d) {
207
211
  }
208
212
  return kept.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
209
213
  }
210
- export async function takeCensus(root) {
214
+ export async function takeCensus(root, opts) {
215
+ const scopeLabel = opts?.scopeLabel;
211
216
  const css = await harvestOwnCss(root);
212
217
  const table = buildTable({ css, source: "yours" });
213
218
  const schemes = parseSchemeBlocks(css);
@@ -215,6 +220,8 @@ export async function takeCensus(root) {
215
220
  const tally = emptyTally();
216
221
  const internal = await internalSpecifiers(root);
217
222
  const defined = [];
223
+ /** What the gate turned away, with the reason. Never dropped in silence. */
224
+ const skips = [];
218
225
  // Kept for the reference check below, which needs every file at once: a name
219
226
  // is only broken if NOTHING declares it, anywhere.
220
227
  const sources = [];
@@ -226,11 +233,39 @@ export async function takeCensus(root) {
226
233
  declaredValues.set(m[1], m[2].trim());
227
234
  }
228
235
  const looks = {};
236
+ /**
237
+ * The directory shape, gathered while the files are already being walked - so
238
+ * naming how the project is organised costs no second pass. See `architecture.ts`.
239
+ */
240
+ const dirs = new Map();
241
+ /** Parent → child → the files that put one inside the other. */
242
+ const nesting = new Map();
243
+ /** Component → what its own definition names and whether it forwards the rest. */
244
+ const propsOf = new Map();
245
+ /**
246
+ * A census over a real monorepo reads 2797 files, and the terminal said nothing for
247
+ * most of it. Twenty silent minutes is indistinguishable from twenty broken ones
248
+ * (dono, 01/08).
249
+ */
250
+ const progress = startProgress("Reading");
229
251
  for await (const file of walk(root)) {
230
252
  const src = await readFile(file, "utf8").catch(() => "");
231
253
  if (!src)
232
254
  continue;
233
255
  const rel = relative(root, file);
256
+ progress.step(rel);
257
+ if (/\.(tsx|jsx|vue|svelte)$/i.test(rel)) {
258
+ const parts = rel.split("/");
259
+ // Every ancestor learns the name of the child directly under it, which is all
260
+ // the shape detector needs and is one pass rather than a tree walk.
261
+ for (let i = 0; i < parts.length - 1; i++) {
262
+ const at = parts.slice(0, i).join("/");
263
+ const entry = dirs.get(at) ?? { dirs: new Set(), files: 0 };
264
+ entry.dirs.add(parts[i]);
265
+ entry.files += 1;
266
+ dirs.set(at, entry);
267
+ }
268
+ }
234
269
  sources.push({ file: rel, source: src });
235
270
  reports.push(scanSource(rel, src, table));
236
271
  // Stories and tests compose components to SHOW them; counting those as the
@@ -238,9 +273,39 @@ export async function takeCensus(root) {
238
273
  // they were set aside.
239
274
  if (!/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
240
275
  scanComponentsInto(tally, rel, src, internal);
241
- // The other half: what this file EXPORTS, with the axes its types
242
- // declare. A library composes almost nothing and exports everything.
243
- const found = scanDefinitions(rel, src);
276
+ /**
277
+ * The other half: what this file EXPORTS, with the axes its types declare. A
278
+ * library composes almost nothing and exports everything.
279
+ *
280
+ * THROUGH THE GATE, which is the difference between 37 and 704. Every test in
281
+ * it is about CATEGORY - a route, a provider, a config object - never quality,
282
+ * and every rejection is carried with its reason.
283
+ */
284
+ // HOW IT IS USED, not only what it is: which props people pass, what it passes
285
+ // on, and what shows up inside it. See `call-sites.ts`.
286
+ readNesting(rel, src, nesting);
287
+ const found = [];
288
+ for (const d of scanDefinitions(rel, src)) {
289
+ const verdict = gateComponent({
290
+ name: d.name,
291
+ file: rel,
292
+ source: src,
293
+ internal,
294
+ });
295
+ if (verdict.ok) {
296
+ found.push(d);
297
+ continue;
298
+ }
299
+ skips.push({
300
+ name: d.name,
301
+ file: rel,
302
+ why: verdict.why,
303
+ because: verdict.because,
304
+ });
305
+ }
306
+ for (const d of found) {
307
+ propsOf.set(d.name, readDefinitionProps(d.name, src));
308
+ }
244
309
  defined.push(...found);
245
310
  // THE LOOK, from their own class names. One transcription per file, keyed
246
311
  // by the component it defines - the root element's classes are the
@@ -258,8 +323,59 @@ export async function takeCensus(root) {
258
323
  }
259
324
  }
260
325
  }
326
+ progress.done();
327
+ const architectures = detectArchitectures([...dirs.entries()].map(([path, v]) => ({
328
+ path: path || ".",
329
+ dirs: [...v.dirs],
330
+ files: v.files,
331
+ })));
261
332
  const d = diagnose(reports);
262
- const inventory = tallyToInventory(tally);
333
+ const allComposed = tallyToInventory(tally, 400);
334
+ /**
335
+ * THE EVIDENCE PASS - a second walk that touches ONE reading.
336
+ *
337
+ * Only the tally. Not `css`, not `declaredValues`, not `looks`, not `defined`,
338
+ * not `reports`: an app's classes are choices made from a scale, and folding
339
+ * them into the census would put the average back where the declaration goes.
340
+ *
341
+ * Deliberately its own tally so the LIBRARY QUESTION is asked of the scope
342
+ * alone. Fold the app in first and every export has a count, `isLibrary` reads
343
+ * false, and the crosswalk starts substituting the components it was just
344
+ * taught to keep.
345
+ */
346
+ const usageTally = emptyTally();
347
+ const usageSeen = new Set();
348
+ const usageProgress = (opts?.usage ?? []).length > 0 ? startProgress("Evidence") : null;
349
+ /**
350
+ * THE SCOPE'S OWN PACKAGE NAME IS INTERNAL, always.
351
+ *
352
+ * An app importing the system says `import { Button } from "@acme/ui"`, and the
353
+ * alias reader finds that name by walking the workspace for sibling manifests. It
354
+ * usually does - but when it cannot, every component the app composes files as
355
+ * THIRD-PARTY and the evidence pass counts nothing. The scope is the system by
356
+ * definition, so its name never needs discovering.
357
+ */
358
+ const scopePkg = await readFile(join(root, "package.json"), "utf8")
359
+ .then((raw) => JSON.parse(raw).name ?? null)
360
+ .catch(() => null);
361
+ const shared = scopePkg ? [...internal, scopePkg] : internal;
362
+ for (const u of opts?.usage ?? []) {
363
+ const uInternal = await internalSpecifiers(u.path);
364
+ for await (const file of walk(u.path)) {
365
+ const src = await readFile(file, "utf8").catch(() => "");
366
+ if (!src)
367
+ continue;
368
+ const rel = join(u.label, relative(u.path, file));
369
+ if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
370
+ continue;
371
+ }
372
+ usageSeen.add(rel);
373
+ usageProgress?.step(rel);
374
+ scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
375
+ }
376
+ }
377
+ usageProgress?.done();
378
+ const usedThere = tallyToInventory(usageTally, 400);
263
379
  /**
264
380
  * THE TWO READINGS BECOME ONE LIST before anything is judged.
265
381
  *
@@ -273,6 +389,16 @@ export async function takeCensus(root) {
273
389
  * is the better source anyway - usage shows what somebody picked, the
274
390
  * declaration shows what the author designed.
275
391
  */
392
+ /**
393
+ * THE GATE APPLIES FROM BOTH SIDES.
394
+ *
395
+ * A page reaches the inventory two ways: exported (caught above) and COMPOSED -
396
+ * `<AuthLayout>` wrapping a tree is a JSX tag like any other. Filtering only the
397
+ * definitions would have let it back in through the usage door under the same
398
+ * name, and the vitrine cannot tell which door a name came through.
399
+ */
400
+ const turnedAway = new Set(skips.map((s) => s.name));
401
+ const inventory = allComposed.filter((c) => !turnedAway.has(c.name));
276
402
  const composed = new Set(inventory.map((c) => c.name));
277
403
  const fromTypes = defined
278
404
  .filter((d) => !composed.has(d.name))
@@ -285,6 +411,9 @@ export async function takeCensus(root) {
285
411
  ...Object.fromEntries(d.flags.map((f) => [f, ["true"]])),
286
412
  },
287
413
  propFiles: {},
414
+ // Their own rung, off the folder. It travels from the DEFINITION because
415
+ // that is the only reading that knows which file the component lives in.
416
+ ...(d.tier ? { tier: d.tier } : {}),
288
417
  }));
289
418
  /**
290
419
  * A composed component that is ALSO defined keeps its usage and gains the
@@ -306,22 +435,157 @@ export async function takeCensus(root) {
306
435
  const declaredBy = new Map(defined.map((d) => [d.name, d]));
307
436
  const enriched = inventory.map((c) => {
308
437
  const d = declaredBy.get(c.name);
309
- if (!d || Object.keys(d.axes).length === 0)
438
+ if (!d)
310
439
  return c;
311
- return { ...c, declaredAxes: d.axes };
440
+ const tier = d.tier ? { tier: d.tier } : {};
441
+ if (Object.keys(d.axes).length === 0) {
442
+ return Object.keys(tier).length > 0 ? { ...c, ...tier } : c;
443
+ }
444
+ return { ...c, declaredAxes: d.axes, ...tier };
312
445
  });
313
- const merged = [...enriched, ...fromTypes];
446
+ const scoped = [...enriched, ...fromTypes];
447
+ /**
448
+ * THE EVIDENCE, FOLDED IN - counts and chosen values, never new components.
449
+ *
450
+ * A component the app has and the system does not is the app's own, and adopting
451
+ * it here would make the system a union of two codebases. It is reported instead:
452
+ * a number somebody can act on beats a component nobody asked for.
453
+ *
454
+ * The counts ADD because they are the same act measured in two places, and the
455
+ * prop values union because both are things somebody typed. `propFiles` is what
456
+ * the laws read, and it is the reason this pass exists: three files agreeing is
457
+ * what makes a habit, and a library has one file per component.
458
+ */
459
+ const own = new Set(scoped.map((c) => c.name));
460
+ const evidence = new Map(usedThere.filter((c) => !c.from).map((c) => [c.name, c]));
461
+ const merged = scoped.map((c) => {
462
+ const e = evidence.get(c.name);
463
+ if (!e)
464
+ return c;
465
+ /**
466
+ * A COMPONENT COMPOSED NOWHERE IN THE SCOPE carries its DECLARATION in `props`,
467
+ * because for a library that is the only reading there is. The moment real usage
468
+ * arrives, that declaration has to move where declarations live - otherwise the
469
+ * two readings merge and `props` stops meaning "what the code passed".
470
+ *
471
+ * It is the same mistake `declaredAxes` exists to prevent, arriving from the
472
+ * other side: unioned, a Button declaring solid|ghost and only ever passed solid
473
+ * reports two options, and the law that all four files agree can never be
474
+ * written.
475
+ */
476
+ const declaredOnly = (c.count ?? 0) === 0;
477
+ const props = declaredOnly ? {} : { ...c.props };
478
+ for (const [prop, values] of Object.entries(e.props)) {
479
+ props[prop] = [...new Set([...(props[prop] ?? []), ...values])];
480
+ }
481
+ const declaredAxes = c.declaredAxes ?? (declaredOnly ? c.props : undefined);
482
+ const propFiles = { ...(c.propFiles ?? {}) };
483
+ for (const [prop, n] of Object.entries(e.propFiles ?? {})) {
484
+ propFiles[prop] = (propFiles[prop] ?? 0) + n;
485
+ }
486
+ return {
487
+ ...c,
488
+ count: (c.count ?? 0) + (e.count ?? 0),
489
+ files: (c.files ?? 0) + (e.files ?? 0),
490
+ props,
491
+ propFiles,
492
+ ...(declaredAxes && Object.keys(declaredAxes).length > 0
493
+ ? { declaredAxes }
494
+ : {}),
495
+ };
496
+ });
497
+ const theirsOnly = usedThere.filter((c) => !c.from && !own.has(c.name));
314
498
  // The verdict travels WITH the payload: the platform reads one reading rather
315
499
  // than computing a second opinion from the same numbers, which is how two
316
500
  // implementations of the same judgement start disagreeing.
317
- const verdicts = new Map(crosswalk(merged).map((r) => [
501
+ /**
502
+ * IS THIS SCOPE A LIBRARY? The answer changes what the crosswalk is FOR.
503
+ *
504
+ * In an app, "your Pill reads as our badge" saves them a duplicate. In a library it
505
+ * would delete their component - and 16 of 37 in a real one were deleted that way
506
+ * (dono, 01/08). Measured, not guessed: a library declares and does not compose
507
+ * itself. See `isLibrary`.
508
+ */
509
+ const library = isLibrary(scoped);
510
+ const verdicts = new Map(crosswalk(merged, { library }).map((r) => [
318
511
  r.component.name,
319
512
  { bucket: r.bucket, canonical: r.canonical, because: r.because },
320
513
  ]));
514
+ if (usedThere.length > 0) {
515
+ console.log("");
516
+ console.log(body(`Read ${usageSeen.size} more file${usageSeen.size === 1 ? "" : "s"} for EVIDENCE only - ${(opts?.usage ?? []).map((u) => paint.strong(u.label)).join(", ")}. What comes from there is how often each component is used and which values get picked, which is what turns a settled choice into a law. The palette, the scales and the convention stay from ${paint.strong(scopeLabel ?? "the system")}: an app's average is not a scale, it is a set of choices made from one.`));
517
+ if (theirsOnly.length > 0) {
518
+ const shown = theirsOnly.slice(0, 6).map((c) => c.name);
519
+ console.log(body(paint.faint(`(${theirsOnly.length} component${theirsOnly.length === 1 ? "" : "s"} live there and not in the system - ${shown.join(", ")}${theirsOnly.length > shown.length ? ", …" : ""}. Left out on purpose: the system is one place, and a union of two codebases is not a system. Point --scope at them if they belong in it.)`)));
520
+ }
521
+ }
522
+ /**
523
+ * WHAT THE GATE TURNED AWAY - said out loud, grouped, with the reason.
524
+ *
525
+ * The sentence a person needs when a number drops by an order of magnitude: enough
526
+ * to trust it, and enough to notice if it dropped too far.
527
+ */
528
+ /**
529
+ * HOW THE PROJECT IS ORGANISED - and the choice it leaves open, when there is one.
530
+ *
531
+ * The skill asks which has priority; the chosen one becomes a system-wide rule that
532
+ * reaches CLAUDE.md, so every prompt after this respects where a new component goes.
533
+ */
534
+ if (architectures.length > 0) {
535
+ console.log("");
536
+ console.log(section("How this project is organised"));
537
+ const choice = describeChoice(architectures);
538
+ if (choice)
539
+ console.log(body(choice));
540
+ for (const a of architectures.slice(0, 4)) {
541
+ console.log(body(paint.faint(` ${describeArchitecture(a)}`)));
542
+ }
543
+ }
544
+ if (skips.length > 0) {
545
+ console.log("");
546
+ console.log(body(describeGate(scoped.length, skips.length)));
547
+ for (const group of groupSkips(skips)) {
548
+ const shown = group.names.slice(0, 5);
549
+ const more = group.names.length - shown.length;
550
+ console.log(body(paint.faint(` ${group.names.length} ${group.label}: ${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}`)));
551
+ }
552
+ }
553
+ if (library) {
554
+ console.log("");
555
+ console.log(body(`This scope DECLARES its components and composes almost none of them inside itself - which is what a component library looks like from the inside. So every component it exports arrives as YOURS, with its own recipe. Where one reads like something in our catalogue, that is recorded as a note instead of replacing it.`));
556
+ }
557
+ /**
558
+ * THE LAWS READ THE MERGED LIST, not the scope's.
559
+ *
560
+ * `RULE_MIN_FILES` is three, and a library has one file per component - so
561
+ * measured against the scope alone, "every file that passes it agrees" can never
562
+ * be true and the habit rung stays empty forever. The app is where the agreeing
563
+ * happens, which is the whole reason `--usage` exists.
564
+ */
321
565
  const laws = new Map();
322
- for (const r of observedRules(inventory)) {
566
+ for (const r of observedRules(merged)) {
323
567
  laws.set(r.component, [...(laws.get(r.component) ?? []), r.text]);
324
568
  }
569
+ /**
570
+ * THE OWNER'S STEP FIVE, in the recipe's own `usage`.
571
+ *
572
+ * Whether a component forwards the rest of its props is the answer to "can I put an
573
+ * aria-label on it", and it is never in a type. And which components appear INSIDE it
574
+ * at its call sites is the composition half of a library's story - empty by
575
+ * construction until the app is read, because a library composes nothing internally.
576
+ */
577
+ const systemOwns = new Set(merged.filter((c) => !c.from).map((c) => c.name));
578
+ for (const c of merged) {
579
+ const read = propsOf.get(c.name);
580
+ if (!read)
581
+ continue;
582
+ for (const rule of propRules(c.name, read)) {
583
+ laws.set(c.name, [...(laws.get(c.name) ?? []), rule.text]);
584
+ }
585
+ }
586
+ for (const pair of nestingRules(nesting, systemOwns)) {
587
+ laws.set(pair.component, [...(laws.get(pair.component) ?? []), pair.text]);
588
+ }
325
589
  const components = merged.map((c) => ({
326
590
  ...c,
327
591
  ...(verdicts.get(c.name) ?? {}),
@@ -361,6 +625,19 @@ export async function takeCensus(root) {
361
625
  observed: distinctValues(d),
362
626
  ...(components.length > 0 ? { components } : {}),
363
627
  ...(defined.length > 0 ? { defined } : {}),
628
+ ...(architectures.length > 0
629
+ ? { architectures: architectures.slice(0, 6) }
630
+ : {}),
631
+ ...(skips.length > 0
632
+ ? {
633
+ skipped: skips
634
+ .slice(0, 400)
635
+ .map(({ name, why, because }) => ({ name, why, because })),
636
+ }
637
+ : {}),
638
+ ...((opts?.usage ?? []).length > 0
639
+ ? { usage: (opts?.usage ?? []).map((u) => u.label) }
640
+ : {}),
364
641
  totals: {
365
642
  scanned: d.scanned,
366
643
  values: d.findings.length,
@@ -670,9 +947,22 @@ async function sayIfWorkspace(root) {
670
947
  console.log(body("That is a fine DIAGNOSIS and a poor system: a light app and a dark one"));
671
948
  console.log(body("average into a palette that is neither."));
672
949
  console.log("");
950
+ /**
951
+ * BOTH ROLES IN ONE LINE, because a person reading this has both folders open.
952
+ *
953
+ * `--scope` alone is where the library came back with no laws at all: the system
954
+ * is one place, but the EVIDENCE that a choice is settled is somewhere else, and
955
+ * naming only the first left the second unreachable (dono, 01/08).
956
+ */
957
+ const usageFlags = apps
958
+ .slice(0, 2)
959
+ .map((a) => `--usage ${a}`)
960
+ .join(" ");
673
961
  if (shared.length > 0) {
674
962
  console.log(body("The shape that works, if your vocabulary is shared:"));
675
- console.log(body(` ${paint.strong(`synthesisui import --scope ${shared[0]}`)} ${paint.faint("the system comes from here")}`));
963
+ console.log(body(` ${paint.strong(`synthesisui import --scope ${shared[0]} ${usageFlags}`)}`));
964
+ console.log(body(paint.faint(` --scope is the SYSTEM: the tokens, the components, the way you name a class. One place.`)));
965
+ console.log(body(paint.faint(` --usage is the EVIDENCE: how often each component is used and which values get picked. As many as you have.`)));
676
966
  }
677
967
  else {
678
968
  console.log(body("The shape that works:"));
@@ -1017,11 +1307,40 @@ export async function runImport(opts) {
1017
1307
  * decisions live in one package and the governance belongs at the root, and
1018
1308
  * conflating the two meant every re-measure scattered another census.
1019
1309
  */
1020
- const scope = opts.scope
1021
- ?.trim()
1310
+ const tidy = (p) => p
1311
+ .trim()
1022
1312
  .replace(/^\.\/+/, "")
1023
1313
  .replace(/\/+$/, "");
1314
+ const scope = opts.scope ? tidy(opts.scope) : undefined;
1024
1315
  const readFrom = scope ? join(root, scope) : root;
1316
+ /**
1317
+ * THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
1318
+ * the palette, the scales and the convention. `--usage` is the EVIDENCE - as
1319
+ * many places as they have, and the only source of counts, chosen values and
1320
+ * laws.
1321
+ *
1322
+ * They were one flag, and one flag could only answer one of the two questions.
1323
+ * Pointed at a real library, `usage` came back empty on all 23 components and
1324
+ * the habit rung of the ladder was unreachable by construction: a law needs
1325
+ * three files that agree and a library has one file per component (dono, 01/08).
1326
+ */
1327
+ const usage = [];
1328
+ for (const raw of opts.usage ?? []) {
1329
+ const label = tidy(raw);
1330
+ if (!label || label === scope)
1331
+ continue;
1332
+ const path = join(root, label);
1333
+ const ok = await stat(path)
1334
+ .then((st) => st.isDirectory())
1335
+ .catch(() => false);
1336
+ if (!ok) {
1337
+ console.log(section("Import"));
1338
+ console.log(body(`\`--usage ${label}\` is not a folder under ${paint.strong(root)}. Nothing was read.`));
1339
+ return;
1340
+ }
1341
+ if (!usage.some((u) => u.label === label))
1342
+ usage.push({ path, label });
1343
+ }
1025
1344
  // A census handed to us (an agent annotated it) is sent as-is; the numbers
1026
1345
  // inside were still ours to begin with.
1027
1346
  let census;
@@ -1060,7 +1379,10 @@ export async function runImport(opts) {
1060
1379
  }
1061
1380
  else {
1062
1381
  console.log(section("Reading your project"));
1063
- census = await takeCensus(readFrom);
1382
+ census = await takeCensus(readFrom, {
1383
+ ...(usage.length > 0 ? { usage } : {}),
1384
+ ...(scope ? { scopeLabel: scope } : {}),
1385
+ });
1064
1386
  if (scope)
1065
1387
  census.scope = scope;
1066
1388
  }
@@ -232,6 +232,15 @@ async function describeComponent(root, name) {
232
232
  const out = [
233
233
  `${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
234
234
  ];
235
+ // THEIR OWN RUNG, when their repo has one. An agent about to compose this should
236
+ // know whether it is a primitive or a whole region before it starts.
237
+ if (recipe.tier) {
238
+ out.push(recipe.tier === "atom"
239
+ ? "An atom in your own ladder - nothing else in the system breaks down into it."
240
+ : recipe.tier === "molecule"
241
+ ? "A molecule in your own ladder - built out of your atoms, so reach for those inside it."
242
+ : "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.");
243
+ }
235
244
  // WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
236
245
  // their own, because the surface belongs to what they return.
237
246
  const p = recipe.preview;
@@ -516,6 +516,7 @@ const FORM_TAG = {
516
516
  icon: { tag: "span" },
517
517
  row: { tag: "div" },
518
518
  stack: { tag: "div" },
519
+ slot: { tag: "div" },
519
520
  };
520
521
  /** Only these two arrange; the rest are leaves. */
521
522
  const ARRANGES = new Set(["row", "stack"]);
@@ -555,6 +556,16 @@ function emitTree(nodes, comp, indent) {
555
556
  lines.push(`${indent}{/* ${node.from} renders here - a third party's component, so its markup is theirs */}`);
556
557
  continue;
557
558
  }
559
+ // The caller's content, so the generated code takes it as a prop rather than
560
+ // inventing a sample. A slot that also carries styles keeps its part element
561
+ // around the children; a bare one is just `{children}`.
562
+ if (node.as === "slot") {
563
+ const inner = `{children}${node.expects ? ` {/* ${node.expects} */}` : ""}`;
564
+ lines.push(node.part
565
+ ? `${indent}<${comp}${pascal(node.part)}>${inner}</${comp}${pascal(node.part)}>`
566
+ : `${indent}${inner}`);
567
+ continue;
568
+ }
558
569
  if (!node.part) {
559
570
  // Pure structure with no styles of its own: keep the arrangement, skip the
560
571
  // element - a div that carries nothing is a div nobody needs.