@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bcbdbb8: `craft phone-first` now enforces the portal standard's phone lessons itself (DOM-1053, from DOM-1044's `phone-faults.md`). **This tightens the gate: a portal that upgrades can fail where it passed before.** Four checks:
8
+
9
+ - **The baseline only shrinks.** The run compares `phone-first.baseline.json` with the base branch's copy (`origin/main`, the pull request's target branch, or `--base <ref>`) and fails on an entry main does not have, or a higher count. An entry with a non-empty `reason` is a named exception and may be added or raised; the new optional `reason` field is kept by `--update-baseline`, and old baselines read unchanged. The runner must see the base branch (`fetch-depth: 0`); if it cannot, the gate fails rather than skips.
10
+ - **Every route is measured.** With `--measured`, every Next `app/**/page.tsx` route (from `appDir`, or a `source` folder called `app`, or `routesManifest`) must be among the measured routes, or in `excludeRoutes` with a reason. Add the routes of a redirect-only page to `excludeRoutes`.
11
+ - **Field floor 48px** (tightening): a text field, select or textarea under 48px tall fails the `field` check, to match portal standard rule 5. The gate failed only under 44px before. Set `floors: { fieldHeightPx: 44 }` to keep the old floor; the default is 48. `measurePhone` now takes `floors` too.
12
+ - **Tailwind 3 `var()` opacity:** on an app whose `tailwindcss` is v3, `/NN` opacity on an arbitrary `var()` colour (`bg-[var(--bg-1)]/95`) fails the source scan, because Tailwind 3 emits no CSS for it. The `tailwind` key in `phone-first.json` overrides the detected version.
13
+
14
+ A portal adopting this version fixes its new findings where they are (a field gets `min-h-12`), or records a named exception with a reason; it cannot grow the baseline to make the upgrade pass.
15
+
16
+ ### Patch Changes
17
+
18
+ - 140142f: New review-tier copy tell `matey-register` (catalogue 2026.10.4) reports the put-on, over-familiar British voice that COPY.md's "Register: British and professional" section bans: "Alright Dave,", "Oi Carlo", "mate" as a form of address, "no worries", "not fussed", "we'll get you sorted", "proper" as an intensifier, "chuffed", "give me a shout", "when you get a sec", "Right then,"; and clipped speech that drops the small words: "take look on these", "give it go", "Quick one:", "built you a site". Ordinary uses pass: "proper planning", "a sorted list", "the right then left turn", "a mate who knows", "give us a ring". It prints for review and never blocks, following the house rule that a new tell starts in review.
19
+
3
20
  ## 0.20.5
4
21
 
5
22
  ### Patch Changes
package/CHARACTER.md CHANGED
@@ -1341,7 +1341,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
1341
1341
  after changing an entry; a test fails until you do.
1342
1342
 
1343
1343
  <!-- craft:catalogue:start -->
1344
- Catalogue version `2026.10.3`, 71 tells.
1344
+ Catalogue version `2026.10.4`, 72 tells.
1345
1345
 
1346
1346
  | Id | Gen | Surface | Severity | Tell | Why it is a default |
1347
1347
  | --- | --- | --- | --- | --- | --- |
@@ -1378,6 +1378,7 @@ Catalogue version `2026.10.3`, 71 tells.
1378
1378
  | `review-phrase` | 1 | copy | warn | Review-tier phrase | Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking. |
1379
1379
  | `sales-tagline` | 1 | copy | warn | Sales tagline | 'So your budget goes where it counts', 'Real results', 'pages that convert': advert lines that sound like a pitch and say nothing a reader can check. |
1380
1380
  | `outcome-promise` | 1 | copy | warn | Outcome promise | 'We'll turn that into enquiries', 'once the first areas are working', 'competitors you can outdo': a hoped-for result written as if it were delivered. Rankings and enquiries depend on things nobody controls. |
1381
+ | `matey-register` | 2 | copy | warn | Matey register | '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. |
1381
1382
  | `hollow-imperative` | 1 | copy | warn | Hollow imperative | 'Discover', 'Experience', 'Transform your' as the opening verb of a heading or button is a call to action with nothing in it. |
1382
1383
  | `rhetorical-opener` | 1 | copy | warn | Rhetorical question opener | 'Looking for...?', 'Tired of...?', 'Ready to...?' opens with the model guessing at the reader instead of telling them something. |
1383
1384
  | `em-dash` | 2 | copy | warn | Em dash | Generated copy leans on the em dash to join clauses. House copy does not use it. |
package/COPY.md CHANGED
@@ -89,6 +89,7 @@ person reading decides which instance earns its place.
89
89
  | --- | --- |
90
90
  | `review-phrase` | truly, genuinely, deeply, ultimately, clear and clearly ("a clear next step", "clear pricing": say what the reader actually gets), great question, absolutely, one-size-fits-all, and superlative over-claiming: "the single most", "by far the most", "hands down the", "arguably the best", "bar none" |
91
91
  | `sales-tagline` | advert lines that say nothing a reader can check: "so your budget goes where it counts", "real results", "results that matter", "work harder for you", "pages that convert" |
92
+ | `matey-register` | the put-on British voice: "Alright Dave,", "cheers mate", "no worries", "we'll get you sorted", "proper good", "give me a shout", "chuffed", "Right then,"; and clipped speech that drops the small words: "have look at", "give it go", "Quick one:", "built you a site". Write plain, complete British English |
92
93
  | `outcome-promise` | a hoped-for result written as delivered: "we'll turn that into enquiries", "once the first areas are working", "competitors you can outdo", "exactly what convinces", "guaranteed rankings", "you'll get more". Promise the work; measure and report the results |
93
94
  | `ai-vocabulary` | tapestry, testament, realm, elevate, nestled, meticulous, unparalleled, moreover; and underscore, showcasing, pivotal, measured 10 to 14 times over-used in post-2022 text |
94
95
  | `stock-email-line` | the form-email opener and closer: "I hope this email finds you well", "hope you're having a great week", "let me know if you have any questions", "don't hesitate to contact us". Start with the reason for writing; end with the next step and who to ask |
@@ -195,8 +196,11 @@ writer's head, and it comes out anyway. Check against the lists afterwards.
195
196
  The tool cannot grade register; a person does. British spelling and phrasing
196
197
  (colour, organise, licence as a noun, centre, towards, enquiries). Professional
197
198
  and confident, never matey: no over-familiar asides ("with me nowhere near
198
- it"), no American idiom ("shallow wins", "looks busy"), no "honestly" as an
199
- intensifier. Do not change a brand name, domain or legal entity wording.
199
+ it"), no put-on British voice with the small words clipped out ("Alright
200
+ Dave,", "no worries", "have look at"; `matey-register` reports the common
201
+ forms for review), no American idiom ("shallow wins", "looks busy"), no
202
+ "honestly" as an intensifier. Do not change a brand name, domain or legal
203
+ entity wording.
200
204
 
201
205
  For a register other than the house one (plain, persuasive, warm, literary),
202
206
  see `REGISTERS.md`: a brief to read before drafting, and a check that places
package/README.md CHANGED
@@ -266,19 +266,89 @@ and the source folders `phone-first.json` names, against the repo's baseline:
266
266
  - **Static:** a `max-width` media query (any spelling, range syntax too) or a
267
267
  Tailwind `max-*:` variant fails. A genuine exception takes
268
268
  `mobile-first-ok: <reason>` on its line or the line above, or the whole file
269
- in `phone-first.json` `exceptions` as `{ "file", "reason" }`.
269
+ in `phone-first.json` `exceptions` as `{ "file", "reason" }`. On a Tailwind 3
270
+ app, `/NN` opacity on an arbitrary `var()` colour also fails (check 4 below).
270
271
  - **Measured at 375px:** the page scrolls sideways (against the layout
271
272
  viewport: under mobile emulation `innerWidth` grows with the page, so it
272
273
  never shows the scroll), a page with no `width=device-width` viewport meta,
273
274
  a target under 44 x 44 (a link inside a sentence is exempt; a field's label
274
- counts), a text field under 16px, text under 12px, a cut-off `h1`.
275
+ counts), a text field under 16px or under 48px tall, text under 12px, a
276
+ cut-off `h1`.
275
277
 
276
278
  `phone-first.baseline.json` lists what a repo already breaks, counted per file
277
279
  and per route (digits folded, so fixture dates do not split one button into
278
280
  many). The gate fails on anything the baseline does not list, and on a
279
281
  baseline that claims more than is there, so it only shrinks: fix a breach and
280
282
  lower its count in the same PR. `--update-baseline` writes the baseline that
281
- makes the current run pass; run it once when the gate goes in.
283
+ makes the current run pass; run it once when the gate goes in, and after that
284
+ only to lower counts. It keeps every entry's `reason`.
285
+
286
+ ### Four checks that hold the portal standard's phone lessons
287
+
288
+ The causes behind the October 2026 phone fixes are in the portal standard
289
+ (`references/phone-faults.md`, DOM-1044). These four make the gate itself
290
+ refuse them (DOM-1053).
291
+
292
+ 1. **The baseline only shrinks.** The run compares the baseline with the base
293
+ branch's copy and fails on an entry main does not have, or on a higher
294
+ count. Fix the breach where it is. The one way in is a named exception: an
295
+ entry with a non-empty `reason` (third-party UI that cannot be restyled, a
296
+ destructive-only form). Old baselines, with no `reason`, read as before.
297
+ A branch that left main before main shrank its baseline looks like it added
298
+ entries: merge main.
299
+
300
+ ```json
301
+ { "route": "/admin/content", "check": "text", "element": "<p> \"Sign in\"", "count": 14,
302
+ "reason": "Sanity Studio's sign-in dialog, third-party UI" }
303
+ ```
304
+
305
+ The base is `origin/main`, or the pull request's target branch when
306
+ `GITHUB_BASE_REF` is set, or `--base <ref>`. The runner has to be able to see
307
+ it: with `actions/checkout`, use `fetch-depth: 0` or run
308
+ `git fetch origin main` first. If the base cannot be seen the gate fails and
309
+ says so; it never skips. It passes without looking when every entry carries a
310
+ `reason`, when the base has no baseline yet (the pull request that brings the
311
+ gate in), and outside a git checkout (it prints a note).
312
+
313
+ 2. **Every route is measured.** With `--measured`, the measured routes are the
314
+ gate's route list. Every page route of the app must be in it. The pages come
315
+ from the Next app folders (`appDir`; left out, any `source` folder called
316
+ `app`) and from `routesManifest`, a built app's
317
+ `app-path-routes-manifest.json`. Route groups, parallel slots, private
318
+ folders and intercepting routes are not pages of their own. A measured route
319
+ covers the page it opens: `/portal/projects/p1?theme=dark` covers
320
+ `/portal/projects/[id]`, and so does the literal label
321
+ `/portal/projects/[id]`; a static sibling such as `/portal/projects/new`
322
+ does not. A page the gate cannot measure (one that only redirects) goes in
323
+ `excludeRoutes`, the only way out, with a reason; `"/admin/*"` takes a prefix
324
+ and everything under it. An entry with no reason, or naming no page, fails.
325
+ When no app folder with pages is found, the run prints that route coverage
326
+ was not checked.
327
+
328
+ ```json
329
+ { "source": ["app", "components"],
330
+ "excludeRoutes": [{ "route": "/portal/projects", "reason": "redirects to /portal; measured there" }] }
331
+ ```
332
+
333
+ 3. **Fields are 48px tall.** A text field, select or textarea under 48px fails
334
+ the `field` check ("44px tall, under 48px (use min-h-12)"), matching portal
335
+ standard rule 5. Until now the gate only failed fields under 44px, as
336
+ targets. Configurable: `fieldHeightPx` in `floors`, for `measurePhone`,
337
+ `measureViewports`, `measureSite` and `viewports.json`; the default is 48.
338
+
339
+ ```ts
340
+ await measureViewports(page, { route, floors: { fieldHeightPx: 44 } }); // the old gate
341
+ ```
342
+
343
+ 4. **Tailwind 3 `var()` opacity.** Tailwind 3 cannot apply `/NN` opacity to a
344
+ `var()` colour and emits no CSS, so `bg-[var(--bg-1)]/95` leaves a fixed bar
345
+ transparent. On a Tailwind 3 app the source scan fails on
346
+ `var\([^)]*\)\]/[0-9]+`. The version is read per `source` folder from the
347
+ installed `node_modules/tailwindcss`, else the declared range in the nearest
348
+ `package.json`; set `"tailwind": 3` or `4` in `phone-first.json` when that
349
+ cannot find it. Tailwind 4 supports the syntax and is not scanned. It is a
350
+ bug rather than a style, so a `mobile-first-ok` comment does not excuse it;
351
+ a file-level `exceptions` entry still does.
282
352
 
283
353
  ## Every viewport
284
354
 
@@ -9,10 +9,10 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-YFS32BZ2.js";
12
+ } from "./chunk-6DRBFDWT.js";
13
13
  import "./chunk-JYNXGCYZ.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
  PROVENANCE_BUDGET_MS,
package/dist/audit.cjs CHANGED
@@ -257,6 +257,33 @@ var OUTCOME_PROMISES = [
257
257
  /\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
258
258
  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")
259
259
  ];
260
+ var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
261
+ var MATEY = [
262
+ // "Alright Carlo,", "Oi Dave", "Alright, mate"
263
+ /\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
264
+ // "mate" as a form of address, not "a mate who knows"
265
+ /,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
266
+ /\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
267
+ /\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
268
+ // "Right then," opening a sentence, not "the right then left turn"
269
+ new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
270
+ // "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
271
+ /\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,
272
+ new RegExp(`${SENTENCE_START}Sorted[.!]`),
273
+ // "proper" as an intensifier before an adjective, not "proper planning"
274
+ /\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
275
+ /\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
276
+ /\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
277
+ /\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
278
+ /\bcheck\s+out\s+(?:our|my|this|these)\b/i
279
+ ];
280
+ var CLIPPED = [
281
+ /\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
282
+ /\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
283
+ new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
284
+ // "Alright Carlo, built you a proper site": the "I" left out
285
+ /(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
286
+ ];
260
287
  var NEGATION = [
261
288
  // "It's not just a haircut, it's an experience."
262
289
  /\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,
@@ -493,6 +520,40 @@ var COPY_TELLS = [
493
520
  ]
494
521
  }
495
522
  },
523
+ {
524
+ id: "matey-register",
525
+ name: "Matey register",
526
+ generation: 2,
527
+ severity: "warn",
528
+ surface: "copy",
529
+ 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.",
530
+ 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'.",
531
+ detect: phrases([...MATEY, ...CLIPPED], quoted),
532
+ fixtures: {
533
+ flag: [
534
+ // From the portal's seeded prospect follow-ups, 2026-10-19.
535
+ f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
536
+ f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
537
+ f("emails/followup.md", "Carlo, built the shop a proper site."),
538
+ f("content/contact.md", "No worries, we'll get you sorted."),
539
+ f("emails/quote.md", "Take look on these photos and give it go."),
540
+ f("emails/quote.md", "Quick one: is Thursday still good?"),
541
+ f("emails/quote.md", "Right then, here is the plan."),
542
+ f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
543
+ ],
544
+ pass: [
545
+ f("content/home.md", "Proper planning saves a second visit."),
546
+ f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
547
+ f("content/directions.md", "At the junction, take the right then left turn."),
548
+ // Chair and Blade's own approved line, and "mate" as a noun.
549
+ f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
550
+ f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
551
+ f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
552
+ f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
553
+ f("content/home.md", "Built to last, and made for older houses.")
554
+ ]
555
+ }
556
+ },
496
557
  {
497
558
  id: "hollow-imperative",
498
559
  name: "Hollow imperative",
@@ -3109,6 +3170,8 @@ var HOUSE = {
3109
3170
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
3110
3171
  // New 2026-10-19: review first, like every new tell, until estate hits are read.
3111
3172
  "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
3173
+ // New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
3174
+ "matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
3112
3175
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
3113
3176
  "stock-phrase": { tier: "review", label: "stock phrase" },
3114
3177
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3850,7 +3913,7 @@ var IMAGERY_TELLS = [
3850
3913
  ];
3851
3914
 
3852
3915
  // src/character/check.ts
3853
- var CATALOGUE_VERSION = "2026.10.3";
3916
+ var CATALOGUE_VERSION = "2026.10.4";
3854
3917
  var CATALOGUE = [
3855
3918
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3856
3919
  ...SPECIFICITY_COPY_TELLS,
package/dist/audit.js CHANGED
@@ -222,6 +222,33 @@ var OUTCOME_PROMISES = [
222
222
  /\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
223
223
  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")
224
224
  ];
225
+ var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
226
+ var MATEY = [
227
+ // "Alright Carlo,", "Oi Dave", "Alright, mate"
228
+ /\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
229
+ // "mate" as a form of address, not "a mate who knows"
230
+ /,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
231
+ /\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
232
+ /\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
233
+ // "Right then," opening a sentence, not "the right then left turn"
234
+ new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
235
+ // "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
236
+ /\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,
237
+ new RegExp(`${SENTENCE_START}Sorted[.!]`),
238
+ // "proper" as an intensifier before an adjective, not "proper planning"
239
+ /\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
240
+ /\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
241
+ /\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
242
+ /\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
243
+ /\bcheck\s+out\s+(?:our|my|this|these)\b/i
244
+ ];
245
+ var CLIPPED = [
246
+ /\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
247
+ /\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
248
+ new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
249
+ // "Alright Carlo, built you a proper site": the "I" left out
250
+ /(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
251
+ ];
225
252
  var NEGATION = [
226
253
  // "It's not just a haircut, it's an experience."
227
254
  /\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,
@@ -458,6 +485,40 @@ var COPY_TELLS = [
458
485
  ]
459
486
  }
460
487
  },
488
+ {
489
+ id: "matey-register",
490
+ name: "Matey register",
491
+ generation: 2,
492
+ severity: "warn",
493
+ surface: "copy",
494
+ 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.",
495
+ 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'.",
496
+ detect: phrases([...MATEY, ...CLIPPED], quoted),
497
+ fixtures: {
498
+ flag: [
499
+ // From the portal's seeded prospect follow-ups, 2026-10-19.
500
+ f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
501
+ f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
502
+ f("emails/followup.md", "Carlo, built the shop a proper site."),
503
+ f("content/contact.md", "No worries, we'll get you sorted."),
504
+ f("emails/quote.md", "Take look on these photos and give it go."),
505
+ f("emails/quote.md", "Quick one: is Thursday still good?"),
506
+ f("emails/quote.md", "Right then, here is the plan."),
507
+ f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
508
+ ],
509
+ pass: [
510
+ f("content/home.md", "Proper planning saves a second visit."),
511
+ f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
512
+ f("content/directions.md", "At the junction, take the right then left turn."),
513
+ // Chair and Blade's own approved line, and "mate" as a noun.
514
+ f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
515
+ f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
516
+ f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
517
+ f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
518
+ f("content/home.md", "Built to last, and made for older houses.")
519
+ ]
520
+ }
521
+ },
461
522
  {
462
523
  id: "hollow-imperative",
463
524
  name: "Hollow imperative",
@@ -3074,6 +3135,8 @@ var HOUSE = {
3074
3135
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
3075
3136
  // New 2026-10-19: review first, like every new tell, until estate hits are read.
3076
3137
  "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
3138
+ // New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
3139
+ "matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
3077
3140
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
3078
3141
  "stock-phrase": { tier: "review", label: "stock phrase" },
3079
3142
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3815,7 +3878,7 @@ var IMAGERY_TELLS = [
3815
3878
  ];
3816
3879
 
3817
3880
  // src/character/check.ts
3818
- var CATALOGUE_VERSION = "2026.10.3";
3881
+ var CATALOGUE_VERSION = "2026.10.4";
3819
3882
  var CATALOGUE = [
3820
3883
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3821
3884
  ...SPECIFICITY_COPY_TELLS,
@@ -10,7 +10,7 @@ import {
10
10
  holdHouseBlocks,
11
11
  houseRule,
12
12
  scanSource
13
- } from "./chunk-PFVSKIPM.js";
13
+ } from "./chunk-DE2ZAV4G.js";
14
14
  import {
15
15
  compareCompetitor,
16
16
  compareFacts,
@@ -21,7 +21,7 @@ import {
21
21
  pageCopy,
22
22
  protectedFacts,
23
23
  visibleText
24
- } from "./chunk-FBWBDBW4.js";
24
+ } from "./chunk-ONA73THJ.js";
25
25
 
26
26
  // src/character/config.ts
27
27
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -622,7 +622,7 @@ async function pageSnapshot(target, cwd) {
622
622
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
623
623
  }
624
624
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
625
- const { snapshotUrl } = await import("./audit-5I2WWTL6.js");
625
+ const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
626
626
  return snapshotUrl(target);
627
627
  }
628
628
  async function briefFor(flags, cwd) {
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
633
633
  if (typeof brief === "string" && brief.trim()) return brief;
634
634
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
635
635
  if (flags.null) {
636
- const { loadNull } = await import("./cli-V4QXTBWR.js");
636
+ const { loadNull } = await import("./cli-PBCU7Z64.js");
637
637
  return loadNull(flags.null, cwd).brief;
638
638
  }
639
639
  return void 0;
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
659
659
  function run(argv, io) {
660
660
  const [command, ...rest] = argv;
661
661
  if (command === "direction") {
662
- return import("./cli-JQYZXFRH.js").then((m) => m.runDirection(rest, io));
662
+ return import("./cli-RHDNHL4U.js").then((m) => m.runDirection(rest, io));
663
663
  }
664
664
  if (command === "report" || command === "retrofit") {
665
- return import("./cli-W6FNSHRN.js").then((m) => m.runReport(command, rest, io));
665
+ return import("./cli-YSGPGQ7R.js").then((m) => m.runReport(command, rest, io));
666
666
  }
667
667
  if (command === "register") {
668
- return import("./cli-NIH3WTNE.js").then((m) => m.runRegister(rest, io));
668
+ return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
669
669
  }
670
670
  if (command === "estate") {
671
- return import("./cli-SQC27VYC.js").then((m) => m.runEstate(rest, io));
671
+ return import("./cli-5QI6NIZN.js").then((m) => m.runEstate(rest, io));
672
672
  }
673
673
  if (command === "null") {
674
- return import("./cli-V4QXTBWR.js").then((m) => m.runNull(rest, io));
674
+ return import("./cli-PBCU7Z64.js").then((m) => m.runNull(rest, io));
675
675
  }
676
676
  if (command === "calibrate") {
677
- return import("./cli-BI6XCMCL.js").then((m) => m.runCalibrate(rest, io));
677
+ return import("./cli-5VHX5EJF.js").then((m) => m.runCalibrate(rest, io));
678
678
  }
679
679
  if (command === "tells" && rest[0] === "harvest") {
680
- return import("./cli-V4QXTBWR.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
680
+ return import("./cli-PBCU7Z64.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
681
681
  io.err(`craft: ${error.message}`);
682
682
  return 2;
683
683
  });
684
684
  }
685
685
  if (command === "phone-first") {
686
- return import("./cli-56H5IPWT.js").then((m) => m.runPhoneFirst(rest, io));
686
+ return import("./cli-EYHRJJAX.js").then((m) => m.runPhoneFirst(rest, io));
687
687
  }
688
688
  if (command === "viewports") {
689
- return import("./viewports-cli-C4DWICYI.js").then((m) => m.runViewports(rest, io));
689
+ return import("./viewports-cli-XYYX4SKV.js").then((m) => m.runViewports(rest, io));
690
690
  }
691
691
  if (command === "snapshot" || command === "audit") {
692
- return import("./cli-OH6427MC.js").then((m) => m.runAudit(command, rest, io));
692
+ return import("./cli-XAWJWK7I.js").then((m) => m.runAudit(command, rest, io));
693
693
  }
694
694
  if (!command || command === "help" || command === "--help" || command === "-h") {
695
695
  io.out(HELP);
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-PFVSKIPM.js";
7
+ } from "./chunk-DE2ZAV4G.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";