@domandigital/craft 0.9.0 → 0.10.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.
@@ -8,7 +8,7 @@ import {
8
8
  fileKind,
9
9
  houseRule,
10
10
  scanSource
11
- } from "./chunk-ZT3WLN7R.js";
11
+ } from "./chunk-3NGWXZZE.js";
12
12
 
13
13
  // src/character/cli.ts
14
14
  import { execFileSync } from "child_process";
@@ -96,6 +96,120 @@ function formatComparison(result, beforePath, afterPath) {
96
96
  return lines.join("\n");
97
97
  }
98
98
 
99
+ // src/character/claims.ts
100
+ var CLAIM_KINDS = /* @__PURE__ */ new Set(["money", "number", "date"]);
101
+ var PROPER = "[A-Z][\\w&.\u2019'-]*(?:\\s+(?:of|and|&|the|for)?\\s*[A-Z][\\w&.\u2019'-]*)*";
102
+ var NOT_A_SOURCE = /^(?:We|I|It|This|That|These|Those|They|He|She|You|Our|Your|The|A|An|Each|Every|Most|Many|Some|One|Research|Studies|Experts|Data|Analysis|If|When|So|But|And|Or|In|On|At|From|For|By|As|Since|After|Before|Across|Over|Under|Its|Their|External|Source|UK|US|EU|Sometimes|Public|Recent|Industry|Independent|What|How|Why|Who|Where|Businesses|Customers|Clients|Patients|Consumers|People|Users|Online)$/;
103
+ var NOUN = "study|survey|report|research|analysis|benchmark|benchmarks|guidelines|docs|documentation|help centre|help center|pricing page|figures|statistics|case study";
104
+ var OWNED_NOUN = "guide|audit|support|pricing|data|site|website|blog";
105
+ var VERB = "found|finds|reports?|reported|says|said|states?|stated|estimates?|estimated|recommends?|publishes|published|advertises|advertised|shows|showed|claims";
106
+ var ATTRIBUTION = [
107
+ new RegExp(`\\b[Aa]ccording to\\s+(?:the\\s+)?(${PROPER})`, "g"),
108
+ new RegExp(`\\b(?:${NOUN})\\s+(?:by|from|commissioned by)\\s+(?:the\\s+)?(${PROPER})`, "g"),
109
+ // "Akamai’s 2017 retail study", "the 2020 Deloitte study", "Bark’s UK guide", "Wix’s own developer guidelines".
110
+ new RegExp(`\\b(${PROPER})(?:(?:\u2019s|'s)\\s+(?:own\\s+)?(?:\\d{4}\\s+)?(?:[A-Za-z]+\\s+)?(?:${NOUN}|${OWNED_NOUN})|\\s+(?:\\d{4}\\s+)?(?:[a-z]+\\s+)?(?:${NOUN}))\\b`, "g"),
111
+ // "Google also reported", "Shopify UK currently advertises".
112
+ new RegExp(`\\b(${PROPER})\\s+(?:also\\s+|now\\s+|currently\\s+|still\\s+)?(?:${VERB})\\b`, "g"),
113
+ /\((?:[Ss]ources?:\s*|via\s+(?=[A-Z]))([^)]+)\)/g,
114
+ /^\s*(?:source|sources)\s*:\s*(.+)$/gim
115
+ ];
116
+ var LINK = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|\bhttps?:\/\/[^\s<>"')\]]+/g;
117
+ function cleanName(raw) {
118
+ const words = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
119
+ while (words.length && NOT_A_SOURCE.test(words[0])) words.shift();
120
+ return words.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
121
+ }
122
+ var IMPERATIVE = /^(?:Add|Use|Check|Make|Get|Set|Include|Create|Update|List|Keep|Ask|Write|Track|Test|Claim|Build|Link|Submit|Verify|Review|Fix|Remove|Upload|Respond|Reply|Choose|Pick|Start|Run|Open|Install|Connect|Enable|Turn|Put|Give|Send|Book|Call|Read|See|Try|Compare|Aim|Target|Avoid|Post|Share|Mark|Tag)$/;
123
+ var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
124
+ function namedSources(sentence) {
125
+ const found = /* @__PURE__ */ new Set();
126
+ ATTRIBUTION.forEach((pattern, i) => {
127
+ for (const m of sentence.matchAll(pattern)) {
128
+ const raw = m[1].trim();
129
+ const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
130
+ if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
131
+ if (IMPERATIVE.test(raw.split(/\s+/)[0]) && atStart(sentence, m.index ?? 0)) continue;
132
+ const name = cleanName(raw);
133
+ if (!name || name.length < 2 || !/\s/.test(name) && /[a-z]ing$/.test(name)) continue;
134
+ found.add(name);
135
+ }
136
+ });
137
+ for (const m of sentence.matchAll(LINK)) found.add(m[2] ?? m[0]);
138
+ return [...found];
139
+ }
140
+ var FOOTNOTE_REF = /\[\^([\w-]+)\](?!:)/g;
141
+ var FOOTNOTE_DEF = /^[ \t]*\[\^([\w-]+)\]:[ \t]*(.+)$/gm;
142
+ var ASSUMPTION = /^(?:\*\*)?(?:assume|suppose|imagine|say|let's say|for example, say|picture)\b/i;
143
+ var LEAD = /^\s*(?:#{1,6}\s+|>\s*|[-*+]\s+|\d+[.)]\s+)*/;
144
+ function sentences(text) {
145
+ const out = [];
146
+ let start = 0;
147
+ let paragraph = 0;
148
+ const push = (end) => {
149
+ const raw = text.slice(start, end);
150
+ const lead = raw.length - raw.trimStart().length;
151
+ if (raw.trim()) out.push({ text: raw.trim(), offset: start + lead, paragraph });
152
+ };
153
+ for (const m of text.matchAll(/(?<!(?:^|\n)[ \t]*\d{1,3})[.!?](?:\[\^[\w-]+\])*(?=["”’)]?[ \t]+["“(*]*[A-Z0-9£$€])|\n+/g)) {
154
+ const end = (m.index ?? 0) + m[0].length;
155
+ push(end);
156
+ start = end;
157
+ if (/\n\s*\n/.test(m[0]) || /^\s*(?:#|\|)/.test(text.slice(end))) paragraph += 1;
158
+ }
159
+ push(text.length);
160
+ return out;
161
+ }
162
+ var MAX = 180;
163
+ var trim = (s) => s.length > MAX ? `${s.slice(0, MAX - 1).trimEnd()}\u2026` : s;
164
+ function findClaims(files) {
165
+ const claims = [];
166
+ for (const file of files) {
167
+ for (const block of extractCopy(file)) {
168
+ const notes = new Map([...block.text.matchAll(FOOTNOTE_DEF)].map((m) => [m[1], m[2].trim()]));
169
+ let carried;
170
+ for (const s of sentences(block.text)) {
171
+ if (/^\s*#{1,6}\s/.test(s.text) || /^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
172
+ const refs = [...s.text.matchAll(FOOTNOTE_REF)].map((m) => notes.get(m[1]) ?? `footnote ${m[1]}`);
173
+ const text = s.text.replace(FOOTNOTE_REF, "");
174
+ const body = text.replace(LEAD, "");
175
+ const own = [...namedSources(text), ...refs];
176
+ if (own.length) carried = { paragraph: s.paragraph, sources: own };
177
+ if (ASSUMPTION.test(body)) continue;
178
+ const facts = protectedFacts(body).filter((f) => CLAIM_KINDS.has(f.kind));
179
+ if (facts.length === 0 && own.length === 0) continue;
180
+ const nearby = own.length === 0 && carried?.paragraph === s.paragraph ? carried.sources : void 0;
181
+ const at = block.offset + s.offset;
182
+ claims.push({
183
+ path: file.path,
184
+ line: file.text.slice(0, at).split("\n").length,
185
+ sentence: trim(text.replace(/\s+/g, " ")),
186
+ facts,
187
+ sources: own,
188
+ ...nearby ? { nearby } : {}
189
+ });
190
+ }
191
+ }
192
+ }
193
+ return { files: files.length, claims, unsourced: claims.filter((c) => c.sources.length === 0).length };
194
+ }
195
+ function formatClaims(report, label = "craft copy claims") {
196
+ const { claims, unsourced } = report;
197
+ if (claims.length === 0) return `${label}: no prices, figures, dates or named sources in ${report.files} file${report.files === 1 ? "" : "s"}.`;
198
+ const lines = [
199
+ `${label}: ${claims.length} claim${claims.length === 1 ? "" : "s"} to check, ${unsourced} with no source named.`,
200
+ " Check each against its primary source before it ships. Sourced means a source is named, not that it agrees."
201
+ ];
202
+ for (const c of claims) {
203
+ const what = [
204
+ ...c.facts.map((f) => f.value),
205
+ ...c.sources.map((s) => `source: ${s}`),
206
+ ...c.nearby ? [`${c.nearby.join(", ")} named earlier in the paragraph: does it cover this?`] : []
207
+ ].join(" \xB7 ");
208
+ lines.push("", ` ${c.path}:${c.line} ${c.sources.length ? "sourced " : "UNSOURCED"} ${what}`, ` ${c.sentence}`);
209
+ }
210
+ return lines.join("\n");
211
+ }
212
+
99
213
  // src/character/format.ts
100
214
  function formatReport(report, title) {
101
215
  const lines = [];
@@ -141,6 +255,7 @@ Usage
141
255
  craft scan [paths...] [--staged] [--json] [--strict] [--direction <file>]
142
256
  craft copy <paths...> [--gate] [--json] [--strict] [--direction <file>]
143
257
  craft copy compare <before> <after> [--json]
258
+ craft copy claims <paths...> [--json]
144
259
  craft tells list [--json]
145
260
  craft tells harvest <null.json | dir>... [--share 0.25] [--json]
146
261
  craft snapshot <url> [--out <file>] [--width <px>] [--height <px>]
@@ -162,6 +277,9 @@ copy The prose in Markdown, markup and content files.
162
277
  compare lists the protected facts (prices, numbers, dates, times,
163
278
  phones, emails, links, postcodes, names) a rewrite lost or added.
164
279
  Exit 1 if any: restore it, source it, or say why.
280
+ claims lists every sentence holding a price, figure, date or named
281
+ source, marked sourced or UNSOURCED, for a person to check against
282
+ the primary source. It cannot tell true from false. Always exit 0.
165
283
  tells The catalogue this build judges against. harvest reads null models
166
284
  for choices the model keeps making that the catalogue does not know.
167
285
  snapshot Render a page in a browser and save what it looks like, as JSON.
@@ -274,25 +392,25 @@ var NOT_COPY_DIR = (name) => name.startsWith("_");
274
392
  function run(argv, io) {
275
393
  const [command, ...rest] = argv;
276
394
  if (command === "direction") {
277
- return import("./cli-3XPEGBUR.js").then((m) => m.runDirection(rest, io));
395
+ return import("./cli-2WBGTBKD.js").then((m) => m.runDirection(rest, io));
278
396
  }
279
397
  if (command === "report" || command === "retrofit") {
280
- return import("./cli-V55MTIOP.js").then((m) => m.runReport(command, rest, io));
398
+ return import("./cli-ADQQURNR.js").then((m) => m.runReport(command, rest, io));
281
399
  }
282
400
  if (command === "estate") {
283
- return import("./cli-J7C6TI2B.js").then((m) => m.runEstate(rest, io));
401
+ return import("./cli-VQUNNIZM.js").then((m) => m.runEstate(rest, io));
284
402
  }
285
403
  if (command === "null") {
286
- return import("./cli-4VAOH7ST.js").then((m) => m.runNull(rest, io));
404
+ return import("./cli-PRWTNFVP.js").then((m) => m.runNull(rest, io));
287
405
  }
288
406
  if (command === "tells" && rest[0] === "harvest") {
289
- return import("./cli-4VAOH7ST.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
407
+ return import("./cli-PRWTNFVP.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
290
408
  io.err(`craft: ${error.message}`);
291
409
  return 2;
292
410
  });
293
411
  }
294
412
  if (command === "snapshot" || command === "audit") {
295
- return import("./cli-WDSVVHKG.js").then((m) => m.runAudit(command, rest, io));
413
+ return import("./cli-EK3GSAOW.js").then((m) => m.runAudit(command, rest, io));
296
414
  }
297
415
  if (!command || command === "help" || command === "--help" || command === "-h") {
298
416
  io.out(HELP);
@@ -334,6 +452,14 @@ function run(argv, io) {
334
452
  io.out(flags.json ? JSON.stringify(result, null, 2) : formatComparison(result, flags.positional[0], flags.positional[1]));
335
453
  return result.lost.length || result.added.length ? 1 : 0;
336
454
  }
455
+ if (command === "copy" && rest[0] === "claims") {
456
+ const flags = parseFlags(rest.slice(1));
457
+ if (typeof flags === "string") throw new Error(flags);
458
+ if (flags.positional.length === 0) throw new Error("usage: craft copy claims <paths...> [--json]");
459
+ const report = findClaims(readPaths(flags.positional, io.cwd, COPY_FILE, NOT_COPY_DIR));
460
+ io.out(flags.json ? JSON.stringify(report, null, 2) : formatClaims(report));
461
+ return 0;
462
+ }
337
463
  if (command === "scan" || command === "copy") {
338
464
  const flags = parseFlags(rest);
339
465
  if (typeof flags === "string") throw new Error(flags);
@@ -6,7 +6,7 @@ import {
6
6
  isCream,
7
7
  normaliseFamily,
8
8
  parseColour
9
- } from "./chunk-ZT3WLN7R.js";
9
+ } from "./chunk-3NGWXZZE.js";
10
10
 
11
11
  // src/direction/types.ts
12
12
  var DIRECTION_VERSION = 1;
@@ -3,7 +3,7 @@ import {
3
3
  fontClass,
4
4
  normaliseFamily,
5
5
  parseColour
6
- } from "./chunk-ZT3WLN7R.js";
6
+ } from "./chunk-3NGWXZZE.js";
7
7
 
8
8
  // src/fingerprint/index.ts
9
9
  var FINGERPRINT_VERSION = 1;
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  fingerprint
3
- } from "./chunk-EHNEGKFR.js";
3
+ } from "./chunk-QMYERCBB.js";
4
4
  import {
5
5
  SNAPSHOT_VERSION,
6
6
  auditSnapshot
7
- } from "./chunk-ZT3WLN7R.js";
7
+ } from "./chunk-3NGWXZZE.js";
8
8
 
9
9
  // src/audit/collect.ts
10
10
  function collectInPage() {
@@ -1,26 +1,26 @@
1
1
  import {
2
2
  snapshotUrl
3
- } from "./chunk-BYY32NIN.js";
3
+ } from "./chunk-TSKRBIOC.js";
4
4
  import {
5
5
  describeTypicality,
6
6
  loadNull,
7
7
  typicality
8
- } from "./chunk-VKQDOUHO.js";
8
+ } from "./chunk-APO4BT2F.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
12
12
  loadExceptions,
13
13
  parseFlags,
14
14
  readPaths
15
- } from "./chunk-YLQE35PN.js";
15
+ } from "./chunk-JKIDWHG3.js";
16
16
  import {
17
17
  fingerprint
18
- } from "./chunk-EHNEGKFR.js";
18
+ } from "./chunk-QMYERCBB.js";
19
19
  import {
20
20
  SNAPSHOT_VERSION,
21
21
  auditSnapshot,
22
22
  scanSource
23
- } from "./chunk-ZT3WLN7R.js";
23
+ } from "./chunk-3NGWXZZE.js";
24
24
 
25
25
  // src/audit/cli.ts
26
26
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  DIRECTION_VERSION,
3
3
  validateDirection
4
- } from "./chunk-JRMU6GYX.js";
4
+ } from "./chunk-JWHC67ZS.js";
5
5
  import {
6
6
  parseFlags
7
- } from "./chunk-YLQE35PN.js";
7
+ } from "./chunk-JKIDWHG3.js";
8
8
  import {
9
9
  fingerprint
10
- } from "./chunk-EHNEGKFR.js";
10
+ } from "./chunk-QMYERCBB.js";
11
11
  import {
12
12
  deltaEOk,
13
13
  formatHex,
@@ -16,7 +16,7 @@ import {
16
16
  isCream,
17
17
  oklchToRgb,
18
18
  parseColour
19
- } from "./chunk-ZT3WLN7R.js";
19
+ } from "./chunk-3NGWXZZE.js";
20
20
 
21
21
  // src/direction/cli.ts
22
22
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  validateDirection
3
- } from "./chunk-JRMU6GYX.js";
3
+ } from "./chunk-JWHC67ZS.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-DAENYJUO.js";
7
- import "./chunk-BYY32NIN.js";
6
+ } from "./chunk-U2JTFQKO.js";
7
+ import "./chunk-TSKRBIOC.js";
8
8
  import {
9
9
  SIBLING_AT,
10
10
  compareToEstate,
11
11
  loadEstate
12
- } from "./chunk-5LI2VFN5.js";
12
+ } from "./chunk-2NQFIDLE.js";
13
13
  import {
14
14
  loadNull,
15
15
  typicality
16
- } from "./chunk-VKQDOUHO.js";
16
+ } from "./chunk-APO4BT2F.js";
17
17
  import {
18
18
  COPY_FILE,
19
19
  NOT_COPY_DIR,
@@ -21,17 +21,17 @@ import {
21
21
  loadExceptions,
22
22
  parseFlags,
23
23
  readPaths
24
- } from "./chunk-YLQE35PN.js";
24
+ } from "./chunk-JKIDWHG3.js";
25
25
  import {
26
26
  fingerprint
27
- } from "./chunk-EHNEGKFR.js";
27
+ } from "./chunk-QMYERCBB.js";
28
28
  import {
29
29
  CATALOGUE,
30
30
  SNAPSHOT_VERSION,
31
31
  auditSnapshot,
32
32
  checkCopy,
33
33
  scanSource
34
- } from "./chunk-ZT3WLN7R.js";
34
+ } from "./chunk-3NGWXZZE.js";
35
35
 
36
36
  // src/report/cli.ts
37
37
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -259,7 +259,7 @@ async function runReport(command, args, io) {
259
259
  snap = JSON.parse(readFileSync(local, "utf8"));
260
260
  if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target2} is not a snapshot`);
261
261
  } else {
262
- const { snapshotUrl } = await import("./audit-5P65UMFL.js");
262
+ const { snapshotUrl } = await import("./audit-EGDNWSQB.js");
263
263
  snap = await snapshotUrl(target2);
264
264
  }
265
265
  const fp = fingerprint(snap);
@@ -0,0 +1,15 @@
1
+ import {
2
+ describeFingerprint,
3
+ merge,
4
+ runAudit
5
+ } from "./chunk-U2JTFQKO.js";
6
+ import "./chunk-TSKRBIOC.js";
7
+ import "./chunk-APO4BT2F.js";
8
+ import "./chunk-JKIDWHG3.js";
9
+ import "./chunk-QMYERCBB.js";
10
+ import "./chunk-3NGWXZZE.js";
11
+ export {
12
+ describeFingerprint,
13
+ merge,
14
+ runAudit
15
+ };
@@ -6,10 +6,10 @@ import {
6
6
  nullPath,
7
7
  runHarvest,
8
8
  runNull
9
- } from "./chunk-VKQDOUHO.js";
10
- import "./chunk-YLQE35PN.js";
11
- import "./chunk-EHNEGKFR.js";
12
- import "./chunk-ZT3WLN7R.js";
9
+ } from "./chunk-APO4BT2F.js";
10
+ import "./chunk-JKIDWHG3.js";
11
+ import "./chunk-QMYERCBB.js";
12
+ import "./chunk-3NGWXZZE.js";
13
13
  export {
14
14
  describeTypicality,
15
15
  formatHarvest,
@@ -0,0 +1,16 @@
1
+ import {
2
+ formatMatches,
3
+ formatPairs,
4
+ loadEstate,
5
+ runEstate
6
+ } from "./chunk-2NQFIDLE.js";
7
+ import "./chunk-APO4BT2F.js";
8
+ import "./chunk-JKIDWHG3.js";
9
+ import "./chunk-QMYERCBB.js";
10
+ import "./chunk-3NGWXZZE.js";
11
+ export {
12
+ formatMatches,
13
+ formatPairs,
14
+ loadEstate,
15
+ runEstate
16
+ };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-YLQE35PN.js";
5
- import "./chunk-ZT3WLN7R.js";
4
+ } from "./chunk-JKIDWHG3.js";
5
+ import "./chunk-3NGWXZZE.js";
6
6
 
7
7
  // src/cli.ts
8
8
  process.stdout.on("error", (error) => {