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/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
|
-
|
|
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
|
-
|
|
7
|
+
## Direct design (default)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Install the skill, then ask your coding agent to design or redesign a page. The skill provides:
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
|
|
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", "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 =
|
|
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
|
|
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
|
-
|
|
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": {
|