synthesisui 0.16.103 → 0.16.105
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 +15 -0
- package/dist/commands/mcp.js +8 -0
- package/dist/doctor/signals.js +59 -3
- package/dist/doctor/transcribe.js +125 -78
- package/dist/skill-import.js +7 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -636,6 +636,21 @@ export async function takeCensus(root, opts) {
|
|
|
636
636
|
}
|
|
637
637
|
usageSeen.add(rel);
|
|
638
638
|
usageProgress?.step(rel);
|
|
639
|
+
/**
|
|
640
|
+
* THE THEME IS SWITCHED WHERE THE APP IS, and this loop was walking right
|
|
641
|
+
* past the file that says so. Measured on a real monorepo (dono, 02/08):
|
|
642
|
+
* `packages/ui` declares 346 dark utilities across 58 files and NONE of the
|
|
643
|
+
* four switch fields; `apps/web-dashboard` declares `data-theme`,
|
|
644
|
+
* `defaultTheme="dark"`, `enableSystem={false}` and an `<html>` carrying both
|
|
645
|
+
* - so "which face does it open in" was unanswerable from the census and cost
|
|
646
|
+
* a hand read of a file this loop already had open.
|
|
647
|
+
*
|
|
648
|
+
* The scope is read first and wins on every field, so this only fills what a
|
|
649
|
+
* library legitimately does not have - and the label is recorded, because the
|
|
650
|
+
* system being measured here and the theme being switched there are two facts,
|
|
651
|
+
* not one average.
|
|
652
|
+
*/
|
|
653
|
+
readSignalsInto(signals, importTally, rel, src, u.label);
|
|
639
654
|
scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
|
|
640
655
|
// WHERE THE PAIRS LIVE. A library composes almost none of itself inside
|
|
641
656
|
// itself - `Button` goes inside `Modal` in the APP, not in the library
|
package/dist/commands/mcp.js
CHANGED
|
@@ -467,6 +467,14 @@ async function resolveAtNodes(root, entries) {
|
|
|
467
467
|
* this sends the evidence rather than a second implementation of it.
|
|
468
468
|
*/
|
|
469
469
|
...(look?.rootTag ? { rootTag: look.rootTag } : {}),
|
|
470
|
+
/**
|
|
471
|
+
* HOW LONG THE SKETCH IS, so the far side can tell an `at` that names a
|
|
472
|
+
* node from an `at` that names nothing. A reading points at the sketch by
|
|
473
|
+
* INDEX - that is what keeps a class string from being retyped - and an
|
|
474
|
+
* index past the end resolves to no classes at all, which is the silent
|
|
475
|
+
* version of sending none (dono, 02/08).
|
|
476
|
+
*/
|
|
477
|
+
...(sketch ? { sketchNodes: sketch.length } : {}),
|
|
470
478
|
}
|
|
471
479
|
: recipe0;
|
|
472
480
|
entry = { ...entry, recipe: withLook };
|
package/dist/doctor/signals.js
CHANGED
|
@@ -75,6 +75,7 @@ export function emptySignals() {
|
|
|
75
75
|
enableSystem: null,
|
|
76
76
|
prefersColorScheme: 0,
|
|
77
77
|
htmlCarries: [],
|
|
78
|
+
switchedIn: null,
|
|
78
79
|
page: [],
|
|
79
80
|
},
|
|
80
81
|
libraries: [],
|
|
@@ -113,7 +114,13 @@ export function libraryOf(specifier) {
|
|
|
113
114
|
* Fold one file into the signals. Called from the walk the census already does, so nothing
|
|
114
115
|
* here costs a second pass over the disk.
|
|
115
116
|
*/
|
|
116
|
-
export function readSignalsInto(into, imports, file, source
|
|
117
|
+
export function readSignalsInto(into, imports, file, source,
|
|
118
|
+
/**
|
|
119
|
+
* WHERE THIS FILE IS, when it is not the system's own folder - the `--usage`
|
|
120
|
+
* label. Present means "read for evidence", and the only thing it changes is
|
|
121
|
+
* that a switch found here is RECORDED AS FOUND HERE. See `switchedIn`.
|
|
122
|
+
*/
|
|
123
|
+
from) {
|
|
117
124
|
const isCode = /\.(tsx|jsx|ts|js|mjs|cjs|vue|svelte)$/i.test(file);
|
|
118
125
|
const isStyle = /\.(css|scss|sass|less)$/i.test(file);
|
|
119
126
|
if (isStyle) {
|
|
@@ -146,6 +153,16 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* THE SCOPE WINS, ALWAYS - `??=` is the whole rule. The system's own folder is
|
|
158
|
+
* read first, so a switch it declares is never overwritten by an app's; an app
|
|
159
|
+
* only fills what was null, which is the four fields a library legitimately does
|
|
160
|
+
* not have.
|
|
161
|
+
*/
|
|
162
|
+
const before = into.theme.attribute === null &&
|
|
163
|
+
into.theme.defaultTheme === null &&
|
|
164
|
+
into.theme.enableSystem === null &&
|
|
165
|
+
into.theme.htmlCarries.length === 0;
|
|
149
166
|
into.theme.attribute ??= THEME_ATTRIBUTE.exec(source)?.[1] ?? null;
|
|
150
167
|
into.theme.defaultTheme ??= DEFAULT_THEME.exec(source)?.[1] ?? null;
|
|
151
168
|
if (into.theme.enableSystem === null) {
|
|
@@ -160,12 +177,42 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
160
177
|
*/
|
|
161
178
|
const html = HTML_ATTRS.exec(source);
|
|
162
179
|
if (html) {
|
|
163
|
-
|
|
164
|
-
const attr = `${m[1]}="${m[2] ?? m[3] ?? ""}"`;
|
|
180
|
+
const carry = (attr) => {
|
|
165
181
|
if (!into.theme.htmlCarries.includes(attr)) {
|
|
166
182
|
into.theme.htmlCarries.push(attr);
|
|
167
183
|
}
|
|
184
|
+
};
|
|
185
|
+
for (const m of html[1].matchAll(/([a-zA-Z-]+)\s*=\s*(?:["']([^"']*)["']|\{["']([^"']*)["']\})/g)) {
|
|
186
|
+
carry(`${m[1]}="${m[2] ?? m[3] ?? ""}"`);
|
|
168
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* `style={{ colorScheme: "dark" }}` - the SECOND thing that decides what a dark
|
|
190
|
+
* rule matches, and the attribute pass above cannot see it: its value is not a
|
|
191
|
+
* quoted string sitting after an `=`, it is a property inside an object.
|
|
192
|
+
*
|
|
193
|
+
* It is on the owner's own root layout, right beside the `data-theme` the pass
|
|
194
|
+
* DID read, and it is what tells the browser which face to paint its own
|
|
195
|
+
* widgets - a scrollbar and a form control follow `color-scheme`, not a class
|
|
196
|
+
* (dono, 02/08).
|
|
197
|
+
*/
|
|
198
|
+
const scheme = /\bcolorScheme\s*:\s*["']([^"']+)["']/.exec(html[1]);
|
|
199
|
+
if (scheme)
|
|
200
|
+
carry(`colorScheme="${scheme[1]}"`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* WHOSE FILE ANSWERED IT. Stamped only when the switch was null before this file
|
|
204
|
+
* and is not null after, so the label names the file's ROOT that actually
|
|
205
|
+
* supplied it - and only for a `--usage` root, because the scope needs no
|
|
206
|
+
* attribution: it is the system by definition.
|
|
207
|
+
*/
|
|
208
|
+
if (from &&
|
|
209
|
+
into.theme.switchedIn === null &&
|
|
210
|
+
before &&
|
|
211
|
+
(into.theme.attribute !== null ||
|
|
212
|
+
into.theme.defaultTheme !== null ||
|
|
213
|
+
into.theme.enableSystem !== null ||
|
|
214
|
+
into.theme.htmlCarries.length > 0)) {
|
|
215
|
+
into.theme.switchedIn = from;
|
|
169
216
|
}
|
|
170
217
|
/**
|
|
171
218
|
* THE PAGE PAINTS, from the markup. A `min-h-screen` container with a background is
|
|
@@ -310,6 +357,15 @@ export function describeSignals(s) {
|
|
|
310
357
|
bits.push("with the OS allowed a vote");
|
|
311
358
|
out.push(`${bits.join(", ")}.`);
|
|
312
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* TWO PLACES, TWO FACTS. A library that declares 346 dark utilities and no
|
|
362
|
+
* switch is not a library missing something - the switch belongs to the app that
|
|
363
|
+
* mounts it. Saying WHERE keeps the sentence true and stops the reader from
|
|
364
|
+
* concluding the system declares a provider it does not have.
|
|
365
|
+
*/
|
|
366
|
+
if (t.switchedIn) {
|
|
367
|
+
out.push(`the system is measured where you scoped it; the theme is switched in \`${t.switchedIn}\` - that is where the attribute, the default and the \`<html>\` came from.`);
|
|
368
|
+
}
|
|
313
369
|
if (t.htmlCarries.length > 0) {
|
|
314
370
|
out.push(`\`<html>\` carries ${t.htmlCarries.map((a) => `\`${a}\``).join(" ")} - which is the ancestor every dark rule of theirs matches, and therefore the one ours must match.`);
|
|
315
371
|
}
|
|
@@ -354,6 +354,9 @@ const STATE_SPELLING = {
|
|
|
354
354
|
"read-only": "readOnly",
|
|
355
355
|
required: "required",
|
|
356
356
|
placeholder: "placeholder",
|
|
357
|
+
/** Pseudo-ELEMENTS, which the contract spells like any other selector key. */
|
|
358
|
+
after: "after",
|
|
359
|
+
before: "before",
|
|
357
360
|
highlighted: "highlighted",
|
|
358
361
|
even: "even",
|
|
359
362
|
odd: "odd",
|
|
@@ -1004,6 +1007,78 @@ function composeGradient(utilities, declared) {
|
|
|
1004
1007
|
broken,
|
|
1005
1008
|
};
|
|
1006
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* WHICH HALF OF THE TRANSCRIPTION A MODIFIER LIST BELONGS TO.
|
|
1012
|
+
*
|
|
1013
|
+
* Extracted because a GRADIENT needs the same answer: it is three utilities that
|
|
1014
|
+
* only mean something together, so it is composed per modifier group and then has
|
|
1015
|
+
* to land in the slot that group shares. Two copies of this routing would be two
|
|
1016
|
+
* answers to "where does `dark:after:` go", and the day they differ the reader is
|
|
1017
|
+
* told a decision arrived somewhere it did not.
|
|
1018
|
+
*
|
|
1019
|
+
* MUTATES `out`: a layer or a state block is created on demand, so only call it
|
|
1020
|
+
* once there is something to write.
|
|
1021
|
+
*/
|
|
1022
|
+
function slotFor(out, modifiers) {
|
|
1023
|
+
const isDark = modifiers.includes("dark");
|
|
1024
|
+
const rest = modifiers.filter((m) => m !== "dark");
|
|
1025
|
+
if (rest.length === 0) {
|
|
1026
|
+
return { target: isDark ? out.dark : out.base, unslotted: false };
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
|
|
1030
|
+
*
|
|
1031
|
+
* This used to route a bare state and drop everything else, on a comment
|
|
1032
|
+
* that said "a recipe has no slot for it" - true when it was written and
|
|
1033
|
+
* false since the transcription grew `layers`, which hold `scheme`,
|
|
1034
|
+
* `state`, `within` and `at` TOGETHER. Measured on the owner's library:
|
|
1035
|
+
* 29 real decisions dropped on that stale line, most of them a `dark:`
|
|
1036
|
+
* over a state (not-expressed.md, dono, 01/08).
|
|
1037
|
+
*
|
|
1038
|
+
* A recipe that reconstructs the component must carry them, so the router
|
|
1039
|
+
* reads the whole modifier list and only gives up on a modifier that
|
|
1040
|
+
* genuinely has no home - and then it SAYS so.
|
|
1041
|
+
*/
|
|
1042
|
+
const when = {};
|
|
1043
|
+
let at;
|
|
1044
|
+
let unslotted = false;
|
|
1045
|
+
for (const mod of rest) {
|
|
1046
|
+
/** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
|
|
1047
|
+
const group = /^group-(.+)$/.exec(mod);
|
|
1048
|
+
const bare = group ? group[1] : mod;
|
|
1049
|
+
const raw = STATE_SPELLING[bare] != null
|
|
1050
|
+
? bare
|
|
1051
|
+
: (DATA_STATE.exec(bare)?.[1] ?? null);
|
|
1052
|
+
const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
|
|
1053
|
+
if (state) {
|
|
1054
|
+
if (group)
|
|
1055
|
+
when.within = state;
|
|
1056
|
+
else
|
|
1057
|
+
when.state = state;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
/** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
|
|
1061
|
+
if (BREAKPOINT.test(mod) && !at) {
|
|
1062
|
+
at = mod;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
unslotted = true;
|
|
1066
|
+
}
|
|
1067
|
+
if (unslotted)
|
|
1068
|
+
return { target: null, unslotted: true };
|
|
1069
|
+
if (isDark)
|
|
1070
|
+
when.scheme = "dark";
|
|
1071
|
+
// A bare single state with no scheme keeps its own slot: `states` is what
|
|
1072
|
+
// the compiler and every lens already read, and moving it to a layer
|
|
1073
|
+
// would be a shape change nothing asked for.
|
|
1074
|
+
if (when.state && !when.within && !when.scheme && !at && rest.length === 1) {
|
|
1075
|
+
return {
|
|
1076
|
+
target: out.states[when.state] ?? (out.states[when.state] = {}),
|
|
1077
|
+
unslotted: false,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return { target: layerFor(out, when, at).style, unslotted: false };
|
|
1081
|
+
}
|
|
1007
1082
|
export function transcribe(classes, declared) {
|
|
1008
1083
|
const out = {
|
|
1009
1084
|
base: {},
|
|
@@ -1016,92 +1091,64 @@ export function transcribe(classes, declared) {
|
|
|
1016
1091
|
fromLiteral: 0,
|
|
1017
1092
|
};
|
|
1018
1093
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1094
|
+
* A GRADIENT IS THREE UTILITIES, AND IT BELONGS TO THE SLOT THEY SHARE.
|
|
1095
|
+
*
|
|
1096
|
+
* `composeGradient` ran on the BARE utilities only, so a gradient under any
|
|
1097
|
+
* modifier was invisible - and the one gradient in a real 36-component library
|
|
1098
|
+
* is under two of them: a Card draws its dark border as a masked `::after`
|
|
1099
|
+
* filled with `dark:after:bg-gradient-to-b dark:after:from-electric-indigo-500
|
|
1100
|
+
* dark:after:to-vivid-pink-500`. With the mask read and the paint missing, that
|
|
1101
|
+
* edge is a shape with no colour, which is the same invisible as before
|
|
1102
|
+
* (not-expressed.md, dono, 02/08).
|
|
1103
|
+
*
|
|
1104
|
+
* Grouped by the modifier list because that is what a gradient's three parts
|
|
1105
|
+
* have in common, and routed through the same `slotFor` the loop below uses.
|
|
1023
1106
|
*/
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
.map((cls) => parseClass(cls).utility);
|
|
1027
|
-
const gradient = composeGradient(bareUtilities, declared);
|
|
1028
|
-
if (gradient.value)
|
|
1029
|
-
out.base.backgroundImage = gradient.value;
|
|
1030
|
-
for (const broke of gradient.broken)
|
|
1031
|
-
out.unreadable.push(broke);
|
|
1107
|
+
const consumedByGroup = new Set();
|
|
1108
|
+
const groups = new Map();
|
|
1032
1109
|
for (const cls of classes) {
|
|
1033
1110
|
const { modifiers, utility } = parseClass(cls);
|
|
1034
|
-
|
|
1111
|
+
const key = modifiers.join(":");
|
|
1112
|
+
const entry = groups.get(key) ?? { modifiers, utilities: [] };
|
|
1113
|
+
entry.utilities.push(utility);
|
|
1114
|
+
groups.set(key, entry);
|
|
1115
|
+
}
|
|
1116
|
+
for (const group of groups.values()) {
|
|
1117
|
+
const gradient = composeGradient(group.utilities, declared);
|
|
1118
|
+
if (gradient.consumed.size === 0)
|
|
1119
|
+
continue;
|
|
1120
|
+
const prefix = group.modifiers.length
|
|
1121
|
+
? `${group.modifiers.join(":")}:`
|
|
1122
|
+
: "";
|
|
1123
|
+
for (const utility of gradient.consumed)
|
|
1124
|
+
consumedByGroup.add(`${prefix}${utility}`);
|
|
1125
|
+
if (gradient.value) {
|
|
1126
|
+
// Slotted only now: `slotFor` creates the layer it answers with, and an
|
|
1127
|
+
// empty layer is a shape the lint rejects on the way out.
|
|
1128
|
+
const { target, unslotted } = slotFor(out, group.modifiers);
|
|
1129
|
+
if (unslotted || !target) {
|
|
1130
|
+
for (const utility of gradient.consumed)
|
|
1131
|
+
out.skipped.push(`${prefix}${utility}`);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
target.backgroundImage = gradient.value;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
for (const broke of gradient.broken) {
|
|
1138
|
+
const at = `${prefix}${broke}`;
|
|
1139
|
+
if (!out.unreadable.includes(at))
|
|
1140
|
+
out.unreadable.push(at);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const cls of classes) {
|
|
1144
|
+
const { modifiers, utility } = parseClass(cls);
|
|
1145
|
+
if (consumedByGroup.has(cls))
|
|
1035
1146
|
continue;
|
|
1036
|
-
const isDark = modifiers.includes("dark");
|
|
1037
|
-
const rest = modifiers.filter((m) => m !== "dark");
|
|
1038
1147
|
// THE SLOT IS DECIDED BEFORE THE VALUE IS READ. Resolving first meant a
|
|
1039
1148
|
// `dark:hover:` whose colour happened to be undeclared vanished in silence
|
|
1040
1149
|
// rather than being reported as a look we have nowhere to put - two
|
|
1041
1150
|
// different problems, and only one of them is theirs to hear about.
|
|
1042
|
-
|
|
1043
|
-
let unslotted = false;
|
|
1044
|
-
if (rest.length === 0) {
|
|
1045
|
-
target = isDark ? out.dark : out.base;
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
/**
|
|
1049
|
-
* EVERY MODIFIER GETS A SLOT, and the contract has had one all along.
|
|
1050
|
-
*
|
|
1051
|
-
* This used to route a bare state and drop everything else, on a comment
|
|
1052
|
-
* that said "a recipe has no slot for it" - true when it was written and
|
|
1053
|
-
* false since the transcription grew `layers`, which hold `scheme`,
|
|
1054
|
-
* `state`, `within` and `at` TOGETHER. Measured on the owner's library:
|
|
1055
|
-
* 29 real decisions dropped on that stale line, most of them a `dark:`
|
|
1056
|
-
* over a state (not-expressed.md, dono, 01/08).
|
|
1057
|
-
*
|
|
1058
|
-
* A recipe that reconstructs the component must carry them, so the router
|
|
1059
|
-
* reads the whole modifier list and only gives up on a modifier that
|
|
1060
|
-
* genuinely has no home - and then it SAYS so.
|
|
1061
|
-
*/
|
|
1062
|
-
const when = {};
|
|
1063
|
-
let at;
|
|
1064
|
-
for (const mod of rest) {
|
|
1065
|
-
/** `group-hover:`, `group-data-[checked]:` - the PARENT's state. */
|
|
1066
|
-
const group = /^group-(.+)$/.exec(mod);
|
|
1067
|
-
const bare = group ? group[1] : mod;
|
|
1068
|
-
const raw = STATE_SPELLING[bare] != null
|
|
1069
|
-
? bare
|
|
1070
|
-
: (DATA_STATE.exec(bare)?.[1] ?? null);
|
|
1071
|
-
const state = raw ? (STATE_SPELLING[raw] ?? null) : null;
|
|
1072
|
-
if (state) {
|
|
1073
|
-
if (group)
|
|
1074
|
-
when.within = state;
|
|
1075
|
-
else
|
|
1076
|
-
when.state = state;
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
/** `md:`, `lg:` - a breakpoint of theirs, by their own name. */
|
|
1080
|
-
if (BREAKPOINT.test(mod) && !at) {
|
|
1081
|
-
at = mod;
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
unslotted = true;
|
|
1085
|
-
}
|
|
1086
|
-
if (!unslotted) {
|
|
1087
|
-
if (isDark)
|
|
1088
|
-
when.scheme = "dark";
|
|
1089
|
-
// A bare single state with no scheme keeps its own slot: `states` is what
|
|
1090
|
-
// the compiler and every lens already read, and moving it to a layer
|
|
1091
|
-
// would be a shape change nothing asked for.
|
|
1092
|
-
if (when.state &&
|
|
1093
|
-
!when.within &&
|
|
1094
|
-
!when.scheme &&
|
|
1095
|
-
!at &&
|
|
1096
|
-
rest.length === 1) {
|
|
1097
|
-
target = out.states[when.state] ?? (out.states[when.state] = {});
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
const layer = layerFor(out, when, at);
|
|
1101
|
-
target = layer.style;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1151
|
+
const { target, unslotted } = slotFor(out, modifiers);
|
|
1105
1152
|
const decl = readUtility(utility, declared);
|
|
1106
1153
|
if (unslotted) {
|
|
1107
1154
|
// Only worth saying when it IS a design value; an unslotted `sm:flex` is
|
package/dist/skill-import.js
CHANGED
|
@@ -331,6 +331,13 @@ census carries is the failure this pipeline keeps paying for. The sketch now run
|
|
|
331
331
|
so a big component arrives WHOLE: \`TextEditor\` used to lose its editable region at node 60 and
|
|
332
332
|
that single truncation is what forced a hand-read of the file (dono, 01/08).
|
|
333
333
|
|
|
334
|
+
\`signals.theme\` includes the SWITCH - \`attribute\`, \`defaultTheme\`, \`enableSystem\` and what
|
|
335
|
+
\`<html>\` carries, \`colorScheme\` included - read from the \`--usage\` roots as well as the scope,
|
|
336
|
+
because a component library declares the dark utilities and the APP declares the switch. When it
|
|
337
|
+
came from an app, \`switchedIn\` names which one: say "the system is measured in \`packages/ui\`, the
|
|
338
|
+
theme is switched in \`apps/web-dashboard\`" rather than reporting that the library declares a
|
|
339
|
+
provider it does not have. Answering 2d never needs a layout file opened.
|
|
340
|
+
|
|
334
341
|
**Validate in ONE call, at the end.** Build every recipe in memory first, then send them all to
|
|
335
342
|
\`validate_recipes\` - one round trip for the whole reading instead of one per component. A reader
|
|
336
343
|
with 35 anatomies spent 35 turns asking the same question, and none of that was thinking. Only
|
package/package.json
CHANGED