dreative 0.5.3 → 0.5.4
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/cli/audit.js +87 -11
- package/dist/cli/audit.test.js +40 -1
- package/dist/server/index.js +10 -5
- package/dist/shared/artifacts.js +100 -5
- package/dist/shared/artifacts.test.js +101 -0
- package/dist/shared/design.js +209 -24
- package/dist/shared/design.test.js +70 -0
- package/dist/shared/ruleSystem.js +1 -1
- package/dist/ui/assets/{index-CKwmbx2j.js → index-BQhYTdSa.js} +11 -11
- package/dist/ui/index.html +1 -1
- package/package.json +5 -5
- package/skill/dreative/DESIGN.md +13 -9
- package/skill/dreative/PLAN.md +13 -3
- package/skill/dreative/SKILL.md +15 -4
- package/skill/dreative/references/ARTIFACTS.md +26 -8
- package/skill/dreative/references/TIERS.md +7 -1
- package/skill/dreative/schemas/plan.schema.json +87 -5
- package/skill/dreative/schemas/verify.schema.json +8 -2
- package/skill/dreative/skills/mobile.md +6 -1
package/dist/shared/design.js
CHANGED
|
@@ -17,16 +17,6 @@ const AESTHETIC_DIALS = {
|
|
|
17
17
|
"dark-tech": [6, 5, 5],
|
|
18
18
|
trust: [3, 2, 5],
|
|
19
19
|
};
|
|
20
|
-
/** Layout families rotated across sections so no page repeats itself. */
|
|
21
|
-
const FAMILIES = [
|
|
22
|
-
"asymmetric-split",
|
|
23
|
-
"bento-grid",
|
|
24
|
-
"full-width-statement",
|
|
25
|
-
"stacked-vertical",
|
|
26
|
-
"offset-two-col",
|
|
27
|
-
"horizontal-scroll-strip",
|
|
28
|
-
"editorial-columns",
|
|
29
|
-
];
|
|
30
20
|
function resolveDials(brief) {
|
|
31
21
|
const base = AESTHETIC_DIALS[brief?.aesthetic ?? ""] ?? [7, 5, 4];
|
|
32
22
|
return {
|
|
@@ -106,24 +96,36 @@ export function lintLayout(layout) {
|
|
|
106
96
|
lints.push(`${buttons} buttons on one page — consolidate duplicate CTA intents (one label per intent)`);
|
|
107
97
|
return lints;
|
|
108
98
|
}
|
|
99
|
+
const STYLE_ONLY = /\b(colou?r|font|typograph|radius|shadow|gradient|animation|fade|slide|scale|hover|spacing|token)\b/i;
|
|
100
|
+
const STRUCTURAL_LANGUAGE = /\b(architecture|boundary|workflow|navigation|hierarchy|order|interaction|task|workspace|model|rebuild|recompose|replace|merge|move|remove)\b/i;
|
|
101
|
+
/** Blocking contract lints used by tests and agent-facing runtime plans. */
|
|
102
|
+
export function lintDesignPlan(plan) {
|
|
103
|
+
const lints = [];
|
|
104
|
+
const delta = plan.structuralDelta;
|
|
105
|
+
const deltaText = [delta.proposedModel, delta.proposedParadigm, delta.depthHonesty, ...delta.materialChanges].join(" ");
|
|
106
|
+
if ((plan.depth === "restructure" || plan.depth === "reimagine") && (!STRUCTURAL_LANGUAGE.test(deltaText) || (STYLE_ONLY.test(deltaText) && !STRUCTURAL_LANGUAGE.test(delta.materialChanges.join(" ")))))
|
|
107
|
+
lints.push(`${plan.depth} requires a concrete architectural or interaction-model delta; stylesheet and generic motion changes cannot satisfy it`);
|
|
108
|
+
if (plan.depth !== "restyle" && delta.materialChanges.length < 2)
|
|
109
|
+
lints.push(`${plan.depth} requires multiple concrete changes to composition, hierarchy, architecture, or interaction`);
|
|
110
|
+
const mobileText = [plan.mobileBlueprint.mobileOnlyComposition, plan.mobileBlueprint.composition390, plan.mobileBlueprint.fallback320, plan.mobileBlueprint.stackingRejection].join(" ");
|
|
111
|
+
if (/^\s*stack(?:ed)?(?:\s+vertically)?\s*$/i.test(plan.mobileBlueprint.mobileOnlyComposition) || !/\b(order|disclosure|navigation|task|action|media|thumb|viewport|recompose|context)\b/i.test(mobileText))
|
|
112
|
+
lints.push("mobile blueprint must define a mobile-native composition; ‘stack vertically’ alone is blocking");
|
|
113
|
+
if ((plan.tier === "expressive" || plan.tier === "award") && !plan.expression)
|
|
114
|
+
lints.push(`${plan.tier} requires a project-specific expression contract or a documented intentional-calm exception in the direct plan`);
|
|
115
|
+
if (plan.expression && /\b(fade|slide|scale|hover|gradient|large typography)\b/i.test(plan.expression.mechanism) && !/\b(state|selection|progress|navigation|transform|content|spatial)\b/i.test(plan.expression.communicates))
|
|
116
|
+
lints.push("expression mechanism is decorative-only and does not communicate content, state, progression, selection, navigation, or transformation");
|
|
117
|
+
return lints;
|
|
118
|
+
}
|
|
109
119
|
/** Build the compact, executable plan sent to the agent with design-page. */
|
|
110
120
|
export function buildDesignPlan(page, brief, assignedSkills) {
|
|
111
121
|
const dials = resolveDials(brief);
|
|
112
122
|
const aesthetic = brief?.aesthetic || "auto (infer per DESIGN.md, then commit)";
|
|
113
123
|
const allowedSkills = assignedSkills ? new Set(resolveSkillDependencies(assignedSkills)) : undefined;
|
|
114
|
-
const sections = (page.layout.children ?? []).map((s
|
|
115
|
-
let family;
|
|
116
|
-
if (s.type === "nav")
|
|
117
|
-
family = "nav-single-line";
|
|
118
|
-
else if (s.type === "hero")
|
|
119
|
-
family = dials.variance > 4 ? "asymmetric-split-hero" : "centered-hero";
|
|
120
|
-
else if (s.type === "footer")
|
|
121
|
-
family = "footer";
|
|
122
|
-
else
|
|
123
|
-
family = FAMILIES[i % FAMILIES.length];
|
|
124
|
+
const sections = (page.layout.children ?? []).map((s) => {
|
|
124
125
|
const detected = resolveSkillDependencies(detectSpecialistSkills(blockTexts(s)));
|
|
125
126
|
const secSkills = allowedSkills ? detected.filter((skill) => allowedSkills.has(skill)) : detected;
|
|
126
|
-
|
|
127
|
+
const role = s.type === "nav" ? "navigation" : s.type === "footer" ? "closure" : s.type === "form" ? "task-input" : s.type === "hero" ? "orientation-or-promotion" : "content-or-task-support";
|
|
128
|
+
return secSkills.length ? { id: s.id, label: s.label, role, skills: secSkills } : { id: s.id, label: s.label, role };
|
|
127
129
|
});
|
|
128
130
|
// page-level specialist skills: brief/prompt keywords + section hits + motion dial
|
|
129
131
|
const requestedSkills = new Set([
|
|
@@ -138,6 +140,11 @@ export function buildDesignPlan(page, brief, assignedSkills) {
|
|
|
138
140
|
aesthetic,
|
|
139
141
|
texts: [brief?.vibe, brief?.notes, page.designPrompt, ...blockTexts(page.layout)],
|
|
140
142
|
});
|
|
143
|
+
const depth = depthFor(brief, page);
|
|
144
|
+
const register = inferRegister(page);
|
|
145
|
+
const delta = structuralDelta(page, depth, register);
|
|
146
|
+
const mobile = mobileBlueprint(page, register);
|
|
147
|
+
const expression = expressionContract(page, tier, register);
|
|
141
148
|
const spacing = dials.density <= 3 ? "py-24/py-32 section gaps" : dials.density <= 6 ? "py-16/py-20" : "py-8/py-12, hairline dividers, mono numerals";
|
|
142
149
|
const motionBudget = dials.motion <= 3
|
|
143
150
|
? "hover/active states only, no scroll animation"
|
|
@@ -148,9 +155,12 @@ export function buildDesignPlan(page, brief, assignedSkills) {
|
|
|
148
155
|
`aesthetic: ${aesthetic}; dials variance=${dials.variance} motion=${dials.motion} density=${dials.density}`,
|
|
149
156
|
`spacing scale: ${spacing}`,
|
|
150
157
|
`motion budget: ${motionBudget}`,
|
|
158
|
+
`transformation depth: ${depth}; source strategy: ${sourceStrategyFor(depth)}; ambition remains independently ${tier}`,
|
|
159
|
+
`page register: ${register}; compose for this page's task model rather than inheriting a shared marketing shell`,
|
|
151
160
|
"choose the simplest mechanism that makes each intended transformation convincing; do not default blindly to fade/slide/scale or to WebGL/3D",
|
|
152
161
|
"one accent color, one neutral family, one radius system, locked across ALL pages of this project",
|
|
153
|
-
"section
|
|
162
|
+
"section entries describe existing semantic roles only; author the proposed composition from the structural delta instead of cycling through layout families",
|
|
163
|
+
"preserve routes, handlers, data flow, fields, states, accessibility, analytics, required copy, and public APIs; do not preserve DOM hierarchy, card shells, section order, desktop composition, visual grouping, or motion by default",
|
|
154
164
|
...(dials.variance > 4 ? ["avoid centered-hero-over-gradient; use split or asymmetric composition"] : []),
|
|
155
165
|
...(skills.length
|
|
156
166
|
? [
|
|
@@ -159,17 +169,191 @@ export function buildDesignPlan(page, brief, assignedSkills) {
|
|
|
159
169
|
: []),
|
|
160
170
|
];
|
|
161
171
|
return {
|
|
162
|
-
version:
|
|
172
|
+
version: 2,
|
|
163
173
|
dials,
|
|
164
174
|
aesthetic,
|
|
165
175
|
tier,
|
|
176
|
+
depth,
|
|
177
|
+
sourceStrategy: sourceStrategyFor(depth),
|
|
178
|
+
register,
|
|
179
|
+
structuralDelta: delta,
|
|
180
|
+
mobileBlueprint: mobile,
|
|
181
|
+
...(expression ? { expression } : {}),
|
|
166
182
|
sections,
|
|
167
183
|
directives,
|
|
168
|
-
lints: lintLayout(page.layout),
|
|
184
|
+
lints: [...lintLayout(page.layout), ...lintDesignPlan({ depth, structuralDelta: delta, mobileBlueprint: mobile, tier, expression })],
|
|
169
185
|
skills,
|
|
170
186
|
verification: TIER_REQUIREMENTS[tier],
|
|
171
187
|
};
|
|
172
188
|
}
|
|
189
|
+
function depthFor(brief, page) {
|
|
190
|
+
if (brief?.transformationDepth)
|
|
191
|
+
return brief.transformationDepth;
|
|
192
|
+
const text = `${page.designPrompt ?? ""} ${brief?.notes ?? ""}`;
|
|
193
|
+
if (/\breimagine\b/i.test(text))
|
|
194
|
+
return "reimagine";
|
|
195
|
+
if (/\brestructure\b|\brebuild\b/i.test(text))
|
|
196
|
+
return "restructure";
|
|
197
|
+
if (/\brelayout\b|\brecompose\b/i.test(text))
|
|
198
|
+
return "relayout";
|
|
199
|
+
return "restyle";
|
|
200
|
+
}
|
|
201
|
+
export function sourceStrategyFor(depth) {
|
|
202
|
+
if (depth === "restyle")
|
|
203
|
+
return "patch";
|
|
204
|
+
if (depth === "relayout")
|
|
205
|
+
return "recompose";
|
|
206
|
+
return "rebuild-from-contracts";
|
|
207
|
+
}
|
|
208
|
+
export function buildDesignSourceContext(depth, generatedFile) {
|
|
209
|
+
const strategy = sourceStrategyFor(depth);
|
|
210
|
+
if (strategy === "patch") {
|
|
211
|
+
return { strategy, previousFile: generatedFile, compositionDirective: "Patch the existing composition while preserving its markup and interaction architecture." };
|
|
212
|
+
}
|
|
213
|
+
if (strategy === "recompose") {
|
|
214
|
+
return {
|
|
215
|
+
strategy,
|
|
216
|
+
behaviorReferenceFile: generatedFile,
|
|
217
|
+
compositionDirective: "Use the existing file for behavior and content inventory, but recompose ordering, hierarchy, and layout before styling.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
strategy,
|
|
222
|
+
behaviorReferenceFile: generatedFile,
|
|
223
|
+
compositionDirective: "Create the blueprint independently from the previous visual tree. Read the reference only for routes, handlers, data flow, fields, states, accessibility, analytics, copy, and public APIs; rebuild markup and component boundaries from the approved contracts.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function inferRegister(page) {
|
|
227
|
+
const text = [page.name, page.designPrompt, ...blockTexts(page.layout)].join(" ").toLowerCase();
|
|
228
|
+
if (/\b(login|sign in|sign up|password|authentication)\b/.test(text))
|
|
229
|
+
return "authentication";
|
|
230
|
+
if (/\b(admin|moderation|manage users|cms)\b/.test(text))
|
|
231
|
+
return "administration";
|
|
232
|
+
if (/\b(account|profile|order status|billing|settings)\b/.test(text))
|
|
233
|
+
return "account-status";
|
|
234
|
+
if (/\b(search|select|checkout|order|booking|form|continue|payment|outlet)\b/.test(text))
|
|
235
|
+
return "task-transaction";
|
|
236
|
+
if (/\b(error|empty|loading|not found)\b/.test(text))
|
|
237
|
+
return "system-state";
|
|
238
|
+
if (/\b(dashboard|table|analytics|report|data)\b/.test(text))
|
|
239
|
+
return "data-dense-utility";
|
|
240
|
+
if (/\b(catalog|browse|collection|discover|filter)\b/.test(text))
|
|
241
|
+
return "discovery-browse";
|
|
242
|
+
return "marketing-storytelling";
|
|
243
|
+
}
|
|
244
|
+
function describeExisting(page) {
|
|
245
|
+
const sections = (page.layout.children ?? []).map((section) => section.label).filter(Boolean);
|
|
246
|
+
return sections.length ? sections.join(" followed by ") : page.layout.label;
|
|
247
|
+
}
|
|
248
|
+
function taskFor(page) {
|
|
249
|
+
const form = (page.layout.children ?? []).find((section) => section.type === "form");
|
|
250
|
+
const action = blockTexts(page.layout).find((text) => /\b(select|continue|submit|search|choose|buy|book|save|sign in)\b/i.test(text));
|
|
251
|
+
return action ?? form?.label ?? `complete ${page.name}`;
|
|
252
|
+
}
|
|
253
|
+
function structuralDelta(page, depth, register) {
|
|
254
|
+
const existing = describeExisting(page);
|
|
255
|
+
const task = taskFor(page);
|
|
256
|
+
if (depth === "restyle") {
|
|
257
|
+
return {
|
|
258
|
+
existingModel: existing,
|
|
259
|
+
proposedModel: `${existing}, retained intentionally with a newly authored visual and state language`,
|
|
260
|
+
existingParadigm: "Existing DOM order, component boundaries, and interaction flow",
|
|
261
|
+
proposedParadigm: "The same interaction architecture with a coherent visual, type, surface, and authored-state treatment",
|
|
262
|
+
materialChanges: ["Restyle tokens, typography, surfaces, responsive polish, and state presentation without claiming structural transformation"],
|
|
263
|
+
survivingBoundaries: (page.layout.children ?? []).map((section) => section.label),
|
|
264
|
+
rebuiltBoundaries: [],
|
|
265
|
+
preservedContracts: ["Routes, handlers, data flow, field names, states, accessibility, analytics, copy, and public APIs"],
|
|
266
|
+
retainedPatterns: [{ pattern: "Existing section and task order", rationale: "Restyle explicitly permits structural continuity." }],
|
|
267
|
+
forbiddenCarryovers: ["Unexamined accessibility defects", "Decorative motion presented as structural change"],
|
|
268
|
+
depthHonesty: "This is a restyle: structure is retained and the plan makes no relayout, restructure, or reimagine claim.",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const proposedModel = register === "task-transaction"
|
|
272
|
+
? `A task-first ${page.name} workspace organized around “${task}”, with supporting context revealed at the point of decision`
|
|
273
|
+
: depth === "reimagine"
|
|
274
|
+
? `A new ${page.name} experience model organized around its ${register.replaceAll("-", " ")} purpose rather than the inherited section sequence`
|
|
275
|
+
: `A recomposed ${page.name} hierarchy organized by user intent rather than the inherited ${existing}`;
|
|
276
|
+
const deep = depth === "restructure" || depth === "reimagine";
|
|
277
|
+
return {
|
|
278
|
+
existingModel: existing,
|
|
279
|
+
proposedModel,
|
|
280
|
+
existingParadigm: `Linear inherited composition: ${existing}`,
|
|
281
|
+
proposedParadigm: register === "task-transaction" ? "Task-first contextual workspace with action and decision state in one flow" : "Intent-led page composition with page-specific hierarchy",
|
|
282
|
+
materialChanges: deep
|
|
283
|
+
? [
|
|
284
|
+
`Rebuild the page architecture around “${task}” instead of preserving the inherited section tree`,
|
|
285
|
+
"Move supporting or promotional content after the primary task unless it is required for the decision",
|
|
286
|
+
"Reconcile preserved behavior only after the new component boundaries exist",
|
|
287
|
+
]
|
|
288
|
+
: ["Change section order and hierarchy", "Replace the inherited layout family with an intent-led composition", "Translate desktop grouping deliberately for mobile"],
|
|
289
|
+
survivingBoundaries: ["Behavior, data, route, and accessibility contracts"],
|
|
290
|
+
rebuiltBoundaries: deep ? (page.layout.children ?? []).filter((section) => !["nav", "footer"].includes(section.type)).map((section) => section.label) : ["Page composition boundary"],
|
|
291
|
+
preservedContracts: ["Routes, handlers, data flow, field names, states, accessibility, analytics, required copy, and public APIs"],
|
|
292
|
+
retainedPatterns: [{ pattern: "Semantic task controls", rationale: "Their behavior is contractual even when their visual grouping and DOM position change." }],
|
|
293
|
+
forbiddenCarryovers: ["Inherited DOM hierarchy as the composition source", "Card shells and section order without page-specific rationale", "Desktop grouping copied directly to mobile"],
|
|
294
|
+
depthHonesty: deep
|
|
295
|
+
? `${depth} changes page architecture and component boundaries; token, font, color, radius, shadow, and entrance-animation changes cannot satisfy it.`
|
|
296
|
+
: "Relayout changes ordering, hierarchy, and layout family while preserving the underlying workflow and most component responsibilities.",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function mobileBlueprint(page, register) {
|
|
300
|
+
const task = taskFor(page);
|
|
301
|
+
const taskFirst = register === "task-transaction" || register === "authentication" || register === "data-dense-utility";
|
|
302
|
+
return {
|
|
303
|
+
primaryTask: task,
|
|
304
|
+
firstViewportPurpose: taskFirst ? `Let the user begin “${task}” immediately, before promotional or decorative material` : `Establish the ${page.name} purpose and expose its primary next action`,
|
|
305
|
+
contentOrder: taskFirst ? ["compact navigation/context", `controls and information required to ${task}`, "current state or result", "supporting explanation", "promotional or decorative material"] : ["identity and purpose", "primary action or navigation", "core content", "supporting content"],
|
|
306
|
+
beforeFirstScroll: ["Page identity", `The control or information needed to ${task}`, "Primary action or current selection state"],
|
|
307
|
+
primaryThumbAction: { action: task, placement: "Within the lower-thumb reach zone; sticky only when it does not cover content or duplicate navigation" },
|
|
308
|
+
stickyElements: ["Only task-critical action or compact navigation; never duplicate the same CTA"],
|
|
309
|
+
safeArea: "Inset fixed controls with env(safe-area-inset-bottom) and reserve matching content padding",
|
|
310
|
+
navigationModel: "Compact mobile navigation appropriate to this page register, with no duplicate desktop navigation row",
|
|
311
|
+
mobileOnlyComposition: taskFirst ? "Collapse supporting context into contextual disclosure while keeping task controls and state in one continuous decision surface" : "Use mobile pacing and disclosure designed around reading and thumb reach, not desktop columns",
|
|
312
|
+
desktopTranslation: { retained: ["Required functionality and semantic content"], translated: ["Spatial grouping, navigation, media, and state transitions"], removed: ["Nonessential decorative blockers before the task"], replaced: ["Hover-only affordances with visible tap/focus controls"] },
|
|
313
|
+
mediaStrategy: "Use task-supporting media at mobile scale; defer or remove decorative media that delays the primary task",
|
|
314
|
+
motionStrategy: "Translate authored state changes to short, interruptible mobile transitions; preserve visible defaults and reduced motion",
|
|
315
|
+
keyboardAndForms: "Keep focused fields and the primary action visible above the software keyboard; use appropriate input modes and no focus traps",
|
|
316
|
+
composition390: `At 390×844, ${taskFirst ? "the task begins in the first viewport and its action remains reachable" : "identity, purpose, and primary navigation form a complete first viewport"}`,
|
|
317
|
+
fallback320: "At 320px, remove nonessential decoration, allow labels to wrap, keep controls within the viewport, and avoid fixed minimum typography or widths",
|
|
318
|
+
stackingRejection: "The mobile content order, disclosure, navigation, task action, and media treatment are explicitly recomposed; desktop columns are not merely stacked.",
|
|
319
|
+
verificationChecks: [
|
|
320
|
+
"no-horizontal-overflow", "no-clipped-content", "fixed-elements-clear-content", "safe-area-spacing",
|
|
321
|
+
"primary-task-discoverable", "primary-action-reachable", "touch-targets", "software-keyboard-usable",
|
|
322
|
+
"no-hover-only", "intentional-mobile-composition", "mobile-content-order", "motion-media-translated",
|
|
323
|
+
"no-decorative-task-blocker",
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function expressionContract(page, tier, register) {
|
|
328
|
+
if (tier !== "expressive" && tier !== "award")
|
|
329
|
+
return undefined;
|
|
330
|
+
const task = taskFor(page);
|
|
331
|
+
return {
|
|
332
|
+
mechanism: register === "task-transaction" ? `The user’s current “${task}” selection becomes the persistent context for the next task state` : `${page.name} navigation changes form to reflect the user’s current content or progression state`,
|
|
333
|
+
communicates: register === "task-transaction" ? "Selection, operational state, and readiness to continue" : "Position, progression, and relationship between content states",
|
|
334
|
+
projectFit: `The behavior is derived from ${page.name} and its ${register.replaceAll("-", " ")} role, rather than a reusable reveal effect.`,
|
|
335
|
+
location: `The primary ${page.name} task and its transition to the next meaningful state`,
|
|
336
|
+
mobileTranslation: "Keep the state-carrying behavior, shorten travel, and place feedback beside the thumb action",
|
|
337
|
+
reducedMotion: "Render the communicated state directly with a short cross-state update and no spatial travel",
|
|
338
|
+
fallback: "Semantic state text and conventional navigation preserve the complete task without animation",
|
|
339
|
+
verification: "Verify that content or application state—not only opacity, translate, scale, color, or decoration—changes and remains understandable on mobile and reduced motion",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export function buildRuntimeCoherence(pages, brief) {
|
|
343
|
+
return {
|
|
344
|
+
globalVisualLanguage: "Share tokens, type roles, material logic, and state markers without sharing one page composition.",
|
|
345
|
+
globalInteractionLanguage: "Use consistent control feedback and continuity; let each page register define navigation, density, and task flow.",
|
|
346
|
+
pages: pages.map((page) => {
|
|
347
|
+
const register = inferRegister(page);
|
|
348
|
+
return { pageId: page.id, name: page.name, register, primaryTask: taskFor(page) };
|
|
349
|
+
}),
|
|
350
|
+
prohibitedRepeatedShells: [
|
|
351
|
+
"Oversized headline plus decorative image panel plus rounded card container on unrelated routes",
|
|
352
|
+
"The same layout family assigned by page or section array position",
|
|
353
|
+
"Pages differing only by headline, color, and image",
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
173
357
|
/** Build page plans from a user-approved pool. Unselected optional skills are
|
|
174
358
|
* suggestions only; explicit page assignments win. */
|
|
175
359
|
export function buildMultiPageDesignPlans(pages, brief, selection) {
|
|
@@ -182,6 +366,7 @@ export function buildMultiPageDesignPlans(pages, brief, selection) {
|
|
|
182
366
|
});
|
|
183
367
|
return {
|
|
184
368
|
routing,
|
|
369
|
+
coherence: buildRuntimeCoherence(pages, brief),
|
|
185
370
|
pages: pages.map((page) => ({ pageId: page.id, plan: buildDesignPlan(page, brief, routing.byPage[page.id]) })),
|
|
186
371
|
};
|
|
187
372
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildDesignPlan, buildDesignSourceContext, buildRuntimeCoherence, lintDesignPlan } from "./design.js";
|
|
4
|
+
function transactionalPage() {
|
|
5
|
+
const section = (id, type, label, children = []) => ({ id, type, label, direction: "column", children });
|
|
6
|
+
return {
|
|
7
|
+
id: "outlet-selection",
|
|
8
|
+
name: "Choose an outlet",
|
|
9
|
+
status: "designed",
|
|
10
|
+
canvasPos: { x: 0, y: 0 },
|
|
11
|
+
generatedFile: "generated/outlet-selection.tsx",
|
|
12
|
+
layout: section("root", "column", "Outlet selection page", [
|
|
13
|
+
section("header", "nav", "Standard header"),
|
|
14
|
+
section("intro", "hero", "Promotional chicken-rice introduction"),
|
|
15
|
+
section("search", "form", "Search outlets", [{ id: "search-button", type: "button", label: "Search nearby outlets", text: "Search" }]),
|
|
16
|
+
section("cards", "card-grid", "Repeated outlet cards"),
|
|
17
|
+
section("continue", "section", "Continuation action", [{ id: "continue-button", type: "button", label: "Continue with selected outlet", text: "Continue" }]),
|
|
18
|
+
]),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function plan(brief) {
|
|
22
|
+
return buildDesignPlan(transactionalPage(), brief);
|
|
23
|
+
}
|
|
24
|
+
test("case A: restyle + expressive retains structure without claiming transformation", () => {
|
|
25
|
+
const result = plan({ transformationDepth: "restyle", variance: 8, motion: 8 });
|
|
26
|
+
assert.equal(result.depth, "restyle");
|
|
27
|
+
assert.equal(result.tier, "expressive");
|
|
28
|
+
assert.equal(result.sourceStrategy, "patch");
|
|
29
|
+
assert.equal(result.structuralDelta.rebuiltBoundaries.length, 0);
|
|
30
|
+
assert.match(result.structuralDelta.depthHonesty, /restyle/i);
|
|
31
|
+
});
|
|
32
|
+
test("case B: restructure + solid produces a task-specific architecture", () => {
|
|
33
|
+
const result = plan({ transformationDepth: "restructure", aesthetic: "trust", variance: 3, motion: 2 });
|
|
34
|
+
assert.equal(result.tier, "solid");
|
|
35
|
+
assert.equal(result.depth, "restructure");
|
|
36
|
+
assert.equal(result.register, "task-transaction");
|
|
37
|
+
assert.match(result.structuralDelta.proposedModel, /task-first.*workspace/i);
|
|
38
|
+
assert.ok(result.structuralDelta.materialChanges.length >= 3);
|
|
39
|
+
assert.equal(lintDesignPlan(result).length, 0);
|
|
40
|
+
});
|
|
41
|
+
test("case C: restructure + expressive carries structural, expression, mobile, preservation, and verification directives", () => {
|
|
42
|
+
const result = plan({ transformationDepth: "restructure", variance: 8, motion: 8 });
|
|
43
|
+
assert.equal(result.sourceStrategy, "rebuild-from-contracts");
|
|
44
|
+
assert.ok(result.expression);
|
|
45
|
+
assert.match(result.expression.mechanism, /selection.*persistent context/i);
|
|
46
|
+
assert.match(result.mobileBlueprint.firstViewportPurpose, /immediately.*before promotional/i);
|
|
47
|
+
assert.match(result.structuralDelta.preservedContracts.join(" "), /routes.*handlers.*data flow/i);
|
|
48
|
+
assert.ok(result.verification.length > 0);
|
|
49
|
+
});
|
|
50
|
+
test("case D: stack-only mobile plan is blocking", () => {
|
|
51
|
+
const result = plan({ transformationDepth: "restructure", variance: 3, motion: 2 });
|
|
52
|
+
result.mobileBlueprint.mobileOnlyComposition = "stack vertically";
|
|
53
|
+
assert.ok(lintDesignPlan(result).some((message) => message.includes("mobile-native")));
|
|
54
|
+
});
|
|
55
|
+
test("case E: deep redesign treats previous file as behavior reference, never composition source", () => {
|
|
56
|
+
const deep = buildDesignSourceContext("restructure", "generated/outlet-selection.tsx");
|
|
57
|
+
assert.equal(deep.strategy, "rebuild-from-contracts");
|
|
58
|
+
assert.equal(deep.previousFile, undefined);
|
|
59
|
+
assert.equal(deep.behaviorReferenceFile, "generated/outlet-selection.tsx");
|
|
60
|
+
assert.match(deep.compositionDirective, /independently from the previous visual tree/i);
|
|
61
|
+
const light = buildDesignSourceContext("restyle", "generated/outlet-selection.tsx");
|
|
62
|
+
assert.equal(light.previousFile, "generated/outlet-selection.tsx");
|
|
63
|
+
});
|
|
64
|
+
test("multi-page coherence records page registers without assigning a repeated shell", () => {
|
|
65
|
+
const outlet = transactionalPage();
|
|
66
|
+
const account = { ...transactionalPage(), id: "account", name: "Account status", designPrompt: "Profile and billing settings" };
|
|
67
|
+
const coherence = buildRuntimeCoherence([outlet, account], { transformationDepth: "restructure" });
|
|
68
|
+
assert.deepEqual(coherence.pages.map((item) => item.register), ["task-transaction", "account-status"]);
|
|
69
|
+
assert.ok(coherence.prohibitedRepeatedShells.some((item) => item.includes("unrelated routes")));
|
|
70
|
+
});
|
|
@@ -16,7 +16,7 @@ function genericMotionOnly(value) {
|
|
|
16
16
|
return basic.test(text) && !structural.test(text);
|
|
17
17
|
}
|
|
18
18
|
export function validateRuleControls(plan, registry, reflexFonts, verification) {
|
|
19
|
-
if (plan.doctrineVersion !== 2)
|
|
19
|
+
if (plan.doctrineVersion !== 2 && plan.doctrineVersion !== 3)
|
|
20
20
|
return [];
|
|
21
21
|
const errors = [];
|
|
22
22
|
const rules = new Map(registry.rules.map((rule) => [rule.id, rule]));
|