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
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,162 @@
|
|
|
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
|
+
for (const skill of section.skills ?? []) {
|
|
63
|
+
if (!page.skills?.includes(skill))
|
|
64
|
+
errors.push(`${prefix} uses skill ${skill} not assigned to its page`);
|
|
65
|
+
}
|
|
66
|
+
for (const asset of section.assets ?? []) {
|
|
67
|
+
if (!nonEmpty(asset.id) || !nonEmpty(asset.path) || !nonEmpty(asset.purpose))
|
|
68
|
+
errors.push(`${prefix} contains an incomplete asset`);
|
|
69
|
+
if ((asset.status === "fallback" || asset.status === "cut") && !nonEmpty(asset.reason))
|
|
70
|
+
errors.push(`${prefix} asset ${asset.id} needs a reason`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const skill of plan.skills ?? []) {
|
|
75
|
+
if (!coveredSkills.has(skill))
|
|
76
|
+
errors.push(`selected skill ${skill} is not assigned to any page`);
|
|
77
|
+
}
|
|
78
|
+
for (const assignment of plan.skillPolicy?.userAssignments ?? []) {
|
|
79
|
+
const page = plan.pages?.find((candidate) => candidate.id === assignment.pageId);
|
|
80
|
+
if (!page) {
|
|
81
|
+
errors.push(`user assignment references unknown page ${assignment.pageId}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const skill of assignment.skills) {
|
|
85
|
+
if (!plan.skills?.includes(skill))
|
|
86
|
+
errors.push(`user assignment uses unselected skill ${skill}`);
|
|
87
|
+
if (!page.skills.includes(skill))
|
|
88
|
+
errors.push(`user-pinned skill ${skill} is missing from page ${page.name}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!nonEmpty(plan.preservationManifest) || !nonEmpty(plan.decisionLedger))
|
|
92
|
+
errors.push("plan must reference preservationManifest and decisionLedger");
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
export function validatePreservationManifest(value) {
|
|
96
|
+
const errors = [];
|
|
97
|
+
const manifest = value;
|
|
98
|
+
if (!manifest || typeof manifest !== "object")
|
|
99
|
+
return ["preservation manifest must be an object"];
|
|
100
|
+
if (manifest.version !== 1)
|
|
101
|
+
errors.push("preservation.version must be 1");
|
|
102
|
+
if (!Array.isArray(manifest.items))
|
|
103
|
+
errors.push("preservation.items must be an array");
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
for (const [index, item] of (manifest.items ?? []).entries()) {
|
|
106
|
+
if (!nonEmpty(item.id) || !nonEmpty(item.kind) || !nonEmpty(item.file) || !nonEmpty(item.needle) || !nonEmpty(item.purpose))
|
|
107
|
+
errors.push(`preservation.items[${index}] is incomplete`);
|
|
108
|
+
if (ids.has(item.id))
|
|
109
|
+
errors.push(`duplicate preservation id: ${item.id}`);
|
|
110
|
+
ids.add(item.id);
|
|
111
|
+
if (item.intentionallyChanged && !nonEmpty(item.changeReason))
|
|
112
|
+
errors.push(`${item.id} needs changeReason`);
|
|
113
|
+
}
|
|
114
|
+
return errors;
|
|
115
|
+
}
|
|
116
|
+
export function validateDecisionLedger(value) {
|
|
117
|
+
const ledger = value;
|
|
118
|
+
if (!ledger || typeof ledger !== "object")
|
|
119
|
+
return ["decision ledger must be an object"];
|
|
120
|
+
if (ledger.version !== 1)
|
|
121
|
+
return ["decision ledger version must be 1"];
|
|
122
|
+
if (!Array.isArray(ledger.entries))
|
|
123
|
+
return ["decision ledger entries must be an array"];
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
export function validateVerificationReport(value) {
|
|
127
|
+
const report = value;
|
|
128
|
+
if (!report || typeof report !== "object")
|
|
129
|
+
return ["verification report must be an object"];
|
|
130
|
+
const errors = [];
|
|
131
|
+
if (report.version !== 1)
|
|
132
|
+
errors.push("verification report version must be 1");
|
|
133
|
+
if (!Array.isArray(report.evidence))
|
|
134
|
+
errors.push("verification evidence must be an array");
|
|
135
|
+
if ((report.evidence ?? []).some((item) => item.status === "fail"))
|
|
136
|
+
errors.push("verification report contains failing evidence");
|
|
137
|
+
for (const [index, item] of (report.evidence ?? []).entries()) {
|
|
138
|
+
if (!nonEmpty(item.id) || !nonEmpty(item.criterion) || !nonEmpty(item.evidence))
|
|
139
|
+
errors.push(`verification.evidence[${index}] is incomplete`);
|
|
140
|
+
const proof = item.proof;
|
|
141
|
+
if (!proof || !nonEmpty(proof.timestamp) || Number.isNaN(Date.parse(proof.timestamp))) {
|
|
142
|
+
errors.push(`verification.evidence[${index}].proof requires a valid timestamp`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const hasConcreteProof = nonEmpty(proof.artifactPath) ||
|
|
146
|
+
nonEmpty(proof.command) ||
|
|
147
|
+
typeof proof.consoleErrorCount === "number" ||
|
|
148
|
+
nonEmpty(proof.testedUrl) ||
|
|
149
|
+
typeof proof.averageFps === "number" ||
|
|
150
|
+
typeof proof.maxFrameTimeMs === "number" ||
|
|
151
|
+
nonEmpty(proof.playwrightTestId);
|
|
152
|
+
if (!hasConcreteProof)
|
|
153
|
+
errors.push(`verification.evidence[${index}].proof needs a command, artifact, URL, runtime measurement, or test id`);
|
|
154
|
+
if ((nonEmpty(proof.command) && typeof proof.exitCode !== "number") || (!nonEmpty(proof.command) && typeof proof.exitCode === "number"))
|
|
155
|
+
errors.push(`verification.evidence[${index}] command and exitCode must appear together`);
|
|
156
|
+
if (item.status === "pass" && typeof proof.exitCode === "number" && proof.exitCode !== 0)
|
|
157
|
+
errors.push(`verification.evidence[${index}] cannot pass with exitCode ${proof.exitCode}`);
|
|
158
|
+
if (item.status === "pass" && typeof proof.consoleErrorCount === "number" && proof.consoleErrorCount !== 0)
|
|
159
|
+
errors.push(`verification.evidence[${index}] cannot pass with console errors`);
|
|
160
|
+
}
|
|
161
|
+
return errors;
|
|
162
|
+
}
|
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,16 +121,23 @@ 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"
|
|
@@ -156,11 +151,36 @@ export function buildDesignPlan(page, brief) {
|
|
|
156
151
|
"one accent color, one neutral family, one radius system, locked across ALL pages of this project",
|
|
157
152
|
"section layout families are assigned below — follow them, do not default to repeated card rows",
|
|
158
153
|
...(dials.variance > 4 ? ["avoid centered-hero-over-gradient; use split or asymmetric composition"] : []),
|
|
159
|
-
...(skills.
|
|
154
|
+
...(skills.length
|
|
160
155
|
? [
|
|
161
|
-
`specialist skills required: ${
|
|
156
|
+
`specialist skills required: ${skills.join(", ")} — read skills/<name>.md (next to DESIGN.md) before writing code; sections tagged with "skills" get that treatment`,
|
|
162
157
|
]
|
|
163
158
|
: []),
|
|
164
159
|
];
|
|
165
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
dials,
|
|
163
|
+
aesthetic,
|
|
164
|
+
tier,
|
|
165
|
+
sections,
|
|
166
|
+
directives,
|
|
167
|
+
lints: lintLayout(page.layout),
|
|
168
|
+
skills,
|
|
169
|
+
verification: TIER_REQUIREMENTS[tier],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Build page plans from a user-approved pool. Unselected optional skills are
|
|
173
|
+
* suggestions only; explicit page assignments win. */
|
|
174
|
+
export function buildMultiPageDesignPlans(pages, brief, selection) {
|
|
175
|
+
const routing = routeSkillsAcrossPages({
|
|
176
|
+
pages: pages.map((page) => ({
|
|
177
|
+
id: page.id,
|
|
178
|
+
texts: [page.name, page.designPrompt, brief?.vibe, brief?.notes, ...blockTexts(page.layout)],
|
|
179
|
+
})),
|
|
180
|
+
...selection,
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
routing,
|
|
184
|
+
pages: pages.map((page) => ({ pageId: page.id, plan: buildDesignPlan(page, brief, routing.byPage[page.id]) })),
|
|
185
|
+
};
|
|
166
186
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
function normalized(value) {
|
|
2
|
+
return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
|
|
3
|
+
}
|
|
4
|
+
function distinct(values) {
|
|
5
|
+
return [...new Set(values.map(normalized).filter(Boolean))];
|
|
6
|
+
}
|
|
7
|
+
function specific(value, minLength) {
|
|
8
|
+
if (!value || value.trim().length < minLength)
|
|
9
|
+
return false;
|
|
10
|
+
return !/^(it |this )?(did not fit|didn't fit|felt better|was unnecessary|was not needed|looked better|fits? the design|restraint)$/i.test(value.trim());
|
|
11
|
+
}
|
|
12
|
+
export function validateRuleControls(plan, registry, reflexFonts, verification) {
|
|
13
|
+
if (plan.doctrineVersion !== 2)
|
|
14
|
+
return [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const rules = new Map(registry.rules.map((rule) => [rule.id, rule]));
|
|
17
|
+
const evidence = new Map((verification?.evidence ?? []).map((item) => [item.id, item]));
|
|
18
|
+
for (const exception of plan.ruleExceptions ?? []) {
|
|
19
|
+
if (exception.decision !== "substituted")
|
|
20
|
+
errors.push(`${exception.ruleId || "unknown rule"}: decision must be substituted`);
|
|
21
|
+
const rule = rules.get(exception.ruleId);
|
|
22
|
+
if (!rule) {
|
|
23
|
+
errors.push(`unknown rule exception: ${exception.ruleId}`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (rule.category === "hard-gate" || !rule.exceptionAllowed) {
|
|
27
|
+
errors.push(`hard gate ${exception.ruleId} cannot be substituted`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!specific(exception.reason, 30))
|
|
31
|
+
errors.push(`${exception.ruleId}: reason is vague or too short`);
|
|
32
|
+
if (!specific(exception.alternative, 40))
|
|
33
|
+
errors.push(`${exception.ruleId}: alternative is vague or too short`);
|
|
34
|
+
const criteria = Array.isArray(exception.successCriteria) ? exception.successCriteria : [];
|
|
35
|
+
if (distinct(criteria).length < 2 || criteria.some((criterion) => criterion.trim().length < 12))
|
|
36
|
+
errors.push(`${exception.ruleId}: needs at least two observable success criteria`);
|
|
37
|
+
if (!plan.implementationStartedAt || Number.isNaN(Date.parse(plan.implementationStartedAt)))
|
|
38
|
+
errors.push(`${exception.ruleId}: implementationStartedAt is required to prove planning-time declaration`);
|
|
39
|
+
else if (Number.isNaN(Date.parse(exception.declaredAt)) || Date.parse(exception.declaredAt) > Date.parse(plan.implementationStartedAt))
|
|
40
|
+
errors.push(`${exception.ruleId}: exception must be declared before implementation starts`);
|
|
41
|
+
const evidenceIds = Array.isArray(exception.evidenceIds) ? exception.evidenceIds : [];
|
|
42
|
+
if (evidenceIds.length === 0)
|
|
43
|
+
errors.push(`${exception.ruleId}: substitution requires verification evidence`);
|
|
44
|
+
for (const id of evidenceIds) {
|
|
45
|
+
const item = evidence.get(id);
|
|
46
|
+
if (!item)
|
|
47
|
+
errors.push(`${exception.ruleId}: missing verification evidence ${id}`);
|
|
48
|
+
else if (item.status !== "pass")
|
|
49
|
+
errors.push(`${exception.ruleId}: evidence ${id} is not passing`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!plan.fontDecision) {
|
|
53
|
+
errors.push("fontDecision is required for doctrineVersion 2 plans");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const decision = plan.fontDecision;
|
|
57
|
+
const candidates = Array.isArray(decision.candidates) ? decision.candidates : [];
|
|
58
|
+
if (distinct(candidates.map((candidate) => candidate.name)).length < 3)
|
|
59
|
+
errors.push("fontDecision requires at least three distinct candidates");
|
|
60
|
+
if (!candidates.some((candidate) => normalized(candidate.name) === normalized(decision.selected)))
|
|
61
|
+
errors.push("selected font must appear in font candidates");
|
|
62
|
+
const reflexSet = new Set(reflexFonts.fonts.map(normalized));
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
if (candidate.reflex !== reflexSet.has(normalized(candidate.name)))
|
|
65
|
+
errors.push(`${candidate.name}: reflex marker does not match REFLEX_FONTS.json`);
|
|
66
|
+
}
|
|
67
|
+
const selectedIsReflex = reflexSet.has(normalized(decision.selected));
|
|
68
|
+
if (selectedIsReflex) {
|
|
69
|
+
const reason = decision.justification ?? "";
|
|
70
|
+
const validKinds = new Set(reflexFonts.validReasonKinds ?? []);
|
|
71
|
+
if (!(decision.reasonKinds ?? []).some((kind) => validKinds.has(kind)))
|
|
72
|
+
errors.push(`${decision.selected}: reflex font requires at least one registered reason kind`);
|
|
73
|
+
if (!specific(reason, 40) || reflexFonts.invalidGenericReasons.some((phrase) => normalized(reason).includes(normalized(phrase))))
|
|
74
|
+
errors.push(`${decision.selected}: reflex font requires a specific non-generic justification`);
|
|
75
|
+
}
|
|
76
|
+
if ((decision.recentDisplayFonts ?? []).map(normalized).includes(normalized(decision.selected)) && !specific(decision.repeatJustification, 40))
|
|
77
|
+
errors.push(`${decision.selected}: repeating a recent display font requires a stronger justification`);
|
|
78
|
+
}
|
|
79
|
+
if (plan.tier === "expressive" || plan.tier === "award") {
|
|
80
|
+
const strategy = plan.creativeStrategy;
|
|
81
|
+
if (!strategy)
|
|
82
|
+
errors.push(`${plan.tier} plans require a diversity-or-development creativeStrategy`);
|
|
83
|
+
else if (strategy.path === "diversity") {
|
|
84
|
+
if (distinct(strategy.mechanisms ?? []).length < 4)
|
|
85
|
+
errors.push("diversity path requires four distinct mechanisms");
|
|
86
|
+
if (distinct(strategy.drivers ?? []).length < 3)
|
|
87
|
+
errors.push("diversity path requires three distinct drivers");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (!specific(strategy.signatureMechanism, 12))
|
|
91
|
+
errors.push("development path requires a named signature mechanism");
|
|
92
|
+
if (distinct(strategy.states ?? []).length < 3 || (strategy.states ?? []).some((state) => state.trim().length < 12))
|
|
93
|
+
errors.push("development path requires three materially different described states");
|
|
94
|
+
if (distinct(strategy.secondaryMechanisms ?? []).length < 2)
|
|
95
|
+
errors.push("development path requires two quieter secondary mechanisms");
|
|
96
|
+
if (distinct(strategy.drivers ?? []).length < 2)
|
|
97
|
+
errors.push("development path requires two distinct drivers");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (plan.skills.includes("experimental")) {
|
|
101
|
+
const experimental = plan.experimentalPlan;
|
|
102
|
+
if (!experimental)
|
|
103
|
+
errors.push("experimental skill requires experimentalPlan");
|
|
104
|
+
else {
|
|
105
|
+
const candidates = Array.isArray(experimental.candidates) ? experimental.candidates : [];
|
|
106
|
+
const covered = new Set(candidates.map((candidate) => candidate.sectionId));
|
|
107
|
+
for (const sectionId of experimental.majorSectionIds ?? []) {
|
|
108
|
+
if (!covered.has(sectionId))
|
|
109
|
+
errors.push(`experimental exploration is missing major section ${sectionId}`);
|
|
110
|
+
}
|
|
111
|
+
const selected = candidates.filter((candidate) => candidate.selected);
|
|
112
|
+
if (selected.length < 2 || selected.length > 3)
|
|
113
|
+
errors.push("experimental delivery must select only the strongest two or three provocations");
|
|
114
|
+
if (candidates.some((candidate) => candidate.idea.trim().length < 20))
|
|
115
|
+
errors.push("experimental candidates must be concrete, non-obvious ideas");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if ((plan.recipeAccess?.length ?? 0) > 0) {
|
|
119
|
+
if (!plan.conceptExploration || distinct(plan.conceptExploration.concepts.map((concept) => concept.concept)).length < 3)
|
|
120
|
+
errors.push("three original concepts must be recorded before recipe access");
|
|
121
|
+
else {
|
|
122
|
+
const exploredAt = Date.parse(plan.conceptExploration.recordedAt);
|
|
123
|
+
for (const access of plan.recipeAccess ?? []) {
|
|
124
|
+
if (Number.isNaN(Date.parse(access.loadedAt)) || Date.parse(access.loadedAt) < exploredAt)
|
|
125
|
+
errors.push(`${access.file}: recipe was loaded before concept exploration was recorded`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return errors;
|
|
130
|
+
}
|