@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,112 @@
1
+ import {
2
+ snapshotUrl
3
+ } from "./chunk-OA7SGPN4.js";
4
+ import {
5
+ describeTypicality,
6
+ loadNull,
7
+ typicality
8
+ } from "./chunk-GV3QOTQU.js";
9
+ import {
10
+ SOURCE_FILE,
11
+ finish,
12
+ loadExceptions,
13
+ parseFlags,
14
+ readPaths
15
+ } from "./chunk-YYC4EGER.js";
16
+ import {
17
+ fingerprint
18
+ } from "./chunk-Q54JSNC5.js";
19
+ import {
20
+ SNAPSHOT_VERSION,
21
+ auditSnapshot,
22
+ scanSource
23
+ } from "./chunk-3AP4PJMX.js";
24
+
25
+ // src/audit/cli.ts
26
+ import { existsSync, readFileSync, writeFileSync } from "fs";
27
+ import { resolve } from "path";
28
+ var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
29
+ function describeFingerprint(fp) {
30
+ return [
31
+ "Fingerprint",
32
+ ` accent ${fmt(fp.accent)}`,
33
+ ` ground ${fmt(fp.ground)}`,
34
+ ` display ${fp.display.family} (${fp.display.class})`,
35
+ ` body ${fp.body.family} (${fp.body.class})`,
36
+ ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
37
+ ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
38
+ ` effects ${fp.effects.join(", ") || "none"}`,
39
+ ` layout ${fp.layout.join(" > ") || "none"}`
40
+ ].join("\n");
41
+ }
42
+ function merge(a, b) {
43
+ const byTell = { ...a.summary.byTell };
44
+ for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
45
+ return {
46
+ catalogueVersion: a.catalogueVersion,
47
+ findings: [...a.findings, ...b.findings],
48
+ rejectedExceptions: a.rejectedExceptions,
49
+ excepted: [...a.excepted, ...b.excepted],
50
+ suppressed: [...a.suppressed, ...b.suppressed],
51
+ summary: {
52
+ files: a.summary.files + b.summary.files,
53
+ findings: a.summary.findings + b.summary.findings,
54
+ byGeneration: { 1: a.summary.byGeneration[1] + b.summary.byGeneration[1], 2: a.summary.byGeneration[2] + b.summary.byGeneration[2], 3: a.summary.byGeneration[3] + b.summary.byGeneration[3] },
55
+ byTell,
56
+ blocking: a.summary.blocking + b.summary.blocking
57
+ }
58
+ };
59
+ }
60
+ function readSnapshot(path) {
61
+ const data = JSON.parse(readFileSync(path, "utf8"));
62
+ if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
63
+ return data;
64
+ }
65
+ async function runAudit(command, args, io) {
66
+ try {
67
+ const flags = parseFlags(args);
68
+ if (typeof flags === "string") throw new Error(flags);
69
+ const target = flags.positional[0];
70
+ if (!target) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json" : ""}>`);
71
+ const viewport = flags.width || flags.height ? { width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) } : void 0;
72
+ const local = resolve(io.cwd, target);
73
+ const snapshot = command === "audit" && /\.json$/i.test(target) && existsSync(local) ? readSnapshot(local) : await snapshotUrl(target, { viewport });
74
+ if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(snapshot, null, 2)}
75
+ `);
76
+ if (command === "snapshot") {
77
+ if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
78
+ else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
79
+ return 0;
80
+ }
81
+ const exceptions = loadExceptions(flags, flags.repo ? resolve(io.cwd, flags.repo) : io.cwd);
82
+ let report = auditSnapshot(snapshot, { exceptions });
83
+ if (flags.repo) {
84
+ const repo = resolve(io.cwd, flags.repo);
85
+ report = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), report);
86
+ }
87
+ const fp = fingerprint(snapshot);
88
+ const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
89
+ 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;
90
+ if (flags.json) {
91
+ io.out(JSON.stringify({ report, fingerprint: fp, typicality: typical, snapshot: flags.out ?? null }, null, 2));
92
+ return report.summary.blocking > 0 || flags.strict && report.summary.findings > 0 ? 1 : 0;
93
+ }
94
+ const code = finish(report, { ...flags, json: false }, `craft audit ${snapshot.url}`, io);
95
+ io.out("");
96
+ io.out(describeFingerprint(fp));
97
+ if (typical) {
98
+ io.out("");
99
+ io.out(describeTypicality(typical, nulls.reduce((s, m) => s + m.runs.length, 0), nulls.length));
100
+ }
101
+ return code;
102
+ } catch (error) {
103
+ io.err(`craft: ${error.message}`);
104
+ return 2;
105
+ }
106
+ }
107
+
108
+ export {
109
+ describeFingerprint,
110
+ merge,
111
+ runAudit
112
+ };
@@ -0,0 +1,175 @@
1
+ import {
2
+ REFLEX_FONTS_1,
3
+ REFLEX_FONTS_2,
4
+ deltaEOk,
5
+ isAiViolet,
6
+ isCream,
7
+ normaliseFamily,
8
+ parseColour
9
+ } from "./chunk-3AP4PJMX.js";
10
+
11
+ // src/direction/types.ts
12
+ var DIRECTION_VERSION = 1;
13
+ var SOURCE_KINDS = [
14
+ "livery",
15
+ "shopfront",
16
+ "signage",
17
+ "packaging",
18
+ "product",
19
+ "uniform",
20
+ "interior",
21
+ "place",
22
+ "material",
23
+ "trade",
24
+ "print",
25
+ "photo",
26
+ "founder"
27
+ ];
28
+ var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
29
+
30
+ // src/direction/validate.ts
31
+ var PREFERENCE = /\b(?:client (?:likes|liked|wants|wanted|asked|chose|prefers?)|(?:we|they|i) (?:like|liked|love|loved|prefer)|on[- ]trend|trendy|popular|what (?:everyone|competitors) (?:uses?|does)|brand (?:colou?r|font|guidelines?) (?:is|are|says?))\b/i;
32
+ var MOOD = /\b(?:modern|clean|fresh|professional|trustworthy|premium|elegant|sleek|timeless|minimal(?:ist)?|bold|vibrant|friendly|approachable|luxur(?:y|ious)|sophisticated|contemporary|stylish|warm|inviting|high[- ]end)\b/gi;
33
+ var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
34
+ var STOP = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every"]);
35
+ function words(text) {
36
+ return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !STOP.has(w) && !VALUE_WORDS.has(w)));
37
+ }
38
+ function tellFor(key, value) {
39
+ if (key === "accent") {
40
+ const c = parseColour(value);
41
+ return c && isAiViolet(c.oklch) ? "ai-violet" : null;
42
+ }
43
+ if (key === "ground") {
44
+ const c = parseColour(value);
45
+ return c && isCream(c.oklch) ? "cream-palette" : null;
46
+ }
47
+ if (key === "display" || key === "body") {
48
+ const name = normaliseFamily(value).toLowerCase();
49
+ if (REFLEX_FONTS_1.some((f) => f.toLowerCase() === name)) return "reflex-font";
50
+ if (REFLEX_FONTS_2.some((f) => f.toLowerCase() === name)) return "reflex-font-2";
51
+ }
52
+ return null;
53
+ }
54
+ function validateDirection(input, ctx = {}) {
55
+ const problems = [];
56
+ const err = (at, message) => problems.push({ severity: "error", at, message });
57
+ const warn = (at, message) => problems.push({ severity: "warn", at, message });
58
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
59
+ err("", "art-direction.json must be a JSON object");
60
+ return { valid: false, problems, decided: 0 };
61
+ }
62
+ const d = input;
63
+ const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
64
+ if (legacy) {
65
+ warn("", "exceptions only: run craft direction init to record the choices and their reasons");
66
+ return { valid: true, problems, decided: 0 };
67
+ }
68
+ if (d.version !== DIRECTION_VERSION) err("version", `version must be ${DIRECTION_VERSION}`);
69
+ if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
70
+ if (typeof d.brief !== "string" || d.brief.trim().split(/\s+/).length < 8) err("brief", "the brief needs a sentence: who, where, and what they do");
71
+ const sources = Array.isArray(d.sources) ? d.sources : [];
72
+ if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
73
+ if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
74
+ const ids = /* @__PURE__ */ new Set();
75
+ sources.forEach((s, i) => {
76
+ const at = `sources[${i}]`;
77
+ if (!s || typeof s !== "object") return err(at, "a source must be an object");
78
+ if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
79
+ else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
80
+ else ids.add(s.id);
81
+ if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
82
+ if (typeof s.note !== "string" || s.note.trim().split(/\s+/).length < 3) err(`${at}.note`, "say what it is and where");
83
+ if (s.path && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
84
+ for (const [j, c] of (s.colours ?? []).entries()) if (!parseColour(c)) err(`${at}.colours[${j}]`, `"${c}" is not a colour`);
85
+ });
86
+ const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
87
+ if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
88
+ const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
89
+ let decided = 0;
90
+ for (const key of Object.keys(choices)) {
91
+ if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}. Layout and navigation stay conventional.`);
92
+ }
93
+ for (const key of CHOICE_KEYS) {
94
+ const c = choices[key];
95
+ const at = `choices.${key}`;
96
+ if (!c) {
97
+ warn(at, "not decided yet");
98
+ continue;
99
+ }
100
+ let ok = true;
101
+ if (typeof c.value !== "string" || !c.value.trim()) {
102
+ err(`${at}.value`, "no value");
103
+ ok = false;
104
+ } else if ((key === "accent" || key === "ground") && !parseColour(c.value)) {
105
+ err(`${at}.value`, `"${c.value}" is not a colour`);
106
+ ok = false;
107
+ }
108
+ const because = typeof c.because === "string" ? c.because.trim() : "";
109
+ const evidence = Array.isArray(c.evidence) ? c.evidence : [];
110
+ if (because.split(/\s+/).filter(Boolean).length < 8) {
111
+ err(`${at}.because`, "a reason is at least a sentence. Without one this is a default, however good it looks.");
112
+ ok = false;
113
+ } else if (because.startsWith("PROPOSED:")) {
114
+ err(`${at}.because`, "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
115
+ ok = false;
116
+ } else {
117
+ if (PREFERENCE.test(because)) {
118
+ err(`${at}.because`, "that is a preference, not a reason. Say what in the client's world this comes from.");
119
+ ok = false;
120
+ }
121
+ const moods = because.match(MOOD) ?? [];
122
+ if (moods.length >= 2) {
123
+ err(`${at}.because`, `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
124
+ ok = false;
125
+ }
126
+ }
127
+ if (evidence.length === 0) {
128
+ err(`${at}.evidence`, "cite at least one source");
129
+ ok = false;
130
+ }
131
+ for (const id of evidence) {
132
+ if (!ids.has(id)) {
133
+ err(`${at}.evidence`, `no source called "${id}"`);
134
+ ok = false;
135
+ }
136
+ }
137
+ if (ok) {
138
+ const cited = sources.filter((s) => evidence.includes(s.id));
139
+ const reasonWords = words(because);
140
+ const tied = cited.some((s) => reasonWords.has(s.id) || [...words(`${s.note} ${s.lettering ?? ""}`)].some((w) => reasonWords.has(w)));
141
+ if (!tied) {
142
+ err(`${at}.because`, `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
143
+ ok = false;
144
+ }
145
+ }
146
+ if (typeof c.value === "string") {
147
+ const tell = tellFor(key, c.value);
148
+ if (tell && !exceptions.some((e) => e.tell === tell)) {
149
+ err(`${at}.value`, `${c.value} is on the tell catalogue (${tell}). Keep it only with an exception for ${tell}, carrying the same reason.`);
150
+ ok = false;
151
+ }
152
+ }
153
+ const fp = ctx.fingerprint;
154
+ if (fp && typeof c.value === "string") {
155
+ if ((key === "accent" || key === "ground") && parseColour(c.value)) {
156
+ const declared = parseColour(c.value).oklch;
157
+ const measured = key === "accent" ? fp.accent : fp.ground;
158
+ if (measured && deltaEOk(declared, measured) > 0.08) {
159
+ warn(`${at}.value`, `the page does not show it: its ${key} measures oklch(${measured.l.toFixed(2)} ${measured.c.toFixed(3)} ${measured.h.toFixed(0)})`);
160
+ }
161
+ }
162
+ if (key === "display" || key === "body") {
163
+ const measured = key === "display" ? fp.display.family : fp.body.family;
164
+ if (normaliseFamily(c.value).toLowerCase() !== measured.toLowerCase()) warn(`${at}.value`, `the page sets ${measured}, not ${c.value}`);
165
+ }
166
+ }
167
+ if (ok) decided += 1;
168
+ }
169
+ return { valid: problems.every((p) => p.severity !== "error"), problems, decided };
170
+ }
171
+
172
+ export {
173
+ DIRECTION_VERSION,
174
+ validateDirection
175
+ };