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.
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 +45 -9
  7. package/dist/server/preview.js +73 -73
  8. package/dist/server/store.js +11 -0
  9. package/dist/shared/artifacts.js +220 -0
  10. package/dist/shared/design.js +45 -24
  11. package/dist/shared/ruleSystem.js +220 -0
  12. package/dist/shared/ruleSystem.test.js +262 -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 +144 -108
  19. package/skill/dreative/PLAN.md +285 -116
  20. package/skill/dreative/SKILL.md +237 -144
  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 +68 -0
  31. package/skill/dreative/references/ARTIFACTS.md +196 -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 +44 -0
  36. package/skill/dreative/schemas/plan.schema.json +309 -0
  37. package/skill/dreative/schemas/verify.schema.json +75 -0
  38. package/skill/dreative/skills/3d.md +94 -267
  39. package/skill/dreative/skills/cinematic.md +56 -232
  40. package/skill/dreative/skills/experimental.md +108 -95
  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 +149 -564
  44. package/skill/dreative/skills/mobile.md +129 -117
  45. package/skill/dreative/skills/motion.md +126 -256
  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/README.md CHANGED
@@ -1,35 +1,57 @@
1
- # Dreative
1
+ # Dreative
2
+
3
+ Frontend design **skill for coding CLIs** (Claude Code, Codex, etc.) with an optional visual round-trip editor. By default the agent plans a design, selects specialist treatments, edits the real application directly, preserves existing behavior, and verifies the result. No server or extraction is involved in that mode.
2
4
 
3
- Visual round-trip UI editing **skill for coding CLIs** (Claude Code etc.). The CLI agent extracts your app's current UI page-by-page into editable wireframes; you tweak them in the browser (drag-drop, ref images, text prompts); clicking **Finish** hands a compact layout **diff** back to the agent, which rewrites your real code to match.
5
+ Dreative itself has **no AI** your coding agent is the intelligence. See `skill/dreative/SKILL.md` for the direct-design workflow and optional editor mode.
4
6
 
5
- Dreative itself has **no AI** — the web UI is a dumb visual editor plus a request queue. Your coding agent is the intelligence: it services UI prompts via `dreative wait` / `dreative respond` and applies the finish diff. See `skill/dreative/SKILL.md` for the agent workflow (install it into `.claude/skills/dreative/`).
7
+ ## Direct design (default)
6
8
 
7
- ## Run
9
+ Install the skill, then ask your coding agent to design or redesign a page. The skill provides:
8
10
 
9
- ```sh
10
- npm install
11
- npm run build
12
- npm link # once makes the `dreative` command available globally
13
- ```
14
-
15
- Then in any project folder:
11
+ - four explicit ambition tiers, from solid product UI through award-site work;
12
+ - ten composable specialist skills with dependency resolution;
13
+ - user-approved multi-page skill routing with explicit per-page assignments;
14
+ - a rule registry that separates hard gates, evidence-backed defaults, and
15
+ creative provocations, with audited substitutions for equally ambitious alternatives;
16
+ - reflex-font review and diversity-or-development planning instead of fixed aesthetic recipes;
17
+ - framework adapters for React/Vite, Next.js, Vue/Nuxt, SvelteKit, and styling systems;
18
+ - typed plan, preservation, decision-ledger, and verification artifacts;
19
+ - `dreative audit`, which checks delivery status, preservation, assets, evidence, and common frontend risks.
16
20
 
17
21
  ```sh
18
- dreative # starts the local server and opens http://localhost:4820
22
+ dreative install-skill --codex # or omit --codex for Claude Code
23
+ dreative audit # run after implementation
24
+ dreative docs-check # validate the packaged doctrine and references
19
25
  ```
20
26
 
21
- State lives in `.dreative/` in that folder (`project.json`, `refs/`, `generated/`).
22
-
23
- ## Workflow
24
-
25
- 1. **Extract** — the CLI agent reads your app and writes `.dreative/project.json` (wireframe per page, real labels, `source` pointers to owning files), then snapshots it: `dreative baseline`.
26
- 2. **Tweak** — in the browser: drag blocks, add/remove/duplicate, attach reference images and prompts per page/block/element. Prompt-driven actions (propose layouts, block edits, design passes) are queued for your agent, which services them via `dreative wait` → `dreative respond`.
27
- 3. **Finish** — click ✅ Finish; the agent receives only the diff vs the baseline (changed/moved/added/removed blocks + annotations) and applies it to the real codebase using the `source` pointers — token-efficient by construction.
28
-
29
- CLI: `dreative` / `dreative start` (server+UI) · `dreative install-skill` (copy skill into ./.claude/skills) · `dreative wait` (block for next UI event) · `dreative respond <id> [result.json|--error msg]` · `dreative baseline`.
30
-
31
- ## Dev
32
-
33
- ```sh
34
- npm run dev # server on :4820 (tsx watch) + Vite UI on :5199 with proxy
35
- ```
27
+ The browser workflow below is optional and only used when explicitly requested.
28
+
29
+ ## Run
30
+
31
+ ```sh
32
+ npm install
33
+ npm run build
34
+ npm link # once — makes the `dreative` command available globally
35
+ ```
36
+
37
+ Then in any project folder:
38
+
39
+ ```sh
40
+ dreative # starts the local server and opens http://localhost:4820
41
+ ```
42
+
43
+ State lives in `.dreative/` in that folder (`project.json`, `refs/`, `generated/`).
44
+
45
+ ## Workflow
46
+
47
+ 1. **Extract** — the CLI agent reads your app and writes `.dreative/project.json` (wireframe per page, real labels, `source` pointers to owning files), then snapshots it: `dreative baseline`.
48
+ 2. **Tweak** — in the browser: drag blocks, add/remove/duplicate, attach reference images and prompts per page/block/element. Prompt-driven actions (propose layouts, block edits, design passes) are queued for your agent, which services them via `dreative wait` → `dreative respond`.
49
+ 3. **Finish** — click ✅ Finish; the agent receives only the diff vs the baseline (changed/moved/added/removed blocks + annotations) and applies it to the real codebase using the `source` pointers — token-efficient by construction.
50
+
51
+ CLI: `dreative` / `dreative start` (server+UI) · `dreative install-skill` (copy skill into ./.claude/skills) · `dreative wait` (block for next UI event) · `dreative respond <id> [result.json|--error msg]` · `dreative baseline`.
52
+
53
+ ## Dev
54
+
55
+ ```sh
56
+ npm run dev # server on :4820 (tsx watch) + Vite UI on :5199 with proxy
57
+ ```
@@ -0,0 +1,206 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { validateDecisionLedger, validatePlan, validatePreservationManifest, validateVerificationReport, } from "../shared/artifacts.js";
5
+ import { resolveSkillDependencies } from "../shared/skillSystem.js";
6
+ import { validateRuleControls, } from "../shared/ruleSystem.js";
7
+ function readJson(file) {
8
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
9
+ }
10
+ const packagedSkillDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skill", "dreative");
11
+ function loadRuleFiles() {
12
+ return {
13
+ registry: readJson(path.join(packagedSkillDir, "references", "RULES.json")),
14
+ reflexFonts: readJson(path.join(packagedSkillDir, "references", "REFLEX_FONTS.json")),
15
+ };
16
+ }
17
+ function finding(level, check, message) {
18
+ return { level, check, message };
19
+ }
20
+ function checkArtifact(file, check, validate) {
21
+ if (!fs.existsSync(file))
22
+ return [finding("error", check, `missing ${path.basename(file)}`)];
23
+ try {
24
+ return validate(readJson(file)).map((message) => finding("error", check, message));
25
+ }
26
+ catch (error) {
27
+ return [finding("error", check, `cannot parse ${path.basename(file)}: ${String(error)}`)];
28
+ }
29
+ }
30
+ function checkPreservation(projectDir, manifestFile) {
31
+ const findings = checkArtifact(manifestFile, "preservation", validatePreservationManifest);
32
+ if (findings.some((item) => item.level === "error"))
33
+ return findings;
34
+ const manifest = readJson(manifestFile);
35
+ for (const item of manifest.items) {
36
+ if (item.intentionallyChanged)
37
+ continue;
38
+ const target = path.resolve(projectDir, item.file);
39
+ if (!target.startsWith(path.resolve(projectDir) + path.sep)) {
40
+ findings.push(finding("error", "preservation", `${item.id}: file escapes the project root`));
41
+ continue;
42
+ }
43
+ if (!fs.existsSync(target)) {
44
+ findings.push(finding("error", "preservation", `${item.id}: missing ${item.file}`));
45
+ continue;
46
+ }
47
+ if (!fs.readFileSync(target, "utf-8").includes(item.needle))
48
+ findings.push(finding("error", "preservation", `${item.id}: expected content is absent from ${item.file}`));
49
+ }
50
+ return findings;
51
+ }
52
+ function checkSkillClosure(plan) {
53
+ const findings = [];
54
+ const requested = plan.skills;
55
+ const missing = resolveSkillDependencies(requested).filter((skill) => !requested.includes(skill));
56
+ if (missing.length)
57
+ findings.push(finding("error", "skills", `missing required skill dependencies: ${missing.join(", ")}`));
58
+ for (const page of plan.pages) {
59
+ const pageMissing = resolveSkillDependencies(page.skills).filter((skill) => !page.skills.includes(skill));
60
+ if (pageMissing.length)
61
+ findings.push(finding("error", "skills", `${page.name} is missing dependencies: ${pageMissing.join(", ")}`));
62
+ }
63
+ return findings;
64
+ }
65
+ function checkAssets(projectDir, plan) {
66
+ const findings = [];
67
+ for (const page of plan.pages) {
68
+ for (const section of page.sections) {
69
+ for (const asset of section.assets) {
70
+ if (asset.status !== "shipped")
71
+ continue;
72
+ const target = path.resolve(projectDir, asset.path);
73
+ if (!target.startsWith(path.resolve(projectDir) + path.sep) || !fs.existsSync(target))
74
+ findings.push(finding("error", "assets", `${page.name}/${section.name}/${asset.id}: shipped asset missing at ${asset.path}`));
75
+ }
76
+ }
77
+ }
78
+ return findings;
79
+ }
80
+ const SOURCE_EXTENSIONS = new Set([".tsx", ".jsx", ".vue", ".svelte", ".html", ".css", ".scss"]);
81
+ const SKIP_DIRS = new Set([".git", ".dreative", "node_modules", "dist", "build", ".next", ".nuxt"]);
82
+ function collectSourceFiles(root, current = root) {
83
+ const files = [];
84
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
85
+ if (SKIP_DIRS.has(entry.name))
86
+ continue;
87
+ const absolute = path.join(current, entry.name);
88
+ if (entry.isDirectory())
89
+ files.push(...collectSourceFiles(root, absolute));
90
+ else if (SOURCE_EXTENSIONS.has(path.extname(entry.name)))
91
+ files.push(absolute);
92
+ }
93
+ return files;
94
+ }
95
+ function checkStaticQuality(projectDir, plan) {
96
+ const findings = [];
97
+ const files = collectSourceFiles(projectDir);
98
+ let combined = "";
99
+ for (const file of files) {
100
+ const source = fs.readFileSync(file, "utf-8");
101
+ combined += `\n${source}`;
102
+ const relative = path.relative(projectDir, file);
103
+ for (const match of source.matchAll(/<img\b([^>]*?)>/gis)) {
104
+ const attributes = match[1];
105
+ if (!/\balt\s*=/.test(attributes))
106
+ findings.push(finding("error", "accessibility", `${relative}: image is missing alt`));
107
+ if (!/\b(width|height)\s*=/.test(attributes) && !/\baspect-/.test(attributes))
108
+ findings.push(finding("warning", "layout", `${relative}: image has no explicit dimensions or aspect token`));
109
+ }
110
+ if (/<(?:div|span)\b[^>]*\bonClick\s*=/is.test(source) && !/\b(role|tabIndex|onKeyDown)\s*=/is.test(source))
111
+ findings.push(finding("warning", "accessibility", `${relative}: non-semantic click target may lack keyboard support`));
112
+ if (/outline\s*:\s*none|outline-none/.test(source) && !/focus-visible/.test(source))
113
+ findings.push(finding("warning", "accessibility", `${relative}: focus outline is removed without a visible replacement`));
114
+ if (/href\s*=\s*["']#["']/.test(source))
115
+ findings.push(finding("warning", "navigation", `${relative}: placeholder href="#" remains`));
116
+ }
117
+ if (plan.skills.some((skill) => ["motion", "3d", "immersive", "cinematic", "experimental"].includes(skill))) {
118
+ if (!/prefers-reduced-motion|useReducedMotion|reducedMotion/i.test(combined))
119
+ findings.push(finding("error", "motion", "selected effects require an implemented reduced-motion path"));
120
+ }
121
+ return findings;
122
+ }
123
+ function checkVerificationProof(projectDir, verificationFile) {
124
+ if (!fs.existsSync(verificationFile))
125
+ return [];
126
+ let report;
127
+ try {
128
+ report = readJson(verificationFile);
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ const findings = [];
134
+ for (const item of report.evidence ?? []) {
135
+ const proof = item.proof;
136
+ if (!proof)
137
+ continue;
138
+ if (proof.artifactPath) {
139
+ const target = path.resolve(projectDir, proof.artifactPath);
140
+ if (!target.startsWith(path.resolve(projectDir) + path.sep))
141
+ findings.push(finding("error", "verification", `${item.id}: artifact path escapes the project root`));
142
+ else if (!fs.existsSync(target))
143
+ findings.push(finding("error", "verification", `${item.id}: evidence artifact is missing at ${proof.artifactPath}`));
144
+ }
145
+ if (proof.viewport && (proof.viewport.width <= 0 || proof.viewport.height <= 0 || (proof.viewport.dpr ?? 1) <= 0))
146
+ findings.push(finding("error", "verification", `${item.id}: viewport dimensions and DPR must be positive`));
147
+ if (typeof proof.averageFps === "number" && proof.averageFps <= 0)
148
+ findings.push(finding("error", "verification", `${item.id}: averageFps must be positive`));
149
+ if (typeof proof.maxFrameTimeMs === "number" && proof.maxFrameTimeMs < 0)
150
+ findings.push(finding("error", "verification", `${item.id}: maxFrameTimeMs cannot be negative`));
151
+ }
152
+ return findings;
153
+ }
154
+ export function runDirectDesignAudit(projectDir) {
155
+ const root = path.join(projectDir, ".dreative");
156
+ const planFile = path.join(root, "plan.json");
157
+ const findings = checkArtifact(planFile, "plan", validatePlan);
158
+ if (!fs.existsSync(planFile) || findings.some((item) => item.level === "error"))
159
+ return { ok: false, findings };
160
+ const plan = readJson(planFile);
161
+ const preservationFile = path.resolve(projectDir, plan.preservationManifest);
162
+ const ledgerFile = path.resolve(projectDir, plan.decisionLedger);
163
+ const verificationFile = path.join(root, "verify.json");
164
+ findings.push(...checkPreservation(projectDir, preservationFile));
165
+ findings.push(...checkArtifact(ledgerFile, "ledger", validateDecisionLedger));
166
+ findings.push(...checkArtifact(verificationFile, "verification", validateVerificationReport));
167
+ findings.push(...checkVerificationProof(projectDir, verificationFile));
168
+ if (plan.doctrineVersion !== 2) {
169
+ findings.push(finding("warning", "migration", "legacy v2 plan accepted; add doctrineVersion: 2 and the new creative-control fields on the next design run"));
170
+ }
171
+ else {
172
+ try {
173
+ const { registry, reflexFonts } = loadRuleFiles();
174
+ const verification = fs.existsSync(verificationFile) ? readJson(verificationFile) : undefined;
175
+ for (const message of validateRuleControls(plan, registry, reflexFonts, verification))
176
+ findings.push(finding("error", "rules", message));
177
+ }
178
+ catch (error) {
179
+ findings.push(finding("error", "rules", `cannot load rule registries: ${String(error)}`));
180
+ }
181
+ }
182
+ findings.push(...checkSkillClosure(plan));
183
+ findings.push(...checkAssets(projectDir, plan));
184
+ findings.push(...checkStaticQuality(projectDir, plan));
185
+ const verify = fs.existsSync(verificationFile) ? JSON.stringify(readJson(verificationFile)) : "";
186
+ for (const page of plan.pages) {
187
+ for (const section of page.sections) {
188
+ for (const criterion of section.verification) {
189
+ if (!verify.includes(criterion))
190
+ findings.push(finding("warning", "verification", `${page.name}/${section.name}: no evidence references criterion: ${criterion}`));
191
+ }
192
+ }
193
+ }
194
+ return { ok: !findings.some((item) => item.level === "error"), findings };
195
+ }
196
+ export function printAudit(report, json) {
197
+ if (json) {
198
+ console.log(JSON.stringify(report, null, 2));
199
+ return;
200
+ }
201
+ if (report.findings.length === 0)
202
+ console.log("Dreative audit passed with no findings.");
203
+ for (const item of report.findings)
204
+ console.log(`${item.level === "error" ? "ERROR" : "WARN "} [${item.check}] ${item.message}`);
205
+ console.log(report.ok ? "Dreative audit passed." : "Dreative audit failed.");
206
+ }
@@ -0,0 +1,79 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { runDirectDesignAudit } from "./audit.js";
7
+ test("direct-design audit verifies artifacts and preservation needles", () => {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "dreative-audit-"));
9
+ try {
10
+ fs.mkdirSync(path.join(root, ".dreative"));
11
+ fs.mkdirSync(path.join(root, "src"));
12
+ fs.writeFileSync(path.join(root, "src", "App.tsx"), 'export const App = () => <a href="/pricing">Pricing</a>;');
13
+ fs.writeFileSync(path.join(root, ".dreative", "plan.json"), JSON.stringify({
14
+ version: 2,
15
+ request: "Polish pricing",
16
+ createdAt: new Date().toISOString(),
17
+ tier: "solid",
18
+ depth: "restyle",
19
+ skills: ["ux", "mobile"],
20
+ skillPolicy: { mode: "hybrid", global: ["ux", "mobile"], routingApproved: true, userAssignments: [] },
21
+ designRead: { register: "product", concept: "clear value", signature: "pricing rail" },
22
+ preservationManifest: ".dreative/preservation.json",
23
+ decisionLedger: ".dreative/ledger.json",
24
+ pages: [
25
+ {
26
+ id: "pricing-page",
27
+ name: "Pricing page",
28
+ skills: ["ux", "mobile"],
29
+ sections: [
30
+ {
31
+ id: "pricing",
32
+ name: "Pricing",
33
+ layoutFamily: "comparison",
34
+ skills: ["ux", "mobile"],
35
+ interactions: [],
36
+ mobile: "stacked plans",
37
+ fallback: "semantic table",
38
+ verification: ["Pricing link remains"],
39
+ assets: [],
40
+ status: "shipped",
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ }));
46
+ fs.writeFileSync(path.join(root, ".dreative", "preservation.json"), JSON.stringify({
47
+ version: 1,
48
+ createdAt: new Date().toISOString(),
49
+ items: [{ id: "pricing-link", kind: "link", file: "src/App.tsx", needle: 'href="/pricing"', purpose: "Pricing navigation" }],
50
+ }));
51
+ fs.writeFileSync(path.join(root, ".dreative", "ledger.json"), JSON.stringify({ version: 1, entries: [] }));
52
+ fs.writeFileSync(path.join(root, ".dreative", "verify.json"), JSON.stringify({
53
+ version: 1,
54
+ generatedAt: new Date().toISOString(),
55
+ evidence: [
56
+ {
57
+ id: "pricing-link",
58
+ criterion: "Pricing link remains",
59
+ status: "pass",
60
+ evidence: "Clicked /pricing in production build",
61
+ proof: { timestamp: new Date().toISOString(), testedUrl: "http://localhost:3000/pricing", consoleErrorCount: 0 },
62
+ },
63
+ ],
64
+ }));
65
+ const report = runDirectDesignAudit(root);
66
+ assert.equal(report.ok, true, JSON.stringify(report.findings));
67
+ assert.ok(report.findings.some((item) => item.check === "migration" && item.level === "warning"));
68
+ const verifyFile = path.join(root, ".dreative", "verify.json");
69
+ const verify = JSON.parse(fs.readFileSync(verifyFile, "utf-8"));
70
+ verify.evidence[0].proof.artifactPath = ".dreative/screenshots/missing.png";
71
+ fs.writeFileSync(verifyFile, JSON.stringify(verify));
72
+ const missingArtifact = runDirectDesignAudit(root);
73
+ assert.equal(missingArtifact.ok, false);
74
+ assert.ok(missingArtifact.findings.some((item) => item.message.includes("evidence artifact is missing")));
75
+ }
76
+ finally {
77
+ fs.rmSync(root, { recursive: true, force: true });
78
+ }
79
+ });
@@ -0,0 +1,163 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { SKILL_DEFINITIONS, resolveSkillDependencies } from "../shared/skillSystem.js";
4
+ const UNIVERSAL_RULE = "Universal foundation: ux and baseline mobile apply to every web page.";
5
+ const UNIVERSAL_FILES = ["SKILL.md", "references/SKILL_CONTRACT.md", "skills/ux.md", "skills/mobile.md"];
6
+ const RUNTIME_MARKDOWN = new Set(["plan.md", "verify.md", "system.md", "ledger.md"]);
7
+ function walk(root, current = root) {
8
+ return fs.readdirSync(current, { withFileTypes: true }).flatMap((entry) => {
9
+ const absolute = path.join(current, entry.name);
10
+ return entry.isDirectory() ? walk(root, absolute) : [path.relative(root, absolute).split(path.sep).join("/")];
11
+ });
12
+ }
13
+ function add(findings, check, file, message) {
14
+ findings.push({ check, file, message });
15
+ }
16
+ function resolveReference(skillDir, from, reference) {
17
+ if (reference.includes("<") ||
18
+ reference.includes(">") ||
19
+ reference.includes(".dreative/") ||
20
+ reference.includes(".dreative\\") ||
21
+ path.isAbsolute(reference) ||
22
+ RUNTIME_MARKDOWN.has(path.basename(reference)))
23
+ return true;
24
+ const candidates = [
25
+ path.resolve(skillDir, path.dirname(from), reference),
26
+ path.resolve(skillDir, reference),
27
+ path.resolve(skillDir, "skills", path.basename(reference)),
28
+ path.resolve(skillDir, "references", path.basename(reference)),
29
+ path.resolve(skillDir, "frameworks", path.basename(reference)),
30
+ ];
31
+ return candidates.some((candidate) => fs.existsSync(candidate));
32
+ }
33
+ export function runDocsCheck(skillDir) {
34
+ const findings = [];
35
+ const files = walk(skillDir).filter((file) => file.endsWith(".md") || file.endsWith(".json"));
36
+ const contents = new Map(files.map((file) => [file, fs.readFileSync(path.join(skillDir, file), "utf-8")]));
37
+ for (const [file, content] of contents) {
38
+ if (/^(<<<<<<<|=======|>>>>>>>)/m.test(content))
39
+ add(findings, "merge-markers", file, "contains unresolved merge markers");
40
+ if (/\bAskUserQuestion\b/.test(content))
41
+ add(findings, "obsolete-tool", file, "names AskUserQuestion instead of using tool-neutral wording");
42
+ if (file.endsWith(".md")) {
43
+ for (const match of content.matchAll(/`([^`\n]+\.md)`/g)) {
44
+ if (!resolveReference(skillDir, file, match[1]))
45
+ add(findings, "references", file, `missing referenced file: ${match[1]}`);
46
+ }
47
+ }
48
+ }
49
+ const schemaFile = "schemas/plan.schema.json";
50
+ try {
51
+ const schema = JSON.parse(contents.get(schemaFile) ?? "{}");
52
+ const tiers = schema.properties?.tier?.enum ?? [];
53
+ const expected = ["solid", "premium", "expressive", "award"];
54
+ if (JSON.stringify(tiers) !== JSON.stringify(expected))
55
+ add(findings, "tiers", schemaFile, `tier enum must be ${expected.join(" | ")}`);
56
+ for (const file of ["SKILL.md", "PLAN.md", "references/ARTIFACTS.md", "references/TIERS.md"]) {
57
+ const content = contents.get(file) ?? "";
58
+ for (const tier of expected) {
59
+ if (!new RegExp(`\\b${tier}\\b`, "i").test(content))
60
+ add(findings, "tiers", file, `does not mention canonical tier ${tier}`);
61
+ }
62
+ }
63
+ const plan = contents.get("PLAN.md") ?? "";
64
+ for (const tier of expected) {
65
+ if (!plan.includes(`(\`${tier}\`)`))
66
+ add(findings, "tiers", "PLAN.md", `choice label must expose stored id (${tier})`);
67
+ }
68
+ if (/\*\*(safe|award-site)\*\*/i.test(plan))
69
+ add(findings, "tiers", "PLAN.md", "contains a legacy tier choice label");
70
+ }
71
+ catch (error) {
72
+ add(findings, "tiers", schemaFile, `cannot parse tier schema: ${String(error)}`);
73
+ }
74
+ for (const skill of ["media", "motion", "3d", "immersive", "cinematic"]) {
75
+ const specialist = `skills/${skill}.md`;
76
+ const recipe = `recipes/${skill}-recipes.md`;
77
+ if (!contents.has(recipe))
78
+ add(findings, "recipes", recipe, "recipe reference is missing");
79
+ if (!(contents.get(specialist) ?? "").includes(`../${recipe}`))
80
+ add(findings, "recipes", specialist, "does not point to its progressively loaded recipe reference");
81
+ if (/^## (?:\d+(?:\.\d+)*\.?\s+)?(?:implementation\s+)?recipes?(?:\s|$)/im.test(contents.get(specialist) ?? ""))
82
+ add(findings, "recipes", specialist, "inline recipe catalog remains in the primary specialist file");
83
+ }
84
+ const planSchema = contents.get("schemas/plan.schema.json") ?? "";
85
+ for (const field of ["doctrineVersion", "ruleExceptions", "creativeStrategy", "motionComplexityBudget", "motionTreatment", "preparation", "fontDecision", "experimentalPlan", "conceptExploration", "recipeAccess"]) {
86
+ if (!planSchema.includes(`\"${field}\"`))
87
+ add(findings, "schema", "schemas/plan.schema.json", `missing creative-control field ${field}`);
88
+ }
89
+ const main = contents.get("SKILL.md") ?? "";
90
+ for (const definition of SKILL_DEFINITIONS) {
91
+ const file = `skills/${definition.name}.md`;
92
+ if (!contents.has(file))
93
+ add(findings, "skills", file, "specialist file is missing");
94
+ if (!main.includes(`\`${definition.name}\``))
95
+ add(findings, "skills", "SKILL.md", `picker omits ${definition.name}`);
96
+ for (const dependency of resolveSkillDependencies([definition.name])) {
97
+ if (!SKILL_DEFINITIONS.some((candidate) => candidate.name === dependency))
98
+ add(findings, "skills", file, `dependency ${dependency} has no definition`);
99
+ }
100
+ }
101
+ for (const file of UNIVERSAL_FILES) {
102
+ if (!(contents.get(file) ?? "").includes(UNIVERSAL_RULE))
103
+ add(findings, "universal-rules", file, "universal ux/mobile rule differs from the canonical wording");
104
+ }
105
+ try {
106
+ const registry = JSON.parse(contents.get("references/RULES.json") ?? "{}");
107
+ const ids = new Set();
108
+ for (const rule of registry.rules ?? []) {
109
+ if (!rule.id || ids.has(rule.id))
110
+ add(findings, "rules", "references/RULES.json", `missing or duplicate rule id: ${rule.id}`);
111
+ ids.add(rule.id);
112
+ if (!["hard-gate", "evidence-backed-default", "creative-provocation"].includes(rule.category))
113
+ add(findings, "rules", "references/RULES.json", `${rule.id}: invalid category`);
114
+ if (rule.category === "hard-gate" && rule.exceptionAllowed)
115
+ add(findings, "rules", "references/RULES.json", `${rule.id}: hard gates cannot allow exceptions`);
116
+ if (!rule.observedFailure || !rule.defaultRemedy || !rule.exceptionTest)
117
+ add(findings, "rules", "references/RULES.json", `${rule.id}: missing history, remedy, or exception test`);
118
+ }
119
+ if (!registry.wordingPrinciple?.includes("measurable success criteria"))
120
+ add(findings, "rules", "references/RULES.json", "missing bounded-exception wording principle");
121
+ }
122
+ catch (error) {
123
+ add(findings, "rules", "references/RULES.json", `cannot parse registry: ${String(error)}`);
124
+ }
125
+ try {
126
+ const reflex = JSON.parse(contents.get("references/REFLEX_FONTS.json") ?? "{}");
127
+ if (!Array.isArray(reflex.fonts) || reflex.fonts.length === 0)
128
+ add(findings, "fonts", "references/REFLEX_FONTS.json", "reflex font list is empty");
129
+ if (!Array.isArray(reflex.validReasonKinds) || reflex.validReasonKinds.length === 0)
130
+ add(findings, "fonts", "references/REFLEX_FONTS.json", "valid reason-kind list is empty");
131
+ if (!Array.isArray(reflex.invalidGenericReasons) || reflex.invalidGenericReasons.length === 0)
132
+ add(findings, "fonts", "references/REFLEX_FONTS.json", "generic-reason list is empty");
133
+ }
134
+ catch (error) {
135
+ add(findings, "fonts", "references/REFLEX_FONTS.json", `cannot parse registry: ${String(error)}`);
136
+ }
137
+ const design = contents.get("DESIGN.md") ?? "";
138
+ const lines = design.split(/\r?\n/);
139
+ const bannedTokens = new Set();
140
+ for (const line of lines) {
141
+ if (/\b(banned|never)\b/i.test(line))
142
+ for (const match of line.matchAll(/`([^`]+)`/g))
143
+ bannedTokens.add(match[1]);
144
+ }
145
+ for (const token of bannedTokens) {
146
+ const contradictory = lines.find((line) => line.includes(`\`${token}\``) && /\b(allowed|valid|preferred|required|use|source)\b/i.test(line) && !/\b(banned|never|instead of|do not)\b/i.test(line));
147
+ if (contradictory)
148
+ add(findings, "doctrine", "DESIGN.md", `implementation ${token} is both banned and presented positively`);
149
+ }
150
+ const phaseRule = "Run one short decision phase containing several sequential single-question";
151
+ for (const file of ["SKILL.md", "PLAN.md"]) {
152
+ if (!(contents.get(file) ?? "").includes(phaseRule))
153
+ add(findings, "questions", file, "decision-phase rule is inconsistent");
154
+ }
155
+ return { ok: findings.length === 0, findings };
156
+ }
157
+ export function printDocsCheck(report, json) {
158
+ if (json)
159
+ return void console.log(JSON.stringify(report, null, 2));
160
+ for (const item of report.findings)
161
+ console.log(`ERROR [${item.check}] ${item.file}: ${item.message}`);
162
+ console.log(report.ok ? "Dreative docs check passed." : "Dreative docs check failed.");
163
+ }
@@ -0,0 +1,8 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "node:path";
4
+ import { runDocsCheck } from "./docsCheck.js";
5
+ test("packaged skill documents are internally consistent", () => {
6
+ const report = runDocsCheck(path.resolve("skill", "dreative"));
7
+ assert.equal(report.ok, true, JSON.stringify(report.findings, null, 2));
8
+ });
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);
@@ -17,8 +19,31 @@ const USAGE = `usage: dreative [command]
17
19
  --codex install for Codex CLI instead (.codex/skills/ + AGENTS.md pointer)
18
20
  --check verify the installed skill matches this package (exit 1 on drift)
19
21
  wait (agent) block until the UI needs something; prints one JSON event
20
- respond <id> [result.json | --error msg] (agent) answer a request
21
- 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
+ }
22
47
  async function main() {
23
48
  if (args.includes("--help") || args.includes("-h")) {
24
49
  console.log(USAGE);
@@ -40,11 +65,12 @@ async function main() {
40
65
  return;
41
66
  }
42
67
  case "install-skill": {
43
- const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skill", "dreative");
68
+ const srcDir = packagedSkillDir;
44
69
  const skillsDir = path.join(srcDir, "skills");
45
70
  const available = fs.existsSync(skillsDir)
46
71
  ? fs.readdirSync(skillsDir).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""))
47
72
  : [];
73
+ const coreFiles = walkFiles(srcDir).filter((relative) => !relative.startsWith(`skills${path.sep}`));
48
74
  if (args.includes("--list")) {
49
75
  console.log(`specialist skills (installed by default, pick with --skills a,b):`);
50
76
  for (const s of available) {
@@ -61,8 +87,7 @@ async function main() {
61
87
  console.error(`skill not installed at ${destDir} — run \`dreative install-skill${args.includes("--codex") ? " --codex" : ""}\``);
62
88
  process.exit(1);
63
89
  }
64
- const rootFiles = fs.readdirSync(srcDir).filter((f) => fs.statSync(path.join(srcDir, f)).isFile());
65
- const packaged = [...rootFiles, ...available.map((s) => path.join("skills", `${s}.md`))];
90
+ const packaged = [...coreFiles, ...available.map((s) => path.join("skills", `${s}.md`))];
66
91
  const stale = [];
67
92
  const missingCore = [];
68
93
  for (const rel of packaged) {
@@ -121,10 +146,7 @@ async function main() {
121
146
  ? path.join(process.cwd(), ".codex", "skills", "dreative")
122
147
  : path.join(process.cwd(), ".claude", "skills", "dreative");
123
148
  fs.mkdirSync(destDir, { recursive: true });
124
- for (const f of fs.readdirSync(srcDir)) {
125
- if (fs.statSync(path.join(srcDir, f)).isFile())
126
- fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
127
- }
149
+ copyRelativeFiles(srcDir, destDir, coreFiles);
128
150
  if (picked.length) {
129
151
  fs.mkdirSync(path.join(destDir, "skills"), { recursive: true });
130
152
  for (const s of picked) {
@@ -149,6 +171,20 @@ async function main() {
149
171
  console.log(`next: ask your coding agent to "open dreative" or "redesign my app's UI visually"`);
150
172
  return;
151
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
+ }
152
188
  // Agent: block until the UI needs something; print one event as JSON.
153
189
  // Output: {"kind":"request",...} | {"kind":"finish","diff":...} | {"kind":"none"}
154
190
  case "wait": {