@domandigital/craft 0.14.0 → 0.15.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.
@@ -4,11 +4,13 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-PBK6SYH5.js";
8
- import "./chunk-XVBR2UPH.js";
9
- import "./chunk-3OTKMKJQ.js";
7
+ } from "./chunk-QU6Q2QZZ.js";
8
+ import "./chunk-YDURAOYR.js";
9
+ import "./chunk-RXXRXV3V.js";
10
+ import "./chunk-OQXTQQAU.js";
11
+ import "./chunk-MUVFECFC.js";
10
12
  import "./chunk-WUACK2HX.js";
11
- import "./chunk-GQVEYTBY.js";
13
+ import "./chunk-LFNSB6W7.js";
12
14
  import "./chunk-QYBAVU3P.js";
13
15
  export {
14
16
  formatComponentPairs,
@@ -4,13 +4,14 @@ import {
4
4
  merge,
5
5
  resolvePages,
6
6
  runAudit
7
- } from "./chunk-7T5WR74C.js";
8
- import "./chunk-Q7ILL2HV.js";
7
+ } from "./chunk-2IQ5OXFR.js";
8
+ import "./chunk-OUQ25LY2.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-XVBR2UPH.js";
11
- import "./chunk-3OTKMKJQ.js";
10
+ import "./chunk-RXXRXV3V.js";
11
+ import "./chunk-OQXTQQAU.js";
12
+ import "./chunk-MUVFECFC.js";
12
13
  import "./chunk-WUACK2HX.js";
13
- import "./chunk-GQVEYTBY.js";
14
+ import "./chunk-LFNSB6W7.js";
14
15
  import "./chunk-QYBAVU3P.js";
15
16
  export {
16
17
  describeComponents,
@@ -1,14 +1,17 @@
1
- import {
2
- DIRECTION_VERSION,
3
- validateDirection
4
- } from "./chunk-OOZOEWGD.js";
5
1
  import {
6
2
  decodePng
7
3
  } from "./chunk-JYNXGCYZ.js";
4
+ import {
5
+ DIRECTION_VERSION,
6
+ PAGE_TYPES,
7
+ PROPOSED,
8
+ researchPrompt,
9
+ validateDirection
10
+ } from "./chunk-YDURAOYR.js";
8
11
  import {
9
12
  parseFlags,
10
13
  toJson
11
- } from "./chunk-3OTKMKJQ.js";
14
+ } from "./chunk-MUVFECFC.js";
12
15
  import {
13
16
  readSnapshot
14
17
  } from "./chunk-WUACK2HX.js";
@@ -21,7 +24,7 @@ import {
21
24
  isCream,
22
25
  oklchToRgb,
23
26
  parseColour
24
- } from "./chunk-GQVEYTBY.js";
27
+ } from "./chunk-LFNSB6W7.js";
25
28
  import "./chunk-QYBAVU3P.js";
26
29
 
27
30
  // src/direction/cli.ts
@@ -44,11 +47,31 @@ function initDirection(input) {
44
47
  body: empty(fp.body.family),
45
48
  ...fp.roundness !== null ? { shape: empty(fp.roundness >= 0.45 ? "pill buttons" : fp.roundness <= 0.05 ? "square corners" : `corners at ${Math.round(fp.roundness * 100)}% of button height`) } : {}
46
49
  } : {};
50
+ const part = () => ({ value: "", evidence: [] });
47
51
  return {
48
52
  $schema: "https://unpkg.com/@domandigital/craft/art-direction.schema.json",
49
53
  version: DIRECTION_VERSION,
50
54
  client: input.client ?? "",
51
55
  brief: input.brief ?? "",
56
+ // Empty on purpose, like the reasons: the first validate lists the job and the structure as undecided.
57
+ job: {
58
+ statement: { verb: "", object: "", context: "" },
59
+ functional: part(),
60
+ emotional: part(),
61
+ social: part(),
62
+ consideration: { value: "low", because: "", evidence: [] },
63
+ objections: [],
64
+ language: [],
65
+ evidence: []
66
+ },
67
+ hierarchy: {
68
+ home: {
69
+ primaryAction: { value: "", positions: [], because: "" },
70
+ order: fp?.sections?.length ? fp.sections.map((s) => ({ role: s.role, because: "" })) : [],
71
+ lead: { value: "", because: "" },
72
+ journey: []
73
+ }
74
+ },
52
75
  sources: [],
53
76
  choices,
54
77
  ...input.exceptions?.length ? { exceptions: input.exceptions } : {}
@@ -56,7 +79,6 @@ function initDirection(input) {
56
79
  }
57
80
 
58
81
  // src/direction/propose.ts
59
- var PROPOSED = "PROPOSED:";
60
82
  function srgbToLinear(c) {
61
83
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
62
84
  }
@@ -118,7 +140,13 @@ function accentCandidates(input) {
118
140
  const parsed = parseColour(c);
119
141
  if (!parsed || !chromatic(parsed.oklch)) continue;
120
142
  const o = parsed.oklch;
121
- const siblings = (input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 }));
143
+ const siblings = [
144
+ ...(input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 })),
145
+ ...(input.declared ?? []).flatMap((e) => {
146
+ const p = parseColour(e.accent);
147
+ return p ? [{ id: `${e.id} (declared)`, d: deltaEOk(o, p.oklch) }] : [];
148
+ })
149
+ ];
122
150
  const nearest = siblings.sort((a, b) => a.d - b.d)[0];
123
151
  const penalty = isAiViolet(o) ? 1 : 0;
124
152
  out.push({
@@ -174,15 +202,26 @@ function propose(input) {
174
202
 
175
203
  // src/direction/cli.ts
176
204
  var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
177
- function fingerprintFrom(path, cwd) {
178
- return path ? fingerprint(readJson(resolve(cwd, path))) : void 0;
205
+ async function snapshotFrom(target, cwd) {
206
+ if (!target) return void 0;
207
+ if (/^https?:\/\//.test(target)) {
208
+ const { snapshotUrl } = await import("./audit-MTUC24XU.js");
209
+ return snapshotUrl(target);
210
+ }
211
+ return readSnapshot(readJson(resolve(cwd, target)), target);
212
+ }
213
+ function declaredFrom(path, cwd) {
214
+ if (!path || !path.endsWith(".json")) return [];
215
+ const register = readJson(resolve(cwd, path));
216
+ return register.sites ?? [];
179
217
  }
218
+ var TEXT_SOURCE = /\.(?:txt|md|markdown|csv|json|html?)$/i;
180
219
  function estateFrom(dir, cwd) {
181
220
  if (!dir) return [];
182
221
  const root = resolve(cwd, dir);
183
222
  if (root.endsWith(".json")) {
184
223
  const register = readJson(root);
185
- return (register.sites ?? []).map((s) => ({ id: s.id, fingerprint: s.fingerprint }));
224
+ return (register.sites ?? []).filter((s) => s.fingerprint).map((s) => ({ id: s.id, fingerprint: s.fingerprint }));
186
225
  }
187
226
  return readdirSync(root).filter((f) => f.endsWith(".json")).flatMap((f) => {
188
227
  try {
@@ -193,23 +232,33 @@ function estateFrom(dir, cwd) {
193
232
  }
194
233
  });
195
234
  }
196
- function runDirection(args, io) {
235
+ async function runDirection(args, io) {
197
236
  try {
198
237
  const flags = parseFlags(args);
199
238
  if (typeof flags === "string") throw new Error(flags);
200
239
  const [sub] = flags.positional;
201
240
  const file = resolve(io.cwd, flags.direction ?? "art-direction.json");
202
- const current = fingerprintFrom(flags.snapshot, io.cwd);
241
+ if (sub === "research") {
242
+ const base = existsSync(file) ? readJson(file) : {};
243
+ const brief = flags.brief ?? base.brief;
244
+ if (!brief || brief.trim().split(/\s+/).length < 5) throw new Error('usage: craft direction research --brief "<who, where, what they do>" [--visual], or run it next to an art-direction.json with a brief');
245
+ io.out(researchPrompt({ brief, client: flags.client ?? base.client, visual: flags.visual }));
246
+ return 0;
247
+ }
248
+ const page = flags.page ?? "home";
249
+ if (!PAGE_TYPES.includes(page)) throw new Error(`--page takes one of ${PAGE_TYPES.join(", ")}`);
250
+ const snapshot = await snapshotFrom(flags.snapshot, io.cwd);
251
+ const current = snapshot ? fingerprint(snapshot) : void 0;
203
252
  if (sub === "init") {
204
253
  const out = resolve(io.cwd, flags.out ?? "art-direction.json");
205
254
  if (existsSync(out) && !flags.out) {
206
255
  const existing = readJson(out);
207
256
  if (existing.version !== void 0) throw new Error(`${out} already records choices. Pass --out to write elsewhere.`);
208
- const next = initDirection({ client: flags.client, current, exceptions: existing.exceptions });
257
+ const next = initDirection({ client: flags.client, brief: flags.brief, current, exceptions: existing.exceptions });
209
258
  writeFileSync(out, `${JSON.stringify(next, null, 2)}
210
259
  `);
211
260
  } else {
212
- writeFileSync(out, `${JSON.stringify(initDirection({ client: flags.client, current }), null, 2)}
261
+ writeFileSync(out, `${JSON.stringify(initDirection({ client: flags.client, brief: flags.brief, current }), null, 2)}
213
262
  `);
214
263
  }
215
264
  io.out(`craft direction: wrote ${out}. Add the sources and a reason for every choice, then run craft direction validate.`);
@@ -217,14 +266,30 @@ function runDirection(args, io) {
217
266
  }
218
267
  if (sub === "validate") {
219
268
  if (!existsSync(file)) throw new Error(`no ${file}. Run craft direction init.`);
220
- const report = validateDirection(readJson(file), {
269
+ const direction = readJson(file);
270
+ const estate = declaredFrom(flags.estate, io.cwd);
271
+ const report = validateDirection(direction, {
221
272
  fingerprint: current,
222
- pathExists: (p) => existsSync(resolve(dirname(file), p))
273
+ snapshot,
274
+ page,
275
+ strict: flags.strict,
276
+ pathExists: (p) => existsSync(resolve(dirname(file), p)),
277
+ readSource: (s) => {
278
+ if (!s.path || /^https?:/.test(s.path) || !TEXT_SOURCE.test(s.path)) return void 0;
279
+ const path = resolve(dirname(file), s.path);
280
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
281
+ },
282
+ estate,
283
+ estateId: estate.find((s) => s.client && s.client === direction.client)?.id
223
284
  });
224
285
  if (flags.json) io.out(toJson(report));
225
286
  else {
226
287
  for (const p of report.problems) io.out(` ${p.severity === "error" ? "error" : "warn "} ${p.at || "(file)"} ${p.message}`);
227
- io.out(`craft direction: ${report.valid ? "valid" : "not valid"}, ${report.decided} of 7 choices decided with a reason.`);
288
+ const l = report.layers;
289
+ const pages = l.hierarchy.map((h) => `${h.page} ${h.decided} of ${h.total}`).join(", ");
290
+ io.out(
291
+ `craft direction: ${report.valid ? "valid" : "not valid"}. Job ${l.job ? "decided" : "not decided"}; hierarchy ${pages || "not decided"}; tokens ${l.tokens.decided} of ${l.tokens.total}.${l.complete ? " Every layer decided." : ""}`
292
+ );
228
293
  }
229
294
  return report.valid ? 0 : 1;
230
295
  }
@@ -243,7 +308,8 @@ function runDirection(args, io) {
243
308
  return s;
244
309
  }
245
310
  });
246
- const result = propose({ client: base.client, brief: base.brief, sources, current, estate: estateFrom(flags.estate, io.cwd) });
311
+ const declared = declaredFrom(flags.estate, io.cwd).flatMap((s) => s.direction?.accent && s.client !== base.client ? [{ id: s.id, accent: s.direction.accent }] : []);
312
+ const result = propose({ client: base.client, brief: base.brief, sources, current, estate: estateFrom(flags.estate, io.cwd), declared });
247
313
  const merged = { ...base, sources, choices: { ...result.direction.choices, ...stripUndecided(base.choices) }, ...base.exceptions ? { exceptions: base.exceptions } : {} };
248
314
  const text = `${JSON.stringify(merged, null, 2)}
249
315
  `;
@@ -254,7 +320,7 @@ function runDirection(args, io) {
254
320
  }
255
321
  return 0;
256
322
  }
257
- throw new Error("usage: craft direction init | validate | propose");
323
+ throw new Error("usage: craft direction init | validate | propose | research");
258
324
  } catch (error) {
259
325
  io.err(`craft: ${error.message}`);
260
326
  return 2;
@@ -1,19 +1,21 @@
1
- import {
2
- validateDirection
3
- } from "./chunk-OOZOEWGD.js";
4
1
  import {
5
2
  merge
6
- } from "./chunk-7T5WR74C.js";
7
- import "./chunk-Q7ILL2HV.js";
3
+ } from "./chunk-2IQ5OXFR.js";
4
+ import "./chunk-OUQ25LY2.js";
8
5
  import "./chunk-JYNXGCYZ.js";
9
6
  import {
10
7
  SIBLING_AT,
11
8
  compareToEstate,
12
9
  loadEstate
13
- } from "./chunk-PBK6SYH5.js";
10
+ } from "./chunk-QU6Q2QZZ.js";
11
+ import {
12
+ PAGE_TYPES,
13
+ validateDirection
14
+ } from "./chunk-YDURAOYR.js";
14
15
  import {
15
16
  loadNull
16
- } from "./chunk-XVBR2UPH.js";
17
+ } from "./chunk-RXXRXV3V.js";
18
+ import "./chunk-OQXTQQAU.js";
17
19
  import {
18
20
  COPY_FILE,
19
21
  NOT_COPY_DIR,
@@ -24,7 +26,7 @@ import {
24
26
  parseFlags,
25
27
  readPaths,
26
28
  toJson
27
- } from "./chunk-3OTKMKJQ.js";
29
+ } from "./chunk-MUVFECFC.js";
28
30
  import {
29
31
  readSnapshot
30
32
  } from "./chunk-WUACK2HX.js";
@@ -35,7 +37,7 @@ import {
35
37
  fingerprint,
36
38
  scanSource,
37
39
  typicality
38
- } from "./chunk-GQVEYTBY.js";
40
+ } from "./chunk-LFNSB6W7.js";
39
41
  import "./chunk-QYBAVU3P.js";
40
42
 
41
43
  // src/report/cli.ts
@@ -141,8 +143,16 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
141
143
  }
142
144
  if (input.direction) {
143
145
  const d = input.direction;
144
- const raised2 = !d.valid || d.decided < MIN_DECIDED;
145
- signals.push({ id: "reasons", raised: raised2, detail: `${d.decided} of 7 choices decided with a reason${d.valid ? "" : ", art-direction.json has errors"}` });
146
+ const layers = d.layers;
147
+ const raised2 = !d.valid || d.decided < MIN_DECIDED || !layers.complete;
148
+ const pages = layers.hierarchy.map((p) => `${p.page} ${p.decided} of ${p.total}`).join(", ");
149
+ const detail = [
150
+ `job ${layers.job ? "decided" : "not decided"}`,
151
+ `hierarchy ${pages || "not decided"}`,
152
+ `${d.decided} of 7 choices decided with a reason`
153
+ ].join("; ");
154
+ signals.push({ id: "reasons", raised: raised2, detail: `${detail}${d.valid ? "" : ", art-direction.json has errors"}` });
155
+ if (!layers.job) actions.push({ area: "direction", what: "Write the job map: what the visitor is trying to get done, from reviews, enquiries and conversations. Run craft direction research for the questions.", why: "structure and look are decided from the job", priority: 1 });
146
156
  for (const p of d.problems) {
147
157
  const key = p.at.match(/^choices\.(\w+)/)?.[1];
148
158
  if (p.severity === "error" || p.message === "not decided yet") {
@@ -258,12 +268,14 @@ async function runReport(command, args, io) {
258
268
  if (typeof flags === "string") throw new Error(flags);
259
269
  const target2 = flags.positional[0];
260
270
  if (!target2) throw new Error(USAGE);
271
+ const page = flags.page ?? "home";
272
+ if (!PAGE_TYPES.includes(page)) throw new Error(`--page takes one of ${PAGE_TYPES.join(", ")}`);
261
273
  const local = resolve(io.cwd, target2);
262
274
  let snap;
263
275
  if (/\.json$/i.test(target2) && existsSync(local)) {
264
276
  snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
265
277
  } else {
266
- const { snapshotUrl } = await import("./audit-6WHR7ZTV.js");
278
+ const { snapshotUrl } = await import("./audit-MTUC24XU.js");
267
279
  snap = await snapshotUrl(target2);
268
280
  }
269
281
  const fp = fingerprint(snap);
@@ -290,7 +302,18 @@ async function runReport(command, args, io) {
290
302
  let directionReport = null;
291
303
  if (existsSync(directionFile)) {
292
304
  direction = JSON.parse(readFileSync(directionFile, "utf8"));
293
- directionReport = validateDirection(direction, { fingerprint: fp, pathExists: (p) => existsSync(resolve(dirname(directionFile), p)) });
305
+ directionReport = validateDirection(direction, {
306
+ fingerprint: fp,
307
+ snapshot: snap,
308
+ page,
309
+ strict: flags.strict,
310
+ pathExists: (p) => existsSync(resolve(dirname(directionFile), p)),
311
+ readSource: (s) => {
312
+ if (!s.path || /^https?:/.test(s.path) || !/\.(?:txt|md|markdown|csv|json|html?)$/i.test(s.path)) return void 0;
313
+ const path = resolve(dirname(directionFile), s.path);
314
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
315
+ }
316
+ });
294
317
  } else if (flags.direction) {
295
318
  throw new Error(`no such file: ${flags.direction}`);
296
319
  }
@@ -6,10 +6,11 @@ import {
6
6
  nullPath,
7
7
  runHarvest,
8
8
  runNull
9
- } from "./chunk-XVBR2UPH.js";
10
- import "./chunk-3OTKMKJQ.js";
9
+ } from "./chunk-RXXRXV3V.js";
10
+ import "./chunk-OQXTQQAU.js";
11
+ import "./chunk-MUVFECFC.js";
11
12
  import "./chunk-WUACK2HX.js";
12
- import "./chunk-GQVEYTBY.js";
13
+ import "./chunk-LFNSB6W7.js";
13
14
  import "./chunk-QYBAVU3P.js";
14
15
  export {
15
16
  describeTypicality,
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-3OTKMKJQ.js";
5
- import "./chunk-GQVEYTBY.js";
4
+ } from "./chunk-MUVFECFC.js";
5
+ import "./chunk-LFNSB6W7.js";
6
6
  import "./chunk-QYBAVU3P.js";
7
7
 
8
8
  // src/cli.ts
@@ -0,0 +1,6 @@
1
+ import {
2
+ generate
3
+ } from "./chunk-OQXTQQAU.js";
4
+ export {
5
+ generate
6
+ };