@domandigital/craft 0.13.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-GMVXADJ6.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.
@@ -529,7 +460,8 @@ var VALUE_FLAGS = {
529
460
  "--sarif": "sarif",
530
461
  "--pages": "pages",
531
462
  "--viewport": "viewport",
532
- "--component": "component"
463
+ "--component": "component",
464
+ "--competitor": "competitor"
533
465
  };
534
466
  function parseFlags(args) {
535
467
  const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
@@ -637,28 +569,69 @@ function finish(report, flags, title, io) {
637
569
  var SOURCE_FILE = (p) => ["markup", "script", "css"].includes(fileKind(p)) && !/\.d\.ts$|\.min\.js$/.test(p);
638
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);
639
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
+ }
640
613
  function run(argv, io) {
641
614
  const [command, ...rest] = argv;
642
615
  if (command === "direction") {
643
- return import("./cli-DRKAMFAH.js").then((m) => m.runDirection(rest, io));
616
+ return import("./cli-O3CDSONO.js").then((m) => m.runDirection(rest, io));
644
617
  }
645
618
  if (command === "report" || command === "retrofit") {
646
- return import("./cli-J5ECRCD4.js").then((m) => m.runReport(command, rest, io));
619
+ return import("./cli-WG3UJNUF.js").then((m) => m.runReport(command, rest, io));
647
620
  }
648
621
  if (command === "estate") {
649
- return import("./cli-BKJMLD3H.js").then((m) => m.runEstate(rest, io));
622
+ return import("./cli-3CGUKXY4.js").then((m) => m.runEstate(rest, io));
650
623
  }
651
624
  if (command === "null") {
652
- return import("./cli-ILH7OO5G.js").then((m) => m.runNull(rest, io));
625
+ return import("./cli-Z2IQG3LC.js").then((m) => m.runNull(rest, io));
653
626
  }
654
627
  if (command === "tells" && rest[0] === "harvest") {
655
- return import("./cli-ILH7OO5G.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
628
+ return import("./cli-Z2IQG3LC.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
656
629
  io.err(`craft: ${error.message}`);
657
630
  return 2;
658
631
  });
659
632
  }
660
633
  if (command === "snapshot" || command === "audit") {
661
- return import("./cli-N2IDXH7W.js").then((m) => m.runAudit(command, rest, io));
634
+ return import("./cli-WSHPSWVU.js").then((m) => m.runAudit(command, rest, io));
662
635
  }
663
636
  if (!command || command === "help" || command === "--help" || command === "-h") {
664
637
  io.out(HELP);
@@ -690,6 +663,7 @@ function run(argv, io) {
690
663
  if (command === "copy" && rest[0] === "compare") {
691
664
  const flags = parseFlags(rest.slice(1));
692
665
  if (typeof flags === "string") throw new Error(flags);
666
+ if (flags.competitor) return compareWithCompetitor(flags, io);
693
667
  if (flags.positional.length !== 2) throw new Error("usage: craft copy compare <before> <after> [--json]");
694
668
  const [before, after] = flags.positional.map((p) => {
695
669
  const abs = resolve(io.cwd, p);
@@ -1,14 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-ZLDA2SYB.js";
4
+ } from "./chunk-Q7ILL2HV.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-ESCVTKPN.js";
9
- import {
10
- readSnapshot
11
- } from "./chunk-CZDOMIVS.js";
8
+ } from "./chunk-XVBR2UPH.js";
12
9
  import {
13
10
  SOURCE_FILE,
14
11
  finish,
@@ -18,14 +15,17 @@ import {
18
15
  prepareReport,
19
16
  readPaths,
20
17
  toJson
21
- } from "./chunk-3VTPI7BU.js";
18
+ } from "./chunk-3OTKMKJQ.js";
19
+ import {
20
+ readSnapshot
21
+ } from "./chunk-WUACK2HX.js";
22
22
  import {
23
23
  auditSnapshot,
24
24
  componentTypicalities,
25
25
  fingerprint,
26
26
  scanSource,
27
27
  typicality
28
- } from "./chunk-GMVXADJ6.js";
28
+ } from "./chunk-GQVEYTBY.js";
29
29
 
30
30
  // src/audit/cli.ts
31
31
  import { existsSync, readFileSync, writeFileSync } from "fs";