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.
- package/dist/anatomy-read.js +15 -1
- package/dist/commands/import.js +338 -16
- package/dist/commands/mcp.js +9 -0
- package/dist/component-codegen.js +11 -0
- package/dist/doctor/architecture.js +134 -0
- package/dist/doctor/call-sites.js +171 -0
- package/dist/doctor/component-gate.js +235 -0
- package/dist/doctor/components-scan.js +70 -2
- package/dist/doctor/crosswalk.js +59 -2
- package/dist/doctor/definitions-scan.js +32 -0
- package/dist/doctor/transcribe.js +116 -0
- package/dist/doctor/variant-read.js +584 -0
- package/dist/index.js +35 -5
- package/dist/progress.js +71 -0
- package/dist/skill-import.js +200 -11
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -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
|
|
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)) {
|
package/dist/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
|
438
|
+
if (!d)
|
|
310
439
|
return c;
|
|
311
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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]}
|
|
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
|
|
1021
|
-
|
|
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
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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.
|