@domandigital/craft 0.20.4 → 0.21.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CHARACTER.md +3 -1
  3. package/COPY.md +9 -3
  4. package/README.md +73 -3
  5. package/dist/{audit-BN2GSFKT.js → audit-LQ2TDISV.js} +3 -3
  6. package/dist/audit.cjs +104 -3
  7. package/dist/audit.js +104 -3
  8. package/dist/{chunk-NQAYRK2K.js → chunk-4WAIOZ4W.js} +14 -14
  9. package/dist/{chunk-YOZLPNJS.js → chunk-6DRBFDWT.js} +1 -1
  10. package/dist/chunk-BQ7H23QP.js +261 -0
  11. package/dist/{chunk-2276FJVN.js → chunk-BYXDX66D.js} +4 -4
  12. package/dist/{chunk-JGWOA4LI.js → chunk-DE2ZAV4G.js} +6 -2
  13. package/dist/{chunk-KQF44RI3.js → chunk-FNB3NP57.js} +5 -5
  14. package/dist/{chunk-S3L7TEJF.js → chunk-G47ZCBD5.js} +2 -2
  15. package/dist/{chunk-7DQDF3VL.js → chunk-ONA73THJ.js} +99 -2
  16. package/dist/{chunk-GBDPNNC2.js → chunk-STWH5EYU.js} +5 -5
  17. package/dist/{cli-QGJB6OUB.js → cli-5QI6NIZN.js} +6 -6
  18. package/dist/{cli-AK4EQHUF.js → cli-5VHX5EJF.js} +5 -5
  19. package/dist/{cli-ZXDGVDYU.js → cli-7QL26YPR.js} +1 -1
  20. package/dist/cli-EYHRJJAX.js +241 -0
  21. package/dist/{cli-AFKXDGUH.js → cli-PBCU7Z64.js} +4 -4
  22. package/dist/{cli-6AZVLNWL.js → cli-RHDNHL4U.js} +5 -5
  23. package/dist/{cli-OYVZOHHV.js → cli-XAWJWK7I.js} +6 -6
  24. package/dist/{cli-7HKVVL3P.js → cli-YSGPGQ7R.js} +9 -9
  25. package/dist/cli.js +3 -3
  26. package/dist/index.cjs +104 -3
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +104 -3
  30. package/dist/phone.cjs +137 -11
  31. package/dist/phone.d.cts +86 -12
  32. package/dist/phone.d.ts +86 -12
  33. package/dist/phone.js +128 -11
  34. package/dist/{viewports-cli-WWWWEBKI.js → viewports-cli-XYYX4SKV.js} +12 -8
  35. package/package.json +1 -1
  36. package/word-lists.json +1 -1
  37. package/dist/chunk-IJUZ7RV4.js +0 -151
  38. package/dist/cli-6INEFJYN.js +0 -122
@@ -0,0 +1,261 @@
1
+ // src/phone/judge.ts
2
+ var hasReason = (entry) => typeof entry.reason === "string" && entry.reason.trim() !== "";
3
+ function parseBaseline(raw, where) {
4
+ const b = raw ?? {};
5
+ const entries = [...b.static ?? [], ...b.measured ?? []];
6
+ const bad = entries.find((e) => e.reason !== void 0 && typeof e.reason !== "string");
7
+ if (bad) throw new Error(`${where}: an entry has a "reason" that is not text: ${JSON.stringify(bad)}`);
8
+ return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
9
+ }
10
+ var EMPTY_BASELINE = { static: [], measured: [] };
11
+ var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
12
+ var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
13
+ var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
14
+ var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
15
+ var OK = /mobile-first-ok\b:?(.*)/;
16
+ var VAR_OPACITY = /[^\s"'`]*var\([^)]*\)\]\/[0-9]+/g;
17
+ function reasonOf(line) {
18
+ const m = line?.match(OK);
19
+ return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
20
+ }
21
+ function scanPhoneFirst(files, exceptions = [], options = {}) {
22
+ const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
23
+ const findings = [];
24
+ let excused = 0;
25
+ for (const { path, text } of files) {
26
+ if (excusedFiles.has(path)) continue;
27
+ const lines = text.split("\n");
28
+ const lineOf = (index) => text.slice(0, index).split("\n").length;
29
+ const found = [];
30
+ for (const m of text.matchAll(MEDIA)) {
31
+ if (CAPS_WIDTH.test(m[1])) found.push({ line: lineOf(m.index), what: `${m[0].slice(0, m[0].length - m[1].length)}${m[1].replace(/\s+/g, " ").trimEnd()}` });
32
+ }
33
+ for (const m of text.matchAll(VARIANT)) {
34
+ if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
35
+ }
36
+ const silent = /* @__PURE__ */ new Set();
37
+ if (options.tailwind3?.(path)) {
38
+ for (const m of text.matchAll(VAR_OPACITY)) {
39
+ silent.add(found.push({ line: lineOf(m.index), what: `${m[0]} (Tailwind 3 emits no CSS for opacity on a var() colour)` }) - 1);
40
+ }
41
+ }
42
+ const order = found.map((f, i) => ({ ...f, silent: silent.has(i) })).sort((a, b) => a.line - b.line);
43
+ for (const { line, what, silent: cannotExcuse } of order) {
44
+ if (cannotExcuse) {
45
+ findings.push({ file: path, line, what });
46
+ continue;
47
+ }
48
+ const here = reasonOf(lines[line - 1]);
49
+ const above = reasonOf(lines[line - 2]);
50
+ if (here || above) {
51
+ excused++;
52
+ continue;
53
+ }
54
+ findings.push({ file: path, line, what: here === "" || above === "" ? `${what} (mobile-first-ok with no reason)` : what });
55
+ }
56
+ }
57
+ return { findings, excused };
58
+ }
59
+ function checkExceptions(exceptions, exists) {
60
+ const problems = [];
61
+ exceptions.forEach((e, i) => {
62
+ const at = `exceptions[${i}]`;
63
+ const x = e;
64
+ if (typeof x?.file !== "string" || !x.file.trim()) problems.push(`${at}: names no file`);
65
+ else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.file}): no reason. Say why this file needs a max-width query, or remove the exception.`);
66
+ else if (!exists(x.file)) problems.push(`${at}: ${x.file} is not there. Remove the exception.`);
67
+ });
68
+ return problems;
69
+ }
70
+ var normaliseElement = (element) => element.replace(/("[^]*)$/, (text) => text.replace(/\d+/g, "#"));
71
+ var tally = (items, key) => {
72
+ const m = /* @__PURE__ */ new Map();
73
+ for (const item of items) {
74
+ const k = key(item);
75
+ const had = m.get(k);
76
+ if (had) had.count++;
77
+ else m.set(k, { item, count: 1 });
78
+ }
79
+ return m;
80
+ };
81
+ function judgeStatic(findings, baseline) {
82
+ const byFile = tally(findings, (f) => f.file);
83
+ const allowed = new Map(baseline.static.map((b) => [b.file, b.count]));
84
+ const fresh = [];
85
+ const stale = [];
86
+ let known = 0;
87
+ for (const [file, { count }] of byFile) {
88
+ const allow = allowed.get(file) ?? 0;
89
+ known += Math.min(count, allow);
90
+ if (count > allow) {
91
+ const here = findings.filter((f) => f.file === file);
92
+ const lead = allow ? `${file}: ${count} desktop-first rules, the baseline allows ${allow}. One of these is new:` : `${file}:`;
93
+ fresh.push(`${lead}
94
+ ${here.map((f) => ` line ${f.line}: ${f.what}`).join("\n")}`);
95
+ }
96
+ }
97
+ for (const { file, count } of baseline.static) {
98
+ const now = byFile.get(file)?.count ?? 0;
99
+ if (now < count) stale.push(`${file}: the baseline allows ${count}, there ${now === 1 ? "is" : "are"} ${now} now. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
100
+ }
101
+ return { fresh, stale, known };
102
+ }
103
+ var measuredKey = (route, check, element) => `${route}\0${check}\0${element}`;
104
+ function judgeMeasured(routes, baseline) {
105
+ const fresh = [];
106
+ const stale = [];
107
+ let known = 0;
108
+ const allowed = new Map(baseline.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
109
+ const seen = /* @__PURE__ */ new Map();
110
+ const measured = new Set(routes.map((r) => r.route));
111
+ for (const { route, findings } of routes) {
112
+ const counted = tally(findings, (f) => measuredKey(route, f.check, normaliseElement(f.element)));
113
+ for (const [key, { item, count }] of counted) {
114
+ seen.set(key, count);
115
+ const allow = allowed.get(key) ?? 0;
116
+ known += Math.min(count, allow);
117
+ if (count > allow) {
118
+ const times = allow ? ` (${count} found, the baseline allows ${allow})` : count > 1 ? ` (${count} of them)` : "";
119
+ fresh.push(`${route}: ${LABEL[item.check]} ${item.element}: ${item.detail}${times}`);
120
+ }
121
+ }
122
+ }
123
+ const missing = /* @__PURE__ */ new Set();
124
+ for (const b of baseline.measured) {
125
+ const key = measuredKey(b.route, b.check, b.element);
126
+ if (!measured.has(b.route)) {
127
+ if (!missing.has(b.route)) stale.push(`${b.route}: in the baseline but not measured this run. Measure it, or remove its lines.`);
128
+ missing.add(b.route);
129
+ continue;
130
+ }
131
+ const now = seen.get(key) ?? 0;
132
+ if (now < b.count) stale.push(`${b.route}: ${LABEL[b.check]} ${b.element}: the baseline allows ${b.count}, ${now} found. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
133
+ }
134
+ return { fresh, stale, known };
135
+ }
136
+ var LABEL = {
137
+ scroll: "scrolls sideways",
138
+ target: "small target",
139
+ field: "small field",
140
+ text: "small text",
141
+ title: "title cut off",
142
+ overlap: "text overlaps text",
143
+ action: "main action out of reach",
144
+ layers: "fixed layers cover too much",
145
+ landscape: "landscape screen eaten by fixed layers",
146
+ image: "image not sized for its place",
147
+ input: "wrong keyboard",
148
+ autocomplete: "field the phone cannot fill in",
149
+ tel: "phone number is not a link",
150
+ linelength: "line too long"
151
+ };
152
+ function writeBaseline(findings, routes, note, previous) {
153
+ const byFile = tally(findings, (f) => f.file);
154
+ const measured = routes.flatMap(
155
+ ({ route, findings: findings2 }) => [...tally(findings2, (f) => measuredKey(route, f.check, normaliseElement(f.element))).values()].map(({ item, count }) => ({ route, check: item.check, element: normaliseElement(item.element), count }))
156
+ );
157
+ const staticReasons = new Map((previous?.static ?? []).filter(hasReason).map((b) => [b.file, b.reason]));
158
+ const measuredReasons = new Map((previous?.measured ?? []).filter(hasReason).map((b) => [measuredKey(b.route, b.check, b.element), b.reason]));
159
+ return {
160
+ ...note ? { note } : {},
161
+ static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what), ...staticReasons.has(item.file) ? { reason: staticReasons.get(item.file) } : {} })).sort((a, b) => a.file.localeCompare(b.file)),
162
+ measured: measured.map((m) => measuredReasons.has(measuredKey(m.route, m.check, m.element)) ? { ...m, reason: measuredReasons.get(measuredKey(m.route, m.check, m.element)) } : m).sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
163
+ };
164
+ }
165
+ function judgeGrowth(current, base, baseName = "main") {
166
+ const out = [];
167
+ const baseStatic = new Map(base.static.map((b) => [b.file, b.count]));
168
+ for (const b of current.static) {
169
+ const was = baseStatic.get(b.file) ?? 0;
170
+ if (b.count > was && !hasReason(b)) out.push(`${b.file}: ${was ? `${b.count} desktop-first rules, ${baseName} allows ${was}` : `${b.count} desktop-first rule${b.count === 1 ? "" : "s"} added to the baseline`}`);
171
+ }
172
+ const baseMeasured = new Map(base.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
173
+ for (const b of current.measured) {
174
+ const was = baseMeasured.get(measuredKey(b.route, b.check, b.element)) ?? 0;
175
+ if (b.count > was && !hasReason(b)) out.push(`${b.route}: ${LABEL[b.check]} ${b.element}: ${was ? `${b.count} in the baseline, ${baseName} allows ${was}` : `added to the baseline (${b.count})`}`);
176
+ }
177
+ return out;
178
+ }
179
+ var hasUnexcusedEntries = (b) => [...b.static, ...b.measured].some((e) => !hasReason(e));
180
+ var PAGE_FILE = /(?:^|\/)page\.(?:tsx|ts|jsx|js|mdx)$/;
181
+ var INTERCEPTING = /^\(\.{1,3}\)/;
182
+ function routesFromPages(files) {
183
+ const routes = /* @__PURE__ */ new Set();
184
+ for (const file of files) {
185
+ const rel = file.replace(/\\/g, "/");
186
+ if (!PAGE_FILE.test(rel)) continue;
187
+ const dirs = rel.split("/").slice(0, -1);
188
+ if (dirs.some((d) => INTERCEPTING.test(d) || d.startsWith("_"))) continue;
189
+ const url = dirs.filter((d) => !/^\(.*\)$/.test(d) && !d.startsWith("@"));
190
+ routes.add(`/${url.join("/")}`);
191
+ }
192
+ return [...routes].sort();
193
+ }
194
+ function routesFromManifest(manifest) {
195
+ const routes = /* @__PURE__ */ new Set();
196
+ for (const [key, route] of Object.entries(manifest ?? {})) {
197
+ if (!/(?:^|\/)page$/.test(key) || typeof route !== "string") continue;
198
+ if (route.split("/").some((d) => d.startsWith("_"))) continue;
199
+ routes.add(route);
200
+ }
201
+ return [...routes].sort();
202
+ }
203
+ var segmentsOf = (route) => route.split("/").filter(Boolean);
204
+ var rank = (seg) => seg.startsWith("[[...") ? 3 : seg.startsWith("[...") ? 2 : seg.startsWith("[") ? 1 : 0;
205
+ var cleanRoute = (route) => {
206
+ const path = route.replace(/[?#].*$/, "").replace(/\/+$/, "");
207
+ return path === "" ? "/" : path;
208
+ };
209
+ function routeMatches(pattern, route) {
210
+ if (pattern === route) return true;
211
+ const want = segmentsOf(pattern);
212
+ const have = segmentsOf(route);
213
+ for (let i = 0; i < want.length; i++) {
214
+ const seg = want[i];
215
+ if (seg.startsWith("[[...")) return true;
216
+ if (seg.startsWith("[...")) return have.length > i;
217
+ if (have[i] === void 0) return false;
218
+ if (!seg.startsWith("[") && seg !== have[i]) return false;
219
+ }
220
+ return want.length === have.length;
221
+ }
222
+ var moreSpecific = (a, b) => {
223
+ const x = segmentsOf(a).map(rank);
224
+ const y = segmentsOf(b).map(rank);
225
+ for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i];
226
+ return x.length > y.length;
227
+ };
228
+ var excludes = (entry, page) => entry.endsWith("/*") ? page === entry.slice(0, -2) || page.startsWith(entry.slice(0, -1)) : entry === page;
229
+ function judgeRoutes(pages, measuredRoutes, excluded = []) {
230
+ const problems = [];
231
+ excluded.forEach((e, i) => {
232
+ const at = `excludeRoutes[${i}]`;
233
+ const x = e;
234
+ if (typeof x?.route !== "string" || !x.route.startsWith("/")) problems.push(`${at}: names no route (start it with /)`);
235
+ else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.route}): no reason. Say why the gate cannot measure this page, or remove the entry.`);
236
+ else if (!pages.some((p) => excludes(x.route, p))) problems.push(`${at}: ${x.route} is not a page route of the app. Remove the entry.`);
237
+ });
238
+ const named = excluded.filter((e) => typeof e?.route === "string" && typeof e.reason === "string" && e.reason.trim());
239
+ const covered = /* @__PURE__ */ new Set();
240
+ for (const route of measuredRoutes.map(cleanRoute)) {
241
+ const hit = pages.filter((p) => routeMatches(p, route)).sort((a, b) => moreSpecific(a, b) ? -1 : moreSpecific(b, a) ? 1 : 0)[0];
242
+ if (hit) covered.add(hit);
243
+ }
244
+ for (const page of pages) if (named.some((e) => excludes(e.route, page))) covered.add(page);
245
+ return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
246
+ }
247
+
248
+ export {
249
+ parseBaseline,
250
+ EMPTY_BASELINE,
251
+ scanPhoneFirst,
252
+ checkExceptions,
253
+ judgeStatic,
254
+ judgeMeasured,
255
+ writeBaseline,
256
+ judgeGrowth,
257
+ hasUnexcusedEntries,
258
+ routesFromPages,
259
+ routesFromManifest,
260
+ judgeRoutes
261
+ };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-YOZLPNJS.js";
4
+ } from "./chunk-6DRBFDWT.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-KQF44RI3.js";
8
+ } from "./chunk-FNB3NP57.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-NQAYRK2K.js";
17
+ } from "./chunk-4WAIOZ4W.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -28,7 +28,7 @@ import {
28
28
  rhythmOf,
29
29
  scanSource,
30
30
  typicality
31
- } from "./chunk-JGWOA4LI.js";
31
+ } from "./chunk-DE2ZAV4G.js";
32
32
 
33
33
  // src/audit/cli.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -9,7 +9,7 @@ import {
9
9
  parseFile,
10
10
  sentencesOf,
11
11
  specificity
12
- } from "./chunk-7DQDF3VL.js";
12
+ } from "./chunk-ONA73THJ.js";
13
13
  import {
14
14
  SNAPSHOT_VERSION
15
15
  } from "./chunk-QYBAVU3P.js";
@@ -2428,6 +2428,10 @@ var HOUSE = {
2428
2428
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
2429
2429
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
2430
2430
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
2431
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
2432
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
2433
+ // New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
2434
+ "matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
2431
2435
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
2432
2436
  "stock-phrase": { tier: "review", label: "stock phrase" },
2433
2437
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3023,7 +3027,7 @@ var IMAGERY_TELLS = [
3023
3027
  ];
3024
3028
 
3025
3029
  // src/character/check.ts
3026
- var CATALOGUE_VERSION = "2026.10.2";
3030
+ var CATALOGUE_VERSION = "2026.10.4";
3027
3031
  var CATALOGUE = [
3028
3032
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3029
3033
  ...SPECIFICITY_COPY_TELLS,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-NQAYRK2K.js";
6
+ } from "./chunk-4WAIOZ4W.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -22,10 +22,10 @@ import {
22
22
  hueSwatch,
23
23
  nullPrompt,
24
24
  scanSource
25
- } from "./chunk-JGWOA4LI.js";
25
+ } from "./chunk-DE2ZAV4G.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-7DQDF3VL.js";
28
+ } from "./chunk-ONA73THJ.js";
29
29
 
30
30
  // src/null/cli.ts
31
31
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
@@ -120,7 +120,7 @@ async function build(flags, io) {
120
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
121
121
  if (toShoot.length) {
122
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
123
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
123
+ const { snapshotUrls } = await import("./audit-LQ2TDISV.js");
124
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
125
125
  results.forEach((r, i) => {
126
126
  const id = toShoot[i];
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
241
241
  const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
242
  if (toShoot.length) {
243
243
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
244
+ const { snapshotUrls } = await import("./audit-LQ2TDISV.js");
245
245
  const servers = /* @__PURE__ */ new Map();
246
246
  try {
247
247
  for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
@@ -10,10 +10,10 @@ import {
10
10
  levenshtein,
11
11
  normaliseFamily,
12
12
  parseColour
13
- } from "./chunk-JGWOA4LI.js";
13
+ } from "./chunk-DE2ZAV4G.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
- } from "./chunk-7DQDF3VL.js";
16
+ } from "./chunk-ONA73THJ.js";
17
17
 
18
18
  // src/direction/types.ts
19
19
  var DIRECTION_VERSION = 2;
@@ -645,6 +645,33 @@ var OUTCOME_PROMISES = [
645
645
  /\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
646
646
  new RegExp(`\\b(?:will|we['\u2019]ll)\\s+(?:(?:bring|drive|generate|get)\\s+you|bring|drive|generate)\\s+more\\s+(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i")
647
647
  ];
648
+ var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
649
+ var MATEY = [
650
+ // "Alright Carlo,", "Oi Dave", "Alright, mate"
651
+ /\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
652
+ // "mate" as a form of address, not "a mate who knows"
653
+ /,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
654
+ /\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
655
+ /\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
656
+ // "Right then," opening a sentence, not "the right then left turn"
657
+ new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
658
+ // "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
659
+ /\b(?:all|get(?:ting)?\s+(?:it|that|this|you|them|everything)|got\s+(?:it|that|you)|you['’]re)\s+sorted\b(?!\s+(?:by|in|into|alphabetically)\b)/i,
660
+ new RegExp(`${SENTENCE_START}Sorted[.!]`),
661
+ // "proper" as an intensifier before an adjective, not "proper planning"
662
+ /\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
663
+ /\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
664
+ /\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
665
+ /\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
666
+ /\bcheck\s+out\s+(?:our|my|this|these)\b/i
667
+ ];
668
+ var CLIPPED = [
669
+ /\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
670
+ /\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
671
+ new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
672
+ // "Alright Carlo, built you a proper site": the "I" left out
673
+ /(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
674
+ ];
648
675
  var NEGATION = [
649
676
  // "It's not just a haircut, it's an experience."
650
677
  /\b(?:it['’]?s|it is|this is|this isn['’]t|we['’]re|we are|they['’]re)\s+(?:not|more than)\s+(?:just|only|merely|simply)?\s*[^.!?\n]{1,60}?[,;.—-]\s*(?:it['’]?s|it is|this is|we['’]re|we are|they['’]re)\b/i,
@@ -881,6 +908,40 @@ var COPY_TELLS = [
881
908
  ]
882
909
  }
883
910
  },
911
+ {
912
+ id: "matey-register",
913
+ name: "Matey register",
914
+ generation: 2,
915
+ severity: "warn",
916
+ surface: "copy",
917
+ why: "'Alright Dave,', 'no worries, we'll get you sorted', 'Quick one:', 'have look at': a put-on, over-familiar British voice, with the small words clipped out. A model asked to sound British writes it; the UK is broad, and a client reads it as cringe and a little rude.",
918
+ fix: "Write plain, complete British English. 'Alright Dave, built you a site' becomes 'Hi Dave, I have built you a site'; 'No worries, we'll get you sorted' becomes 'We can fit you in on Thursday'.",
919
+ detect: phrases([...MATEY, ...CLIPPED], quoted),
920
+ fixtures: {
921
+ flag: [
922
+ // From the portal's seeded prospect follow-ups, 2026-10-19.
923
+ f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
924
+ f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
925
+ f("emails/followup.md", "Carlo, built the shop a proper site."),
926
+ f("content/contact.md", "No worries, we'll get you sorted."),
927
+ f("emails/quote.md", "Take look on these photos and give it go."),
928
+ f("emails/quote.md", "Quick one: is Thursday still good?"),
929
+ f("emails/quote.md", "Right then, here is the plan."),
930
+ f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
931
+ ],
932
+ pass: [
933
+ f("content/home.md", "Proper planning saves a second visit."),
934
+ f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
935
+ f("content/directions.md", "At the junction, take the right then left turn."),
936
+ // Chair and Blade's own approved line, and "mate" as a noun.
937
+ f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
938
+ f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
939
+ f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
940
+ f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
941
+ f("content/home.md", "Built to last, and made for older houses.")
942
+ ]
943
+ }
944
+ },
884
945
  {
885
946
  id: "hollow-imperative",
886
947
  name: "Hollow imperative",
@@ -1081,7 +1142,12 @@ var FALSE_RANGE = [
1081
1142
  ];
1082
1143
  var RESIDUE = [
1083
1144
  /\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
1084
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
1145
+ // At the start of a line, or after a sentence that ends in a full stop or
1146
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
1147
+ // is the handover request." slipped through when only a line start counted
1148
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
1149
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
1150
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
1085
1151
  /\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
1086
1152
  /\blet me know if you(?:['’]d| would) like me to\b/i,
1087
1153
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -1097,6 +1163,13 @@ var RESIDUE = [
1097
1163
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
1098
1164
  /【\d+(?:[:†][^】]{0,40})?】/
1099
1165
  ];
1166
+ var STOCK_EMAIL = [
1167
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
1168
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
1169
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
1170
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
1171
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
1172
+ ];
1100
1173
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
1101
1174
  function residueHits(ctx) {
1102
1175
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -1178,15 +1251,39 @@ var RESEARCH_TELLS = [
1178
1251
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
1179
1252
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
1180
1253
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
1181
- f("content/home.md", "Here's a more concise version for the hero.")
1254
+ f("content/home.md", "Here's a more concise version for the hero."),
1255
+ // The client handover email that a clean copy-check let through, 2026-10-19.
1256
+ f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions.")
1182
1257
  ],
1183
1258
  pass: [
1184
1259
  f("content/home.md", "Certainly the busiest week of the year: book early."),
1260
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
1185
1261
  f("content/home.md", "Here's the price list for 2026."),
1186
1262
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
1187
1263
  ]
1188
1264
  }
1189
1265
  },
1266
+ {
1267
+ id: "stock-email-line",
1268
+ name: "Stock email line",
1269
+ generation: 1,
1270
+ severity: "warn",
1271
+ surface: "copy",
1272
+ why: "'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time.",
1273
+ fix: "Cut the opener and start with the reason for writing. Replace the closer with the next step and who to ask: 'Ring me on 07700 900123 if the date does not work.'",
1274
+ detect: phrases(STOCK_EMAIL, quoted),
1275
+ fixtures: {
1276
+ flag: [
1277
+ f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions."),
1278
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
1279
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
1280
+ ],
1281
+ pass: [
1282
+ f("emails/handover.md", "Hi Sam,\n\nThe handover pack is attached: certificates, the board schedule and the warranty. Ring me on 07700 900123 if anything is missing."),
1283
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
1284
+ ]
1285
+ }
1286
+ },
1190
1287
  {
1191
1288
  id: "prompt-context",
1192
1289
  name: "Prompt context",
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-S3L7TEJF.js";
7
+ } from "./chunk-G47ZCBD5.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-KQF44RI3.js";
10
+ } from "./chunk-FNB3NP57.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-NQAYRK2K.js";
13
+ } from "./chunk-4WAIOZ4W.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -30,7 +30,7 @@ import {
30
30
  rhythmOf,
31
31
  shapeName,
32
32
  temperatureName
33
- } from "./chunk-JGWOA4LI.js";
33
+ } from "./chunk-DE2ZAV4G.js";
34
34
 
35
35
  // src/estate/cli.ts
36
36
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -133,7 +133,7 @@ async function snapshotOf(target, cwd) {
133
133
  return snap;
134
134
  }
135
135
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
136
- const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
136
+ const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
137
137
  return snapshotUrl(target);
138
138
  }
139
139
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -4,15 +4,15 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-GBDPNNC2.js";
8
- import "./chunk-S3L7TEJF.js";
9
- import "./chunk-KQF44RI3.js";
7
+ } from "./chunk-STWH5EYU.js";
8
+ import "./chunk-G47ZCBD5.js";
9
+ import "./chunk-FNB3NP57.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-NQAYRK2K.js";
11
+ import "./chunk-4WAIOZ4W.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-JGWOA4LI.js";
15
- import "./chunk-7DQDF3VL.js";
14
+ import "./chunk-DE2ZAV4G.js";
15
+ import "./chunk-ONA73THJ.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
18
18
  formatComponentPairs,
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-KQF44RI3.js";
7
+ } from "./chunk-FNB3NP57.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-NQAYRK2K.js";
11
+ } from "./chunk-4WAIOZ4W.js";
12
12
  import {
13
13
  toJson
14
14
  } from "./chunk-QIEDBL4J.js";
@@ -21,8 +21,8 @@ import {
21
21
  fingerprint,
22
22
  scanSource,
23
23
  typicality
24
- } from "./chunk-JGWOA4LI.js";
25
- import "./chunk-7DQDF3VL.js";
24
+ } from "./chunk-DE2ZAV4G.js";
25
+ import "./chunk-ONA73THJ.js";
26
26
  import {
27
27
  SNAPSHOT_VERSION
28
28
  } from "./chunk-QYBAVU3P.js";
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
188
188
  if (toTake.length) {
189
189
  io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
190
190
  try {
191
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
191
+ const { snapshotUrls } = await import("./audit-LQ2TDISV.js");
192
192
  const results = await snapshotUrls(toTake.map((t) => t.url));
193
193
  results.forEach((r, i) => {
194
194
  const { entry, keep } = toTake[i];
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-7DQDF3VL.js";
12
+ } from "./chunk-ONA73THJ.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";