@domandigital/craft 0.12.0 → 0.14.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,13 +4,19 @@ import {
4
4
  HOUSE_LOCKED,
5
5
  applyHouseGate,
6
6
  checkCopy,
7
+ compareCompetitor,
8
+ compareFacts,
7
9
  extractCopy,
8
- extractStrings,
9
10
  fileKind,
11
+ formatComparison,
12
+ formatCompetitor,
10
13
  holdHouseBlocks,
11
14
  houseRule,
12
- scanSource
13
- } from "./chunk-AL6MRG6V.js";
15
+ pageCopy,
16
+ protectedFacts,
17
+ scanSource,
18
+ visibleText
19
+ } from "./chunk-GQVEYTBY.js";
14
20
 
15
21
  // src/character/config.ts
16
22
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -205,87 +211,6 @@ function toSarif(report, options) {
205
211
  };
206
212
  }
207
213
 
208
- // src/character/facts.ts
209
- var MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)";
210
- var PATTERNS = [
211
- ["url", /\bhttps?:\/\/[^\s<>"'`)\]]+|\bwww\.[^\s<>"'`)\]]+/gi],
212
- ["email", /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/g],
213
- ["phone", /(?:\+44\s?\(?0?\)?\s?|\b0)\d(?:[\s-]?\d){8,9}\b/g],
214
- ["postcode", /\b[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}\b/g],
215
- ["money", /[£$€]\s?\d[\d,]*(?:\.\d{1,2})?(?:\s?(?:k|m|bn)\b)?|\b\d[\d,]*(?:\.\d{1,2})?\s?(?:pounds|GBP)\b/gi],
216
- ["date", new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}\\b(?:\\s+\\d{4})?|\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?\\b(?:,?\\s+\\d{4})?|\\b${MONTH}\\s+\\d{4}\\b|\\b\\d{1,2}\\/\\d{1,2}\\/\\d{2,4}\\b|\\b(?:19|20)\\d{2}\\b`, "g")],
217
- ["time", /\b\d{1,2}(?:[:.]\d{2})?\s?(?:am|pm)\b|\b\d{1,2}:\d{2}\b/gi],
218
- ["number", /\b\d[\d,]*(?:\.\d+)?%?|\b\d+(?:\.\d+)?\s?(?:per ?cent)\b/gi]
219
- ];
220
- var NAME = /[A-Z][a-zA-Z’'&-]*[a-zA-Z](?:\s+(?:of|and|&|the)?\s*[A-Z][a-zA-Z’'&-]*[a-zA-Z])*/g;
221
- var normalise = (kind, value) => {
222
- const v = value.trim();
223
- if (kind === "phone") return v.replace(/[^\d+]/g, "").replace(/^\+44\(?0?\)?/, "0").replace(/^\+44/, "0");
224
- if (kind === "money" || kind === "number") return v.replace(/[\s,]/g, "").toLowerCase();
225
- if (kind === "postcode") return v.replace(/\s/g, "").toUpperCase();
226
- if (kind === "url") return v.replace(/[.,;:]+$/, "").toLowerCase();
227
- return v.replace(/\s+/g, " ").toLowerCase();
228
- };
229
- function visibleText(file) {
230
- const prose = /\.(md|markdown|mdx|txt)$/i.test(file.path);
231
- return (prose ? extractCopy(file) : extractStrings(file)).map((b) => b.text).join("\n");
232
- }
233
- function protectedFacts(text) {
234
- const facts = [];
235
- let rest = text;
236
- const mask = (start, length) => {
237
- rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
238
- };
239
- for (const [kind, pattern] of PATTERNS) {
240
- for (const m of [...rest.matchAll(pattern)]) {
241
- const value = m[0].replace(/[.,;:]+$/, "");
242
- facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
243
- mask(m.index ?? 0, m[0].length);
244
- }
245
- }
246
- const lines = rest.split("\n");
247
- for (const line of lines) {
248
- if (/^\s{0,3}#{1,6}\s/.test(line)) continue;
249
- for (const m of line.matchAll(NAME)) {
250
- const at = m.index ?? 0;
251
- const before = line.slice(0, at).trimEnd();
252
- const opensSentence = before === "" || /[.!?:"“(>*-]$/.test(before) || /^\s*(?:[-*+]|\d+\.)$/.test(before);
253
- const word = m[0];
254
- const acronym = /^[A-Z]{2,}$/.test(word);
255
- if (opensSentence && !acronym && !/\s/.test(word)) continue;
256
- if (word === "I") continue;
257
- facts.push({ kind: "name", value: word, key: `name:${normalise("name", word)}` });
258
- }
259
- }
260
- return facts;
261
- }
262
- var unique = (facts) => [...new Map(facts.map((f) => [f.key, f])).values()];
263
- function compareFacts(before, after) {
264
- const a = unique(protectedFacts(before));
265
- const b = unique(protectedFacts(after));
266
- const inA = new Set(a.map((f) => f.key));
267
- const inB = new Set(b.map((f) => f.key));
268
- return {
269
- before: a.length,
270
- after: b.length,
271
- lost: a.filter((f) => !inB.has(f.key)),
272
- added: b.filter((f) => !inA.has(f.key))
273
- };
274
- }
275
- function formatComparison(result, beforePath, afterPath) {
276
- const lines = [`craft copy compare: ${beforePath} -> ${afterPath}`, ` ${result.before} protected facts before, ${result.after} after`];
277
- if (result.lost.length) {
278
- lines.push("", ` Lost (${result.lost.length}): restore each, or say why it went`);
279
- for (const f of result.lost) lines.push(` ${f.kind.padEnd(9)} ${f.value}`);
280
- }
281
- if (result.added.length) {
282
- lines.push("", ` Added (${result.added.length}): each needs a source, or it is invented`);
283
- for (const f of result.added) lines.push(` ${f.kind.padEnd(9)} ${f.value}`);
284
- }
285
- if (!result.lost.length && !result.added.length) lines.push("", " Every fact kept, nothing added.");
286
- return lines.join("\n");
287
- }
288
-
289
214
  // src/character/claims.ts
290
215
  var CLAIM_KINDS = /* @__PURE__ */ new Set(["money", "number", "date"]);
291
216
  var PROPER = "[A-Z][\\w&.\u2019'-]*(?:\\s+(?:of|and|&|the|for)?\\s*[A-Z][\\w&.\u2019'-]*)*";
@@ -445,6 +370,7 @@ Usage
445
370
  craft scan [paths...] [--staged] [--json] [--strict] [--direction <file>] [ci options]
446
371
  craft copy [paths...] [--gate] [--json] [--strict] [--direction <file>] [ci options]
447
372
  craft copy compare <before> <after> [--json]
373
+ craft copy compare <url | snapshot.json> --competitor <url | snapshot.json> [--brief "<text>"] [--json]
448
374
  craft copy claims <paths...> [--json]
449
375
  craft tells list [--json]
450
376
  craft tells harvest <null.json | dir>... [--share 0.25] [--json]
@@ -468,6 +394,11 @@ copy The prose in Markdown, markup and content files.
468
394
  compare lists the protected facts (prices, numbers, dates, times,
469
395
  phones, emails, links, postcodes, names) a rewrite lost or added.
470
396
  Exit 1 if any: restore it, source it, or say why.
397
+ With --competitor, reads both rendered pages instead and lists the
398
+ hero and service sentences holding no specific (place, name,
399
+ number, price, job) the other page does not also say. The brief's
400
+ terms come from --brief, else art-direction.json, else --null's
401
+ null.json. A measure, not a gate: always exit 0.
471
402
  claims lists every sentence holding a price, figure, date or named
472
403
  source, marked sourced or UNSOURCED, for a person to check against
473
404
  the primary source. It cannot tell true from false. Always exit 0.
@@ -528,7 +459,9 @@ var VALUE_FLAGS = {
528
459
  "--baseline": "baseline",
529
460
  "--sarif": "sarif",
530
461
  "--pages": "pages",
531
- "--viewport": "viewport"
462
+ "--viewport": "viewport",
463
+ "--component": "component",
464
+ "--competitor": "competitor"
532
465
  };
533
466
  function parseFlags(args) {
534
467
  const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
@@ -636,28 +569,69 @@ function finish(report, flags, title, io) {
636
569
  var SOURCE_FILE = (p) => ["markup", "script", "css"].includes(fileKind(p)) && !/\.d\.ts$|\.min\.js$/.test(p);
637
570
  var COPY_FILE = (p) => (["markup", "prose", "script"].includes(fileKind(p)) || /\.jsonl?$/i.test(p)) && !NOT_SITE_COPY.test(p.split("/").pop() ?? p) && !/(?:^|\/)(?:package(?:-lock)?|tsconfig[\w.-]*|art-direction)\.json$/.test(p);
638
571
  var NOT_COPY_DIR = (name) => name.startsWith("_");
572
+ async function pageSnapshot(target, cwd) {
573
+ const local = resolve(cwd, target);
574
+ if (/\.json$/i.test(target) && existsSync(local)) {
575
+ const { readSnapshot } = await import("./migrate-VIFAYIBM.js");
576
+ return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
577
+ }
578
+ if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
579
+ const { snapshotUrl } = await import("./audit-6WHR7ZTV.js");
580
+ return snapshotUrl(target);
581
+ }
582
+ async function briefFor(flags, cwd) {
583
+ if (flags.brief) return flags.brief;
584
+ const direction = resolve(cwd, flags.direction ?? "art-direction.json");
585
+ if (existsSync(direction)) {
586
+ const brief = JSON.parse(readFileSync(direction, "utf8")).brief;
587
+ if (typeof brief === "string" && brief.trim()) return brief;
588
+ } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
589
+ if (flags.null) {
590
+ const { loadNull } = await import("./cli-Z2IQG3LC.js");
591
+ return loadNull(flags.null, cwd).brief;
592
+ }
593
+ return void 0;
594
+ }
595
+ async function compareWithCompetitor(flags, io) {
596
+ try {
597
+ if (flags.positional.length !== 1 || !flags.competitor) throw new Error('usage: craft copy compare <url | snapshot.json> --competitor <url | snapshot.json> [--brief "<text>"] [--json]');
598
+ const brief = await briefFor(flags, io.cwd);
599
+ const [ours, theirs] = await Promise.all([pageSnapshot(flags.positional[0], io.cwd), pageSnapshot(flags.competitor, io.cwd)]);
600
+ const copy = [ours, theirs].map((snap, i) => {
601
+ const page = pageCopy(snap);
602
+ if (!page) throw new Error(`${[flags.positional[0], flags.competitor][i]} has no page text: snapshot it again with this craft`);
603
+ return page;
604
+ });
605
+ const result = compareCompetitor(copy[0], copy[1], brief);
606
+ io.out(flags.json ? toJson(result) : formatCompetitor(result));
607
+ return 0;
608
+ } catch (error) {
609
+ io.err(`craft: ${error.message}`);
610
+ return 2;
611
+ }
612
+ }
639
613
  function run(argv, io) {
640
614
  const [command, ...rest] = argv;
641
615
  if (command === "direction") {
642
- return import("./cli-ZPJIWVEP.js").then((m) => m.runDirection(rest, io));
616
+ return import("./cli-O3CDSONO.js").then((m) => m.runDirection(rest, io));
643
617
  }
644
618
  if (command === "report" || command === "retrofit") {
645
- return import("./cli-ZB2VJ7CY.js").then((m) => m.runReport(command, rest, io));
619
+ return import("./cli-WG3UJNUF.js").then((m) => m.runReport(command, rest, io));
646
620
  }
647
621
  if (command === "estate") {
648
- return import("./cli-2DY3V3SF.js").then((m) => m.runEstate(rest, io));
622
+ return import("./cli-3CGUKXY4.js").then((m) => m.runEstate(rest, io));
649
623
  }
650
624
  if (command === "null") {
651
- return import("./cli-C3JLFXMG.js").then((m) => m.runNull(rest, io));
625
+ return import("./cli-Z2IQG3LC.js").then((m) => m.runNull(rest, io));
652
626
  }
653
627
  if (command === "tells" && rest[0] === "harvest") {
654
- return import("./cli-C3JLFXMG.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
628
+ return import("./cli-Z2IQG3LC.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
655
629
  io.err(`craft: ${error.message}`);
656
630
  return 2;
657
631
  });
658
632
  }
659
633
  if (command === "snapshot" || command === "audit") {
660
- return import("./cli-QM2AQQMF.js").then((m) => m.runAudit(command, rest, io));
634
+ return import("./cli-WSHPSWVU.js").then((m) => m.runAudit(command, rest, io));
661
635
  }
662
636
  if (!command || command === "help" || command === "--help" || command === "-h") {
663
637
  io.out(HELP);
@@ -689,6 +663,7 @@ function run(argv, io) {
689
663
  if (command === "copy" && rest[0] === "compare") {
690
664
  const flags = parseFlags(rest.slice(1));
691
665
  if (typeof flags === "string") throw new Error(flags);
666
+ if (flags.competitor) return compareWithCompetitor(flags, io);
692
667
  if (flags.positional.length !== 2) throw new Error("usage: craft copy compare <before> <after> [--json]");
693
668
  const [before, after] = flags.positional.map((p) => {
694
669
  const abs = resolve(io.cwd, p);
@@ -1,15 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-ULPI335S.js";
4
+ } from "./chunk-Q7ILL2HV.js";
5
5
  import {
6
6
  describeTypicality,
7
- loadNull,
8
- typicality
9
- } from "./chunk-LHGY43LL.js";
10
- import {
11
- readSnapshot
12
- } from "./chunk-ZKVJ7K6X.js";
7
+ loadNull
8
+ } from "./chunk-XVBR2UPH.js";
13
9
  import {
14
10
  SOURCE_FILE,
15
11
  finish,
@@ -19,14 +15,17 @@ import {
19
15
  prepareReport,
20
16
  readPaths,
21
17
  toJson
22
- } from "./chunk-J3QUROJ5.js";
18
+ } from "./chunk-3OTKMKJQ.js";
23
19
  import {
24
- fingerprint
25
- } from "./chunk-PZ4MF2VV.js";
20
+ readSnapshot
21
+ } from "./chunk-WUACK2HX.js";
26
22
  import {
27
23
  auditSnapshot,
28
- scanSource
29
- } from "./chunk-AL6MRG6V.js";
24
+ componentTypicalities,
25
+ fingerprint,
26
+ scanSource,
27
+ typicality
28
+ } from "./chunk-GQVEYTBY.js";
30
29
 
31
30
  // src/audit/cli.ts
32
31
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -51,6 +50,12 @@ function parseViewports(value) {
51
50
  }
52
51
 
53
52
  // src/audit/cli.ts
53
+ function describeComponents(components, pageMeasured, nullMeasured) {
54
+ if (!pageMeasured) return " components not measured: the page's snapshot predates version 2";
55
+ if (!nullMeasured) return " components not measured: the null model predates snapshot version 2; rebuild it to compare components";
56
+ if (components.length === 0) return " components none the null model has enough pages of to compare";
57
+ return components.map((c, i) => `${i === 0 ? " components" : " "} ${c.role.padEnd(13)} ${c.score.toFixed(2)}: ${c.typical ? "typical" : "not typical"} (${c.runs} null pages have one)`).join("\n");
58
+ }
54
59
  var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
55
60
  function describeFingerprint(fp) {
56
61
  return [
@@ -167,11 +172,17 @@ ${failed.map((f) => ` ${f.url}: ${f.error}`).join("\n")}`);
167
172
  const pooled = nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r));
168
173
  const pages = audited.map((a) => {
169
174
  const fp = fingerprint(a.snapshot);
170
- return { url: a.snapshot.url, viewport: a.viewport, fingerprint: fp, typicality: nulls.length ? typicality(fp, pooled) : null };
175
+ return {
176
+ url: a.snapshot.url,
177
+ viewport: a.viewport,
178
+ fingerprint: fp,
179
+ typicality: nulls.length ? typicality(fp, pooled) : null,
180
+ components: nulls.length ? componentTypicalities(fp, pooled) : []
181
+ };
171
182
  });
172
183
  const unmeasured = failed.length > 0 && flags.strict ? 1 : 0;
173
184
  if (flags.json) {
174
- const extra = single ? { fingerprint: pages[0].fingerprint, typicality: pages[0].typicality, snapshot: flags.out ?? null } : {};
185
+ const extra = single ? { fingerprint: pages[0].fingerprint, typicality: pages[0].typicality, components: pages[0].components, snapshot: flags.out ?? null } : {};
175
186
  io.out(toJson({ report: prepared, ...extra, pages, failed }));
176
187
  return Math.max(prepared.summary.blocking > 0 || flags.strict && prepared.summary.findings > 0 ? 1 : 0, unmeasured);
177
188
  }
@@ -184,6 +195,7 @@ ${failed.map((f) => ` ${f.url}: ${f.error}`).join("\n")}`);
184
195
  if (page.typicality) {
185
196
  io.out("");
186
197
  io.out(describeTypicality(page.typicality, pooled.length, nulls.length));
198
+ io.out(describeComponents(page.components, page.fingerprint.sections !== void 0, pooled.some((r) => r.fingerprint.sections !== void 0)));
187
199
  }
188
200
  }
189
201
  if (failed.length) {
@@ -199,6 +211,7 @@ ${failed.map((f) => ` ${f.url}: ${f.error}`).join("\n")}`);
199
211
  }
200
212
 
201
213
  export {
214
+ describeComponents,
202
215
  describeFingerprint,
203
216
  merge,
204
217
  resolvePages,