dreative 0.5.3 → 0.5.4
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/dist/cli/audit.js +87 -11
- package/dist/cli/audit.test.js +40 -1
- package/dist/server/index.js +10 -5
- package/dist/shared/artifacts.js +100 -5
- package/dist/shared/artifacts.test.js +101 -0
- package/dist/shared/design.js +209 -24
- package/dist/shared/design.test.js +70 -0
- package/dist/shared/ruleSystem.js +1 -1
- package/dist/ui/assets/{index-CKwmbx2j.js → index-BQhYTdSa.js} +11 -11
- package/dist/ui/index.html +1 -1
- package/package.json +5 -5
- package/skill/dreative/DESIGN.md +13 -9
- package/skill/dreative/PLAN.md +13 -3
- package/skill/dreative/SKILL.md +15 -4
- package/skill/dreative/references/ARTIFACTS.md +26 -8
- package/skill/dreative/references/TIERS.md +7 -1
- package/skill/dreative/schemas/plan.schema.json +87 -5
- package/skill/dreative/schemas/verify.schema.json +8 -2
- package/skill/dreative/skills/mobile.md +6 -1
package/dist/cli/audit.js
CHANGED
|
@@ -77,6 +77,38 @@ function checkAssets(projectDir, plan) {
|
|
|
77
77
|
}
|
|
78
78
|
return findings;
|
|
79
79
|
}
|
|
80
|
+
function checkAntiSlopPlan(plan) {
|
|
81
|
+
if (plan.version !== 3)
|
|
82
|
+
return [];
|
|
83
|
+
const findings = [];
|
|
84
|
+
const signatures = new Map();
|
|
85
|
+
for (const page of plan.pages) {
|
|
86
|
+
const signature = page.sections.map((section) => section.layoutFamily.trim().toLowerCase()).join(" > ");
|
|
87
|
+
signatures.set(signature, [...(signatures.get(signature) ?? []), page.name]);
|
|
88
|
+
const cardSections = page.sections.filter((section) => /\b(card|tile)\b/i.test(section.layoutFamily));
|
|
89
|
+
if (cardSections.length > 2)
|
|
90
|
+
findings.push(finding("warning", "anti-slop", `${page.name}: repeated card-family sections need an explicit page-specific rationale`));
|
|
91
|
+
if (page.sections.some((section) => /^\s*stack(?:ed)?(?:\s+vertically)?[.!]?\s*$/i.test(section.mobile)))
|
|
92
|
+
findings.push(finding("error", "anti-slop", `${page.name}: a section uses stack-only mobile translation`));
|
|
93
|
+
if (page.register === "task-transaction") {
|
|
94
|
+
const first = page.mobileBlueprint?.contentOrder?.[0] ?? "";
|
|
95
|
+
if (/\b(promo|decorative|hero|story)\b/i.test(first))
|
|
96
|
+
findings.push(finding("error", "anti-slop", `${page.name}: promotional or decorative content precedes the primary mobile task`));
|
|
97
|
+
}
|
|
98
|
+
for (const section of page.sections) {
|
|
99
|
+
if (/\b(testimonial|review)\b/i.test(section.name) && !/\b(state|task|concept|product|brand|evidence|decision)\b/i.test([section.layoutFamily, ...section.interactions].join(" ")))
|
|
100
|
+
findings.push(finding("warning", "anti-slop", `${page.name}/${section.name}: generic social-proof section appears detached from the page concept`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [signature, pages] of signatures) {
|
|
104
|
+
if (signature && pages.length > 1)
|
|
105
|
+
findings.push(finding("warning", "anti-slop", `${pages.join(", ")}: unrelated routes repeat the same section-layout shell (${signature})`));
|
|
106
|
+
}
|
|
107
|
+
const compositions = plan.coherence?.pageSpecificCompositions ?? [];
|
|
108
|
+
if (compositions.length > 1 && new Set(compositions.map((item) => `${item.register}:${item.taskModel.trim().toLowerCase()}`)).size === 1)
|
|
109
|
+
findings.push(finding("warning", "anti-slop", "all pages were assigned the same register and task model; verify that composition was not copied mechanically"));
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
80
112
|
const SOURCE_EXTENSIONS = new Set([".tsx", ".jsx", ".vue", ".svelte", ".html", ".css", ".scss"]);
|
|
81
113
|
const SKIP_DIRS = new Set([".git", ".dreative", "node_modules", "dist", "build", ".next", ".nuxt"]);
|
|
82
114
|
function collectSourceFiles(root, current = root) {
|
|
@@ -151,6 +183,46 @@ function checkVerificationProof(projectDir, verificationFile) {
|
|
|
151
183
|
}
|
|
152
184
|
return findings;
|
|
153
185
|
}
|
|
186
|
+
export function checkVerificationCoverage(plan, report) {
|
|
187
|
+
const findings = [];
|
|
188
|
+
if (plan.version !== 3)
|
|
189
|
+
return findings;
|
|
190
|
+
if (report.version !== 2)
|
|
191
|
+
return [finding("error", "migration", "v3 plans require verify.json version 2; legacy verification cannot satisfy depth and mobile guarantees")];
|
|
192
|
+
const passing = report.evidence.filter((item) => item.status === "pass");
|
|
193
|
+
for (const page of plan.pages) {
|
|
194
|
+
for (const viewportClass of ["desktop", "mobile", "narrow-mobile"]) {
|
|
195
|
+
if (!passing.some((item) => item.pageId === page.id && item.viewportClass === viewportClass && item.proof?.viewport))
|
|
196
|
+
findings.push(finding("error", "verification", `${page.name}: missing passing ${viewportClass} evidence with an exact viewport`));
|
|
197
|
+
}
|
|
198
|
+
if ((plan.depth === "restructure" || plan.depth === "reimagine") && !passing.some((item) => item.pageId === page.id && item.kind === "structural-depth"))
|
|
199
|
+
findings.push(finding("error", "verification", `${page.name}: missing structural-depth evidence against its structural delta`));
|
|
200
|
+
if (!passing.some((item) => item.pageId === page.id && item.kind === "preservation"))
|
|
201
|
+
findings.push(finding("error", "verification", `${page.name}: missing preservation evidence`));
|
|
202
|
+
for (const check of page.mobileBlueprint?.verificationChecks ?? []) {
|
|
203
|
+
if (!passing.some((item) => item.pageId === page.id && (item.viewportClass === "mobile" || item.viewportClass === "narrow-mobile") && item.mobileChecks?.includes(check)))
|
|
204
|
+
findings.push(finding("error", "verification", `${page.name}: missing mobile evidence for ${check}`));
|
|
205
|
+
}
|
|
206
|
+
for (const section of page.sections) {
|
|
207
|
+
for (const criterion of section.verification) {
|
|
208
|
+
if (typeof criterion === "string") {
|
|
209
|
+
findings.push(finding("error", "verification", `${page.name}/${section.name}: v3 criteria must be typed objects, not strings`));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
for (const viewport of criterion.viewports) {
|
|
213
|
+
const match = passing.find((item) => item.criterionId === criterion.id &&
|
|
214
|
+
item.pageId === criterion.pageId &&
|
|
215
|
+
item.sectionId === criterion.sectionId &&
|
|
216
|
+
item.kind === criterion.kind &&
|
|
217
|
+
item.viewportClass === viewport);
|
|
218
|
+
if (!match)
|
|
219
|
+
findings.push(finding("error", "verification", `${page.name}/${section.name}: no passing ${viewport} evidence is associated with criterion ${criterion.id}`));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return findings;
|
|
225
|
+
}
|
|
154
226
|
export function runDirectDesignAudit(projectDir) {
|
|
155
227
|
const root = path.join(projectDir, ".dreative");
|
|
156
228
|
const planFile = path.join(root, "plan.json");
|
|
@@ -165,13 +237,13 @@ export function runDirectDesignAudit(projectDir) {
|
|
|
165
237
|
findings.push(...checkArtifact(ledgerFile, "ledger", validateDecisionLedger));
|
|
166
238
|
findings.push(...checkArtifact(verificationFile, "verification", validateVerificationReport));
|
|
167
239
|
findings.push(...checkVerificationProof(projectDir, verificationFile));
|
|
168
|
-
|
|
169
|
-
|
|
240
|
+
const verification = fs.existsSync(verificationFile) ? readJson(verificationFile) : undefined;
|
|
241
|
+
if (plan.version !== 3 || plan.doctrineVersion !== 3) {
|
|
242
|
+
findings.push(finding("warning", "migration", "legacy v2 plan accepted for compatibility only; migrate to plan v3 and verify v2 for structural-depth, mobile-blueprint, expression, and evidence-association guarantees"));
|
|
170
243
|
}
|
|
171
244
|
else {
|
|
172
245
|
try {
|
|
173
246
|
const { registry, reflexFonts } = loadRuleFiles();
|
|
174
|
-
const verification = fs.existsSync(verificationFile) ? readJson(verificationFile) : undefined;
|
|
175
247
|
for (const message of validateRuleControls(plan, registry, reflexFonts, verification))
|
|
176
248
|
findings.push(finding("error", "rules", message));
|
|
177
249
|
}
|
|
@@ -181,15 +253,19 @@ export function runDirectDesignAudit(projectDir) {
|
|
|
181
253
|
}
|
|
182
254
|
findings.push(...checkSkillClosure(plan));
|
|
183
255
|
findings.push(...checkAssets(projectDir, plan));
|
|
256
|
+
findings.push(...checkAntiSlopPlan(plan));
|
|
184
257
|
findings.push(...checkStaticQuality(projectDir, plan));
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
258
|
+
if (verification)
|
|
259
|
+
findings.push(...checkVerificationCoverage(plan, verification));
|
|
260
|
+
if (plan.version === 2) {
|
|
261
|
+
const verifyText = verification ? JSON.stringify(verification) : "";
|
|
262
|
+
for (const page of plan.pages)
|
|
263
|
+
for (const section of page.sections)
|
|
264
|
+
for (const criterion of section.verification) {
|
|
265
|
+
const text = typeof criterion === "string" ? criterion : criterion.claim;
|
|
266
|
+
if (!verifyText.includes(text))
|
|
267
|
+
findings.push(finding("warning", "verification", `${page.name}/${section.name}: no legacy evidence references criterion: ${text}`));
|
|
268
|
+
}
|
|
193
269
|
}
|
|
194
270
|
return { ok: !findings.some((item) => item.level === "error"), findings };
|
|
195
271
|
}
|
package/dist/cli/audit.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { runDirectDesignAudit } from "./audit.js";
|
|
6
|
+
import { checkVerificationCoverage, runDirectDesignAudit } from "./audit.js";
|
|
7
7
|
test("direct-design audit verifies artifacts and preservation needles", () => {
|
|
8
8
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "dreative-audit-"));
|
|
9
9
|
try {
|
|
@@ -77,3 +77,42 @@ test("direct-design audit verifies artifacts and preservation needles", () => {
|
|
|
77
77
|
fs.rmSync(root, { recursive: true, force: true });
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
|
+
test("v3 audit associates criteria by id, page, section, kind, and viewport", () => {
|
|
81
|
+
const criterion = { id: "depth", claim: "Architecture matches delta", kind: "structural-depth", pageId: "outlets", sectionId: "workspace", viewports: ["desktop"] };
|
|
82
|
+
const plan = {
|
|
83
|
+
version: 3,
|
|
84
|
+
doctrineVersion: 3,
|
|
85
|
+
depth: "restructure",
|
|
86
|
+
pages: [{ id: "outlets", name: "Outlets", mobileBlueprint: { verificationChecks: ["no-horizontal-overflow"] }, sections: [{ id: "workspace", name: "Workspace", verification: [criterion] }] }],
|
|
87
|
+
};
|
|
88
|
+
const makeEvidence = (id, kind, viewportClass) => ({
|
|
89
|
+
id,
|
|
90
|
+
criterion: id,
|
|
91
|
+
criterionId: id === "depth-proof" ? "depth" : id,
|
|
92
|
+
pageId: "outlets",
|
|
93
|
+
sectionId: id === "depth-proof" ? "workspace" : undefined,
|
|
94
|
+
kind,
|
|
95
|
+
viewportClass,
|
|
96
|
+
mobileChecks: viewportClass === "desktop" ? undefined : ["no-horizontal-overflow"],
|
|
97
|
+
status: "pass",
|
|
98
|
+
evidence: "Browser and test evidence",
|
|
99
|
+
proof: { timestamp: new Date().toISOString(), viewport: { width: viewportClass === "desktop" ? 1280 : viewportClass === "mobile" ? 390 : 320, height: 844 }, artifactPath: `.dreative/${id}.png` },
|
|
100
|
+
});
|
|
101
|
+
const report = {
|
|
102
|
+
version: 2,
|
|
103
|
+
generatedAt: new Date().toISOString(),
|
|
104
|
+
evidence: [
|
|
105
|
+
makeEvidence("desktop", "responsive", "desktop"),
|
|
106
|
+
makeEvidence("mobile", "responsive", "mobile"),
|
|
107
|
+
makeEvidence("narrow", "responsive", "narrow-mobile"),
|
|
108
|
+
makeEvidence("preserved", "preservation", "desktop"),
|
|
109
|
+
makeEvidence("depth-proof", "structural-depth", "desktop"),
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
assert.deepEqual(checkVerificationCoverage(plan, report), []);
|
|
113
|
+
plan.pages[0].mobileBlueprint.verificationChecks.push("touch-targets");
|
|
114
|
+
assert.ok(checkVerificationCoverage(plan, report).some((item) => item.message.includes("touch-targets")));
|
|
115
|
+
plan.pages[0].mobileBlueprint.verificationChecks.pop();
|
|
116
|
+
report.evidence[4].sectionId = "wrong-section";
|
|
117
|
+
assert.ok(checkVerificationCoverage(plan, report).some((item) => item.message.includes("criterion depth")));
|
|
118
|
+
});
|
package/dist/server/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Store, findBlock, replaceBlock, newId } from "./store.js";
|
|
|
6
6
|
import { requestAgent, respond, nextEvent, pushFinish } from "./agentQueue.js";
|
|
7
7
|
import { computeDiff } from "./diff.js";
|
|
8
8
|
import { buildPreview, previewHtml, replicaHtml } from "./preview.js";
|
|
9
|
-
import { buildDesignPlan } from "../shared/design.js";
|
|
9
|
+
import { buildDesignPlan, buildDesignSourceContext, buildRuntimeCoherence } from "../shared/design.js";
|
|
10
10
|
import { startJob, getJob } from "./jobs.js";
|
|
11
11
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
function cloneWithNewIds(block) {
|
|
@@ -180,6 +180,7 @@ export function createServer(projectDir) {
|
|
|
180
180
|
if (!page)
|
|
181
181
|
throw new Error("page not found");
|
|
182
182
|
const siblingPages = project.pages.filter((p) => p.id !== pageId).map((p) => p.name);
|
|
183
|
+
const projectComposition = buildRuntimeCoherence(project.pages, project.brief);
|
|
183
184
|
const blockRefs = [];
|
|
184
185
|
const collect = (b) => {
|
|
185
186
|
if (b.refImage)
|
|
@@ -188,18 +189,22 @@ export function createServer(projectDir) {
|
|
|
188
189
|
};
|
|
189
190
|
collect(page.layout);
|
|
190
191
|
const outFile = `generated/${pageId}.tsx`;
|
|
192
|
+
const plan = buildDesignPlan(page, project.brief);
|
|
193
|
+
const source = buildDesignSourceContext(plan.depth, page.generatedFile);
|
|
191
194
|
await requestAgent("design-page", {
|
|
192
195
|
pageName: page.name,
|
|
193
196
|
layout: page.layout,
|
|
194
197
|
brief: project.brief,
|
|
195
|
-
// Dreative
|
|
196
|
-
//
|
|
197
|
-
plan
|
|
198
|
+
// Dreative resolves depth, source strategy, contracts, budgets, and
|
|
199
|
+
// blocking lints; the agent authors the page-specific composition.
|
|
200
|
+
plan,
|
|
198
201
|
refImage: page.refImage,
|
|
199
202
|
blockRefs,
|
|
200
203
|
designPrompt: designPrompt ?? page.designPrompt,
|
|
201
|
-
|
|
204
|
+
source,
|
|
205
|
+
...(source.previousFile ? { previousFile: source.previousFile } : {}),
|
|
202
206
|
siblingPages,
|
|
207
|
+
projectComposition,
|
|
203
208
|
outFile,
|
|
204
209
|
}, update);
|
|
205
210
|
if (!fs.existsSync(path.join(store.root, outFile)))
|
package/dist/shared/artifacts.js
CHANGED
|
@@ -1,13 +1,73 @@
|
|
|
1
1
|
function nonEmpty(value) {
|
|
2
2
|
return typeof value === "string" && value.trim().length > 0;
|
|
3
3
|
}
|
|
4
|
+
function concreteList(value, minimum = 1) {
|
|
5
|
+
return Array.isArray(value) && value.length >= minimum && value.every((item) => nonEmpty(item) && !/^(improve|modernize|make better|n\/a|none)$/i.test(item.trim()));
|
|
6
|
+
}
|
|
7
|
+
function validateV3Page(page, prefix, depth, tier) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const validRegisters = ["marketing-storytelling", "discovery-browse", "task-transaction", "account-status", "administration", "data-dense-utility", "authentication", "system-state"];
|
|
10
|
+
if (!page.register || !validRegisters.includes(page.register))
|
|
11
|
+
errors.push(`${prefix}.register is required`);
|
|
12
|
+
const expectedStrategy = depth === "restyle" ? "patch" : depth === "relayout" ? "recompose" : "rebuild-from-contracts";
|
|
13
|
+
if (page.sourceStrategy !== expectedStrategy)
|
|
14
|
+
errors.push(`${prefix}.sourceStrategy must be ${expectedStrategy} for ${depth}`);
|
|
15
|
+
const delta = page.structuralDelta;
|
|
16
|
+
if (!delta)
|
|
17
|
+
errors.push(`${prefix}.structuralDelta is required`);
|
|
18
|
+
else {
|
|
19
|
+
for (const key of ["existingModel", "proposedModel", "existingParadigm", "proposedParadigm", "depthHonesty"]) {
|
|
20
|
+
if (!nonEmpty(delta[key]) || /^(old|new|modern|expressive|same|n\/a)( layout| design)?$/i.test(delta[key].trim()))
|
|
21
|
+
errors.push(`${prefix}.structuralDelta.${key} must be concrete and page-specific`);
|
|
22
|
+
}
|
|
23
|
+
if (!concreteList(delta.materialChanges))
|
|
24
|
+
errors.push(`${prefix}.structuralDelta.materialChanges cannot be empty or generic`);
|
|
25
|
+
if (!Array.isArray(delta.survivingBoundaries) || !Array.isArray(delta.rebuiltBoundaries) || !concreteList(delta.preservedContracts) || !Array.isArray(delta.retainedPatterns) || !concreteList(delta.forbiddenCarryovers))
|
|
26
|
+
errors.push(`${prefix}.structuralDelta requires boundaries, preservation, retained-pattern rationale, and forbidden carryovers`);
|
|
27
|
+
if (delta.retainedPatterns?.some((item) => !nonEmpty(item.pattern) || !nonEmpty(item.rationale)))
|
|
28
|
+
errors.push(`${prefix}.structuralDelta retained patterns need rationale`);
|
|
29
|
+
const structural = [delta.proposedModel, delta.proposedParadigm, ...delta.materialChanges].join(" ");
|
|
30
|
+
if ((depth === "restructure" || depth === "reimagine") && !/\b(architect|component|boundary|workflow|navigation|interaction|model|task|rebuild|replace|merge|remove|reorder|hierarchy|workspace)\b/i.test(structural))
|
|
31
|
+
errors.push(`${prefix}: ${depth} cannot be satisfied by stylesheet, token, typography, card, or animation changes alone`);
|
|
32
|
+
if ((depth === "restructure" || depth === "reimagine") && /\b(same (sections|layout)|only (colou?r|font|style)|new (colou?r|font|shadow|radius))\b/i.test(structural))
|
|
33
|
+
errors.push(`${prefix}: structural delta contradicts selected depth ${depth}`);
|
|
34
|
+
}
|
|
35
|
+
const mobile = page.mobileBlueprint;
|
|
36
|
+
if (!mobile)
|
|
37
|
+
errors.push(`${prefix}.mobileBlueprint is required`);
|
|
38
|
+
else {
|
|
39
|
+
const fields = [mobile.primaryTask, mobile.firstViewportPurpose, mobile.safeArea, mobile.navigationModel, mobile.mobileOnlyComposition, mobile.mediaStrategy, mobile.motionStrategy, mobile.keyboardAndForms, mobile.composition390, mobile.fallback320, mobile.stackingRejection];
|
|
40
|
+
if (fields.some((field) => !nonEmpty(field)) || !concreteList(mobile.contentOrder, 2) || !concreteList(mobile.beforeFirstScroll) || !nonEmpty(mobile.primaryThumbAction?.action) || !nonEmpty(mobile.primaryThumbAction?.placement))
|
|
41
|
+
errors.push(`${prefix}.mobileBlueprint must specify task, order, first viewport, thumb action, safe areas, navigation, media, motion, forms, 390px, and 320px behavior`);
|
|
42
|
+
if (/^\s*stack(?:ed)?(?:\s+vertically)?[.!]?\s*$/i.test(mobile.mobileOnlyComposition) || /^\s*stack(?:ed)?(?:\s+vertically)?[.!]?\s*$/i.test(mobile.stackingRejection))
|
|
43
|
+
errors.push(`${prefix}.mobileBlueprint cannot be only “stack vertically”`);
|
|
44
|
+
if (!mobile.desktopTranslation || !Array.isArray(mobile.desktopTranslation.retained) || !Array.isArray(mobile.desktopTranslation.translated) || !Array.isArray(mobile.desktopTranslation.removed) || !Array.isArray(mobile.desktopTranslation.replaced))
|
|
45
|
+
errors.push(`${prefix}.mobileBlueprint must classify retained, translated, removed, and replaced desktop features`);
|
|
46
|
+
if (!Array.isArray(mobile.verificationChecks) || mobile.verificationChecks.length < 13)
|
|
47
|
+
errors.push(`${prefix}.mobileBlueprint must include the complete mobile verification checklist`);
|
|
48
|
+
}
|
|
49
|
+
if (tier === "expressive" || tier === "award") {
|
|
50
|
+
if (!page.expression && !nonEmpty(page.intentionalCalm))
|
|
51
|
+
errors.push(`${prefix} needs an expression contract or a documented intentional-calm rationale`);
|
|
52
|
+
if (page.expression) {
|
|
53
|
+
const expression = page.expression;
|
|
54
|
+
if ([expression.mechanism, expression.communicates, expression.projectFit, expression.location, expression.mobileTranslation, expression.reducedMotion, expression.fallback, expression.verification].some((field) => !nonEmpty(field)))
|
|
55
|
+
errors.push(`${prefix}.expression is incomplete`);
|
|
56
|
+
if (/\b(fade|slide|stagger|hover|scale|gradient|large typography)\b/i.test(expression.mechanism) && !/\b(content|state|progress|selection|navigation|transform|spatial|relationship)\b/i.test(expression.communicates))
|
|
57
|
+
errors.push(`${prefix}.expression is decorative-only`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return errors;
|
|
61
|
+
}
|
|
4
62
|
export function validatePlan(value) {
|
|
5
63
|
const errors = [];
|
|
6
64
|
const plan = value;
|
|
7
65
|
if (!plan || typeof plan !== "object")
|
|
8
66
|
return ["plan must be an object"];
|
|
9
|
-
if (plan.version !== 2)
|
|
10
|
-
errors.push("plan.version must be 2 (
|
|
67
|
+
if (plan.version !== 2 && plan.version !== 3)
|
|
68
|
+
errors.push("plan.version must be 2 (legacy) or 3");
|
|
69
|
+
if (plan.version === 3 && plan.doctrineVersion !== 3)
|
|
70
|
+
errors.push("plan.doctrineVersion must be 3 for v3 plans");
|
|
11
71
|
if (!nonEmpty(plan.request))
|
|
12
72
|
errors.push("plan.request is required");
|
|
13
73
|
if (!nonEmpty(plan.createdAt) || Number.isNaN(Date.parse(plan.createdAt)))
|
|
@@ -34,6 +94,8 @@ export function validatePlan(value) {
|
|
|
34
94
|
const pagePrefix = `pages[${pageIndex}]`;
|
|
35
95
|
if (!nonEmpty(page.id) || !nonEmpty(page.name))
|
|
36
96
|
errors.push(`${pagePrefix} requires id and name`);
|
|
97
|
+
if (plan.version === 3 && plan.depth && plan.tier)
|
|
98
|
+
errors.push(...validateV3Page(page, pagePrefix, plan.depth, plan.tier));
|
|
37
99
|
if (!Array.isArray(page.skills) || !page.skills.includes("ux") || !page.skills.includes("mobile"))
|
|
38
100
|
errors.push(`${pagePrefix}.skills must include ux and mobile`);
|
|
39
101
|
for (const globalSkill of plan.skillPolicy?.global ?? []) {
|
|
@@ -55,6 +117,15 @@ export function validatePlan(value) {
|
|
|
55
117
|
errors.push(`${prefix} requires mobile and fallback treatments`);
|
|
56
118
|
if (!Array.isArray(section.verification) || section.verification.length === 0)
|
|
57
119
|
errors.push(`${prefix}.verification cannot be empty`);
|
|
120
|
+
if (plan.version === 3) {
|
|
121
|
+
for (const [criterionIndex, criterion] of (section.verification ?? []).entries()) {
|
|
122
|
+
const criterionPrefix = `${prefix}.verification[${criterionIndex}]`;
|
|
123
|
+
if (typeof criterion === "string")
|
|
124
|
+
errors.push(`${criterionPrefix} must be a typed verification criterion`);
|
|
125
|
+
else if (!nonEmpty(criterion.id) || !nonEmpty(criterion.claim) || !criterion.kind || criterion.pageId !== page.id || (criterion.sectionId !== undefined && criterion.sectionId !== section.id) || !Array.isArray(criterion.viewports) || criterion.viewports.length === 0)
|
|
126
|
+
errors.push(`${criterionPrefix} must associate id, claim, kind, page, section, and viewports`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
58
129
|
if (section.status === "planned")
|
|
59
130
|
errors.push(`${prefix} is still planned`);
|
|
60
131
|
if ((section.status === "fallback" || section.status === "cut") && !nonEmpty(section.reason))
|
|
@@ -86,7 +157,7 @@ export function validatePlan(value) {
|
|
|
86
157
|
for (const asset of section.assets ?? []) {
|
|
87
158
|
if (!nonEmpty(asset.id) || !nonEmpty(asset.path) || !nonEmpty(asset.purpose))
|
|
88
159
|
errors.push(`${prefix} contains an incomplete asset`);
|
|
89
|
-
if (plan.doctrineVersion === 2 && (!asset.importance || !asset.preparation))
|
|
160
|
+
if ((plan.doctrineVersion === 2 || plan.doctrineVersion === 3) && (!asset.importance || !asset.preparation))
|
|
90
161
|
errors.push(`${prefix} asset ${asset.id || "unknown"} needs importance and a preparation decision`);
|
|
91
162
|
else if (asset.preparation) {
|
|
92
163
|
if (!asset.importance || !["supporting", "key"].includes(asset.importance))
|
|
@@ -105,6 +176,14 @@ export function validatePlan(value) {
|
|
|
105
176
|
}
|
|
106
177
|
}
|
|
107
178
|
}
|
|
179
|
+
if (plan.version === 3) {
|
|
180
|
+
if (!plan.coherence || !nonEmpty(plan.coherence.globalVisualLanguage) || !nonEmpty(plan.coherence.globalInteractionLanguage) || !concreteList(plan.coherence.sharedPrimitives) || !concreteList(plan.coherence.crossPageContinuity) || !concreteList(plan.coherence.prohibitedRepeatedShells))
|
|
181
|
+
errors.push("plan.coherence must define global languages, shared primitives, continuity, and prohibited repeated shells");
|
|
182
|
+
if (!Array.isArray(plan.coherence?.pageSpecificCompositions) || plan.coherence.pageSpecificCompositions.length !== (plan.pages?.length ?? 0))
|
|
183
|
+
errors.push("plan.coherence.pageSpecificCompositions must cover every page");
|
|
184
|
+
if ((plan.tier === "expressive" || plan.tier === "award") && !(plan.pages ?? []).some((page) => page.expression))
|
|
185
|
+
errors.push(`${plan.tier} multi-page plans require at least one authored expression mechanism across the product`);
|
|
186
|
+
}
|
|
108
187
|
for (const skill of plan.skills ?? []) {
|
|
109
188
|
if (!coveredSkills.has(skill))
|
|
110
189
|
errors.push(`selected skill ${skill} is not assigned to any page`);
|
|
@@ -162,8 +241,8 @@ export function validateVerificationReport(value) {
|
|
|
162
241
|
if (!report || typeof report !== "object")
|
|
163
242
|
return ["verification report must be an object"];
|
|
164
243
|
const errors = [];
|
|
165
|
-
if (report.version !== 1)
|
|
166
|
-
errors.push("verification report version must be 1");
|
|
244
|
+
if (report.version !== 1 && report.version !== 2)
|
|
245
|
+
errors.push("verification report version must be 1 (legacy) or 2");
|
|
167
246
|
if (!Array.isArray(report.evidence))
|
|
168
247
|
errors.push("verification evidence must be an array");
|
|
169
248
|
if (report.refinement) {
|
|
@@ -195,6 +274,22 @@ export function validateVerificationReport(value) {
|
|
|
195
274
|
errors.push(`verification.evidence[${index}] is incomplete`);
|
|
196
275
|
if (item.timelineState && !timelineStates.has(item.timelineState))
|
|
197
276
|
errors.push(`verification.evidence[${index}].timelineState is invalid`);
|
|
277
|
+
if (report.version === 2) {
|
|
278
|
+
if (!item.kind || !["visual", "interaction", "responsive", "preservation", "structural-depth"].includes(item.kind))
|
|
279
|
+
errors.push(`verification.evidence[${index}].kind is required`);
|
|
280
|
+
if (!nonEmpty(item.criterionId) || !nonEmpty(item.pageId) || !item.viewportClass)
|
|
281
|
+
errors.push(`verification.evidence[${index}] must associate criterionId, pageId, and viewportClass`);
|
|
282
|
+
if (item.viewportClass !== "non-visual" && !item.proof?.viewport)
|
|
283
|
+
errors.push(`verification.evidence[${index}] visual/responsive evidence requires an exact viewport`);
|
|
284
|
+
if (item.viewportClass === "desktop" && item.proof?.viewport && item.proof.viewport.width < 1200)
|
|
285
|
+
errors.push(`verification.evidence[${index}] desktop evidence must be at least 1200px wide`);
|
|
286
|
+
if (item.viewportClass === "mobile" && item.proof?.viewport && (item.proof.viewport.width < 375 || item.proof.viewport.width > 430))
|
|
287
|
+
errors.push(`verification.evidence[${index}] mobile evidence must be approximately 390px wide`);
|
|
288
|
+
if (item.viewportClass === "narrow-mobile" && item.proof?.viewport && item.proof.viewport.width > 340)
|
|
289
|
+
errors.push(`verification.evidence[${index}] narrow-mobile evidence must be approximately 320px wide`);
|
|
290
|
+
if ((item.viewportClass === "mobile" || item.viewportClass === "narrow-mobile") && (!Array.isArray(item.mobileChecks) || item.mobileChecks.length === 0))
|
|
291
|
+
errors.push(`verification.evidence[${index}] mobile evidence must name the checks it proves`);
|
|
292
|
+
}
|
|
198
293
|
const proof = item.proof;
|
|
199
294
|
if (!proof || !nonEmpty(proof.timestamp) || Number.isNaN(Date.parse(proof.timestamp))) {
|
|
200
295
|
errors.push(`verification.evidence[${index}].proof requires a valid timestamp`);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { validatePlan, validateVerificationReport } from "./artifacts.js";
|
|
4
|
+
import { buildDesignPlan } from "./design.js";
|
|
5
|
+
const page = {
|
|
6
|
+
id: "checkout",
|
|
7
|
+
name: "Checkout",
|
|
8
|
+
status: "skeleton",
|
|
9
|
+
canvasPos: { x: 0, y: 0 },
|
|
10
|
+
layout: {
|
|
11
|
+
id: "root", type: "column", label: "Checkout shell", children: [
|
|
12
|
+
{ id: "promo", type: "hero", label: "Promotional introduction" },
|
|
13
|
+
{ id: "form", type: "form", label: "Choose delivery and continue", children: [{ id: "continue", type: "button", label: "Continue", text: "Continue" }] },
|
|
14
|
+
{ id: "cards", type: "card-grid", label: "Delivery option cards" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
function v3Plan() {
|
|
19
|
+
const runtime = buildDesignPlan(page, { transformationDepth: "restructure", aesthetic: "trust", variance: 3, motion: 2 });
|
|
20
|
+
return {
|
|
21
|
+
version: 3,
|
|
22
|
+
doctrineVersion: 3,
|
|
23
|
+
request: "Restructure checkout without changing its contracts",
|
|
24
|
+
createdAt: "2026-07-14T00:00:00.000Z",
|
|
25
|
+
tier: "solid",
|
|
26
|
+
depth: "restructure",
|
|
27
|
+
skills: ["ux", "mobile"],
|
|
28
|
+
skillPolicy: { mode: "hybrid", global: ["ux", "mobile"], routingApproved: true, userAssignments: [] },
|
|
29
|
+
designRead: { register: "task transaction", concept: "continuous checkout", signature: "selection carries forward" },
|
|
30
|
+
coherence: {
|
|
31
|
+
globalVisualLanguage: "A restrained operational language with one accent and consistent state markers.",
|
|
32
|
+
globalInteractionLanguage: "Selections persist visibly and primary actions respond to readiness.",
|
|
33
|
+
sharedPrimitives: ["state marker", "task action"],
|
|
34
|
+
pageSpecificCompositions: [{ pageId: page.id, register: runtime.register, taskModel: runtime.mobileBlueprint.primaryTask, expressionLevel: "calm" }],
|
|
35
|
+
crossPageContinuity: ["Selection state carries into the next route"],
|
|
36
|
+
prohibitedRepeatedShells: ["Oversized headline plus decorative panel plus rounded card container"],
|
|
37
|
+
},
|
|
38
|
+
pages: [{
|
|
39
|
+
id: page.id,
|
|
40
|
+
name: page.name,
|
|
41
|
+
register: runtime.register,
|
|
42
|
+
sourceStrategy: runtime.sourceStrategy,
|
|
43
|
+
structuralDelta: runtime.structuralDelta,
|
|
44
|
+
mobileBlueprint: runtime.mobileBlueprint,
|
|
45
|
+
skills: ["ux", "mobile"],
|
|
46
|
+
sections: [{
|
|
47
|
+
id: "task",
|
|
48
|
+
name: "Checkout task",
|
|
49
|
+
layoutFamily: "continuous decision workspace",
|
|
50
|
+
skills: ["ux", "mobile"],
|
|
51
|
+
interactions: ["select delivery"],
|
|
52
|
+
mobile: "Task controls precede supporting content and keep the continuation action reachable.",
|
|
53
|
+
fallback: "Semantic fieldset and submit action",
|
|
54
|
+
verification: [{ id: "checkout-depth", claim: "Implemented checkout matches the approved task-first architecture", kind: "structural-depth", pageId: page.id, sectionId: "task", viewports: ["desktop", "mobile", "narrow-mobile"] }],
|
|
55
|
+
assets: [],
|
|
56
|
+
status: "shipped",
|
|
57
|
+
}],
|
|
58
|
+
}],
|
|
59
|
+
preservationManifest: ".dreative/preservation.json",
|
|
60
|
+
decisionLedger: ".dreative/ledger.json",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
test("v3 plan accepts independent solid ambition with restructure depth", () => {
|
|
64
|
+
assert.deepEqual(validatePlan(v3Plan()), []);
|
|
65
|
+
});
|
|
66
|
+
test("v3 validator rejects stylesheet-only deep transformation", () => {
|
|
67
|
+
const plan = v3Plan();
|
|
68
|
+
plan.pages[0].structuralDelta = {
|
|
69
|
+
...plan.pages[0].structuralDelta,
|
|
70
|
+
proposedModel: "Same sections with modern expressive styling",
|
|
71
|
+
proposedParadigm: "Same layout with new colors and fonts",
|
|
72
|
+
materialChanges: ["New colors, fonts, card radii, shadows, and fade animations"],
|
|
73
|
+
};
|
|
74
|
+
assert.ok(validatePlan(plan).some((error) => error.includes("cannot be satisfied")));
|
|
75
|
+
});
|
|
76
|
+
test("v3 validator rejects stack-only mobile blueprint", () => {
|
|
77
|
+
const plan = v3Plan();
|
|
78
|
+
plan.pages[0].mobileBlueprint.mobileOnlyComposition = "stack vertically";
|
|
79
|
+
assert.ok(validatePlan(plan).some((error) => error.includes("stack vertically")));
|
|
80
|
+
});
|
|
81
|
+
test("verification v2 requires associated page, criterion, kind, and real mobile viewports", () => {
|
|
82
|
+
const report = {
|
|
83
|
+
version: 2,
|
|
84
|
+
generatedAt: "2026-07-14T00:00:00.000Z",
|
|
85
|
+
evidence: [{
|
|
86
|
+
id: "mobile-check",
|
|
87
|
+
criterion: "Mobile task is usable",
|
|
88
|
+
criterionId: "checkout-mobile",
|
|
89
|
+
pageId: "checkout",
|
|
90
|
+
kind: "responsive",
|
|
91
|
+
viewportClass: "mobile",
|
|
92
|
+
mobileChecks: ["no-horizontal-overflow"],
|
|
93
|
+
status: "pass",
|
|
94
|
+
evidence: "Browser screenshot and interaction run",
|
|
95
|
+
proof: { timestamp: "2026-07-14T00:00:00.000Z", viewport: { width: 390, height: 844 }, artifactPath: ".dreative/screenshots/checkout-mobile.png" },
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
assert.deepEqual(validateVerificationReport(report), []);
|
|
99
|
+
report.evidence[0].proof.viewport = { width: 768, height: 844 };
|
|
100
|
+
assert.ok(validateVerificationReport(report).some((error) => error.includes("approximately 390px")));
|
|
101
|
+
});
|