@domandigital/craft 0.20.5 → 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 +17 -0
  2. package/CHARACTER.md +2 -1
  3. package/COPY.md +6 -2
  4. package/README.md +73 -3
  5. package/dist/{audit-5I2WWTL6.js → audit-LQ2TDISV.js} +3 -3
  6. package/dist/audit.cjs +64 -1
  7. package/dist/audit.js +64 -1
  8. package/dist/{chunk-TSO5W32N.js → chunk-4WAIOZ4W.js} +14 -14
  9. package/dist/{chunk-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
  10. package/dist/chunk-BQ7H23QP.js +261 -0
  11. package/dist/{chunk-DMAMGFV7.js → chunk-BYXDX66D.js} +4 -4
  12. package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
  13. package/dist/{chunk-BBJOLR3R.js → chunk-FNB3NP57.js} +5 -5
  14. package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
  15. package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
  16. package/dist/{chunk-N32UUN3S.js → chunk-STWH5EYU.js} +5 -5
  17. package/dist/{cli-SQC27VYC.js → cli-5QI6NIZN.js} +6 -6
  18. package/dist/{cli-BI6XCMCL.js → cli-5VHX5EJF.js} +5 -5
  19. package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
  20. package/dist/cli-EYHRJJAX.js +241 -0
  21. package/dist/{cli-V4QXTBWR.js → cli-PBCU7Z64.js} +4 -4
  22. package/dist/{cli-JQYZXFRH.js → cli-RHDNHL4U.js} +5 -5
  23. package/dist/{cli-OH6427MC.js → cli-XAWJWK7I.js} +6 -6
  24. package/dist/{cli-W6FNSHRN.js → cli-YSGPGQ7R.js} +9 -9
  25. package/dist/cli.js +3 -3
  26. package/dist/index.cjs +64 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +64 -1
  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-C4DWICYI.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-56H5IPWT.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-YFS32BZ2.js";
4
+ } from "./chunk-6DRBFDWT.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-BBJOLR3R.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-TSO5W32N.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-PFVSKIPM.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-FBWBDBW4.js";
12
+ } from "./chunk-ONA73THJ.js";
13
13
  import {
14
14
  SNAPSHOT_VERSION
15
15
  } from "./chunk-QYBAVU3P.js";
@@ -2430,6 +2430,8 @@ var HOUSE = {
2430
2430
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
2431
2431
  // New 2026-10-19: review first, like every new tell, until estate hits are read.
2432
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" },
2433
2435
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
2434
2436
  "stock-phrase": { tier: "review", label: "stock phrase" },
2435
2437
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3025,7 +3027,7 @@ var IMAGERY_TELLS = [
3025
3027
  ];
3026
3028
 
3027
3029
  // src/character/check.ts
3028
- var CATALOGUE_VERSION = "2026.10.3";
3030
+ var CATALOGUE_VERSION = "2026.10.4";
3029
3031
  var CATALOGUE = [
3030
3032
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3031
3033
  ...SPECIFICITY_COPY_TELLS,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-TSO5W32N.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-PFVSKIPM.js";
25
+ } from "./chunk-DE2ZAV4G.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-FBWBDBW4.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-5I2WWTL6.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-5I2WWTL6.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-PFVSKIPM.js";
13
+ } from "./chunk-DE2ZAV4G.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
- } from "./chunk-FBWBDBW4.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",
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-GJY2VMPD.js";
7
+ } from "./chunk-G47ZCBD5.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-BBJOLR3R.js";
10
+ } from "./chunk-FNB3NP57.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-TSO5W32N.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-PFVSKIPM.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-5I2WWTL6.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-N32UUN3S.js";
8
- import "./chunk-GJY2VMPD.js";
9
- import "./chunk-BBJOLR3R.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-TSO5W32N.js";
11
+ import "./chunk-4WAIOZ4W.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-PFVSKIPM.js";
15
- import "./chunk-FBWBDBW4.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-BBJOLR3R.js";
7
+ } from "./chunk-FNB3NP57.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-TSO5W32N.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-PFVSKIPM.js";
25
- import "./chunk-FBWBDBW4.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-5I2WWTL6.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-FBWBDBW4.js";
12
+ } from "./chunk-ONA73THJ.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";