@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.
- package/CHANGELOG.md +23 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +6 -2
- package/README.md +87 -4
- package/dist/{audit-5I2WWTL6.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +64 -1
- package/dist/audit.js +64 -1
- package/dist/{chunk-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/{chunk-BBJOLR3R.js → chunk-6PZG4MJS.js} +5 -5
- package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
- package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
- package/dist/{chunk-TSO5W32N.js → chunk-OVWVKMPS.js} +14 -14
- package/dist/{chunk-N32UUN3S.js → chunk-RBQCGDQQ.js} +5 -5
- package/dist/chunk-XSKLVEI6.js +368 -0
- package/dist/{chunk-DMAMGFV7.js → chunk-ZS7PQNJU.js} +4 -4
- package/dist/{cli-OH6427MC.js → cli-7DL4BKA7.js} +6 -6
- package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
- package/dist/{cli-BI6XCMCL.js → cli-AM4FLXGM.js} +5 -5
- package/dist/{cli-JQYZXFRH.js → cli-R2HBESHI.js} +5 -5
- package/dist/{cli-V4QXTBWR.js → cli-SZ4SZLUN.js} +4 -4
- package/dist/{cli-W6FNSHRN.js → cli-T7A3BU5J.js} +9 -9
- package/dist/{cli-SQC27VYC.js → cli-WZA5IXSR.js} +6 -6
- package/dist/cli-YNIJOEJK.js +244 -0
- package/dist/cli.js +3 -3
- package/dist/index.cjs +64 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -1
- package/dist/phone.cjs +194 -18
- package/dist/phone.d.cts +113 -13
- package/dist/phone.d.ts +113 -13
- package/dist/phone.js +182 -18
- package/dist/{viewports-cli-C4DWICYI.js → viewports-cli-F7KJIVZL.js} +51 -86
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-IJUZ7RV4.js +0 -151
- 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.
|
|
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
|
|
199
|
-
|
|
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
|
|
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-
|
|
12
|
+
} from "./chunk-6DRBFDWT.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
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.
|
|
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.
|
|
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,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
25
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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-
|
|
13
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
14
14
|
import {
|
|
15
15
|
TRADE_NOUNS
|
|
16
|
-
} from "./chunk-
|
|
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",
|