@domandigital/craft 0.20.5 → 0.22.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 +2 -1
  3. package/COPY.md +6 -2
  4. package/README.md +87 -4
  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-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
  9. package/dist/{chunk-BBJOLR3R.js → chunk-6PZG4MJS.js} +5 -5
  10. package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
  11. package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
  12. package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
  13. package/dist/{chunk-TSO5W32N.js → chunk-OVWVKMPS.js} +14 -14
  14. package/dist/{chunk-N32UUN3S.js → chunk-RBQCGDQQ.js} +5 -5
  15. package/dist/chunk-XSKLVEI6.js +368 -0
  16. package/dist/{chunk-DMAMGFV7.js → chunk-ZS7PQNJU.js} +4 -4
  17. package/dist/{cli-OH6427MC.js → cli-7DL4BKA7.js} +6 -6
  18. package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
  19. package/dist/{cli-BI6XCMCL.js → cli-AM4FLXGM.js} +5 -5
  20. package/dist/{cli-JQYZXFRH.js → cli-R2HBESHI.js} +5 -5
  21. package/dist/{cli-V4QXTBWR.js → cli-SZ4SZLUN.js} +4 -4
  22. package/dist/{cli-W6FNSHRN.js → cli-T7A3BU5J.js} +9 -9
  23. package/dist/{cli-SQC27VYC.js → cli-WZA5IXSR.js} +6 -6
  24. package/dist/cli-YNIJOEJK.js +244 -0
  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 +194 -18
  31. package/dist/phone.d.cts +113 -13
  32. package/dist/phone.d.ts +113 -13
  33. package/dist/phone.js +182 -18
  34. package/dist/{viewports-cli-C4DWICYI.js → viewports-cli-F7KJIVZL.js} +51 -86
  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,28 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3902169: The every-viewport measure counts phone screens to the footer (DOM-1052, part of DOM-772): page height at 375 x 667 over 667, with lazy images loaded. `screens` sets a length budget per route, route pattern or prefix (`{ "/": 6.5, "/services/*": 6, "/areas/[town]": 5, "*": 9 }`, the most specific wins), on `measureViewports`, `measureRoute`, `measureSite` and `viewports.json`. A route over its budget fails a new `screens` check ("page too long for a phone"), baselined like the others; with no budget the count is only reported. `ViewportsResult.screens` and the measured file carry the count, `craft viewports` and `craft phone-first --measured` print it for every route, and `screenBudgetFor`, `checkScreenBudgets` and `formatScreens` are exported. The Bellerose home page ran 12.7 phone screens against a target of 6.5.
8
+
9
+ ## 0.21.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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:
14
+
15
+ - **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.
16
+ - **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`.
17
+ - **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.
18
+ - **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.
19
+
20
+ 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.
21
+
22
+ ### Patch Changes
23
+
24
+ - 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.
25
+
3
26
  ## 0.20.5
4
27
 
5
28
  ### 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
 
@@ -301,6 +371,13 @@ the one loaded page in place:
301
371
  `tel:` link.
302
372
  - **On a landscape phone (844 x 390):** header and fixed layers leave at least
303
373
  half the height.
374
+ - **Page length (phone screens):** page height at 375 x 667 over 667, to the
375
+ footer (lazy images loaded). A phone page has a length budget (the Bellerose
376
+ home page ran 12.7 screens against a target of 6.5). Set one per route or
377
+ route pattern with `screens`; a route over its budget fails the `screens`
378
+ check, and with no budget the count is only reported. Keys are a route
379
+ (`/`), a pattern (`/areas/[town]`), a prefix (`/services/*`, the route and
380
+ everything under it) or `*`; the most specific wins.
304
381
  - **At 1440 and 1920:** body lines are at most 80 characters (width over the
305
382
  `ch` of the element's own font) and no image is stretched past its file.
306
383
 
@@ -314,10 +391,16 @@ test.use(PHONE);
314
391
  test(`every viewport: ${route}`, async ({ page }) => {
315
392
  await page.goto(route);
316
393
  // Resizes this page in place; writes phone-first/measured/<route>.json.
317
- await measureViewports(page, { route, mainAction: ".hero a" });
394
+ await measureViewports(page, { route, mainAction: ".hero a", screens: { "/": 6.5, "/services/*": 6 } });
318
395
  });
319
396
  ```
320
397
 
398
+ `result.screens` holds the count, and it is written to the measured file;
399
+ `craft viewports` and `craft phone-first --measured` print it for every route
400
+ (`phone screens to the footer: / 5.8 of 6.5`). In `viewports.json` the budget is
401
+ `"screens": { "/": 6.5, "/services/*": 6 }`; in create-site's
402
+ `site.programme.json` it is `measure.screens`.
403
+
321
404
  `measureViewports` works on the page the suite opened and signed in, so a
322
405
  portal keeps its fixtures. `measureRoute(open, { route, mainAction })` does
323
406
  both halves for such a page: `open("phone")` and `open("wide")` return the
@@ -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,
@@ -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";
@@ -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-OVWVKMPS.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));
@@ -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,
@@ -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",