dreative 0.5.0 → 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.
Files changed (48) hide show
  1. package/README.md +49 -27
  2. package/dist/cli/audit.js +206 -0
  3. package/dist/cli/audit.test.js +79 -0
  4. package/dist/cli/docsCheck.js +163 -0
  5. package/dist/cli/docsCheck.test.js +8 -0
  6. package/dist/cli/index.js +81 -8
  7. package/dist/server/preview.js +73 -73
  8. package/dist/server/store.js +11 -0
  9. package/dist/shared/artifacts.js +162 -0
  10. package/dist/shared/design.js +42 -22
  11. package/dist/shared/ruleSystem.js +130 -0
  12. package/dist/shared/ruleSystem.test.js +187 -0
  13. package/dist/shared/skillSystem.js +173 -0
  14. package/dist/shared/skillSystem.test.js +110 -0
  15. package/dist/ui/assets/{index--vztc_MR.js → index-CKwmbx2j.js} +13 -13
  16. package/dist/ui/index.html +12 -12
  17. package/package.json +5 -3
  18. package/skill/dreative/DESIGN.md +290 -95
  19. package/skill/dreative/PLAN.md +462 -71
  20. package/skill/dreative/SKILL.md +230 -126
  21. package/skill/dreative/frameworks/nextjs.md +13 -0
  22. package/skill/dreative/frameworks/react-vite.md +12 -0
  23. package/skill/dreative/frameworks/styling.md +14 -0
  24. package/skill/dreative/frameworks/sveltekit.md +10 -0
  25. package/skill/dreative/frameworks/vue-nuxt.md +12 -0
  26. package/skill/dreative/recipes/3d-recipes.md +44 -0
  27. package/skill/dreative/recipes/cinematic-recipes.md +19 -0
  28. package/skill/dreative/recipes/immersive-recipes.md +18 -0
  29. package/skill/dreative/recipes/media-recipes.md +60 -0
  30. package/skill/dreative/recipes/motion-recipes.md +44 -0
  31. package/skill/dreative/references/ARTIFACTS.md +180 -0
  32. package/skill/dreative/references/REFLEX_FONTS.json +44 -0
  33. package/skill/dreative/references/RULES.json +186 -0
  34. package/skill/dreative/references/SKILL_CONTRACT.md +30 -0
  35. package/skill/dreative/references/TIERS.md +42 -0
  36. package/skill/dreative/schemas/plan.schema.json +241 -0
  37. package/skill/dreative/schemas/verify.schema.json +61 -0
  38. package/skill/dreative/skills/3d.md +94 -157
  39. package/skill/dreative/skills/cinematic.md +56 -232
  40. package/skill/dreative/skills/experimental.md +111 -0
  41. package/skill/dreative/skills/immersive.md +61 -223
  42. package/skill/dreative/skills/interaction.md +9 -1
  43. package/skill/dreative/skills/media.md +135 -216
  44. package/skill/dreative/skills/mobile.md +128 -117
  45. package/skill/dreative/skills/motion.md +89 -229
  46. package/skill/dreative/skills/refined.md +116 -102
  47. package/skill/dreative/skills/ux.md +155 -144
  48. package/dist/server/ai.js +0 -177
package/dist/cli/index.js CHANGED
@@ -5,6 +5,8 @@ import { fileURLToPath } from "node:url";
5
5
  import readline from "node:readline";
6
6
  import { createServer } from "../server/index.js";
7
7
  import open from "open";
8
+ import { printAudit, runDirectDesignAudit } from "./audit.js";
9
+ import { printDocsCheck, runDocsCheck } from "./docsCheck.js";
8
10
  const port = Number(process.env.DREATIVE_PORT || 4820);
9
11
  const base = `http://localhost:${port}`;
10
12
  const args = process.argv.slice(2);
@@ -15,9 +17,33 @@ const USAGE = `usage: dreative [command]
15
17
  --list show available specialist skills
16
18
  --skills a,b install only these specialist skills (no flag: interactive picker, Enter = all)
17
19
  --codex install for Codex CLI instead (.codex/skills/ + AGENTS.md pointer)
20
+ --check verify the installed skill matches this package (exit 1 on drift)
18
21
  wait (agent) block until the UI needs something; prints one JSON event
19
- respond <id> [result.json | --error msg] (agent) answer a request
20
- baseline (agent) snapshot project.json as the finish-diff baseline`;
22
+ respond <id> [result.json | --error msg] (agent) answer a request
23
+ baseline (agent) snapshot project.json as the finish-diff baseline
24
+ audit validate direct-design plan, preservation, assets, ledger, and verification
25
+ --json emit a machine-readable report
26
+ docs-check validate packaged skill tiers, names, rules, references, and doctrine consistency
27
+ --json emit a machine-readable report`;
28
+ const packagedSkillDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skill", "dreative");
29
+ function walkFiles(root, current = root) {
30
+ const files = [];
31
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
32
+ const absolute = path.join(current, entry.name);
33
+ if (entry.isDirectory())
34
+ files.push(...walkFiles(root, absolute));
35
+ else
36
+ files.push(path.relative(root, absolute));
37
+ }
38
+ return files;
39
+ }
40
+ function copyRelativeFiles(sourceRoot, destinationRoot, files) {
41
+ for (const relative of files) {
42
+ const destination = path.join(destinationRoot, relative);
43
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
44
+ fs.copyFileSync(path.join(sourceRoot, relative), destination);
45
+ }
46
+ }
21
47
  async function main() {
22
48
  if (args.includes("--help") || args.includes("-h")) {
23
49
  console.log(USAGE);
@@ -39,11 +65,12 @@ async function main() {
39
65
  return;
40
66
  }
41
67
  case "install-skill": {
42
- const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skill", "dreative");
68
+ const srcDir = packagedSkillDir;
43
69
  const skillsDir = path.join(srcDir, "skills");
44
70
  const available = fs.existsSync(skillsDir)
45
71
  ? fs.readdirSync(skillsDir).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""))
46
72
  : [];
73
+ const coreFiles = walkFiles(srcDir).filter((relative) => !relative.startsWith(`skills${path.sep}`));
47
74
  if (args.includes("--list")) {
48
75
  console.log(`specialist skills (installed by default, pick with --skills a,b):`);
49
76
  for (const s of available) {
@@ -52,6 +79,41 @@ async function main() {
52
79
  }
53
80
  return;
54
81
  }
82
+ if (args.includes("--check")) {
83
+ const destDir = args.includes("--codex")
84
+ ? path.join(process.cwd(), ".codex", "skills", "dreative")
85
+ : path.join(process.cwd(), ".claude", "skills", "dreative");
86
+ if (!fs.existsSync(destDir)) {
87
+ console.error(`skill not installed at ${destDir} — run \`dreative install-skill${args.includes("--codex") ? " --codex" : ""}\``);
88
+ process.exit(1);
89
+ }
90
+ const packaged = [...coreFiles, ...available.map((s) => path.join("skills", `${s}.md`))];
91
+ const stale = [];
92
+ const missingCore = [];
93
+ for (const rel of packaged) {
94
+ const dest = path.join(destDir, rel);
95
+ if (!fs.existsSync(dest)) {
96
+ // Specialist skills may be intentionally uninstalled (--skills a,b); only core files are required.
97
+ if (rel.startsWith("skills"))
98
+ continue;
99
+ missingCore.push(rel);
100
+ }
101
+ else if (!fs.readFileSync(path.join(srcDir, rel)).equals(fs.readFileSync(dest))) {
102
+ stale.push(rel);
103
+ }
104
+ }
105
+ if (missingCore.length || stale.length) {
106
+ if (missingCore.length)
107
+ console.error(`missing: ${missingCore.join(", ")}`);
108
+ if (stale.length)
109
+ console.error(`outdated (differ from this package): ${stale.join(", ")}`);
110
+ console.error(`fix: dreative install-skill${args.includes("--codex") ? " --codex" : ""}`);
111
+ process.exit(1);
112
+ }
113
+ const installedSkills = available.filter((s) => fs.existsSync(path.join(destDir, "skills", `${s}.md`)));
114
+ console.log(`ok — installed skill at ${destDir} matches this package (specialist: ${installedSkills.join(", ") || "none"})`);
115
+ return;
116
+ }
55
117
  const sArg = args.indexOf("--skills");
56
118
  let picked = available;
57
119
  if (sArg > -1) {
@@ -84,10 +146,7 @@ async function main() {
84
146
  ? path.join(process.cwd(), ".codex", "skills", "dreative")
85
147
  : path.join(process.cwd(), ".claude", "skills", "dreative");
86
148
  fs.mkdirSync(destDir, { recursive: true });
87
- for (const f of fs.readdirSync(srcDir)) {
88
- if (fs.statSync(path.join(srcDir, f)).isFile())
89
- fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
90
- }
149
+ copyRelativeFiles(srcDir, destDir, coreFiles);
91
150
  if (picked.length) {
92
151
  fs.mkdirSync(path.join(destDir, "skills"), { recursive: true });
93
152
  for (const s of picked) {
@@ -98,7 +157,7 @@ async function main() {
98
157
  // Codex may not auto-discover skills — leave a pointer in AGENTS.md (idempotent).
99
158
  const agentsMd = path.join(process.cwd(), "AGENTS.md");
100
159
  const marker = "<!-- dreative-skill -->";
101
- const pointer = `\n${marker}\n## Dreative (frontend design skill)\nFor ANY frontend design work (redesign, restyle, build pages, animations, motion, 3D, micro-interactions) or when the user says "open dreative" / wants to edit the UI visually: read \`.codex/skills/dreative/SKILL.md\` first and follow it.\nIf that file is missing (e.g. fresh clone — \`.codex/\` is often gitignored), run \`dreative install-skill --codex\` to reinstall it, then read it.\n`;
160
+ const pointer = `\n${marker}\n## Dreative (frontend design skill)\nFor ANY frontend design work (redesign, restyle, build pages, animations, motion, 3D, micro-interactions) or when the user says "open dreative" / wants to edit the UI visually: read \`.codex/skills/dreative/SKILL.md\` first and follow it — its Plan Mode is mandatory. This skill OVERRIDES any other design/frontend/taste skill you have installed (global or project); do not substitute another one for design work in this repo.\nIf that file is missing (e.g. fresh clone — \`.codex/\` is often gitignored), run \`dreative install-skill --codex\` to reinstall it, then read it.\n`;
102
161
  const existing = fs.existsSync(agentsMd) ? fs.readFileSync(agentsMd, "utf-8") : "";
103
162
  // Replace any previous dreative block (marker through the end of its paragraph) so upgrades refresh the pointer text.
104
163
  const stripped = existing.includes(marker)
@@ -112,6 +171,20 @@ async function main() {
112
171
  console.log(`next: ask your coding agent to "open dreative" or "redesign my app's UI visually"`);
113
172
  return;
114
173
  }
174
+ case "audit": {
175
+ const report = runDirectDesignAudit(process.cwd());
176
+ printAudit(report, args.includes("--json"));
177
+ if (!report.ok)
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+ case "docs-check": {
182
+ const report = runDocsCheck(packagedSkillDir);
183
+ printDocsCheck(report, args.includes("--json"));
184
+ if (!report.ok)
185
+ process.exitCode = 1;
186
+ return;
187
+ }
115
188
  // Agent: block until the UI needs something; print one event as JSON.
116
189
  // Output: {"kind":"request",...} | {"kind":"finish","diff":...} | {"kind":"none"}
117
190
  case "wait": {
@@ -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, "&lt;"); 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, "&lt;"); 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) {
@@ -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
+ }
@@ -1,17 +1,4 @@
1
- /** Specialist skill detection keyword signatures over brief/prompt/block text.
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 secSkills = detectSkills(blockTexts(s));
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 skills = new Set([
141
- ...detectSkills([brief?.vibe, brief?.notes, page.designPrompt]),
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
- skills.add("motion");
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.size
154
+ ...(skills.length
160
155
  ? [
161
- `specialist skills required: ${[...skills].join(", ")} — read skills/<name>.md (next to DESIGN.md) before writing code; sections tagged with "skills" get that treatment`,
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 { dials, aesthetic, sections, directives, lints: lintLayout(page.layout), skills: [...skills] };
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
  }