synthesisui 0.16.83 → 0.16.86
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/commands/import.js +141 -6
- package/dist/commands/mcp.js +148 -0
- package/dist/doctor/call-sites.js +22 -4
- package/dist/doctor/catalogue-fetch.js +88 -0
- package/dist/doctor/component-gate.js +40 -0
- package/dist/doctor/crosswalk.js +97 -2
- package/dist/doctor/signals.js +286 -0
- package/dist/skill-import.js +72 -6
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -5,17 +5,19 @@ import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../c
|
|
|
5
5
|
import { architectureRule, describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
|
|
6
6
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
7
7
|
import { nestingRules, propRules, readDefinitionProps, readNesting, } from "../doctor/call-sites.js";
|
|
8
|
+
import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
|
|
8
9
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
9
10
|
import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
|
|
10
|
-
import { describeGate, gateComponent, groupSkips, } from "../doctor/component-gate.js";
|
|
11
|
+
import { describeGate, gateComponent, groupSkips, SCREENS_FOR_SYSTEM, } from "../doctor/component-gate.js";
|
|
11
12
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
12
13
|
import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
|
|
13
|
-
import { crosswalk, isLibrary, observedRules } from "../doctor/crosswalk.js";
|
|
14
|
+
import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
|
|
14
15
|
import { moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
|
|
15
16
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
16
17
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
17
18
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
18
19
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
20
|
+
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
19
21
|
import { buildTable } from "../doctor/tokens.js";
|
|
20
22
|
import { readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
21
23
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
@@ -214,8 +216,36 @@ function distinctValues(d) {
|
|
|
214
216
|
}
|
|
215
217
|
return kept.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
216
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* WHICH SCREEN A FILE BELONGS TO.
|
|
221
|
+
*
|
|
222
|
+
* The route segment, because that is what a framework already decided: everything under
|
|
223
|
+
* `app/dashboard/audience` is one screen however deep the folder goes. Route GROUPS are
|
|
224
|
+
* skipped - `(dashboard)` is an organisational parenthesis, not a place a person visits.
|
|
225
|
+
*
|
|
226
|
+
* For a project with no `app/` or `pages/`, the first two folders are the closest honest
|
|
227
|
+
* answer, and a library never reaches this because the test is skipped there.
|
|
228
|
+
*/
|
|
229
|
+
function screenOf(rel) {
|
|
230
|
+
const parts = rel.split("/");
|
|
231
|
+
const i = parts.findIndex((p) => p === "app" || p === "pages");
|
|
232
|
+
if (i === -1)
|
|
233
|
+
return parts.slice(0, 2).join("/");
|
|
234
|
+
const after = parts.slice(i + 1).filter((p) => !p.startsWith("("));
|
|
235
|
+
return `${parts[i]}/${after.slice(0, 2).join("/")}`;
|
|
236
|
+
}
|
|
217
237
|
export async function takeCensus(root, opts) {
|
|
218
238
|
const scopeLabel = opts?.scopeLabel;
|
|
239
|
+
/**
|
|
240
|
+
* THE LIVE CATALOGUE, once, before anything is matched.
|
|
241
|
+
*
|
|
242
|
+
* The written table had 26 names against a catalogue of 41, so twenty of our own
|
|
243
|
+
* components were invisible to matching and their `Textarea` did not meet our
|
|
244
|
+
* `textarea` (dono, 01/08). Fetched rather than shipped, and the fallback is announced
|
|
245
|
+
* with what it costs - a smaller answer is fine, a silently smaller one is not.
|
|
246
|
+
*/
|
|
247
|
+
const fetched = await fetchCatalogue(opts?.registry);
|
|
248
|
+
useLiveCatalogue(fetched.ok ? asCatalogueTable(fetched.index) : null);
|
|
219
249
|
const css = await harvestOwnCss(root);
|
|
220
250
|
const table = buildTable({ css, source: "yours" });
|
|
221
251
|
const schemes = parseSchemeBlocks(css);
|
|
@@ -243,8 +273,25 @@ export async function takeCensus(root, opts) {
|
|
|
243
273
|
const dirs = new Map();
|
|
244
274
|
/** Parent → child → the files that put one inside the other. */
|
|
245
275
|
const nesting = new Map();
|
|
276
|
+
/**
|
|
277
|
+
* WHAT IS COUNTABLE, COUNTED - so the reader decides instead of searching.
|
|
278
|
+
*
|
|
279
|
+
* The skill's theme section said "go and look" and listed five places to grep; a real run
|
|
280
|
+
* swept for motion and icon libraries too, which it never asked for (dono, 01/08).
|
|
281
|
+
* Searching is a tool call, its whole output lands in the context, and two readers
|
|
282
|
+
* sweeping the same repo bring back different slices. All of this is one more pass over
|
|
283
|
+
* files already open. See `signals.ts`.
|
|
284
|
+
*/
|
|
285
|
+
const signals = emptySignals();
|
|
286
|
+
const importTally = new Map();
|
|
246
287
|
/** Component → what its own definition names and whether it forwards the rest. */
|
|
247
288
|
const propsOf = new Map();
|
|
289
|
+
/**
|
|
290
|
+
* Component → the distinct SCREENS that compose it, which is what separates a design
|
|
291
|
+
* system from a folder: 242 of 271 in a real app live on one screen or none, and
|
|
292
|
+
* filtering them takes the vitrine from 271 to 29 (dono, 01/08).
|
|
293
|
+
*/
|
|
294
|
+
const screensOf = new Map();
|
|
248
295
|
/** Component → the axes its own `cva` declares. See `variant-read.ts`. */
|
|
249
296
|
const variantAxes = new Map();
|
|
250
297
|
/** How much came out of CSS Modules, for the coverage report. */
|
|
@@ -282,6 +329,7 @@ export async function takeCensus(root, opts) {
|
|
|
282
329
|
}
|
|
283
330
|
}
|
|
284
331
|
sources.push({ file: rel, source: src });
|
|
332
|
+
readSignalsInto(signals, importTally, rel, src);
|
|
285
333
|
reports.push(scanSource(rel, src, table));
|
|
286
334
|
// Stories and tests compose components to SHOW them; counting those as the
|
|
287
335
|
// product's own composition is the same lie the colour census told before
|
|
@@ -299,6 +347,14 @@ export async function takeCensus(root, opts) {
|
|
|
299
347
|
// HOW IT IS USED, not only what it is: which props people pass, what it passes
|
|
300
348
|
// on, and what shows up inside it. See `call-sites.ts`.
|
|
301
349
|
readNesting(rel, src, nesting);
|
|
350
|
+
// Which screen this file belongs to, and what it composes. One pass, and the
|
|
351
|
+
// answer is a count of PLACES rather than of occurrences - a loop is one place.
|
|
352
|
+
const screen = screenOf(rel);
|
|
353
|
+
for (const m of src.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) {
|
|
354
|
+
const at = screensOf.get(m[1]) ?? new Set();
|
|
355
|
+
at.add(screen);
|
|
356
|
+
screensOf.set(m[1], at);
|
|
357
|
+
}
|
|
302
358
|
const found = [];
|
|
303
359
|
for (const d of scanDefinitions(rel, src)) {
|
|
304
360
|
const verdict = gateComponent({
|
|
@@ -346,6 +402,15 @@ export async function takeCensus(root, opts) {
|
|
|
346
402
|
* paid for twice already.
|
|
347
403
|
*/
|
|
348
404
|
const v = transcribeVariants(src, declaredValues);
|
|
405
|
+
/**
|
|
406
|
+
* THE RESTING OPTION, from the definition itself. `cva` says it in
|
|
407
|
+
* `defaultVariants`; a lookup-record component says it in the destructuring -
|
|
408
|
+
* `{ variant = "body-md" }` - and reading past that left every style behind a
|
|
409
|
+
* `[data-variant]` selector nothing sets, so a real Text previewed as bare
|
|
410
|
+
* unstyled words (dono, 01/08).
|
|
411
|
+
*/
|
|
412
|
+
const destructured = readDefinitionProps(found[0].name, src).defaults;
|
|
413
|
+
const defaults = { ...destructured, ...v.defaults };
|
|
349
414
|
/**
|
|
350
415
|
* THE CSS MODULE, if this component styles that way.
|
|
351
416
|
*
|
|
@@ -418,9 +483,7 @@ export async function takeCensus(root, opts) {
|
|
|
418
483
|
...(tag ? { rootTag: tag } : {}),
|
|
419
484
|
...(layers.length > 0 ? { layers } : {}),
|
|
420
485
|
...(Object.keys(v.axes).length > 0 ? { declaredAxes: v.axes } : {}),
|
|
421
|
-
...(Object.keys(
|
|
422
|
-
? { defaults: v.defaults }
|
|
423
|
-
: {}),
|
|
486
|
+
...(Object.keys(defaults).length > 0 ? { defaults } : {}),
|
|
424
487
|
};
|
|
425
488
|
}
|
|
426
489
|
// Their variant DEFINITION beats what usage happened to pick, and it is the
|
|
@@ -440,6 +503,51 @@ export async function takeCensus(root, opts) {
|
|
|
440
503
|
})));
|
|
441
504
|
const d = diagnose(reports);
|
|
442
505
|
const allComposed = tallyToInventory(tally, 400);
|
|
506
|
+
/**
|
|
507
|
+
* THE FEATURE FILTER, after the walk - because the count needs every file.
|
|
508
|
+
*
|
|
509
|
+
* A component composed on one screen is that screen's own markup; two screens is a
|
|
510
|
+
* decision the product shares. Measured on a real app: 242 of 271 live on one screen or
|
|
511
|
+
* none, and filtering them takes the vitrine from 271 to 29 - `Table`, `Tag`, `Tooltip`,
|
|
512
|
+
* `Button`, `Loader`, `Header` (dono, 01/08).
|
|
513
|
+
*
|
|
514
|
+
* SKIPPED FOR A LIBRARY, and this is the whole reason the test is here rather than in
|
|
515
|
+
* the walk: a library composes nothing inside itself, so every export would read as
|
|
516
|
+
* used on no screen. Measured the same way `isLibrary` measures it, from the tally
|
|
517
|
+
* rather than from a second opinion.
|
|
518
|
+
*/
|
|
519
|
+
/**
|
|
520
|
+
* NO SCREENS MEANS NO TEST, and this is the guard rather than a component count.
|
|
521
|
+
*
|
|
522
|
+
* A first version required four components before calling something a library, and a
|
|
523
|
+
* scope with three had every export filtered as a feature - which is the failure this
|
|
524
|
+
* test was built to prevent, arriving from the other side (spec, 01/08).
|
|
525
|
+
*
|
|
526
|
+
* The honest question is simpler: does this scope HAVE screens? A `packages/ui` has no
|
|
527
|
+
* `app/` and no `pages/`, so there is nothing to count and the count would be zero for
|
|
528
|
+
* everything. An app has them, and then the number means something.
|
|
529
|
+
*/
|
|
530
|
+
const hasScreens = [...screensOf.values()].some((at) => [...at].some((s) => s.startsWith("app/") || s.startsWith("pages/")));
|
|
531
|
+
if (hasScreens) {
|
|
532
|
+
const keep = [];
|
|
533
|
+
for (const d of defined) {
|
|
534
|
+
const screens = screensOf.get(d.name)?.size ?? 0;
|
|
535
|
+
if (screens >= SCREENS_FOR_SYSTEM) {
|
|
536
|
+
keep.push(d);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
skips.push({
|
|
540
|
+
name: d.name,
|
|
541
|
+
file: d.file,
|
|
542
|
+
why: "feature",
|
|
543
|
+
because: screens === 0
|
|
544
|
+
? `\`${d.name}\` is composed on no screen at all - it is markup nothing reaches yet`
|
|
545
|
+
: `\`${d.name}\` is composed on one screen only, so it is that screen's own markup rather than a decision the product shares. Two screens is the line, and the count is from your files`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
defined.length = 0;
|
|
549
|
+
defined.push(...keep);
|
|
550
|
+
}
|
|
443
551
|
/**
|
|
444
552
|
* THE EVIDENCE PASS - a second walk that touches ONE reading.
|
|
445
553
|
*
|
|
@@ -670,6 +778,27 @@ export async function takeCensus(root, opts) {
|
|
|
670
778
|
Object.keys(part.base).length +
|
|
671
779
|
Object.keys(part.dark).length +
|
|
672
780
|
Object.values(part.states).reduce((k, block) => k + Object.keys(block).length, 0), 0), 0));
|
|
781
|
+
if (fetched.ok) {
|
|
782
|
+
console.log("");
|
|
783
|
+
console.log(body(paint.faint(`Matched against the live catalogue - ${fetched.index.count} components.`)));
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
console.log("");
|
|
787
|
+
console.log(body(describeFallback(fetched, floorSize())));
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* THE SIGNALS, said as EVIDENCE. "189 dark utilities across 30 files, bound to
|
|
791
|
+
* `[data-theme=dark]`, opening dark with the OS overruled" is something a person can
|
|
792
|
+
* confirm or correct; "this is a dark product" is a claim they take on faith.
|
|
793
|
+
*/
|
|
794
|
+
const signalLines = describeSignals(signals);
|
|
795
|
+
if (signalLines.length > 0) {
|
|
796
|
+
console.log("");
|
|
797
|
+
console.log(section("What was countable, counted"));
|
|
798
|
+
for (const line of signalLines)
|
|
799
|
+
console.log(body(line));
|
|
800
|
+
console.log(body(paint.faint("Do not go looking for any of this - it is measured, and a sweep would bring back a different slice than the next one.")));
|
|
801
|
+
}
|
|
673
802
|
const coverageLines = describeCoverage(coverage);
|
|
674
803
|
if (coverageLines.length > 0) {
|
|
675
804
|
console.log("");
|
|
@@ -751,14 +880,19 @@ export async function takeCensus(root, opts) {
|
|
|
751
880
|
}));
|
|
752
881
|
const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
753
882
|
let name = null;
|
|
883
|
+
/** Their pinned ranges, so a library signal cites the version rather than going stale. */
|
|
884
|
+
let versions = {};
|
|
754
885
|
if (pkgRaw) {
|
|
755
886
|
try {
|
|
756
|
-
|
|
887
|
+
const pkg = JSON.parse(pkgRaw);
|
|
888
|
+
name = pkg.name ?? null;
|
|
889
|
+
versions = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
757
890
|
}
|
|
758
891
|
catch {
|
|
759
892
|
// an unreadable package.json costs the name, not the run
|
|
760
893
|
}
|
|
761
894
|
}
|
|
895
|
+
finishSignals(signals, importTally, versions);
|
|
762
896
|
// Every name their CSS defines, from the same harvest the token table came
|
|
763
897
|
// from - so a reference is judged against what actually exists, not against
|
|
764
898
|
// the subset we managed to read as a ramp.
|
|
@@ -795,6 +929,7 @@ export async function takeCensus(root, opts) {
|
|
|
795
929
|
* printed it, and never turned it into a rule is a feature that does nothing (dono,
|
|
796
930
|
* 01/08).
|
|
797
931
|
*/
|
|
932
|
+
signals,
|
|
798
933
|
...(coverage.components > 0
|
|
799
934
|
? {
|
|
800
935
|
coverage: {
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { relative, resolve } from "node:path";
|
|
3
|
+
import { readToken, resolveRegistry } from "../config.js";
|
|
3
4
|
import { fileRequest } from "../doctor/requests.js";
|
|
4
5
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
5
6
|
import { nearestToken, tokenFor } from "../doctor/tokens.js";
|
|
@@ -93,6 +94,29 @@ const TOOLS = [
|
|
|
93
94
|
required: ["name"],
|
|
94
95
|
},
|
|
95
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: "recipe_vocabulary",
|
|
99
|
+
description: "What a recipe CAN hold: every state that compiles, every preview form, the floor per kind, and the rules a media region needs. Call this BEFORE writing a recipe, not after - the reader that skipped it sent a `dark:` inside a variant and a state the compiler cannot spell, and both were dropped in silence. Served from the catalogue, so it grows as the contract grows.",
|
|
100
|
+
inputSchema: { type: "object", properties: {} },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "validate_recipe",
|
|
104
|
+
description: "Would this recipe survive being sent? Answers with what would NOT - the properties held by the schema and dropped by the compiler, and what is missing before it can be drawn at all - by name, with the reason. Never a boolean: a boolean tells you to try again, a list tells you what to fix. Call it on every recipe before sending, and write what it returns into `_synthesisui/not-expressed.md`.",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
name: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "The component name, for the report.",
|
|
111
|
+
},
|
|
112
|
+
recipe: {
|
|
113
|
+
type: "object",
|
|
114
|
+
description: "The candidate recipe, in the document's own shape.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ["recipe"],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
96
120
|
{
|
|
97
121
|
name: "request_component",
|
|
98
122
|
description: "File a component request when nothing in the catalogue covers what you need. This is the OTHER HALF of the refusal rule: you already say which entry you considered and why it did not fit - said in chat, that reasoning evaporates; filed here, it becomes the queue the system's author works from. File it at the moment you build the workaround, while the reasoning is still yours.",
|
|
@@ -217,6 +241,126 @@ async function listComponents(root) {
|
|
|
217
241
|
* back says what the platform UNDERSTOOD - so a reading that came out thin is
|
|
218
242
|
* visible instead of being discovered later in a preview.
|
|
219
243
|
*/
|
|
244
|
+
/**
|
|
245
|
+
* WHAT A RECIPE CAN HOLD - served, not written here.
|
|
246
|
+
*
|
|
247
|
+
* The reader had no way to ask, so it sent what it found and the parts we could not hold
|
|
248
|
+
* were dropped in silence: a state the compiler cannot spell, a `dark:` inside a variant,
|
|
249
|
+
* a part name with a dot in it. Four times in one day (dono, 01/08).
|
|
250
|
+
*
|
|
251
|
+
* Fetched rather than embedded, and the owner's reason is the one that decides it: "the
|
|
252
|
+
* properties need to be dynamic - the more we map, the more precision". A list written
|
|
253
|
+
* into a published package is a list that stops matching the contract.
|
|
254
|
+
*/
|
|
255
|
+
async function recipeVocabulary() {
|
|
256
|
+
const answer = await askCatalogue("vocabulary");
|
|
257
|
+
if (!answer.ok)
|
|
258
|
+
return answer.because;
|
|
259
|
+
const v = answer.body;
|
|
260
|
+
const lines = [
|
|
261
|
+
"WHAT A RECIPE CAN HOLD",
|
|
262
|
+
"",
|
|
263
|
+
`properties: any CSS property, camelCase. Part names match ${v.partName} - flat and kebab-case, because a nested name compiles to two classes and is invalid CSS.`,
|
|
264
|
+
"",
|
|
265
|
+
`states that compile (${v.states.length}): ${v.states.join(", ")}`,
|
|
266
|
+
`preview forms: ${v.forms.join(", ")}`,
|
|
267
|
+
`a layer's conditions: ${v.layerConditions.join(", ")} - and they combine, so a ghost button's hover at the md breakpoint is one layer`,
|
|
268
|
+
"",
|
|
269
|
+
"THE FLOOR - below this a preview cannot be told apart from the page:",
|
|
270
|
+
...Object.entries(v.floor).map(([kind, needs]) => ` ${kind}: ${needs.join("; ")}`),
|
|
271
|
+
"",
|
|
272
|
+
"WHAT TO GO LOOK FOR, per kind - work the list and say which ones their code answers:",
|
|
273
|
+
...Object.entries(v.checklist).map(([kind, items]) => ` ${kind}:\n${items.map((i) => ` - ${i}`).join("\n")}`),
|
|
274
|
+
"",
|
|
275
|
+
"A MEDIA REGION cannot be used without these, and four of the five are about what happens when the picture is not there:",
|
|
276
|
+
...v.media.map((m) => ` ${m.need} - ${m.why}`),
|
|
277
|
+
];
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* WOULD THIS RECIPE SURVIVE? - the handshake that makes a loss visible when it happens.
|
|
282
|
+
*
|
|
283
|
+
* Nothing validated a recipe before it was sent: zero mentions of `validate` in the entire
|
|
284
|
+
* import path. So the reader sent, the document held half of it, and nobody noticed until
|
|
285
|
+
* somebody looked at a blank component weeks later.
|
|
286
|
+
*/
|
|
287
|
+
async function validateRecipe(name, recipe) {
|
|
288
|
+
const answer = await askCatalogue("validate", { name, recipe });
|
|
289
|
+
if (!answer.ok)
|
|
290
|
+
return answer.because;
|
|
291
|
+
const v = answer.body;
|
|
292
|
+
if (v.ok) {
|
|
293
|
+
return `${name}: everything you sent would survive. Send it.`;
|
|
294
|
+
}
|
|
295
|
+
const lines = [`${name}: this would NOT arrive whole.`, ""];
|
|
296
|
+
if (v.rejected.length > 0) {
|
|
297
|
+
lines.push("REFUSED by the contract - fix these or the whole recipe is dropped:");
|
|
298
|
+
for (const r of v.rejected)
|
|
299
|
+
lines.push(` ${r.at}: ${r.why}`);
|
|
300
|
+
lines.push("");
|
|
301
|
+
}
|
|
302
|
+
if (v.lost.length > 0) {
|
|
303
|
+
lines.push("HELD BY THE SCHEMA AND DROPPED BY THE COMPILER - the silent half, which is the", "one worth fixing before sending:");
|
|
304
|
+
for (const l of v.lost)
|
|
305
|
+
lines.push(` ${l}`);
|
|
306
|
+
lines.push("");
|
|
307
|
+
}
|
|
308
|
+
if (v.missing.length > 0) {
|
|
309
|
+
lines.push("BELOW THE FLOOR - it would arrive and not be drawable:");
|
|
310
|
+
for (const m of v.missing)
|
|
311
|
+
lines.push(` ${m}`);
|
|
312
|
+
lines.push("");
|
|
313
|
+
}
|
|
314
|
+
if (v.checklist.length > 0) {
|
|
315
|
+
lines.push("Go looking for these before you send it again:");
|
|
316
|
+
for (const c of v.checklist)
|
|
317
|
+
lines.push(` - ${c}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push("", "Write this into `_synthesisui/not-expressed.md` - what we cannot hold is the list", "the system's author works from, and said only in chat it evaporates.");
|
|
320
|
+
return lines.join("\n");
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The catalogue's door, with the session the device login already granted.
|
|
324
|
+
*
|
|
325
|
+
* Served rather than shipped for the two reasons the owner gave (dono, 01/08): a table in
|
|
326
|
+
* a published package can be read by anyone, and it goes stale - the crosswalk's own copy
|
|
327
|
+
* had 26 names against a catalogue of 41.
|
|
328
|
+
*/
|
|
329
|
+
async function askCatalogue(want, post) {
|
|
330
|
+
const token = await readToken();
|
|
331
|
+
if (!token) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
because: "Not signed in, so the catalogue could not answer. Run `synthesisui login` in the terminal and call this again - this one needs a session, unlike `doctor`.",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const base = resolveRegistry();
|
|
338
|
+
try {
|
|
339
|
+
const res = await fetch(`${base}/api/catalogue${post ? "" : `?want=${want}`}`, post
|
|
340
|
+
? {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: {
|
|
343
|
+
Authorization: `Bearer ${token}`,
|
|
344
|
+
"content-type": "application/json",
|
|
345
|
+
},
|
|
346
|
+
body: JSON.stringify(post),
|
|
347
|
+
}
|
|
348
|
+
: { headers: { Authorization: `Bearer ${token}` } });
|
|
349
|
+
if (!res.ok) {
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
because: `The catalogue answered ${res.status}. ${res.status === 401 ? "Run `synthesisui login` and try again." : "Nothing was sent."}`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, body: await res.json() };
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
because: "No network, so the catalogue could not answer. Everything else in this server works offline; this one does not, and pretending otherwise would mean answering from a stale copy.",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
220
364
|
async function describeComponent(root, name) {
|
|
221
365
|
const { documents, requires } = await loadSystem(root);
|
|
222
366
|
let recipe;
|
|
@@ -337,6 +481,10 @@ async function callTool(root, name, args) {
|
|
|
337
481
|
return text(await describeComponent(root, String(args.name ?? "")));
|
|
338
482
|
case "add_component":
|
|
339
483
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
484
|
+
case "recipe_vocabulary":
|
|
485
|
+
return text(await recipeVocabulary());
|
|
486
|
+
case "validate_recipe":
|
|
487
|
+
return text(await validateRecipe(String(args.name ?? "this component"), args.recipe));
|
|
340
488
|
case "request_component": {
|
|
341
489
|
const r = await fileRequest(root, {
|
|
342
490
|
kind: "component",
|
|
@@ -49,12 +49,25 @@ const SPREAD_ONTO = /\{\s*\.\.\.\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
|
49
49
|
*/
|
|
50
50
|
export function readDefinitionProps(name, source) {
|
|
51
51
|
const explicit = [];
|
|
52
|
+
const defaults = {};
|
|
52
53
|
let restName = null;
|
|
54
|
+
/**
|
|
55
|
+
* EXACT NAME FIRST, THEN THE forwardRef SPELLING.
|
|
56
|
+
*
|
|
57
|
+
* `export const Label = forwardRef(LabelComponent)` defines its props on
|
|
58
|
+
* `LabelComponent`, and searching for `Label` alone read past the destructure - so a
|
|
59
|
+
* real Label's `variant = "body-md"` default never travelled (probe, 01/08). A prefix
|
|
60
|
+
* match (`LabelComponent` starts with `Label`) is the deterministic version of that
|
|
61
|
+
* idiom, and the exact name still wins when both exist.
|
|
62
|
+
*/
|
|
53
63
|
DESTRUCTURED.lastIndex = 0;
|
|
54
|
-
|
|
64
|
+
const matches = [...source.matchAll(DESTRUCTURED)];
|
|
65
|
+
const exact = matches.filter((m) => (m[1] ?? m[3]) === name);
|
|
66
|
+
const prefixed = matches.filter((m) => {
|
|
55
67
|
const found = m[1] ?? m[3];
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
return found !== name && found?.startsWith(name);
|
|
69
|
+
});
|
|
70
|
+
for (const m of exact.length > 0 ? exact : prefixed) {
|
|
58
71
|
const body = m[2] ?? m[4] ?? "";
|
|
59
72
|
for (const raw of body.split(",")) {
|
|
60
73
|
const part = raw.trim();
|
|
@@ -69,6 +82,11 @@ export function readDefinitionProps(name, source) {
|
|
|
69
82
|
if (/^[a-zA-Z_$][\w$]*$/.test(prop) && !explicit.includes(prop)) {
|
|
70
83
|
explicit.push(prop);
|
|
71
84
|
}
|
|
85
|
+
// `variant = "body-md"` - the resting option, said by the definition itself.
|
|
86
|
+
// Only a string literal: a computed default is not in the source.
|
|
87
|
+
const def = /=\s*["']([^"']+)["']\s*$/.exec(part);
|
|
88
|
+
if (def && /^[a-zA-Z_$][\w$]*$/.test(prop))
|
|
89
|
+
defaults[prop] = def[1];
|
|
72
90
|
}
|
|
73
91
|
break;
|
|
74
92
|
}
|
|
@@ -82,7 +100,7 @@ export function readDefinitionProps(name, source) {
|
|
|
82
100
|
}
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
|
-
return { explicit, forwards };
|
|
103
|
+
return { explicit, defaults, forwards };
|
|
86
104
|
}
|
|
87
105
|
/**
|
|
88
106
|
* WHICH COMPONENTS APPEAR INSIDE THIS ONE, at its call sites.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CATALOGUE, FETCHED - because a table written into a published package goes stale
|
|
3
|
+
* and can be read by anyone.
|
|
4
|
+
*
|
|
5
|
+
* The crosswalk's own copy was written by hand and had 26 names against a catalogue of 41
|
|
6
|
+
* (dono, 01/08). Twenty of OUR OWN components were invisible to matching - `Textarea`,
|
|
7
|
+
* `Sidebar`, `Menu`, `Stepper`, `EmptyState` and fifteen more - and five names in it no
|
|
8
|
+
* longer existed. Their `Textarea` did not match our `textarea`. At 300 components that
|
|
9
|
+
* gap is 280, and closing it by hand means a version bump and a publish for every
|
|
10
|
+
* component we add. That was forgotten twice in one day.
|
|
11
|
+
*
|
|
12
|
+
* THE FLOOR STAYS. The written table is not deleted: it is what answers when there is no
|
|
13
|
+
* session and no network, which is a real state a person can be in. What changes is that
|
|
14
|
+
* it stops being the truth and starts being the fallback - and the run SAYS when it fell
|
|
15
|
+
* back, with the number that costs, because a silently smaller answer is the thing this
|
|
16
|
+
* whole pipeline keeps being punished for.
|
|
17
|
+
*/
|
|
18
|
+
import { readToken, resolveRegistry } from "../config.js";
|
|
19
|
+
/**
|
|
20
|
+
* One call, at the start of a run.
|
|
21
|
+
*
|
|
22
|
+
* Not per component: a handshake for each of 362 components would be 362 round trips to
|
|
23
|
+
* answer a question one table answers. The index is 60 tokens a component and the caller
|
|
24
|
+
* matches locally against it.
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchCatalogue(registryFlag) {
|
|
27
|
+
const token = await readToken();
|
|
28
|
+
if (!token) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
why: "no-session",
|
|
32
|
+
because: "no session, so the catalogue could not be fetched - `synthesisui login` and re-run to match against the full list",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const base = resolveRegistry(registryFlag);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${base}/api/catalogue?want=index`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
why: res.status === 401 ? "no-session" : "refused",
|
|
44
|
+
because: res.status === 401
|
|
45
|
+
? "the session was refused - `synthesisui login` and re-run"
|
|
46
|
+
: `the catalogue answered ${res.status}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const index = (await res.json());
|
|
50
|
+
if (!Array.isArray(index?.components)) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
why: "refused",
|
|
54
|
+
because: "the catalogue answered in a shape this version does not know",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, index };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
why: "offline",
|
|
63
|
+
because: "no network, so the catalogue could not be fetched",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The fetched index as the crosswalk's own table: canonical name → the words it answers
|
|
69
|
+
* to, plus the axes, which is what `axisOverlap` compares against.
|
|
70
|
+
*/
|
|
71
|
+
export function asCatalogueTable(index) {
|
|
72
|
+
const synonyms = {};
|
|
73
|
+
const axes = {};
|
|
74
|
+
for (const entry of index.components) {
|
|
75
|
+
synonyms[entry.name] = entry.also;
|
|
76
|
+
axes[entry.name] = entry.axes;
|
|
77
|
+
}
|
|
78
|
+
return { synonyms, axes };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What falling back COST, said with the number.
|
|
82
|
+
*
|
|
83
|
+
* "20 of our own components could not be matched" is a sentence somebody can act on;
|
|
84
|
+
* "using the built-in list" is not.
|
|
85
|
+
*/
|
|
86
|
+
export function describeFallback(result, floorSize) {
|
|
87
|
+
return `${result.because}. Matching used the ${floorSize} names built into this CLI instead of the live catalogue, so a component of yours that lines up with something we added recently will read as exclusively yours. That is a smaller answer, not a wrong one - re-run with a session to get the full one.`;
|
|
88
|
+
}
|
|
@@ -136,6 +136,35 @@ function adapterFor(source, internal) {
|
|
|
136
136
|
}
|
|
137
137
|
return seen > 0 ? theirs : null;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* HOW MANY SCREENS USE IT - the question that separates a design system from a folder.
|
|
141
|
+
*
|
|
142
|
+
* The gate took a real app from 704 to 271, and 271 names in a vitrine is the same
|
|
143
|
+
* problem with different numbers. `RegisterMultipleCSV`, `FeatureFlagUpdateForm`,
|
|
144
|
+
* `BoxHeaderInfo`, `SendProgressModal` are exclusive, correctly gated, and not design
|
|
145
|
+
* system: they are pieces of one screen.
|
|
146
|
+
*
|
|
147
|
+
* Enumerated before it was built, which is why the threshold is what it is (dono, 01/08):
|
|
148
|
+
*
|
|
149
|
+
* 42 used on NO screen BoxHeaderInfo, Register, Search, Switcher
|
|
150
|
+
* 200 used on ONE screen FeatureFlagUpdateForm, RegisterMultipleCSV
|
|
151
|
+
* 20 used on two screens SkeletonLoader, Group, Header, UploadButton
|
|
152
|
+
* 2 used on three ConfirmButton, Box
|
|
153
|
+
* 7 used on four or more Table, Tag, Tooltip, Button, Loader
|
|
154
|
+
*
|
|
155
|
+
* 242 of 271 (89%) live on one screen or none. Filtering them takes the vitrine from 271
|
|
156
|
+
* to 29 - and the 29 are `Table`, `Tag`, `Tooltip`, `Button`, `Loader`, `Header`, which is
|
|
157
|
+
* what a design system is.
|
|
158
|
+
*
|
|
159
|
+
* TWO SCREENS IS THE LINE, and the distribution is why: the drop from 200 to 20 happens
|
|
160
|
+
* exactly there. A component two screens share is a decision about the product; a
|
|
161
|
+
* component one screen has is that screen's own markup.
|
|
162
|
+
*
|
|
163
|
+
* NOT APPLIED TO A LIBRARY. A library composes nothing inside itself, so every export
|
|
164
|
+
* would read as used on no screen - the same mistake library mode was built to stop. The
|
|
165
|
+
* caller passes the counts only when it has them, and absent means the test is skipped.
|
|
166
|
+
*/
|
|
167
|
+
export const SCREENS_FOR_SYSTEM = 2;
|
|
139
168
|
export function gateComponent(input) {
|
|
140
169
|
const { name, file, source } = input;
|
|
141
170
|
if (RESERVED_FILE.test(file) || RESERVED_FILE_OTHER.test(file)) {
|
|
@@ -174,6 +203,15 @@ export function gateComponent(input) {
|
|
|
174
203
|
because: `\`${name}\` is a capitalised export in a file with no markup - a constant, a config object or a helper`,
|
|
175
204
|
};
|
|
176
205
|
}
|
|
206
|
+
if (input.screens != null && input.screens < SCREENS_FOR_SYSTEM) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
why: "feature",
|
|
210
|
+
because: input.screens === 0
|
|
211
|
+
? `\`${name}\` is composed on no screen at all - it is a piece of markup nothing reaches yet`
|
|
212
|
+
: `\`${name}\` is composed on one screen only, so it is that screen's own markup rather than a decision the product shares. Two screens is the line, and the count is from your files`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
177
215
|
if (!HAS_STYLING.test(source)) {
|
|
178
216
|
return {
|
|
179
217
|
ok: false,
|
|
@@ -195,6 +233,7 @@ export const SKIP_LABEL = {
|
|
|
195
233
|
screen: "whole screens, and the frames around them",
|
|
196
234
|
provider: "providers, contexts and wrappers",
|
|
197
235
|
adapter: "adapters for another library's components",
|
|
236
|
+
feature: "pieces of one screen, not of the system",
|
|
198
237
|
config: "constants, config and helpers",
|
|
199
238
|
"no-jsx": "capitalised exports with no markup",
|
|
200
239
|
"no-styling": "components that decide nothing about how anything looks",
|
|
@@ -204,6 +243,7 @@ export function groupSkips(skips) {
|
|
|
204
243
|
"screen",
|
|
205
244
|
"route",
|
|
206
245
|
"provider",
|
|
246
|
+
"feature",
|
|
207
247
|
"adapter",
|
|
208
248
|
"no-styling",
|
|
209
249
|
"config",
|
package/dist/doctor/crosswalk.js
CHANGED
|
@@ -155,17 +155,90 @@ const AXIS_SYNONYM = {
|
|
|
155
155
|
placement: "side",
|
|
156
156
|
position: "side",
|
|
157
157
|
};
|
|
158
|
+
/**
|
|
159
|
+
* THE LIVE TABLE, when a run fetched one.
|
|
160
|
+
*
|
|
161
|
+
* Module-level and set once per run, so every call site gets the same answer without
|
|
162
|
+
* threading the table through nine functions that do not care about it. Absent means the
|
|
163
|
+
* written `SYNONYM` floor answers - which is a smaller answer, and the run says so.
|
|
164
|
+
*
|
|
165
|
+
* The written table had 26 names against a catalogue of 41: twenty of our own components
|
|
166
|
+
* could not be matched at all (dono, 01/08). It stays as the offline floor, not as truth.
|
|
167
|
+
*/
|
|
168
|
+
let live = null;
|
|
169
|
+
export function useLiveCatalogue(table) {
|
|
170
|
+
live = table;
|
|
171
|
+
}
|
|
172
|
+
/** How many names the written floor knows - the number a fallback message needs. */
|
|
173
|
+
export function floorSize() {
|
|
174
|
+
return new Set(Object.values(SYNONYM)).size;
|
|
175
|
+
}
|
|
158
176
|
export function canonicalName(name) {
|
|
159
177
|
const head = name
|
|
160
178
|
.split(".")[0]
|
|
161
179
|
.toLowerCase()
|
|
162
180
|
.replace(/[^a-z]/g, "");
|
|
181
|
+
/**
|
|
182
|
+
* The live catalogue first, and its own name counts as a synonym for itself: their
|
|
183
|
+
* `Textarea` matching our `textarea` needed nothing clever, only a table that knew
|
|
184
|
+
* `textarea` exists.
|
|
185
|
+
*/
|
|
186
|
+
if (live) {
|
|
187
|
+
for (const [canonical, also] of Object.entries(live.synonyms)) {
|
|
188
|
+
const words = [canonical, ...also].map((w) => w.replace(/[^a-z]/g, ""));
|
|
189
|
+
if (words.includes(head))
|
|
190
|
+
return canonical;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
163
193
|
if (SYNONYM[head])
|
|
164
194
|
return SYNONYM[head];
|
|
165
195
|
// A compound like `WidgetCard` or `ArrowDownIcon` is only canonical when the
|
|
166
196
|
// WHOLE word is - a widget card is not a card, it is their own thing.
|
|
167
197
|
return null;
|
|
168
198
|
}
|
|
199
|
+
/** The head of a name, the way `canonicalName` reads it. */
|
|
200
|
+
function headOf(name) {
|
|
201
|
+
return name
|
|
202
|
+
.split(".")[0]
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z]/g, "");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The canonical, kept only when the match is an identity or the axes back it up.
|
|
208
|
+
* See the note at the call site - this is the guard, not the policy.
|
|
209
|
+
*/
|
|
210
|
+
export function evidencedCanonical(component) {
|
|
211
|
+
const canonical = canonicalName(component.name);
|
|
212
|
+
if (!canonical)
|
|
213
|
+
return null;
|
|
214
|
+
// The name IS ours, spelled their way. `Text → text`, `Card → card`.
|
|
215
|
+
if (headOf(component.name) === canonical.replace(/-/g, ""))
|
|
216
|
+
return canonical;
|
|
217
|
+
// A synonym hop. Without live axes there is no evidence to weigh - old behaviour.
|
|
218
|
+
const target = live?.axes[canonical];
|
|
219
|
+
if (!target || Object.keys(target).length === 0)
|
|
220
|
+
return canonical;
|
|
221
|
+
// Their side has to DECLARE something for the disagreement to mean anything: a
|
|
222
|
+
// component with no axes offers no witness either way, and the name stands.
|
|
223
|
+
const theirOptions = new Set();
|
|
224
|
+
for (const values of Object.values(component.props)) {
|
|
225
|
+
for (const v of canonicalValues(values))
|
|
226
|
+
theirOptions.add(v);
|
|
227
|
+
}
|
|
228
|
+
if (theirOptions.size < 2)
|
|
229
|
+
return canonical;
|
|
230
|
+
const ourOptions = new Set();
|
|
231
|
+
for (const values of Object.values(target)) {
|
|
232
|
+
for (const v of canonicalValues(values))
|
|
233
|
+
ourOptions.add(v);
|
|
234
|
+
}
|
|
235
|
+
if (ourOptions.size === 0)
|
|
236
|
+
return canonical;
|
|
237
|
+
for (const v of theirOptions)
|
|
238
|
+
if (ourOptions.has(v))
|
|
239
|
+
return canonical;
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
169
242
|
export function canonicalAxes(props) {
|
|
170
243
|
const out = new Set();
|
|
171
244
|
for (const p of Object.keys(props)) {
|
|
@@ -368,7 +441,26 @@ opts) {
|
|
|
368
441
|
: "renders no UI - infrastructure, not a component",
|
|
369
442
|
};
|
|
370
443
|
}
|
|
371
|
-
|
|
444
|
+
/**
|
|
445
|
+
* A SYNONYM HOP NEEDS EVIDENCE; an identity match does not.
|
|
446
|
+
*
|
|
447
|
+
* `label → badge` was decided by the written table on the NAME alone, and their
|
|
448
|
+
* Label is a typography ladder - `variant: display|h1|h2|h3|body-sm|body-md|
|
|
449
|
+
* caption` - with nothing of a badge about it (dono, 01/08, "erros de match").
|
|
450
|
+
* Their `Text → text` is a different kind of claim: the name IS ours, spelled
|
|
451
|
+
* their way, and needs no second witness.
|
|
452
|
+
*
|
|
453
|
+
* So: a canonical reached through a synonym only stands when the axes agree -
|
|
454
|
+
* some shared option value (canonicalised, so `destructive` meets `danger`) with
|
|
455
|
+
* what the LIVE catalogue says that component offers. Their Tag's
|
|
456
|
+
* `color(danger|success|warning|neutral…)` overlaps our badge's intent options
|
|
457
|
+
* and survives; Label's heading ladder overlaps nothing and falls back to being
|
|
458
|
+
* exclusively theirs, which is what it was.
|
|
459
|
+
*
|
|
460
|
+
* Offline the live axes are absent and the old behaviour stands - the fallback
|
|
461
|
+
* message already says the answer is smaller.
|
|
462
|
+
*/
|
|
463
|
+
const canonical = evidencedCanonical(component);
|
|
372
464
|
const twins = mine
|
|
373
465
|
.filter((o) => o.name !== component.name)
|
|
374
466
|
.map((o) => ({
|
|
@@ -379,7 +471,10 @@ opts) {
|
|
|
379
471
|
sharedCount(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= TWIN_MIN_SHARED &&
|
|
380
472
|
sharedMeaning(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= 1)
|
|
381
473
|
.sort((a, b) => b.overlap - a.overlap);
|
|
382
|
-
|
|
474
|
+
// The LIVE catalogue counts as knowing the name: gating on the written table
|
|
475
|
+
// meant a component matched by the live one fell through to `nearly` (the same
|
|
476
|
+
// staleness, one branch down).
|
|
477
|
+
if (canonical && (CATALOGUE[canonical] || live?.axes[canonical])) {
|
|
383
478
|
const alsoClaiming = (claims.get(canonical) ?? []).filter((n) => n !== component.name);
|
|
384
479
|
/**
|
|
385
480
|
* LIBRARY MODE: the match is a NOTE, not a verdict.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT IS COUNTABLE, COUNTED - so the reader decides instead of searching.
|
|
3
|
+
*
|
|
4
|
+
* The skill's theme section said "go and look" and listed five places to grep. Watching a
|
|
5
|
+
* real run, the agent swept for `framer-motion`, `lucide-react` and `@base-ui` too - things
|
|
6
|
+
* the skill never asked for - because "go and look" is an open invitation (dono, 01/08).
|
|
7
|
+
*
|
|
8
|
+
* Searching is expensive for three reasons that compound: every grep is a tool call, its
|
|
9
|
+
* whole output lands in the context, and the output is mostly noise - a `grep -rn "dark:"`
|
|
10
|
+
* over 2797 files returns hundreds of lines to answer a question that is one number.
|
|
11
|
+
*
|
|
12
|
+
* And it is worse than expensive: it is NOT DETERMINISTIC. Two readers sweeping the same
|
|
13
|
+
* repo bring back different slices, so the answer changes between runs.
|
|
14
|
+
*
|
|
15
|
+
* THE PRACTICE, and it holds for any system: give a reader the EVIDENCE and ask it to
|
|
16
|
+
* decide; never give it a question and ask it to search. Everything here is one more pass
|
|
17
|
+
* over files the census already walks - free on this side, and it deletes a handful of tool
|
|
18
|
+
* calls from the other.
|
|
19
|
+
*
|
|
20
|
+
* What is left for a person to judge after this is short and named: which grey is the page,
|
|
21
|
+
* what the product IS, and the anatomy of each component. Those are readings, not counts.
|
|
22
|
+
*/
|
|
23
|
+
/** A dark utility in a class name: `dark:bg-x`, and the `dark:` inside a longer chain. */
|
|
24
|
+
const DARK_UTILITY = /(?:^|["'\s:])dark:/g;
|
|
25
|
+
/** How a project says which theme is on. Ordered: the first hit is the one it uses. */
|
|
26
|
+
const THEME_LIBRARY = [
|
|
27
|
+
[/from\s+["']next-themes["']/, "next-themes"],
|
|
28
|
+
[/from\s+["']@radix-ui\/themes["']/, "@radix-ui/themes"],
|
|
29
|
+
[/from\s+["']styled-components["'][\s\S]*ThemeProvider/, "styled-components"],
|
|
30
|
+
[/\buseColorScheme\b/, "react-native / useColorScheme"],
|
|
31
|
+
[/\bThemeProvider\b/, "a ThemeProvider of their own"],
|
|
32
|
+
];
|
|
33
|
+
/** `attribute="data-theme"` on the provider - the selector every dark rule then needs. */
|
|
34
|
+
const THEME_ATTRIBUTE = /attribute\s*=\s*\{?["']([^"']+)["']/;
|
|
35
|
+
/** `defaultTheme="dark"` - which face it opens in, said by the provider itself. */
|
|
36
|
+
const DEFAULT_THEME = /defaultTheme\s*=\s*\{?["'](light|dark|system)["']/;
|
|
37
|
+
/** `enableSystem={false}` - whether the OS gets a vote. */
|
|
38
|
+
const ENABLE_SYSTEM = /enableSystem\s*=\s*\{(true|false)\}/;
|
|
39
|
+
/** What `<html>` carries by default, which is what a dark rule matches against. */
|
|
40
|
+
const HTML_ATTRS = /<html\b([^>]*)>/;
|
|
41
|
+
const PACKAGE_FAMILIES = [
|
|
42
|
+
{
|
|
43
|
+
family: "motion",
|
|
44
|
+
test: /^(framer-motion|motion|@react-spring\/.+|gsap|popmotion|react-transition-group)$/,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
family: "icons",
|
|
48
|
+
test: /^(lucide-react|react-icons|@heroicons\/react|@tabler\/icons-react|@phosphor-icons\/react|react-feather)$/,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
family: "primitives",
|
|
52
|
+
test: /^(@radix-ui\/.+|@base-ui\/.+|@base-ui-components\/.+|@headlessui\/react|@ariakit\/react|@reach\/.+)$/,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
family: "styling",
|
|
56
|
+
test: /^(class-variance-authority|tailwind-variants|clsx|tailwind-merge|styled-components|@emotion\/.+|@chakra-ui\/.+|@mui\/.+|@pandacss\/dev)$/,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
family: "charts",
|
|
60
|
+
test: /^(recharts|victory|@nivo\/.+|chart\.js|react-chartjs-2|apexcharts|d3)$/,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
family: "editors",
|
|
64
|
+
test: /^(@tiptap\/.+|slate|quill|react-quill|@lexical\/.+|lexical)$/,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
export function emptySignals() {
|
|
68
|
+
return {
|
|
69
|
+
theme: {
|
|
70
|
+
darkUtilities: 0,
|
|
71
|
+
darkFiles: 0,
|
|
72
|
+
library: null,
|
|
73
|
+
attribute: null,
|
|
74
|
+
defaultTheme: null,
|
|
75
|
+
enableSystem: null,
|
|
76
|
+
prefersColorScheme: 0,
|
|
77
|
+
htmlCarries: [],
|
|
78
|
+
},
|
|
79
|
+
libraries: [],
|
|
80
|
+
conflicts: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const ANY_IMPORT = /(?:from\s+["']([^"'.][^"']*)["']|require\(\s*["']([^"'.][^"']*)["']\s*\))/g;
|
|
84
|
+
/** The package a specifier belongs to: `@tiptap/react` and `motion/react` both keep the
|
|
85
|
+
* part a manifest would name. */
|
|
86
|
+
export function packageOf(specifier) {
|
|
87
|
+
if (specifier.startsWith("@")) {
|
|
88
|
+
const parts = specifier.split("/");
|
|
89
|
+
return parts.slice(0, 2).join("/");
|
|
90
|
+
}
|
|
91
|
+
return specifier.split("/")[0];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* THE LIBRARY a package belongs to, which is not the same as the package.
|
|
95
|
+
*
|
|
96
|
+
* `@tiptap/react`, `@tiptap/starter-kit` and nineteen extensions are ONE library, and
|
|
97
|
+
* reporting them as twenty-one entries and a conflict is noise pretending to be a finding
|
|
98
|
+
* (probe, 01/08). A scope is a library; an unscoped name is its own.
|
|
99
|
+
*/
|
|
100
|
+
export function libraryOf(specifier) {
|
|
101
|
+
const pkg = packageOf(specifier);
|
|
102
|
+
return pkg.startsWith("@") ? `${pkg.split("/")[0]}/*` : pkg;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fold one file into the signals. Called from the walk the census already does, so nothing
|
|
106
|
+
* here costs a second pass over the disk.
|
|
107
|
+
*/
|
|
108
|
+
export function readSignalsInto(into, imports, file, source) {
|
|
109
|
+
const isCode = /\.(tsx|jsx|ts|js|mjs|cjs|vue|svelte)$/i.test(file);
|
|
110
|
+
const isStyle = /\.(css|scss|sass|less)$/i.test(file);
|
|
111
|
+
if (isStyle) {
|
|
112
|
+
into.theme.prefersColorScheme += (source.match(/prefers-color-scheme/g) ?? []).length;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!isCode)
|
|
116
|
+
return;
|
|
117
|
+
DARK_UTILITY.lastIndex = 0;
|
|
118
|
+
const dark = (source.match(DARK_UTILITY) ?? []).length;
|
|
119
|
+
if (dark > 0) {
|
|
120
|
+
into.theme.darkUtilities += dark;
|
|
121
|
+
into.theme.darkFiles += 1;
|
|
122
|
+
}
|
|
123
|
+
into.theme.prefersColorScheme += (source.match(/prefers-color-scheme/g) ?? []).length;
|
|
124
|
+
// The first library that matches wins, and the order is deliberate: an explicit
|
|
125
|
+
// dependency beats a `ThemeProvider` that could be anybody's.
|
|
126
|
+
if (!into.theme.library) {
|
|
127
|
+
for (const [test, name] of THEME_LIBRARY) {
|
|
128
|
+
if (test.test(source)) {
|
|
129
|
+
into.theme.library = name;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
into.theme.attribute ??= THEME_ATTRIBUTE.exec(source)?.[1] ?? null;
|
|
135
|
+
into.theme.defaultTheme ??= DEFAULT_THEME.exec(source)?.[1] ?? null;
|
|
136
|
+
if (into.theme.enableSystem === null) {
|
|
137
|
+
const enable = ENABLE_SYSTEM.exec(source);
|
|
138
|
+
if (enable)
|
|
139
|
+
into.theme.enableSystem = enable[1] === "true";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* WHAT `<html>` CARRIES. Every dark rule in the project is written against this
|
|
143
|
+
* ancestor, so reading it is what turns "they have a dark theme" into "their dark rules
|
|
144
|
+
* match `[data-theme=dark]`, which is what ours must match too".
|
|
145
|
+
*/
|
|
146
|
+
const html = HTML_ATTRS.exec(source);
|
|
147
|
+
if (html) {
|
|
148
|
+
for (const m of html[1].matchAll(/([a-zA-Z-]+)\s*=\s*(?:["']([^"']*)["']|\{["']([^"']*)["']\})/g)) {
|
|
149
|
+
const attr = `${m[1]}="${m[2] ?? m[3] ?? ""}"`;
|
|
150
|
+
if (!into.theme.htmlCarries.includes(attr)) {
|
|
151
|
+
into.theme.htmlCarries.push(attr);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
ANY_IMPORT.lastIndex = 0;
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const m of source.matchAll(ANY_IMPORT)) {
|
|
158
|
+
const spec = m[1] ?? m[2];
|
|
159
|
+
if (!spec)
|
|
160
|
+
continue;
|
|
161
|
+
const pkg = packageOf(spec);
|
|
162
|
+
// `motion/react` and `motion` are one package, and a file importing both is one file.
|
|
163
|
+
const key = /^motion(\/|$)/.test(spec) ? spec : pkg;
|
|
164
|
+
if (seen.has(key))
|
|
165
|
+
continue;
|
|
166
|
+
seen.add(key);
|
|
167
|
+
imports.set(key, (imports.get(key) ?? 0) + 1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The libraries, and the conflicts, from the import tally and their manifest.
|
|
172
|
+
*
|
|
173
|
+
* `versions` is `dependencies` merged with `devDependencies`, so the rule can cite the
|
|
174
|
+
* range they pinned rather than going stale the day they upgrade.
|
|
175
|
+
*/
|
|
176
|
+
export function finishSignals(into, imports, versions) {
|
|
177
|
+
const byFamily = new Map();
|
|
178
|
+
for (const [name, files] of imports) {
|
|
179
|
+
const pkg = packageOf(name);
|
|
180
|
+
const family = PACKAGE_FAMILIES.find((f) => f.test.test(pkg) || f.test.test(name));
|
|
181
|
+
if (!family)
|
|
182
|
+
continue;
|
|
183
|
+
const version = versions[pkg] ?? versions[name];
|
|
184
|
+
const signal = {
|
|
185
|
+
family: family.family,
|
|
186
|
+
name,
|
|
187
|
+
files,
|
|
188
|
+
...(version ? { version } : {}),
|
|
189
|
+
};
|
|
190
|
+
byFamily.set(family.family, [
|
|
191
|
+
...(byFamily.get(family.family) ?? []),
|
|
192
|
+
signal,
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* COLLAPSED BY LIBRARY. Twenty-one `@tiptap/*` entries answer nothing a person wanted to
|
|
197
|
+
* know; `@tiptap/* (21 packages, 4 files)` answers it in one line. The file count is the
|
|
198
|
+
* WIDEST of the group rather than the sum: four files import tiptap, not fifty-two.
|
|
199
|
+
*/
|
|
200
|
+
into.libraries = [...byFamily.entries()]
|
|
201
|
+
.flatMap(([family, signals]) => {
|
|
202
|
+
const byLibrary = new Map();
|
|
203
|
+
for (const signal of signals) {
|
|
204
|
+
const key = libraryOf(signal.name);
|
|
205
|
+
byLibrary.set(key, [...(byLibrary.get(key) ?? []), signal]);
|
|
206
|
+
}
|
|
207
|
+
return [...byLibrary.entries()].map(([library, group]) => ({
|
|
208
|
+
family,
|
|
209
|
+
name: group.length > 1 ? library : group[0].name,
|
|
210
|
+
files: Math.max(...group.map((g) => g.files)),
|
|
211
|
+
...(group.length > 1 ? { packages: group.length } : {}),
|
|
212
|
+
...(group[0].version ? { version: group[0].version } : {}),
|
|
213
|
+
}));
|
|
214
|
+
})
|
|
215
|
+
.sort((a, b) => b.files - a.files || a.name.localeCompare(b.name));
|
|
216
|
+
/**
|
|
217
|
+
* TWO IN ONE FAMILY IS A FINDING. `framer-motion` in four files and `motion/react` in
|
|
218
|
+
* three, in one package, is the same library under two names and one of them is dead
|
|
219
|
+
* weight in the bundle. A person should see it; we do not decide it.
|
|
220
|
+
*
|
|
221
|
+
* `styling` and `primitives` are exempt: `clsx` beside `tailwind-merge` is the normal
|
|
222
|
+
* pairing, and two primitive libraries in a large app is a migration in progress, not a
|
|
223
|
+
* mistake.
|
|
224
|
+
*/
|
|
225
|
+
for (const [family, signals] of byFamily) {
|
|
226
|
+
if (family === "styling" || family === "primitives")
|
|
227
|
+
continue;
|
|
228
|
+
// By LIBRARY, not by package: `@tiptap/react` beside `@tiptap/starter-kit` is one
|
|
229
|
+
// library used correctly, and calling it a conflict was noise (probe, 01/08).
|
|
230
|
+
const names = [...new Set(signals.map((x) => libraryOf(x.name)))];
|
|
231
|
+
if (names.length > 1)
|
|
232
|
+
into.conflicts.push({ family, names });
|
|
233
|
+
}
|
|
234
|
+
return into;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The signals as the sentences the reader would otherwise have gone looking for.
|
|
238
|
+
*
|
|
239
|
+
* Written as EVIDENCE rather than as a verdict: "189 dark utilities across 30 files, bound
|
|
240
|
+
* to `[data-theme=dark]`, opening dark with the OS overruled" is something a person can
|
|
241
|
+
* confirm or correct. "This is a dark product" is a claim they have to take on faith.
|
|
242
|
+
*/
|
|
243
|
+
export function describeSignals(s) {
|
|
244
|
+
const out = [];
|
|
245
|
+
const t = s.theme;
|
|
246
|
+
if (t.darkUtilities > 0) {
|
|
247
|
+
out.push(`${t.darkUtilities} dark utilities across ${t.darkFiles} file${t.darkFiles === 1 ? "" : "s"} - so a second scheme is real here, not aspirational.`);
|
|
248
|
+
}
|
|
249
|
+
else if (t.prefersColorScheme > 0) {
|
|
250
|
+
out.push(`no dark utilities, but ${t.prefersColorScheme} \`prefers-color-scheme\` rule${t.prefersColorScheme === 1 ? "" : "s"} - the second scheme lives in CSS rather than in class names.`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
out.push("no dark utilities and no `prefers-color-scheme` - one scheme, and adding a second is a decision rather than a discovery.");
|
|
254
|
+
}
|
|
255
|
+
if (t.library) {
|
|
256
|
+
const bits = [`switched by ${t.library}`];
|
|
257
|
+
if (t.attribute)
|
|
258
|
+
bits.push(`bound to \`${t.attribute}\``);
|
|
259
|
+
if (t.defaultTheme)
|
|
260
|
+
bits.push(`opening ${t.defaultTheme}`);
|
|
261
|
+
if (t.enableSystem === false)
|
|
262
|
+
bits.push("with the OS overruled");
|
|
263
|
+
if (t.enableSystem === true)
|
|
264
|
+
bits.push("with the OS allowed a vote");
|
|
265
|
+
out.push(`${bits.join(", ")}.`);
|
|
266
|
+
}
|
|
267
|
+
if (t.htmlCarries.length > 0) {
|
|
268
|
+
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.`);
|
|
269
|
+
}
|
|
270
|
+
for (const family of ["primitives", "motion", "icons", "charts", "editors"]) {
|
|
271
|
+
const found = s.libraries.filter((l) => l.family === family);
|
|
272
|
+
if (found.length === 0)
|
|
273
|
+
continue;
|
|
274
|
+
out.push(`${family}: ${found.map((l) => `${l.name}${l.version ? ` ${l.version}` : ""} (${l.packages ? `${l.packages} packages, ` : ""}${l.files} file${l.files === 1 ? "" : "s"})`).join(", ")}`);
|
|
275
|
+
}
|
|
276
|
+
for (const c of s.conflicts) {
|
|
277
|
+
// The count is read off the list rather than written into the sentence: it said "TWO"
|
|
278
|
+
// over three names, which is the kind of small lie that costs the next sentence its
|
|
279
|
+
// credibility (probe, 01/08).
|
|
280
|
+
const list = c.names.length === 2
|
|
281
|
+
? c.names.join(" and ")
|
|
282
|
+
: `${c.names.slice(0, -1).join(", ")} and ${c.names[c.names.length - 1]}`;
|
|
283
|
+
out.push(`${c.names.length} ${c.family} LIBRARIES: ${list}. The same job under more than one name, and the extras are weight in the bundle for nothing. Worth your attention; not ours to decide.`);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
package/dist/skill-import.js
CHANGED
|
@@ -237,13 +237,19 @@ This is the field that matters most and the one only you can fill. The ladder ca
|
|
|
237
237
|
places no token reader sees. A real library declared nine near-blacks and five light greys in
|
|
238
238
|
one \`:root\` block, and the import handed its near-black dashboard a white page.
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
**DO NOT GO LOOKING. It is already counted**, under "What was countable, counted" in the
|
|
241
|
+
census output:
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
\`\`\`
|
|
244
|
+
782 dark utilities across 79 files - so a second scheme is real here, not aspirational.
|
|
245
|
+
switched by a ThemeProvider of their own, bound to data-theme, opening dark, with the
|
|
246
|
+
OS overruled.
|
|
247
|
+
html carries lang="en" data-theme="dark" - which is the ancestor every dark rule of
|
|
248
|
+
theirs matches, and therefore the one ours must match.
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
That is a real run. Your job is to CONFIRM OR CORRECT it, not to reproduce it - and a sweep
|
|
252
|
+
would bring back a different slice than the next one, which is the worse property of the two.
|
|
247
253
|
|
|
248
254
|
\`has: ["dark"]\` on a dark-only product is the right answer. It is better than filling a light
|
|
249
255
|
slot on their behalf, and it makes adding light a deliberate act they take later.
|
|
@@ -537,6 +543,49 @@ extensions are configured at construction, not toggled later
|
|
|
537
543
|
of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
|
|
538
544
|
day they upgrade and the information is not lost either.
|
|
539
545
|
|
|
546
|
+
### Ask what a recipe can hold, BEFORE you write one
|
|
547
|
+
|
|
548
|
+
\`\`\`
|
|
549
|
+
recipe_vocabulary what a recipe CAN hold - states, forms, the floor per kind
|
|
550
|
+
validate_recipe(name, r) would this survive? what would NOT, by name
|
|
551
|
+
\`\`\`
|
|
552
|
+
|
|
553
|
+
Both are MCP tools on the \`synthesisui\` server. **Call \`recipe_vocabulary\` once, at the
|
|
554
|
+
start**, and work from what it says rather than from what you remember - it is served from
|
|
555
|
+
the live contract, so it is right about a state that was added last week and this file is
|
|
556
|
+
not.
|
|
557
|
+
|
|
558
|
+
Then **validate every recipe before you send it.** This is not a formality. Nothing checked
|
|
559
|
+
a recipe before these tools existed, and four separate readings were lost in silence: a
|
|
560
|
+
\`dark:\` inside a variant, a state the compiler cannot spell, a part name with a dot in it,
|
|
561
|
+
a layer with an empty style. Each of those was correct work that arrived as nothing.
|
|
562
|
+
|
|
563
|
+
\`validate_recipe\` never answers yes or no. It answers with what would not survive:
|
|
564
|
+
|
|
565
|
+
\`\`\`
|
|
566
|
+
REFUSED by the contract fix it or the WHOLE recipe is dropped
|
|
567
|
+
HELD BY THE SCHEMA AND the silent half - it validates, it compiles to nothing.
|
|
568
|
+
DROPPED BY THE COMPILER This is the one worth fixing before sending.
|
|
569
|
+
BELOW THE FLOOR it would arrive and not be drawable
|
|
570
|
+
\`\`\`
|
|
571
|
+
|
|
572
|
+
### Write down what we could not hold
|
|
573
|
+
|
|
574
|
+
Everything \`validate_recipe\` reports goes into \`_synthesisui/not-expressed.md\`, appended
|
|
575
|
+
per component:
|
|
576
|
+
|
|
577
|
+
\`\`\`
|
|
578
|
+
## MetricCard
|
|
579
|
+
- layers[2].when.scheme … (what the validator said, VERBATIM)
|
|
580
|
+
- below the floor: a fill, so it reads as a surface rather than as bare text
|
|
581
|
+
- the source you read, quoted, so the gap can be checked without opening their repo
|
|
582
|
+
\`\`\`
|
|
583
|
+
|
|
584
|
+
**Verbatim, and with the source beside it.** This file is how a gap on OUR side gets fixed
|
|
585
|
+
instead of being worked around: said only in chat it evaporates, and paraphrased it becomes
|
|
586
|
+
unfindable. If the file ends up empty, say so - that is the good outcome and it is worth
|
|
587
|
+
one line.
|
|
588
|
+
|
|
540
589
|
### The floor: what a preview needs before it can draw anything
|
|
541
590
|
|
|
542
591
|
The owner asked this directly - "to render the button, what do I need to have?" - and the
|
|
@@ -591,6 +640,16 @@ what shows when it is missing a missing asset must never look broken
|
|
|
591
640
|
|
|
592
641
|
### What the CLI now reads for you, so do not spend a turn on it
|
|
593
642
|
|
|
643
|
+
**The rule under all of this: you are given EVIDENCE and asked to DECIDE.** You are never
|
|
644
|
+
given a question and asked to search. A grep is a tool call whose entire output lands in your
|
|
645
|
+
context to answer something that is one number, and two readers sweeping the same repo bring
|
|
646
|
+
back different slices - so the answer changes between runs, which is the part that actually
|
|
647
|
+
costs.
|
|
648
|
+
|
|
649
|
+
Measured on a real run: the census counts the theme signals, the libraries and their
|
|
650
|
+
versions, and the conflicts. The reader that swept for them anyway spent a dozen shell calls
|
|
651
|
+
to arrive at a subset of what was already on screen.
|
|
652
|
+
|
|
594
653
|
Four things are read deterministically from the source. Reporting them back as if you
|
|
595
654
|
found them wastes a turn and risks contradicting the measurement:
|
|
596
655
|
|
|
@@ -599,6 +658,13 @@ found them wastes a turn and risks contradicting the measurement:
|
|
|
599
658
|
- **whether a component forwards the rest of its props**, and which it names
|
|
600
659
|
- **which components appear inside which** at their call sites, as pairs
|
|
601
660
|
- **the folder architecture**, and it will ask you which one has priority
|
|
661
|
+
- **the theme**: how many dark utilities, in how many files, which library switches it, which
|
|
662
|
+
attribute it is bound to, what \`<html>\` carries, and how many \`prefers-color-scheme\` rules
|
|
663
|
+
- **the libraries and their versions** - motion, icons, primitives, charts, editors - with the
|
|
664
|
+
file count for each, and a CONFLICT when one job is done by more than one of them
|
|
665
|
+
- **which of your components are ours**, matched against the LIVE catalogue rather than a
|
|
666
|
+
list baked into the CLI. If a run says it fell back to the built-in names, the match is a
|
|
667
|
+
smaller answer than it should be and the reason is on screen
|
|
602
668
|
|
|
603
669
|
What is still yours: the anatomy tree, the part names, the frontiers, the roles, the
|
|
604
670
|
concept, and the rules that are not about a class name.
|
package/package.json
CHANGED