@domandigital/craft 0.5.0 → 0.7.0

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.
@@ -0,0 +1,312 @@
1
+ import {
2
+ validateDirection
3
+ } from "./chunk-J2BOAIHJ.js";
4
+ import {
5
+ merge
6
+ } from "./chunk-IQTZWNEK.js";
7
+ import "./chunk-OA7SGPN4.js";
8
+ import {
9
+ SIBLING_AT,
10
+ compareToEstate,
11
+ loadEstate
12
+ } from "./chunk-CU5TZHOV.js";
13
+ import {
14
+ loadNull,
15
+ typicality
16
+ } from "./chunk-GV3QOTQU.js";
17
+ import {
18
+ COPY_FILE,
19
+ NOT_COPY_DIR,
20
+ SOURCE_FILE,
21
+ loadExceptions,
22
+ parseFlags,
23
+ readPaths
24
+ } from "./chunk-YYC4EGER.js";
25
+ import {
26
+ fingerprint
27
+ } from "./chunk-Q54JSNC5.js";
28
+ import {
29
+ CATALOGUE,
30
+ SNAPSHOT_VERSION,
31
+ auditSnapshot,
32
+ checkCopy,
33
+ scanSource
34
+ } from "./chunk-3AP4PJMX.js";
35
+
36
+ // src/report/cli.ts
37
+ import { existsSync, readFileSync, writeFileSync } from "fs";
38
+ import { dirname, resolve } from "path";
39
+
40
+ // src/report/index.ts
41
+ var TELL_HEAVY = 3;
42
+ var MIN_DECIDED = 5;
43
+ var TELL_AREA = {
44
+ "reflex-font": { area: "type", choice: "display" },
45
+ "reflex-font-2": { area: "type", choice: "display" },
46
+ "italic-serif-display": { area: "type", choice: "display" },
47
+ "ai-violet": { area: "colour", choice: "accent" },
48
+ "blue-purple-gradient": { area: "colour", choice: "accent" },
49
+ "gradient-text": { area: "colour", choice: "accent" },
50
+ "cream-palette": { area: "colour", choice: "ground" },
51
+ "pill-everything": { area: "shape", choice: "shape" },
52
+ "shadcn-dump": { area: "shape", choice: "shape" },
53
+ "glass-panel": { area: "effects", choice: "motif" },
54
+ "radial-spotlight-glow": { area: "effects", choice: "motif" },
55
+ "grid-background": { area: "effects", choice: "motif" },
56
+ "thin-border-wide-shadow": { area: "effects", choice: "shape" },
57
+ "icon-tile-stack": { area: "effects", choice: "motif" },
58
+ "reveal-everywhere": { area: "motion", choice: "signature" },
59
+ "intro-cinematic": { area: "motion", choice: "signature" },
60
+ marquee: { area: "motion", choice: "signature" },
61
+ "hero-then-proof": { area: "layout" },
62
+ "icon-tile-grid": { area: "layout" },
63
+ "hero-eyebrow-chip": { area: "layout" },
64
+ "bento-grid": { area: "layout" }
65
+ };
66
+ var PART_AREA = {
67
+ display: { area: "type", choice: "display" },
68
+ body: { area: "type", choice: "body" },
69
+ accent: { area: "colour", choice: "accent" },
70
+ ground: { area: "colour", choice: "ground" },
71
+ shape: { area: "shape", choice: "shape" },
72
+ effects: { area: "effects", choice: "motif" },
73
+ motion: { area: "motion", choice: "signature" }
74
+ };
75
+ var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
76
+ function designFindings(findings, copyTells) {
77
+ return (findings?.findings ?? []).filter((f) => !copyTells.has(f.tell));
78
+ }
79
+ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
80
+ const actions = [];
81
+ const signals = [];
82
+ if (input.findings) {
83
+ const design = designFindings(input.findings, copyTells);
84
+ const distinct = [...new Set(design.map((f) => f.tell))];
85
+ signals.push({
86
+ id: "tells",
87
+ raised: distinct.length >= TELL_HEAVY,
88
+ detail: `${distinct.length} distinct design tell${distinct.length === 1 ? "" : "s"}${distinct.length ? `: ${distinct.join(", ")}` : ""}`
89
+ });
90
+ const seen = /* @__PURE__ */ new Set();
91
+ for (const f of design) {
92
+ if (seen.has(f.tell)) continue;
93
+ seen.add(f.tell);
94
+ const map = TELL_AREA[f.tell] ?? { area: "effects" };
95
+ const both = design.some((g) => g.tell === f.tell && g.line === 0) && design.some((g) => g.tell === f.tell && g.line > 0);
96
+ actions.push({ area: map.area, what: f.fix, why: `${f.name} (${f.tell}, gen ${f.generation})${both ? ", in source and on the page" : ""}`, priority: both ? 1 : 2, choice: map.choice });
97
+ }
98
+ const copy = (input.findings.findings ?? []).filter((f) => copyTells.has(f.tell));
99
+ const byTell = /* @__PURE__ */ new Map();
100
+ for (const f of copy) byTell.set(f.tell, [...byTell.get(f.tell) ?? [], f]);
101
+ for (const [tell, list] of byTell) {
102
+ actions.push({ area: "copy", what: list[0].fix, why: `${list[0].name} (${tell}), ${list.length} place${list.length === 1 ? "" : "s"}, first at ${list[0].path}${list[0].line ? `:${list[0].line}` : ""}`, priority: 3 });
103
+ }
104
+ } else {
105
+ signals.push({ id: "tells", raised: null, detail: "not measured" });
106
+ }
107
+ if (input.typicality) {
108
+ const t = input.typicality;
109
+ signals.push({ id: "typicality", raised: t.typical, detail: `score ${t.score.toFixed(2)}, ${t.typical ? "typical" : "not typical"} of what Claude builds for this brief` });
110
+ if (t.typical) {
111
+ for (const s of t.shared) {
112
+ const map = PART_AREA[s.part];
113
+ if (!map || DEFAULTS.test(`${s.part} ${s.value}`)) continue;
114
+ actions.push({ area: map.area, what: `Move off the model's pick for this brief: ${s.part} ${s.value}.`, why: `${s.runs} of ${s.of} null pages chose it`, priority: 1, choice: map.choice });
115
+ }
116
+ }
117
+ } else {
118
+ signals.push({ id: "typicality", raised: null, detail: "not measured: no null model for this brief" });
119
+ }
120
+ if (input.estate) {
121
+ const siblings = input.estate.filter((m) => m.sibling);
122
+ const nearest = input.estate[0];
123
+ signals.push({
124
+ id: "estate",
125
+ raised: siblings.length > 0,
126
+ detail: siblings.length ? `sibling of ${siblings.map((s) => `${s.id} (${s.distance.toFixed(2)})`).join(", ")}` : nearest ? `nearest ${nearest.id} at ${nearest.distance.toFixed(2)}, no sibling` : "the register is empty"
127
+ });
128
+ for (const s of siblings) {
129
+ for (const part of s.shared) {
130
+ if (DEFAULTS.test(part)) continue;
131
+ actions.push({ area: areaOfShared(part), what: `Differ from ${s.id}: both have ${part}.`, why: `sibling at ${s.distance.toFixed(2)}`, priority: 1, choice: choiceOfShared(part) });
132
+ }
133
+ }
134
+ } else {
135
+ signals.push({ id: "estate", raised: null, detail: "not measured: no estate register" });
136
+ }
137
+ if (input.direction) {
138
+ const d = input.direction;
139
+ const raised2 = !d.valid || d.decided < MIN_DECIDED;
140
+ signals.push({ id: "reasons", raised: raised2, detail: `${d.decided} of 7 choices decided with a reason${d.valid ? "" : ", art-direction.json has errors"}` });
141
+ for (const p of d.problems) {
142
+ const key = p.at.match(/^choices\.(\w+)/)?.[1];
143
+ if (p.severity === "error" || p.message === "not decided yet") {
144
+ actions.push({ area: "direction", what: key && p.message === "not decided yet" ? `Decide ${key}, with a source from the client's world.` : `${p.at || "art-direction.json"}: ${p.message}`, why: "the reason rule", priority: 1, choice: key });
145
+ }
146
+ }
147
+ } else {
148
+ signals.push({ id: "reasons", raised: null, detail: "not measured: no art-direction.json" });
149
+ actions.push({ area: "direction", what: "Write art-direction.json: run craft direction init, then give every choice a reason from the client's world.", why: "the reason rule", priority: 1 });
150
+ }
151
+ const raised = signals.filter((s) => s.raised === true).length;
152
+ const unmeasured = signals.filter((s) => s.raised === null).map((s) => s.id);
153
+ const verdict = raised >= 2 ? "default" : raised === 1 ? "mixed" : unmeasured.length ? "unproven" : "decided";
154
+ const summary = verdict === "default" ? `${raised} of 4 signals raised: nobody chose how this looks.` : verdict === "mixed" ? `1 of 4 signals raised: ${signals.find((s) => s.raised).id}.` : verdict === "decided" ? "No signal raised, and every choice carries a reason." : `No signal raised, but ${unmeasured.join(", ")} not measured. Unverified is not a pass.`;
155
+ const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
156
+ const merged = [];
157
+ for (const a of actions) {
158
+ const same = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
159
+ if (same) {
160
+ const fromTell = /\(\S+, gen \d\)/.test(a.why);
161
+ same.why = `${same.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
162
+ same.priority = Math.min(same.priority, a.priority);
163
+ } else merged.push({ ...a });
164
+ }
165
+ const unique = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
166
+ unique.sort((a, b) => a.priority - b.priority || order.indexOf(a.area) - order.indexOf(b.area));
167
+ return { site: input.site, verdict, summary, signals, actions: unique };
168
+ }
169
+ function areaOfShared(part) {
170
+ if (/headline|body$/.test(part)) return "type";
171
+ if (/accent|ground/.test(part)) return "colour";
172
+ if (/buttons/.test(part)) return "shape";
173
+ if (/reveal/.test(part)) return "motion";
174
+ if (/running order/.test(part)) return "layout";
175
+ return "effects";
176
+ }
177
+ function choiceOfShared(part) {
178
+ if (/headline/.test(part)) return "display";
179
+ if (/body$/.test(part)) return "body";
180
+ if (/accent/.test(part)) return "accent";
181
+ if (/ground/.test(part)) return "ground";
182
+ if (/buttons/.test(part)) return "shape";
183
+ if (/reveal/.test(part)) return "signature";
184
+ return /running order/.test(part) ? void 0 : "motif";
185
+ }
186
+
187
+ // src/report/retrofit.ts
188
+ var HEADINGS = {
189
+ direction: "Decide first",
190
+ type: "Type",
191
+ colour: "Colour",
192
+ shape: "Shape",
193
+ effects: "Effects",
194
+ motion: "Motion",
195
+ layout: "How sections look (not where they sit)",
196
+ copy: "Copy"
197
+ };
198
+ var ORDER = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
199
+ function target(a, direction) {
200
+ if (!a.choice || a.area === "direction") return "";
201
+ const c = direction?.choices?.[a.choice];
202
+ if (c && c.because && !c.because.startsWith("PROPOSED:")) return `
203
+ - Use: **${c.value}** for ${a.choice}. ${c.because}`;
204
+ return `
205
+ - Waits on: deciding **${a.choice}** in art-direction.json.`;
206
+ }
207
+ function retrofitPlan(report, direction) {
208
+ const lines = [
209
+ `# Retrofit: ${report.site}`,
210
+ "",
211
+ `**Now:** ${report.verdict}. ${report.summary}`,
212
+ "",
213
+ "| Signal | Raised | Reading |",
214
+ "|---|---|---|",
215
+ ...report.signals.map((s) => `| ${s.id} | ${s.raised === null ? "not measured" : s.raised ? "yes" : "no"} | ${s.detail} |`),
216
+ "",
217
+ "**Done when** `craft report` says *decided*: fewer than three design tells, typicality under 0.10 against this brief's null, no sibling in the estate, and at least five of the seven choices decided with a reason.",
218
+ ""
219
+ ];
220
+ let n = 0;
221
+ for (const area of ORDER) {
222
+ const list = report.actions.filter((a) => a.area === area);
223
+ if (list.length === 0) continue;
224
+ n += 1;
225
+ lines.push(`## ${n}. ${HEADINGS[area]}`, "");
226
+ for (const a of list) lines.push(`- [ ] ${a.what} *(${a.why})*${target(a, direction)}`);
227
+ lines.push("");
228
+ }
229
+ if (n === 0) lines.push("Nothing to change.", "");
230
+ lines.push(
231
+ "## Leave alone",
232
+ "",
233
+ "Navigation, the order of the sections, where the call to action sits, reading order and legibility. Change the look, never the page grammar. A layout tell above is about how a section looks: the icon tiles, the chip, the strip. The section stays where it is.",
234
+ ""
235
+ );
236
+ return lines.join("\n");
237
+ }
238
+
239
+ // src/report/cli.ts
240
+ var USAGE = "usage: craft report|retrofit <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <estate.json>] [--direction <file>] [--json] [--out <file>]";
241
+ var sameUrl = (a, b) => a.replace(/\/+$/, "").toLowerCase() === b.replace(/\/+$/, "").toLowerCase();
242
+ function formatCharacter(r) {
243
+ const lines = [`craft report ${r.site}`, "", `Verdict: ${r.verdict.toUpperCase()}. ${r.summary}`, ""];
244
+ for (const s of r.signals) lines.push(` ${s.id.padEnd(11)} ${s.raised === null ? "\xB7" : s.raised ? "RAISED" : "ok"} ${s.detail}`);
245
+ lines.push("", "What to do, in order:");
246
+ r.actions.slice(0, 12).forEach((a, i) => lines.push(` ${String(i + 1).padStart(2)}. [${a.area}] ${a.what}`, ` ${a.why}`));
247
+ if (r.actions.length > 12) lines.push(` ...and ${r.actions.length - 12} more: craft retrofit writes them all.`);
248
+ return lines.join("\n");
249
+ }
250
+ async function runReport(command, args, io) {
251
+ try {
252
+ const flags = parseFlags(args);
253
+ if (typeof flags === "string") throw new Error(flags);
254
+ const target2 = flags.positional[0];
255
+ if (!target2) throw new Error(USAGE);
256
+ const local = resolve(io.cwd, target2);
257
+ let snap;
258
+ if (/\.json$/i.test(target2) && existsSync(local)) {
259
+ snap = JSON.parse(readFileSync(local, "utf8"));
260
+ if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target2} is not a snapshot`);
261
+ } else {
262
+ const { snapshotUrl } = await import("./audit-YNODYB3U.js");
263
+ snap = await snapshotUrl(target2);
264
+ }
265
+ const fp = fingerprint(snap);
266
+ const repo = flags.repo ? resolve(io.cwd, flags.repo) : null;
267
+ const exceptions = loadExceptions(flags, repo ?? io.cwd);
268
+ let findings = auditSnapshot(snap, { exceptions });
269
+ if (repo) {
270
+ findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), findings);
271
+ findings = merge(findings, checkCopy(readPaths(["."], repo, COPY_FILE, (d) => NOT_COPY_DIR(d) || d === "docs"), { exceptions }));
272
+ }
273
+ const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
274
+ const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
275
+ let estate = null;
276
+ if (flags.estate) {
277
+ const register = loadEstate(resolve(io.cwd, flags.estate));
278
+ const self = register.sites.find((s) => sameUrl(s.url, snap.url));
279
+ estate = compareToEstate(fp, register, { exclude: self?.id, siblingAt: SIBLING_AT });
280
+ }
281
+ const directionFile = resolve(repo ?? io.cwd, flags.direction ?? "art-direction.json");
282
+ let direction = null;
283
+ let directionReport = null;
284
+ if (existsSync(directionFile)) {
285
+ direction = JSON.parse(readFileSync(directionFile, "utf8"));
286
+ directionReport = validateDirection(direction, { fingerprint: fp, pathExists: (p) => existsSync(resolve(dirname(directionFile), p)) });
287
+ } else if (flags.direction) {
288
+ throw new Error(`no such file: ${flags.direction}`);
289
+ }
290
+ const copyTells = new Set(CATALOGUE.filter((t) => t.surface === "copy").map((t) => t.id));
291
+ const report = characterReport({ site: snap.url, findings, typicality: typical, estate, direction: directionReport }, copyTells);
292
+ if (command === "retrofit") {
293
+ const plan = retrofitPlan(report, direction);
294
+ if (flags.out) {
295
+ writeFileSync(resolve(io.cwd, flags.out), plan);
296
+ io.out(`craft retrofit: ${report.actions.length} step${report.actions.length === 1 ? "" : "s"} for ${snap.url} in ${flags.out}`);
297
+ } else io.out(plan);
298
+ return 0;
299
+ }
300
+ io.out(flags.json ? JSON.stringify(report, null, 2) : formatCharacter(report));
301
+ if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(report, null, 2)}
302
+ `);
303
+ return flags.strict && report.verdict !== "decided" ? 1 : 0;
304
+ } catch (error) {
305
+ io.err(`craft: ${error.message}`);
306
+ return 2;
307
+ }
308
+ }
309
+ export {
310
+ formatCharacter,
311
+ runReport
312
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ run
4
+ } from "./chunk-YYC4EGER.js";
5
+ import "./chunk-3AP4PJMX.js";
6
+
7
+ // src/cli.ts
8
+ process.stdout.on("error", (error) => {
9
+ if (error.code === "EPIPE") process.exit(process.exitCode ?? 0);
10
+ throw error;
11
+ });
12
+ void Promise.resolve(
13
+ run(process.argv.slice(2), {
14
+ cwd: process.cwd(),
15
+ out: (text) => process.stdout.write(`${text}
16
+ `),
17
+ err: (text) => process.stderr.write(`${text}
18
+ `)
19
+ })
20
+ ).then((code) => {
21
+ process.exitCode = code;
22
+ });