dreative 0.5.1 → 0.5.3
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 +220 -0
- package/dist/shared/design.js +45 -24
- package/dist/shared/ruleSystem.js +220 -0
- package/dist/shared/ruleSystem.test.js +262 -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 +144 -108
- package/skill/dreative/PLAN.md +285 -116
- package/skill/dreative/SKILL.md +237 -144
- 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 +68 -0
- package/skill/dreative/references/ARTIFACTS.md +196 -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 +44 -0
- package/skill/dreative/schemas/plan.schema.json +309 -0
- package/skill/dreative/schemas/verify.schema.json +75 -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 +149 -564
- package/skill/dreative/skills/mobile.md +129 -117
- package/skill/dreative/skills/motion.md +126 -256
- package/skill/dreative/skills/refined.md +116 -102
- package/skill/dreative/skills/ux.md +155 -144
- package/dist/server/ai.js +0 -177
package/dist/server/preview.js
CHANGED
|
@@ -7,11 +7,11 @@ const here = path.dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const packageRoot = path.resolve(here, "..", "..");
|
|
8
8
|
/** Bundle a generated page component + mount shim into one iframe-ready JS file. */
|
|
9
9
|
export async function buildPreview(generatedFile) {
|
|
10
|
-
const entry = `
|
|
11
|
-
import React from "react";
|
|
12
|
-
import { createRoot } from "react-dom/client";
|
|
13
|
-
import Page from ${JSON.stringify(generatedFile.replace(/\\/g, "/"))};
|
|
14
|
-
createRoot(document.getElementById("root")).render(React.createElement(Page));
|
|
10
|
+
const entry = `
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { createRoot } from "react-dom/client";
|
|
13
|
+
import Page from ${JSON.stringify(generatedFile.replace(/\\/g, "/"))};
|
|
14
|
+
createRoot(document.getElementById("root")).render(React.createElement(Page));
|
|
15
15
|
`;
|
|
16
16
|
const result = await esbuild.build({
|
|
17
17
|
stdin: {
|
|
@@ -30,79 +30,79 @@ export async function buildPreview(generatedFile) {
|
|
|
30
30
|
return result.outputFiles[0].text;
|
|
31
31
|
}
|
|
32
32
|
export function previewHtml(scriptUrl) {
|
|
33
|
-
return `<!doctype html>
|
|
34
|
-
<html>
|
|
35
|
-
<head>
|
|
36
|
-
<meta charset="utf-8" />
|
|
37
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
38
|
-
<style>body{margin:0}</style>
|
|
39
|
-
</head>
|
|
40
|
-
<body>
|
|
41
|
-
<div id="root"></div>
|
|
42
|
-
<script>
|
|
43
|
-
document.addEventListener("click", (e) => {
|
|
44
|
-
const el = e.target.closest("[data-dreative-id]");
|
|
45
|
-
if (!el) return;
|
|
46
|
-
e.preventDefault();
|
|
47
|
-
parent.postMessage({ type: "dreative-select", id: el.getAttribute("data-dreative-id") }, "*");
|
|
48
|
-
}, true);
|
|
49
|
-
document.addEventListener("mouseover", (e) => {
|
|
50
|
-
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
51
|
-
const el = e.target.closest("[data-dreative-id]");
|
|
52
|
-
if (el) el.classList.add("dreative-hover");
|
|
53
|
-
});
|
|
54
|
-
</script>
|
|
55
|
-
<style>.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}</style>
|
|
56
|
-
<script src="${scriptUrl}"></script>
|
|
57
|
-
</body>
|
|
33
|
+
return `<!doctype html>
|
|
34
|
+
<html>
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8" />
|
|
37
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
38
|
+
<style>body{margin:0}</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="root"></div>
|
|
42
|
+
<script>
|
|
43
|
+
document.addEventListener("click", (e) => {
|
|
44
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
45
|
+
if (!el) return;
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
parent.postMessage({ type: "dreative-select", id: el.getAttribute("data-dreative-id") }, "*");
|
|
48
|
+
}, true);
|
|
49
|
+
document.addEventListener("mouseover", (e) => {
|
|
50
|
+
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
51
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
52
|
+
if (el) el.classList.add("dreative-hover");
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
<style>.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}</style>
|
|
56
|
+
<script src="${scriptUrl}"></script>
|
|
57
|
+
</body>
|
|
58
58
|
</html>`;
|
|
59
59
|
}
|
|
60
60
|
/** Replica view: 1:1 stripped page. Interactions are inert; hover shows the
|
|
61
61
|
* agent-written summary of what each element does; click selects the block. */
|
|
62
62
|
export function replicaHtml(scriptUrl, summaries) {
|
|
63
|
-
return `<!doctype html>
|
|
64
|
-
<html>
|
|
65
|
-
<head>
|
|
66
|
-
<meta charset="utf-8" />
|
|
67
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
68
|
-
<style>body{margin:0}
|
|
69
|
-
.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}
|
|
70
|
-
#dreative-tip{position:fixed;z-index:99999;max-width:340px;background:#16181f;color:#e8e8ea;border:1px solid #2c2f3a;border-radius:8px;padding:8px 10px;font:12px/1.45 ui-sans-serif,system-ui;pointer-events:none;display:none;box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
|
71
|
-
#dreative-tip b{color:#a5b4fc}
|
|
72
|
-
</style>
|
|
73
|
-
</head>
|
|
74
|
-
<body>
|
|
75
|
-
<div id="root"></div>
|
|
76
|
-
<div id="dreative-tip"></div>
|
|
77
|
-
<script>
|
|
78
|
-
const SUMMARIES = ${JSON.stringify(summaries)};
|
|
79
|
-
// replica is a mockup: block navigation/submits, keep selection working
|
|
80
|
-
document.addEventListener("click", (e) => {
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
const el = e.target.closest("[data-dreative-id]");
|
|
83
|
-
if (el) parent.postMessage({ type: "dreative-select-block", id: el.getAttribute("data-dreative-id") }, "*");
|
|
84
|
-
}, true);
|
|
85
|
-
document.addEventListener("submit", (e) => e.preventDefault(), true);
|
|
86
|
-
const tip = document.getElementById("dreative-tip");
|
|
87
|
-
document.addEventListener("mousemove", (e) => {
|
|
88
|
-
if (tip.style.display === "block") {
|
|
89
|
-
tip.style.left = Math.min(e.clientX + 14, innerWidth - 360) + "px";
|
|
90
|
-
tip.style.top = Math.min(e.clientY + 14, innerHeight - 80) + "px";
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
document.addEventListener("mouseover", (e) => {
|
|
94
|
-
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
95
|
-
const el = e.target.closest("[data-dreative-id]");
|
|
96
|
-
if (!el) { tip.style.display = "none"; return; }
|
|
97
|
-
el.classList.add("dreative-hover");
|
|
98
|
-
const id = el.getAttribute("data-dreative-id");
|
|
99
|
-
const s = SUMMARIES[id];
|
|
100
|
-
if (s) { tip.innerHTML = "<b>" + id + "</b><br>" + s.replace(/</g, "<"); tip.style.display = "block"; }
|
|
101
|
-
else tip.style.display = "none";
|
|
102
|
-
});
|
|
103
|
-
</script>
|
|
104
|
-
<script src="${scriptUrl}"></script>
|
|
105
|
-
</body>
|
|
63
|
+
return `<!doctype html>
|
|
64
|
+
<html>
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8" />
|
|
67
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
68
|
+
<style>body{margin:0}
|
|
69
|
+
.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}
|
|
70
|
+
#dreative-tip{position:fixed;z-index:99999;max-width:340px;background:#16181f;color:#e8e8ea;border:1px solid #2c2f3a;border-radius:8px;padding:8px 10px;font:12px/1.45 ui-sans-serif,system-ui;pointer-events:none;display:none;box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
|
71
|
+
#dreative-tip b{color:#a5b4fc}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div id="root"></div>
|
|
76
|
+
<div id="dreative-tip"></div>
|
|
77
|
+
<script>
|
|
78
|
+
const SUMMARIES = ${JSON.stringify(summaries)};
|
|
79
|
+
// replica is a mockup: block navigation/submits, keep selection working
|
|
80
|
+
document.addEventListener("click", (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
83
|
+
if (el) parent.postMessage({ type: "dreative-select-block", id: el.getAttribute("data-dreative-id") }, "*");
|
|
84
|
+
}, true);
|
|
85
|
+
document.addEventListener("submit", (e) => e.preventDefault(), true);
|
|
86
|
+
const tip = document.getElementById("dreative-tip");
|
|
87
|
+
document.addEventListener("mousemove", (e) => {
|
|
88
|
+
if (tip.style.display === "block") {
|
|
89
|
+
tip.style.left = Math.min(e.clientX + 14, innerWidth - 360) + "px";
|
|
90
|
+
tip.style.top = Math.min(e.clientY + 14, innerHeight - 80) + "px";
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
document.addEventListener("mouseover", (e) => {
|
|
94
|
+
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
95
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
96
|
+
if (!el) { tip.style.display = "none"; return; }
|
|
97
|
+
el.classList.add("dreative-hover");
|
|
98
|
+
const id = el.getAttribute("data-dreative-id");
|
|
99
|
+
const s = SUMMARIES[id];
|
|
100
|
+
if (s) { tip.innerHTML = "<b>" + id + "</b><br>" + s.replace(/</g, "<"); tip.style.display = "block"; }
|
|
101
|
+
else tip.style.display = "none";
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
<script src="${scriptUrl}"></script>
|
|
105
|
+
</body>
|
|
106
106
|
</html>`;
|
|
107
107
|
}
|
|
108
108
|
export function ensureDir(p) {
|
package/dist/server/store.js
CHANGED
|
@@ -6,6 +6,17 @@ export class Store {
|
|
|
6
6
|
this.root = path.join(projectDir, ".dreative");
|
|
7
7
|
fs.mkdirSync(path.join(this.root, "refs"), { recursive: true });
|
|
8
8
|
fs.mkdirSync(path.join(this.root, "generated"), { recursive: true });
|
|
9
|
+
const readme = path.join(this.root, "README.md");
|
|
10
|
+
if (!fs.existsSync(readme)) {
|
|
11
|
+
fs.writeFileSync(readme, "# Dreative run artifacts — NOT the skill\n\n" +
|
|
12
|
+
"Everything in this folder (plan.md, system.md, verify.md, assets.json,\n" +
|
|
13
|
+
"project.json, mockups/, screenshots/) is OUTPUT of a Dreative design run,\n" +
|
|
14
|
+
"not instructions. The actual skill lives at\n" +
|
|
15
|
+
"`<agent-dir>/skills/dreative/SKILL.md` (look in `.claude/`, `.codex/`, or\n" +
|
|
16
|
+
"`.agents/`). For any new design request: read that SKILL.md and run its\n" +
|
|
17
|
+
"protocol from the start — these artifacts are the previous run's history,\n" +
|
|
18
|
+
"never proof the new request is already done.\n");
|
|
19
|
+
}
|
|
9
20
|
}
|
|
10
21
|
get file() {
|
|
11
22
|
return path.join(this.root, "project.json");
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
function nonEmpty(value) {
|
|
2
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
export function validatePlan(value) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const plan = value;
|
|
7
|
+
if (!plan || typeof plan !== "object")
|
|
8
|
+
return ["plan must be an object"];
|
|
9
|
+
if (plan.version !== 2)
|
|
10
|
+
errors.push("plan.version must be 2 (migrate legacy top-level sections into pages[])");
|
|
11
|
+
if (!nonEmpty(plan.request))
|
|
12
|
+
errors.push("plan.request is required");
|
|
13
|
+
if (!nonEmpty(plan.createdAt) || Number.isNaN(Date.parse(plan.createdAt)))
|
|
14
|
+
errors.push("plan.createdAt must be an ISO date");
|
|
15
|
+
if (!["solid", "premium", "expressive", "award"].includes(String(plan.tier)))
|
|
16
|
+
errors.push("plan.tier is invalid");
|
|
17
|
+
if (!["restyle", "relayout", "restructure", "reimagine"].includes(String(plan.depth)))
|
|
18
|
+
errors.push("plan.depth is invalid");
|
|
19
|
+
if (!Array.isArray(plan.skills) || !plan.skills.includes("ux") || !plan.skills.includes("mobile"))
|
|
20
|
+
errors.push("plan.skills must include ux and mobile");
|
|
21
|
+
if (!plan.skillPolicy ||
|
|
22
|
+
plan.skillPolicy.mode !== "hybrid" ||
|
|
23
|
+
plan.skillPolicy.routingApproved !== true ||
|
|
24
|
+
!Array.isArray(plan.skillPolicy.global) ||
|
|
25
|
+
!plan.skillPolicy.global.includes("ux") ||
|
|
26
|
+
!plan.skillPolicy.global.includes("mobile"))
|
|
27
|
+
errors.push("plan.skillPolicy must use approved hybrid routing with global ux and mobile");
|
|
28
|
+
if (!plan.designRead || !nonEmpty(plan.designRead.register) || !nonEmpty(plan.designRead.concept) || !nonEmpty(plan.designRead.signature))
|
|
29
|
+
errors.push("plan.designRead requires register, concept, and signature");
|
|
30
|
+
if (!Array.isArray(plan.pages) || plan.pages.length === 0)
|
|
31
|
+
errors.push("plan.pages must contain at least one page");
|
|
32
|
+
const coveredSkills = new Set();
|
|
33
|
+
for (const [pageIndex, page] of (plan.pages ?? []).entries()) {
|
|
34
|
+
const pagePrefix = `pages[${pageIndex}]`;
|
|
35
|
+
if (!nonEmpty(page.id) || !nonEmpty(page.name))
|
|
36
|
+
errors.push(`${pagePrefix} requires id and name`);
|
|
37
|
+
if (!Array.isArray(page.skills) || !page.skills.includes("ux") || !page.skills.includes("mobile"))
|
|
38
|
+
errors.push(`${pagePrefix}.skills must include ux and mobile`);
|
|
39
|
+
for (const globalSkill of plan.skillPolicy?.global ?? []) {
|
|
40
|
+
if (!page.skills?.includes(globalSkill))
|
|
41
|
+
errors.push(`${pagePrefix} is missing global skill ${globalSkill}`);
|
|
42
|
+
}
|
|
43
|
+
for (const skill of page.skills ?? []) {
|
|
44
|
+
coveredSkills.add(skill);
|
|
45
|
+
if (!plan.skills?.includes(skill))
|
|
46
|
+
errors.push(`${pagePrefix} uses unselected skill ${skill}`);
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(page.sections) || page.sections.length === 0)
|
|
49
|
+
errors.push(`${pagePrefix}.sections cannot be empty`);
|
|
50
|
+
for (const [sectionIndex, section] of (page.sections ?? []).entries()) {
|
|
51
|
+
const prefix = `${pagePrefix}.sections[${sectionIndex}]`;
|
|
52
|
+
if (!nonEmpty(section.id) || !nonEmpty(section.name) || !nonEmpty(section.layoutFamily))
|
|
53
|
+
errors.push(`${prefix} requires id, name, and layoutFamily`);
|
|
54
|
+
if (!nonEmpty(section.mobile) || !nonEmpty(section.fallback))
|
|
55
|
+
errors.push(`${prefix} requires mobile and fallback treatments`);
|
|
56
|
+
if (!Array.isArray(section.verification) || section.verification.length === 0)
|
|
57
|
+
errors.push(`${prefix}.verification cannot be empty`);
|
|
58
|
+
if (section.status === "planned")
|
|
59
|
+
errors.push(`${prefix} is still planned`);
|
|
60
|
+
if ((section.status === "fallback" || section.status === "cut") && !nonEmpty(section.reason))
|
|
61
|
+
errors.push(`${prefix}.reason is required for ${section.status}`);
|
|
62
|
+
if (section.motionTreatment) {
|
|
63
|
+
const treatment = section.motionTreatment;
|
|
64
|
+
if (!["none", "decorative", "structural", "transformational"].includes(treatment.class))
|
|
65
|
+
errors.push(`${prefix}.motionTreatment.class is invalid`);
|
|
66
|
+
for (const [name, value] of Object.entries({
|
|
67
|
+
staticComposition: treatment.staticComposition,
|
|
68
|
+
startState: treatment.startState,
|
|
69
|
+
endState: treatment.endState,
|
|
70
|
+
handoff: treatment.handoff,
|
|
71
|
+
purpose: treatment.purpose,
|
|
72
|
+
mechanism: treatment.mechanism,
|
|
73
|
+
mobile: treatment.mobile,
|
|
74
|
+
reducedMotion: treatment.reducedMotion,
|
|
75
|
+
})) {
|
|
76
|
+
if (!nonEmpty(value))
|
|
77
|
+
errors.push(`${prefix}.motionTreatment.${name} is required`);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(treatment.changes) || !Array.isArray(treatment.pinnedElements))
|
|
80
|
+
errors.push(`${prefix}.motionTreatment changes and pinnedElements must be arrays`);
|
|
81
|
+
}
|
|
82
|
+
for (const skill of section.skills ?? []) {
|
|
83
|
+
if (!page.skills?.includes(skill))
|
|
84
|
+
errors.push(`${prefix} uses skill ${skill} not assigned to its page`);
|
|
85
|
+
}
|
|
86
|
+
for (const asset of section.assets ?? []) {
|
|
87
|
+
if (!nonEmpty(asset.id) || !nonEmpty(asset.path) || !nonEmpty(asset.purpose))
|
|
88
|
+
errors.push(`${prefix} contains an incomplete asset`);
|
|
89
|
+
if (plan.doctrineVersion === 2 && (!asset.importance || !asset.preparation))
|
|
90
|
+
errors.push(`${prefix} asset ${asset.id || "unknown"} needs importance and a preparation decision`);
|
|
91
|
+
else if (asset.preparation) {
|
|
92
|
+
if (!asset.importance || !["supporting", "key"].includes(asset.importance))
|
|
93
|
+
errors.push(`${prefix} asset ${asset.id} has invalid importance`);
|
|
94
|
+
if (!["flat", "decompose", "variants", "sequence"].includes(asset.preparation.decision))
|
|
95
|
+
errors.push(`${prefix} asset ${asset.id} has an invalid preparation decision`);
|
|
96
|
+
if (!nonEmpty(asset.preparation.rationale))
|
|
97
|
+
errors.push(`${prefix} asset ${asset.id} needs a preparation rationale`);
|
|
98
|
+
if (!Array.isArray(asset.preparation.derivatives))
|
|
99
|
+
errors.push(`${prefix} asset ${asset.id} preparation.derivatives must be an array`);
|
|
100
|
+
if (asset.importance === "key" && asset.preparation.decision !== "flat" && asset.preparation.derivatives.length === 0)
|
|
101
|
+
errors.push(`${prefix} key asset ${asset.id} promises preparation but names no derivatives`);
|
|
102
|
+
}
|
|
103
|
+
if ((asset.status === "fallback" || asset.status === "cut") && !nonEmpty(asset.reason))
|
|
104
|
+
errors.push(`${prefix} asset ${asset.id} needs a reason`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const skill of plan.skills ?? []) {
|
|
109
|
+
if (!coveredSkills.has(skill))
|
|
110
|
+
errors.push(`selected skill ${skill} is not assigned to any page`);
|
|
111
|
+
}
|
|
112
|
+
for (const assignment of plan.skillPolicy?.userAssignments ?? []) {
|
|
113
|
+
const page = plan.pages?.find((candidate) => candidate.id === assignment.pageId);
|
|
114
|
+
if (!page) {
|
|
115
|
+
errors.push(`user assignment references unknown page ${assignment.pageId}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const skill of assignment.skills) {
|
|
119
|
+
if (!plan.skills?.includes(skill))
|
|
120
|
+
errors.push(`user assignment uses unselected skill ${skill}`);
|
|
121
|
+
if (!page.skills.includes(skill))
|
|
122
|
+
errors.push(`user-pinned skill ${skill} is missing from page ${page.name}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!nonEmpty(plan.preservationManifest) || !nonEmpty(plan.decisionLedger))
|
|
126
|
+
errors.push("plan must reference preservationManifest and decisionLedger");
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
export function validatePreservationManifest(value) {
|
|
130
|
+
const errors = [];
|
|
131
|
+
const manifest = value;
|
|
132
|
+
if (!manifest || typeof manifest !== "object")
|
|
133
|
+
return ["preservation manifest must be an object"];
|
|
134
|
+
if (manifest.version !== 1)
|
|
135
|
+
errors.push("preservation.version must be 1");
|
|
136
|
+
if (!Array.isArray(manifest.items))
|
|
137
|
+
errors.push("preservation.items must be an array");
|
|
138
|
+
const ids = new Set();
|
|
139
|
+
for (const [index, item] of (manifest.items ?? []).entries()) {
|
|
140
|
+
if (!nonEmpty(item.id) || !nonEmpty(item.kind) || !nonEmpty(item.file) || !nonEmpty(item.needle) || !nonEmpty(item.purpose))
|
|
141
|
+
errors.push(`preservation.items[${index}] is incomplete`);
|
|
142
|
+
if (ids.has(item.id))
|
|
143
|
+
errors.push(`duplicate preservation id: ${item.id}`);
|
|
144
|
+
ids.add(item.id);
|
|
145
|
+
if (item.intentionallyChanged && !nonEmpty(item.changeReason))
|
|
146
|
+
errors.push(`${item.id} needs changeReason`);
|
|
147
|
+
}
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
export function validateDecisionLedger(value) {
|
|
151
|
+
const ledger = value;
|
|
152
|
+
if (!ledger || typeof ledger !== "object")
|
|
153
|
+
return ["decision ledger must be an object"];
|
|
154
|
+
if (ledger.version !== 1)
|
|
155
|
+
return ["decision ledger version must be 1"];
|
|
156
|
+
if (!Array.isArray(ledger.entries))
|
|
157
|
+
return ["decision ledger entries must be an array"];
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
export function validateVerificationReport(value) {
|
|
161
|
+
const report = value;
|
|
162
|
+
if (!report || typeof report !== "object")
|
|
163
|
+
return ["verification report must be an object"];
|
|
164
|
+
const errors = [];
|
|
165
|
+
if (report.version !== 1)
|
|
166
|
+
errors.push("verification report version must be 1");
|
|
167
|
+
if (!Array.isArray(report.evidence))
|
|
168
|
+
errors.push("verification evidence must be an array");
|
|
169
|
+
if (report.refinement) {
|
|
170
|
+
if (!nonEmpty(report.refinement.inspectedAt) || Number.isNaN(Date.parse(report.refinement.inspectedAt)))
|
|
171
|
+
errors.push("verification refinement needs a valid inspectedAt timestamp");
|
|
172
|
+
if (!Array.isArray(report.refinement.findings) ||
|
|
173
|
+
report.refinement.findings.length === 0 ||
|
|
174
|
+
!Array.isArray(report.refinement.changes) ||
|
|
175
|
+
report.refinement.changes.length === 0 ||
|
|
176
|
+
!Array.isArray(report.refinement.evidenceIds) ||
|
|
177
|
+
report.refinement.evidenceIds.length === 0)
|
|
178
|
+
errors.push("verification refinement needs findings, changes, and evidenceIds arrays");
|
|
179
|
+
}
|
|
180
|
+
if ((report.evidence ?? []).some((item) => item.status === "fail"))
|
|
181
|
+
errors.push("verification report contains failing evidence");
|
|
182
|
+
const timelineStates = new Set([
|
|
183
|
+
"initial",
|
|
184
|
+
"early",
|
|
185
|
+
"mid-transition",
|
|
186
|
+
"final",
|
|
187
|
+
"handoff",
|
|
188
|
+
"pinned-midpoint",
|
|
189
|
+
"pinned-exit",
|
|
190
|
+
"mobile",
|
|
191
|
+
"reduced-motion",
|
|
192
|
+
]);
|
|
193
|
+
for (const [index, item] of (report.evidence ?? []).entries()) {
|
|
194
|
+
if (!nonEmpty(item.id) || !nonEmpty(item.criterion) || !nonEmpty(item.evidence))
|
|
195
|
+
errors.push(`verification.evidence[${index}] is incomplete`);
|
|
196
|
+
if (item.timelineState && !timelineStates.has(item.timelineState))
|
|
197
|
+
errors.push(`verification.evidence[${index}].timelineState is invalid`);
|
|
198
|
+
const proof = item.proof;
|
|
199
|
+
if (!proof || !nonEmpty(proof.timestamp) || Number.isNaN(Date.parse(proof.timestamp))) {
|
|
200
|
+
errors.push(`verification.evidence[${index}].proof requires a valid timestamp`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const hasConcreteProof = nonEmpty(proof.artifactPath) ||
|
|
204
|
+
nonEmpty(proof.command) ||
|
|
205
|
+
typeof proof.consoleErrorCount === "number" ||
|
|
206
|
+
nonEmpty(proof.testedUrl) ||
|
|
207
|
+
typeof proof.averageFps === "number" ||
|
|
208
|
+
typeof proof.maxFrameTimeMs === "number" ||
|
|
209
|
+
nonEmpty(proof.playwrightTestId);
|
|
210
|
+
if (!hasConcreteProof)
|
|
211
|
+
errors.push(`verification.evidence[${index}].proof needs a command, artifact, URL, runtime measurement, or test id`);
|
|
212
|
+
if ((nonEmpty(proof.command) && typeof proof.exitCode !== "number") || (!nonEmpty(proof.command) && typeof proof.exitCode === "number"))
|
|
213
|
+
errors.push(`verification.evidence[${index}] command and exitCode must appear together`);
|
|
214
|
+
if (item.status === "pass" && typeof proof.exitCode === "number" && proof.exitCode !== 0)
|
|
215
|
+
errors.push(`verification.evidence[${index}] cannot pass with exitCode ${proof.exitCode}`);
|
|
216
|
+
if (item.status === "pass" && typeof proof.consoleErrorCount === "number" && proof.consoleErrorCount !== 0)
|
|
217
|
+
errors.push(`verification.evidence[${index}] cannot pass with console errors`);
|
|
218
|
+
}
|
|
219
|
+
return errors;
|
|
220
|
+
}
|
package/dist/shared/design.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Names map to skill/dreative/skills/<name>.md. */
|
|
3
|
-
const SKILL_SIGNATURES = {
|
|
4
|
-
"3d": /\b(3d|three\.?js|webgl|r3f|shader|glsl|particles?|point ?cloud|globe|mesh gradient|orbit|spline)\b/i,
|
|
5
|
-
motion: /\b(animat|motion|parallax|scroll[- ]?(driven|trigger|story|choreo)|gsap|framer|lenis|marquee|kinetic|cinematic|stagger|reveal|transition)\b/i,
|
|
6
|
-
interaction: /\b(micro[- ]?interaction|hover (effect|state)s?|cursor|magnetic|tilt|spotlight|glow|ripple|tactile|interactive|draggable)\b/i,
|
|
7
|
-
immersive: /\b(immersive|award[- ]?(site|winning)|awwwards|spatial( (transition|nav(igation)?))?|scene[- ]?based|world|page[- ]transitions?|camera (move|travel|path)|preloader|scroll (journey|story|as travel)|explorable|experience site|epic\.net)\b/i,
|
|
8
|
-
cinematic: /\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,
|
|
9
|
-
refined: /\b(clean (and )?(modern|professional)|modern clean|business (site|website|use)|corporate|professional look|premium (minimal|clean)|dtc|d2c|e[- ]?commerce|ecommerce|shopify|maswitzerland|lovvelavva|not (too )?(flashy|animated)|calm|understated)\b/i,
|
|
10
|
-
};
|
|
11
|
-
function detectSkills(texts) {
|
|
12
|
-
const hay = texts.filter(Boolean).join(" \n ");
|
|
13
|
-
return Object.keys(SKILL_SIGNATURES).filter((k) => SKILL_SIGNATURES[k].test(hay));
|
|
14
|
-
}
|
|
1
|
+
import { detectSpecialistSkills, resolveAmbitionTier, resolveSkillDependencies, routeSkillsAcrossPages, TIER_REQUIREMENTS, } from "./skillSystem.js";
|
|
15
2
|
/** All text a block subtree carries that can signal a specialist skill. */
|
|
16
3
|
function blockTexts(b) {
|
|
17
4
|
return [
|
|
@@ -120,9 +107,10 @@ export function lintLayout(layout) {
|
|
|
120
107
|
return lints;
|
|
121
108
|
}
|
|
122
109
|
/** Build the compact, executable plan sent to the agent with design-page. */
|
|
123
|
-
export function buildDesignPlan(page, brief) {
|
|
110
|
+
export function buildDesignPlan(page, brief, assignedSkills) {
|
|
124
111
|
const dials = resolveDials(brief);
|
|
125
112
|
const aesthetic = brief?.aesthetic || "auto (infer per DESIGN.md, then commit)";
|
|
113
|
+
const allowedSkills = assignedSkills ? new Set(resolveSkillDependencies(assignedSkills)) : undefined;
|
|
126
114
|
const sections = (page.layout.children ?? []).map((s, i) => {
|
|
127
115
|
let family;
|
|
128
116
|
if (s.type === "nav")
|
|
@@ -133,34 +121,67 @@ export function buildDesignPlan(page, brief) {
|
|
|
133
121
|
family = "footer";
|
|
134
122
|
else
|
|
135
123
|
family = FAMILIES[i % FAMILIES.length];
|
|
136
|
-
const
|
|
124
|
+
const detected = resolveSkillDependencies(detectSpecialistSkills(blockTexts(s)));
|
|
125
|
+
const secSkills = allowedSkills ? detected.filter((skill) => allowedSkills.has(skill)) : detected;
|
|
137
126
|
return secSkills.length ? { id: s.id, label: s.label, family, skills: secSkills } : { id: s.id, label: s.label, family };
|
|
138
127
|
});
|
|
139
128
|
// page-level specialist skills: brief/prompt keywords + section hits + motion dial
|
|
140
|
-
const
|
|
141
|
-
...
|
|
129
|
+
const requestedSkills = new Set([
|
|
130
|
+
...detectSpecialistSkills([brief?.vibe, brief?.notes, page.designPrompt]),
|
|
142
131
|
...sections.flatMap((s) => s.skills ?? []),
|
|
143
132
|
]);
|
|
144
133
|
if (dials.motion >= 6)
|
|
145
|
-
|
|
134
|
+
requestedSkills.add("motion");
|
|
135
|
+
const skills = assignedSkills ? resolveSkillDependencies(assignedSkills) : resolveSkillDependencies(requestedSkills);
|
|
136
|
+
const tier = resolveAmbitionTier({
|
|
137
|
+
...dials,
|
|
138
|
+
aesthetic,
|
|
139
|
+
texts: [brief?.vibe, brief?.notes, page.designPrompt, ...blockTexts(page.layout)],
|
|
140
|
+
});
|
|
146
141
|
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";
|
|
147
142
|
const motionBudget = dials.motion <= 3
|
|
148
143
|
? "hover/active states only, no scroll animation"
|
|
149
144
|
: dials.motion <= 6
|
|
150
|
-
? "
|
|
151
|
-
: "
|
|
145
|
+
? "purposeful local motion; use structural transitions only where content changes state, with visible defaults"
|
|
146
|
+
: "write section motion treatments; concentrate structural/transformational choreography into a few hero moments with calm sections, mobile translations, and reduced-motion states";
|
|
152
147
|
const directives = [
|
|
153
148
|
`aesthetic: ${aesthetic}; dials variance=${dials.variance} motion=${dials.motion} density=${dials.density}`,
|
|
154
149
|
`spacing scale: ${spacing}`,
|
|
155
150
|
`motion budget: ${motionBudget}`,
|
|
151
|
+
"choose the simplest mechanism that makes each intended transformation convincing; do not default blindly to fade/slide/scale or to WebGL/3D",
|
|
156
152
|
"one accent color, one neutral family, one radius system, locked across ALL pages of this project",
|
|
157
153
|
"section layout families are assigned below — follow them, do not default to repeated card rows",
|
|
158
154
|
...(dials.variance > 4 ? ["avoid centered-hero-over-gradient; use split or asymmetric composition"] : []),
|
|
159
|
-
...(skills.
|
|
155
|
+
...(skills.length
|
|
160
156
|
? [
|
|
161
|
-
`specialist skills required: ${
|
|
157
|
+
`specialist skills required: ${skills.join(", ")} — read skills/<name>.md (next to DESIGN.md) before writing code; sections tagged with "skills" get that treatment`,
|
|
162
158
|
]
|
|
163
159
|
: []),
|
|
164
160
|
];
|
|
165
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
version: 1,
|
|
163
|
+
dials,
|
|
164
|
+
aesthetic,
|
|
165
|
+
tier,
|
|
166
|
+
sections,
|
|
167
|
+
directives,
|
|
168
|
+
lints: lintLayout(page.layout),
|
|
169
|
+
skills,
|
|
170
|
+
verification: TIER_REQUIREMENTS[tier],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Build page plans from a user-approved pool. Unselected optional skills are
|
|
174
|
+
* suggestions only; explicit page assignments win. */
|
|
175
|
+
export function buildMultiPageDesignPlans(pages, brief, selection) {
|
|
176
|
+
const routing = routeSkillsAcrossPages({
|
|
177
|
+
pages: pages.map((page) => ({
|
|
178
|
+
id: page.id,
|
|
179
|
+
texts: [page.name, page.designPrompt, brief?.vibe, brief?.notes, ...blockTexts(page.layout)],
|
|
180
|
+
})),
|
|
181
|
+
...selection,
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
routing,
|
|
185
|
+
pages: pages.map((page) => ({ pageId: page.id, plan: buildDesignPlan(page, brief, routing.byPage[page.id]) })),
|
|
186
|
+
};
|
|
166
187
|
}
|