dreative 0.5.1 → 0.5.2
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/README.md +49 -27
- package/dist/cli/audit.js +206 -0
- package/dist/cli/audit.test.js +79 -0
- package/dist/cli/docsCheck.js +163 -0
- package/dist/cli/docsCheck.test.js +8 -0
- package/dist/cli/index.js +45 -9
- package/dist/server/preview.js +73 -73
- package/dist/server/store.js +11 -0
- package/dist/shared/artifacts.js +162 -0
- package/dist/shared/design.js +42 -22
- package/dist/shared/ruleSystem.js +130 -0
- package/dist/shared/ruleSystem.test.js +187 -0
- package/dist/shared/skillSystem.js +173 -0
- package/dist/shared/skillSystem.test.js +110 -0
- package/dist/ui/assets/{index--vztc_MR.js → index-CKwmbx2j.js} +13 -13
- package/dist/ui/index.html +12 -12
- package/package.json +5 -3
- package/skill/dreative/DESIGN.md +121 -93
- package/skill/dreative/PLAN.md +229 -92
- package/skill/dreative/SKILL.md +218 -137
- package/skill/dreative/frameworks/nextjs.md +13 -0
- package/skill/dreative/frameworks/react-vite.md +12 -0
- package/skill/dreative/frameworks/styling.md +14 -0
- package/skill/dreative/frameworks/sveltekit.md +10 -0
- package/skill/dreative/frameworks/vue-nuxt.md +12 -0
- package/skill/dreative/recipes/3d-recipes.md +44 -0
- package/skill/dreative/recipes/cinematic-recipes.md +19 -0
- package/skill/dreative/recipes/immersive-recipes.md +18 -0
- package/skill/dreative/recipes/media-recipes.md +60 -0
- package/skill/dreative/recipes/motion-recipes.md +44 -0
- package/skill/dreative/references/ARTIFACTS.md +180 -0
- package/skill/dreative/references/REFLEX_FONTS.json +44 -0
- package/skill/dreative/references/RULES.json +186 -0
- package/skill/dreative/references/SKILL_CONTRACT.md +30 -0
- package/skill/dreative/references/TIERS.md +42 -0
- package/skill/dreative/schemas/plan.schema.json +241 -0
- package/skill/dreative/schemas/verify.schema.json +61 -0
- package/skill/dreative/skills/3d.md +94 -267
- package/skill/dreative/skills/cinematic.md +56 -232
- package/skill/dreative/skills/experimental.md +108 -95
- package/skill/dreative/skills/immersive.md +61 -223
- package/skill/dreative/skills/interaction.md +9 -1
- package/skill/dreative/skills/media.md +135 -564
- package/skill/dreative/skills/mobile.md +128 -117
- package/skill/dreative/skills/motion.md +89 -256
- package/skill/dreative/skills/refined.md +116 -102
- package/skill/dreative/skills/ux.md +155 -144
- package/dist/server/ai.js +0 -177
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { validateRuleControls } from "./ruleSystem.js";
|
|
6
|
+
const registry = JSON.parse(fs.readFileSync(path.resolve("skill/dreative/references/RULES.json"), "utf-8"));
|
|
7
|
+
const reflex = JSON.parse(fs.readFileSync(path.resolve("skill/dreative/references/REFLEX_FONTS.json"), "utf-8"));
|
|
8
|
+
const planned = "2026-01-01T00:00:00.000Z";
|
|
9
|
+
const started = "2026-01-01T01:00:00.000Z";
|
|
10
|
+
function basePlan(tier = "award") {
|
|
11
|
+
return {
|
|
12
|
+
version: 2,
|
|
13
|
+
doctrineVersion: 2,
|
|
14
|
+
request: "Create an ambitious archival experience",
|
|
15
|
+
createdAt: planned,
|
|
16
|
+
implementationStartedAt: started,
|
|
17
|
+
tier,
|
|
18
|
+
depth: "reimagine",
|
|
19
|
+
skills: ["ux", "mobile", "motion", "media"],
|
|
20
|
+
skillPolicy: { mode: "hybrid", global: ["ux", "mobile"], routingApproved: true, userAssignments: [] },
|
|
21
|
+
designRead: { register: "archival editorial", concept: "living catalog", signature: "transforming specimen" },
|
|
22
|
+
fontDecision: {
|
|
23
|
+
selected: "Source Serif 4",
|
|
24
|
+
candidates: [
|
|
25
|
+
{ name: "Source Serif 4", reflex: false, rationale: "Variable optical sizes support the archival scale shifts." },
|
|
26
|
+
{ name: "Newsreader", reflex: false, rationale: "Editorial contrast supports document-led hierarchy." },
|
|
27
|
+
{ name: "Archivo", reflex: false, rationale: "Catalog labeling connects directly to the archival subject." },
|
|
28
|
+
],
|
|
29
|
+
recentDisplayFonts: [],
|
|
30
|
+
},
|
|
31
|
+
creativeStrategy: {
|
|
32
|
+
path: "diversity",
|
|
33
|
+
mechanisms: ["variable-type morph", "depth-stack", "drag-inertia", "mask-dissolve"],
|
|
34
|
+
drivers: ["scroll", "pointer", "time"],
|
|
35
|
+
},
|
|
36
|
+
pages: [
|
|
37
|
+
{
|
|
38
|
+
id: "home",
|
|
39
|
+
name: "Home",
|
|
40
|
+
skills: ["ux", "mobile", "motion", "media"],
|
|
41
|
+
sections: [
|
|
42
|
+
{
|
|
43
|
+
id: "hero",
|
|
44
|
+
name: "Hero",
|
|
45
|
+
layoutFamily: "archival-index",
|
|
46
|
+
skills: ["ux", "mobile", "motion", "media"],
|
|
47
|
+
interactions: ["drag specimen"],
|
|
48
|
+
mobile: "stacked specimen",
|
|
49
|
+
fallback: "static catalog",
|
|
50
|
+
verification: ["Signature responds to input"],
|
|
51
|
+
assets: [],
|
|
52
|
+
status: "shipped",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
preservationManifest: ".dreative/preservation.json",
|
|
58
|
+
decisionLedger: ".dreative/ledger.json",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function verification(...ids) {
|
|
62
|
+
return {
|
|
63
|
+
version: 1,
|
|
64
|
+
generatedAt: started,
|
|
65
|
+
evidence: ids.map((id) => ({
|
|
66
|
+
id,
|
|
67
|
+
criterion: id,
|
|
68
|
+
status: "pass",
|
|
69
|
+
evidence: "Captured runtime behavior in the production build.",
|
|
70
|
+
proof: { timestamp: started, testedUrl: "http://localhost:3000", consoleErrorCount: 0 },
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
test("valid default award plan", () => {
|
|
75
|
+
assert.deepEqual(validateRuleControls(basePlan(), registry, reflex, verification()), []);
|
|
76
|
+
});
|
|
77
|
+
test("valid 3D substitution using spatial typography", () => {
|
|
78
|
+
const plan = basePlan();
|
|
79
|
+
plan.ruleExceptions = [
|
|
80
|
+
{
|
|
81
|
+
ruleId: "award.spatialSignature",
|
|
82
|
+
decision: "substituted",
|
|
83
|
+
declaredAt: planned,
|
|
84
|
+
reason: "The archival concept has no physical subject, so a product-like prop would misrepresent the material.",
|
|
85
|
+
alternative: "A persistent variable-type specimen becomes index, spatial architecture, and interactive control across four chapters.",
|
|
86
|
+
successCriteria: ["The specimen appears in three materially different roles", "Pointer and scroll input visibly reshape its depth"],
|
|
87
|
+
evidenceIds: ["spatial-type-desktop", "spatial-type-mobile"],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification("spatial-type-desktop", "spatial-type-mobile")), []);
|
|
91
|
+
});
|
|
92
|
+
test("invalid vague 3D exception", () => {
|
|
93
|
+
const plan = basePlan();
|
|
94
|
+
plan.ruleExceptions = [
|
|
95
|
+
{
|
|
96
|
+
ruleId: "award.spatialSignature",
|
|
97
|
+
decision: "substituted",
|
|
98
|
+
declaredAt: planned,
|
|
99
|
+
reason: "It did not fit",
|
|
100
|
+
alternative: "Typography felt better",
|
|
101
|
+
successCriteria: ["Looks good", "Feels spatial"],
|
|
102
|
+
evidenceIds: ["type"],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
const errors = validateRuleControls(plan, registry, reflex, verification("type"));
|
|
106
|
+
assert.ok(errors.some((error) => error.includes("vague")));
|
|
107
|
+
});
|
|
108
|
+
test("attempted hard-gate exception", () => {
|
|
109
|
+
const plan = basePlan();
|
|
110
|
+
plan.ruleExceptions = [
|
|
111
|
+
{
|
|
112
|
+
ruleId: "preservation.noLoss",
|
|
113
|
+
decision: "substituted",
|
|
114
|
+
declaredAt: planned,
|
|
115
|
+
reason: "The old form was intentionally removed to simplify the new conversion route for this campaign.",
|
|
116
|
+
alternative: "A replacement conversion control carries the same submitted data into the existing backend workflow.",
|
|
117
|
+
successCriteria: ["Users can submit the same required fields", "The existing backend receives an equivalent payload"],
|
|
118
|
+
evidenceIds: ["form"],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
assert.ok(validateRuleControls(plan, registry, reflex, verification("form")).some((error) => error.includes("cannot be substituted")));
|
|
122
|
+
});
|
|
123
|
+
test("valid diversity plan", () => {
|
|
124
|
+
const plan = basePlan("expressive");
|
|
125
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification()), []);
|
|
126
|
+
});
|
|
127
|
+
test("valid development plan", () => {
|
|
128
|
+
const plan = basePlan();
|
|
129
|
+
plan.creativeStrategy = {
|
|
130
|
+
path: "development",
|
|
131
|
+
signatureMechanism: "An archival sheet transforms across the complete journey",
|
|
132
|
+
states: ["Loose sheets form a catalog index", "Sheets become a draggable research surface", "Sheets collapse into a spatial tunnel"],
|
|
133
|
+
secondaryMechanisms: ["kinetic labels", "material lighting"],
|
|
134
|
+
drivers: ["scroll", "pointer"],
|
|
135
|
+
};
|
|
136
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification()), []);
|
|
137
|
+
});
|
|
138
|
+
test("invalid repeated-mechanism plan", () => {
|
|
139
|
+
const plan = basePlan();
|
|
140
|
+
plan.creativeStrategy = { path: "diversity", mechanisms: ["fade", "fade", "fade", "fade"], drivers: ["scroll", "scroll", "scroll"] };
|
|
141
|
+
const errors = validateRuleControls(plan, registry, reflex, verification());
|
|
142
|
+
assert.ok(errors.some((error) => error.includes("distinct mechanisms")));
|
|
143
|
+
});
|
|
144
|
+
test("valid reflex-font justification", () => {
|
|
145
|
+
const plan = basePlan("premium");
|
|
146
|
+
plan.fontDecision = {
|
|
147
|
+
selected: "Inter",
|
|
148
|
+
candidates: [
|
|
149
|
+
{ name: "Inter", reflex: true, rationale: "Existing authenticated product metrics and density depend on it." },
|
|
150
|
+
{ name: "Switzer", reflex: false, rationale: "Comparable product clarity with more character." },
|
|
151
|
+
{ name: "Source Sans 3", reflex: false, rationale: "Broad language support and efficient variable delivery." },
|
|
152
|
+
],
|
|
153
|
+
recentDisplayFonts: [],
|
|
154
|
+
reasonKinds: ["existing-brand", "design-system-metrics"],
|
|
155
|
+
justification: "Inter is embedded throughout the authenticated product, preserves established density metrics, and avoids layout regressions across translated views.",
|
|
156
|
+
};
|
|
157
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification()), []);
|
|
158
|
+
});
|
|
159
|
+
test("invalid generic font justification", () => {
|
|
160
|
+
const plan = basePlan("premium");
|
|
161
|
+
plan.fontDecision = {
|
|
162
|
+
selected: "Inter",
|
|
163
|
+
candidates: [
|
|
164
|
+
{ name: "Inter", reflex: true, rationale: "A familiar interface choice." },
|
|
165
|
+
{ name: "Switzer", reflex: false, rationale: "A product-oriented alternative." },
|
|
166
|
+
{ name: "Source Sans 3", reflex: false, rationale: "A broad-language alternative." },
|
|
167
|
+
],
|
|
168
|
+
recentDisplayFonts: [],
|
|
169
|
+
reasonKinds: [],
|
|
170
|
+
justification: "Inter looks clean and modern for this interface.",
|
|
171
|
+
};
|
|
172
|
+
assert.ok(validateRuleControls(plan, registry, reflex, verification()).some((error) => error.includes("non-generic")));
|
|
173
|
+
});
|
|
174
|
+
test("experimental exploration selects only two shipped provocations", () => {
|
|
175
|
+
const plan = basePlan("expressive");
|
|
176
|
+
plan.skills.push("experimental");
|
|
177
|
+
plan.pages[0].skills.push("experimental");
|
|
178
|
+
plan.experimentalPlan = {
|
|
179
|
+
majorSectionIds: ["hero", "archive", "closing"],
|
|
180
|
+
candidates: [
|
|
181
|
+
{ id: "p1", sectionId: "hero", idea: "The title behaves as a physical catalog drawer opened by drag.", selected: true },
|
|
182
|
+
{ id: "p2", sectionId: "archive", idea: "Research sheets reorganize into a navigable spatial evidence wall.", selected: true },
|
|
183
|
+
{ id: "p3", sectionId: "closing", idea: "The final mark briefly assembles from all prior catalog coordinates.", selected: false },
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification()), []);
|
|
187
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export const SKILL_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
name: "ux",
|
|
4
|
+
description: "Working navigation, forms, states, keyboard access, focus, and interaction audits.",
|
|
5
|
+
dependencies: [],
|
|
6
|
+
signature: /\b(ux|accessib|a11y|forms?|navigation|keyboard|focus|states?|dashboard|product ui)\b/i,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "mobile",
|
|
10
|
+
description: "Mobile-native composition, touch ergonomics, responsive effects, and phone verification.",
|
|
11
|
+
dependencies: ["ux"],
|
|
12
|
+
signature: /\b(mobile|phone|tablet|responsive|touch|thumb|small screen)\b/i,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "refined",
|
|
16
|
+
description: "Premium clean business, DTC, and commerce design with restraint and strong photography.",
|
|
17
|
+
dependencies: ["ux", "mobile"],
|
|
18
|
+
signature: /\b(clean (and )?(modern|professional)|modern clean|business (site|website|use)|corporate|professional look|premium (minimal|clean)|dtc|d2c|e[- ]?commerce|ecommerce|shopify|calm|understated)\b/i,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "motion",
|
|
22
|
+
description: "Scroll choreography, entrances, kinetic type, springs, parallax, and transitions.",
|
|
23
|
+
dependencies: ["ux", "mobile"],
|
|
24
|
+
signature: /\b(animat|motion|parallax|scroll[- ]?(driven|trigger|story|choreo)|gsap|framer|lenis|marquee|kinetic|stagger|reveal|transition)\b/i,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "interaction",
|
|
28
|
+
description: "Hover craft, magnetic controls, cursor effects, tactile feedback, and state morphs.",
|
|
29
|
+
dependencies: ["ux", "mobile"],
|
|
30
|
+
signature: /\b(micro[- ]?interaction|hover (effect|state)s?|cursor|magnetic|tilt|spotlight|glow|ripple|tactile|interactive|draggable)\b/i,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "media",
|
|
34
|
+
description: "Generated and sourced image/video production, grading, motion treatments, and media planes.",
|
|
35
|
+
dependencies: ["ux", "mobile"],
|
|
36
|
+
signature: /\b(generated? (image|video|media)|hero video|image treatment|media plane|gallery|photography|video loop|distortion|living thumbnail)\b/i,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "3d",
|
|
40
|
+
description: "Three.js/R3F, WebGL, shaders, particles, models, lighting, and robust fallbacks.",
|
|
41
|
+
dependencies: ["ux", "mobile"],
|
|
42
|
+
signature: /\b(3d|three\.?js|webgl|r3f|shader|glsl|particles?|point ?cloud|globe|mesh gradient|orbit|spline)\b/i,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "immersive",
|
|
46
|
+
description: "Persistent spatial stages, preloaders, scene transitions, and scroll-as-journey.",
|
|
47
|
+
dependencies: ["motion", "interaction", "media", "ux", "mobile"],
|
|
48
|
+
signature: /\b(immersive|award[- ]?(site|winning)|awwwards|spatial|scene[- ]?based|world|camera (move|travel|path)|preloader|scroll (journey|story|as travel)|explorable|experience site)\b/i,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "cinematic",
|
|
52
|
+
description: "Shader-graded experiential interfaces, living surfaces, gesture exploration, and sound.",
|
|
53
|
+
dependencies: ["motion", "interaction", "media", "ux", "mobile"],
|
|
54
|
+
signature: /\b(cinematic|experiential|unseen\.co|fluid (sim(ulation)?|distortion)|gpgpu|drag[- ]to[- ]explore|click ?(&|and) ?hold|film grain|graded|velocity[- ]reactive|sound design|ambient (audio|sound))\b/i,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "experimental",
|
|
58
|
+
description: "Unconventional composition, material shifts, provocations, and high-variance creative direction.",
|
|
59
|
+
dependencies: ["motion", "interaction", "media", "ux", "mobile"],
|
|
60
|
+
signature: /\b(experimental|crazy|bizarre|never seen before|more creative|wow factor|unconventional|unexpected|provocation)\b/i,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const BY_NAME = new Map(SKILL_DEFINITIONS.map((skill) => [skill.name, skill]));
|
|
64
|
+
export function detectSpecialistSkills(texts) {
|
|
65
|
+
const haystack = texts.filter(Boolean).join("\n");
|
|
66
|
+
return SKILL_DEFINITIONS.filter((skill) => skill.signature.test(haystack)).map((skill) => skill.name);
|
|
67
|
+
}
|
|
68
|
+
export function resolveSkillDependencies(requested) {
|
|
69
|
+
const resolved = new Set(["ux", "mobile"]);
|
|
70
|
+
const visit = (name) => {
|
|
71
|
+
if (resolved.has(name))
|
|
72
|
+
return;
|
|
73
|
+
for (const dependency of BY_NAME.get(name)?.dependencies ?? [])
|
|
74
|
+
visit(dependency);
|
|
75
|
+
resolved.add(name);
|
|
76
|
+
};
|
|
77
|
+
for (const name of requested)
|
|
78
|
+
visit(name);
|
|
79
|
+
return SKILL_DEFINITIONS.map((skill) => skill.name).filter((name) => resolved.has(name));
|
|
80
|
+
}
|
|
81
|
+
function skillScore(name, texts) {
|
|
82
|
+
const definition = BY_NAME.get(name);
|
|
83
|
+
if (!definition)
|
|
84
|
+
return 0;
|
|
85
|
+
const haystack = texts.filter(Boolean).join("\n");
|
|
86
|
+
const flags = definition.signature.flags.includes("g") ? definition.signature.flags : `${definition.signature.flags}g`;
|
|
87
|
+
return [...haystack.matchAll(new RegExp(definition.signature.source, flags))].length;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hybrid multi-page routing: user selection is authoritative, explicit page
|
|
91
|
+
* assignments are locked, and only selected-but-unassigned treatments are
|
|
92
|
+
* placed automatically. Detected unselected skills are returned as suggestions
|
|
93
|
+
* and never activated silently.
|
|
94
|
+
*/
|
|
95
|
+
export function routeSkillsAcrossPages(request) {
|
|
96
|
+
const selected = [...new Set(request.selected)];
|
|
97
|
+
const resolved = resolveSkillDependencies(selected);
|
|
98
|
+
const byPageSets = new Map(request.pages.map((page) => [page.id, new Set(["ux", "mobile"])]));
|
|
99
|
+
for (const [pageId, skills] of Object.entries(request.assignments ?? {})) {
|
|
100
|
+
const target = byPageSets.get(pageId);
|
|
101
|
+
if (!target)
|
|
102
|
+
continue;
|
|
103
|
+
for (const skill of skills) {
|
|
104
|
+
if (!resolved.includes(skill))
|
|
105
|
+
continue;
|
|
106
|
+
for (const dependency of resolveSkillDependencies([skill]))
|
|
107
|
+
target.add(dependency);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const optionalSelected = selected.filter((skill) => skill !== "ux" && skill !== "mobile");
|
|
111
|
+
const unassigned = optionalSelected.filter((skill) => ![...byPageSets.values()].some((skills) => skills.has(skill)));
|
|
112
|
+
if (request.autoRouteUnassigned !== false && request.pages.length > 0) {
|
|
113
|
+
for (const skill of unassigned) {
|
|
114
|
+
const ranked = request.pages
|
|
115
|
+
.map((page, index) => ({
|
|
116
|
+
page,
|
|
117
|
+
index,
|
|
118
|
+
score: skillScore(skill, page.texts),
|
|
119
|
+
load: [...(byPageSets.get(page.id) ?? [])].filter((name) => name !== "ux" && name !== "mobile").length,
|
|
120
|
+
}))
|
|
121
|
+
.sort((a, b) => b.score - a.score || a.load - b.load || a.index - b.index);
|
|
122
|
+
const target = byPageSets.get(ranked[0].page.id);
|
|
123
|
+
for (const dependency of resolveSkillDependencies([skill]))
|
|
124
|
+
target.add(dependency);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const suggestions = {};
|
|
128
|
+
for (const page of request.pages) {
|
|
129
|
+
suggestions[page.id] = detectSpecialistSkills(page.texts).filter((skill) => !resolved.includes(skill));
|
|
130
|
+
}
|
|
131
|
+
const byPage = Object.fromEntries(request.pages.map((page) => [
|
|
132
|
+
page.id,
|
|
133
|
+
SKILL_DEFINITIONS.map((definition) => definition.name).filter((name) => byPageSets.get(page.id)?.has(name)),
|
|
134
|
+
]));
|
|
135
|
+
return {
|
|
136
|
+
selected,
|
|
137
|
+
resolved,
|
|
138
|
+
byPage,
|
|
139
|
+
suggestions,
|
|
140
|
+
unassigned: request.autoRouteUnassigned === false ? unassigned : [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function resolveAmbitionTier(input) {
|
|
144
|
+
const haystack = [input.aesthetic, ...(input.texts ?? [])].filter(Boolean).join(" ");
|
|
145
|
+
if (/\b(award|awwwards|immersive|cinematic|experimental|unseen\.co)\b/i.test(haystack) || input.motion >= 9)
|
|
146
|
+
return "award";
|
|
147
|
+
if (input.motion >= 7 || input.variance >= 8)
|
|
148
|
+
return "expressive";
|
|
149
|
+
if (input.motion >= 4 || input.variance >= 5)
|
|
150
|
+
return "premium";
|
|
151
|
+
return "solid";
|
|
152
|
+
}
|
|
153
|
+
export const TIER_REQUIREMENTS = {
|
|
154
|
+
solid: [
|
|
155
|
+
"All existing behavior is preserved and the UX audit passes.",
|
|
156
|
+
"Desktop and mobile layouts are verified with keyboard and reduced-motion coverage.",
|
|
157
|
+
],
|
|
158
|
+
premium: [
|
|
159
|
+
"Solid-tier requirements pass.",
|
|
160
|
+
"The page has a clear design read, one signature detail, and a completed craft pass.",
|
|
161
|
+
"Media is intentionally cropped/graded and interaction states are designed.",
|
|
162
|
+
],
|
|
163
|
+
expressive: [
|
|
164
|
+
"Premium-tier requirements pass.",
|
|
165
|
+
"At least three distinct, purposeful motion or interaction moments ship with fallbacks.",
|
|
166
|
+
"Heavy effects are measured on desktop and translated deliberately for mobile.",
|
|
167
|
+
],
|
|
168
|
+
award: [
|
|
169
|
+
"Expressive-tier requirements pass.",
|
|
170
|
+
"The experience has a distinctive spatial or media system, not isolated decoration.",
|
|
171
|
+
"Runtime, asset-weight, frame-time, occlusion, and fallback evidence is recorded.",
|
|
172
|
+
],
|
|
173
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { detectSpecialistSkills, resolveAmbitionTier, resolveSkillDependencies, routeSkillsAcrossPages } from "./skillSystem.js";
|
|
4
|
+
import { validatePlan, validatePreservationManifest, validateVerificationReport } from "./artifacts.js";
|
|
5
|
+
test("detects every previously un-routed specialist family", () => {
|
|
6
|
+
assert.deepEqual(detectSpecialistSkills(["experimental mobile-first gallery with generated video"]), [
|
|
7
|
+
"mobile",
|
|
8
|
+
"media",
|
|
9
|
+
"experimental",
|
|
10
|
+
]);
|
|
11
|
+
});
|
|
12
|
+
test("resolves universal and transitive skill dependencies", () => {
|
|
13
|
+
assert.deepEqual(resolveSkillDependencies(["cinematic"]), ["ux", "mobile", "motion", "interaction", "media", "cinematic"]);
|
|
14
|
+
});
|
|
15
|
+
test("resolves ambition without forcing award tier", () => {
|
|
16
|
+
assert.equal(resolveAmbitionTier({ variance: 3, motion: 2, aesthetic: "trust" }), "solid");
|
|
17
|
+
assert.equal(resolveAmbitionTier({ variance: 7, motion: 7 }), "expressive");
|
|
18
|
+
assert.equal(resolveAmbitionTier({ variance: 7, motion: 7, texts: ["Awwwards immersive world"] }), "award");
|
|
19
|
+
});
|
|
20
|
+
test("hybrid routing honors page pins and never activates unselected suggestions", () => {
|
|
21
|
+
const routed = routeSkillsAcrossPages({
|
|
22
|
+
pages: [
|
|
23
|
+
{ id: "home", texts: ["cinematic hero with a video loop and 3d globe"] },
|
|
24
|
+
{ id: "shop", texts: ["clean ecommerce catalog"] },
|
|
25
|
+
],
|
|
26
|
+
selected: ["cinematic", "refined"],
|
|
27
|
+
assignments: { shop: ["refined"] },
|
|
28
|
+
});
|
|
29
|
+
assert.ok(routed.byPage.shop.includes("refined"));
|
|
30
|
+
assert.ok(routed.byPage.home.includes("cinematic"));
|
|
31
|
+
assert.ok(routed.byPage.home.includes("motion"));
|
|
32
|
+
assert.ok(!routed.byPage.home.includes("3d"));
|
|
33
|
+
assert.ok(!routed.byPage.shop.includes("3d"));
|
|
34
|
+
assert.ok(routed.suggestions.home.includes("3d"));
|
|
35
|
+
});
|
|
36
|
+
test("select all covers all skills across the build without putting all on every page", () => {
|
|
37
|
+
const all = ["ux", "mobile", "refined", "motion", "interaction", "media", "3d", "immersive", "cinematic", "experimental"];
|
|
38
|
+
const routed = routeSkillsAcrossPages({
|
|
39
|
+
pages: [
|
|
40
|
+
{ id: "home", texts: ["cinematic immersive hero"] },
|
|
41
|
+
{ id: "product", texts: ["3d product"] },
|
|
42
|
+
{ id: "about", texts: ["experimental editorial"] },
|
|
43
|
+
],
|
|
44
|
+
selected: [...all],
|
|
45
|
+
});
|
|
46
|
+
const used = new Set(Object.values(routed.byPage).flat());
|
|
47
|
+
assert.deepEqual([...all].filter((skill) => !used.has(skill)), []);
|
|
48
|
+
assert.ok(Object.values(routed.byPage).some((skills) => skills.length < all.length));
|
|
49
|
+
});
|
|
50
|
+
test("routing can be disabled while waiting for explicit page assignments", () => {
|
|
51
|
+
const routed = routeSkillsAcrossPages({
|
|
52
|
+
pages: [{ id: "home", texts: ["animated landing page"] }],
|
|
53
|
+
selected: ["motion"],
|
|
54
|
+
autoRouteUnassigned: false,
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(routed.unassigned, ["motion"]);
|
|
57
|
+
assert.deepEqual(routed.byPage.home, ["ux", "mobile"]);
|
|
58
|
+
});
|
|
59
|
+
test("plan validator rejects unfinished sections", () => {
|
|
60
|
+
const errors = validatePlan({
|
|
61
|
+
version: 2,
|
|
62
|
+
request: "Redesign",
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
tier: "premium",
|
|
65
|
+
depth: "restructure",
|
|
66
|
+
skills: ["ux", "mobile"],
|
|
67
|
+
skillPolicy: { mode: "hybrid", global: ["ux", "mobile"], routingApproved: true, userAssignments: [] },
|
|
68
|
+
designRead: { register: "editorial", concept: "signal", signature: "rail" },
|
|
69
|
+
preservationManifest: ".dreative/preservation.json",
|
|
70
|
+
decisionLedger: ".dreative/ledger.json",
|
|
71
|
+
pages: [
|
|
72
|
+
{
|
|
73
|
+
id: "home",
|
|
74
|
+
name: "Home",
|
|
75
|
+
skills: ["ux", "mobile"],
|
|
76
|
+
sections: [
|
|
77
|
+
{
|
|
78
|
+
id: "hero",
|
|
79
|
+
name: "Hero",
|
|
80
|
+
layoutFamily: "split",
|
|
81
|
+
skills: ["ux", "mobile"],
|
|
82
|
+
interactions: [],
|
|
83
|
+
mobile: "stack",
|
|
84
|
+
fallback: "static",
|
|
85
|
+
verification: ["Headline visible"],
|
|
86
|
+
assets: [],
|
|
87
|
+
status: "planned",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
assert.ok(errors.some((error) => error.includes("still planned")));
|
|
94
|
+
});
|
|
95
|
+
test("preservation validator requires reasons for intentional changes", () => {
|
|
96
|
+
const errors = validatePreservationManifest({
|
|
97
|
+
version: 1,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
items: [{ id: "cta", kind: "link", file: "src/App.tsx", needle: "/buy", purpose: "Purchase", intentionallyChanged: true }],
|
|
100
|
+
});
|
|
101
|
+
assert.ok(errors.some((error) => error.includes("changeReason")));
|
|
102
|
+
});
|
|
103
|
+
test("verification rejects text-only self-reporting", () => {
|
|
104
|
+
const errors = validateVerificationReport({
|
|
105
|
+
version: 1,
|
|
106
|
+
generatedAt: new Date().toISOString(),
|
|
107
|
+
evidence: [{ id: "mobile", criterion: "Works at 390px", status: "pass", evidence: "Tested at 390px" }],
|
|
108
|
+
});
|
|
109
|
+
assert.ok(errors.some((error) => error.includes("proof")));
|
|
110
|
+
});
|