@domandigital/craft 0.20.4 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/CHARACTER.md +3 -1
- package/COPY.md +9 -3
- package/README.md +73 -3
- package/dist/{audit-BN2GSFKT.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +104 -3
- package/dist/audit.js +104 -3
- package/dist/{chunk-NQAYRK2K.js → chunk-4WAIOZ4W.js} +14 -14
- package/dist/{chunk-YOZLPNJS.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/chunk-BQ7H23QP.js +261 -0
- package/dist/{chunk-2276FJVN.js → chunk-BYXDX66D.js} +4 -4
- package/dist/{chunk-JGWOA4LI.js → chunk-DE2ZAV4G.js} +6 -2
- package/dist/{chunk-KQF44RI3.js → chunk-FNB3NP57.js} +5 -5
- package/dist/{chunk-S3L7TEJF.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-7DQDF3VL.js → chunk-ONA73THJ.js} +99 -2
- package/dist/{chunk-GBDPNNC2.js → chunk-STWH5EYU.js} +5 -5
- package/dist/{cli-QGJB6OUB.js → cli-5QI6NIZN.js} +6 -6
- package/dist/{cli-AK4EQHUF.js → cli-5VHX5EJF.js} +5 -5
- package/dist/{cli-ZXDGVDYU.js → cli-7QL26YPR.js} +1 -1
- package/dist/cli-EYHRJJAX.js +241 -0
- package/dist/{cli-AFKXDGUH.js → cli-PBCU7Z64.js} +4 -4
- package/dist/{cli-6AZVLNWL.js → cli-RHDNHL4U.js} +5 -5
- package/dist/{cli-OYVZOHHV.js → cli-XAWJWK7I.js} +6 -6
- package/dist/{cli-7HKVVL3P.js → cli-YSGPGQ7R.js} +9 -9
- package/dist/cli.js +3 -3
- package/dist/index.cjs +104 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +104 -3
- package/dist/phone.cjs +137 -11
- package/dist/phone.d.cts +86 -12
- package/dist/phone.d.ts +86 -12
- package/dist/phone.js +128 -11
- package/dist/{viewports-cli-WWWWEBKI.js → viewports-cli-XYYX4SKV.js} +12 -8
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-IJUZ7RV4.js +0 -151
- package/dist/cli-6INEFJYN.js +0 -122
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.20.5
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 6047f0e: `chatbot-residue` now finds "Certainly, here is…" at the start of any sentence that follows a full stop or exclamation mark, not only at the start of a line. An email opening "I hope this email finds you well! Certainly, here is the handover request." passed `craft copy --gate` before this. A question mark still does not count, so an FAQ answer such as "Do you work weekends? Absolutely, here are our hours." passes. New review-tier tell `stock-email-line` (catalogue 2026.10.3) reports the stock lines of a form email: "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". It prints for review and never blocks, following the house rule that a new tell starts in review.
|
|
25
|
+
|
|
3
26
|
## 0.20.4
|
|
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.2`, 70 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. |
|
|
@@ -1388,6 +1389,7 @@ Catalogue version `2026.10.2`, 70 tells.
|
|
|
1388
1389
|
| `staccato-triplet` | 2 | copy | warn | Staccato triplet | Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm. |
|
|
1389
1390
|
| `where-x-meets-y` | 2 | copy | warn | 'Where X meets Y' | 'Where luxury meets comfort' is a tagline shape that fits every business and so describes none. |
|
|
1390
1391
|
| `chatbot-residue` | 1 | copy | warn | Chatbot residue | 'Certainly! Here's a revised version', 'as an AI language model', a citeturn0search0 token, a link tracked utm_source=chatgpt.com: text that exists only because a chat reply was pasted whole. This is evidence, not style. |
|
|
1392
|
+
| `stock-email-line` | 1 | copy | warn | Stock email line | 'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time. |
|
|
1391
1393
|
| `prompt-context` | 1 | copy | warn | Prompt context | 'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell. |
|
|
1392
1394
|
| `placeholder` | 1 | copy | warn | Unfilled placeholder | '[Insert testimonial]', '[Your Name]', lorem ipsum, TODO_PLACEHOLDER: scaffolding a reader must never see, and the clearest sign a page shipped before anyone read it. |
|
|
1393
1395
|
| `question-reveal` | 2 | copy | warn | Staged reveal | 'The result? Faster growth.', 'What does this mean for you? It means...': the writer asks the reader's question for them and answers it. An infomercial hook, and a favourite of generated copy. |
|
package/COPY.md
CHANGED
|
@@ -45,7 +45,8 @@ A single hit fails the check. These are not judgment calls.
|
|
|
45
45
|
a signed-in route (`/admin/`, `/portal/`, `/dashboard/`).
|
|
46
46
|
- **"No X, no Y" as a list**, anywhere in a sentence. "No obligation, no
|
|
47
47
|
spam." Defining the business by what it is not, itemised.
|
|
48
|
-
- **Chatbot residue.** "Certainly! Here's a revised version"
|
|
48
|
+
- **Chatbot residue.** "Certainly! Here's a revised version" (at the start of
|
|
49
|
+
any sentence, not only a line), "as an AI
|
|
49
50
|
language model", "let me know if you'd like me to", ChatGPT citation tokens
|
|
50
51
|
and entity markers, links tracked `utm_source=chatgpt.com`. This is not
|
|
51
52
|
style: it is proof a chat reply was pasted in whole. Remove it, then source
|
|
@@ -88,8 +89,10 @@ person reading decides which instance earns its place.
|
|
|
88
89
|
| --- | --- |
|
|
89
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" |
|
|
90
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 |
|
|
91
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 |
|
|
92
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 |
|
|
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 |
|
|
93
96
|
| `stock-phrase` | to the next level, one-stop shop, we pride ourselves, passionate about, peace of mind, "so here's", a journey used as a metaphor |
|
|
94
97
|
| `hollow-imperative` | Discover, Experience, Transform, Elevate as the opening verb of a heading or button |
|
|
95
98
|
| `rhetorical-opener` | "Looking for...?", "Tired of...?", "Ready to...?" |
|
|
@@ -193,8 +196,11 @@ writer's head, and it comes out anyway. Check against the lists afterwards.
|
|
|
193
196
|
The tool cannot grade register; a person does. British spelling and phrasing
|
|
194
197
|
(colour, organise, licence as a noun, centre, towards, enquiries). Professional
|
|
195
198
|
and confident, never matey: no over-familiar asides ("with me nowhere near
|
|
196
|
-
it"), no
|
|
197
|
-
|
|
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.
|
|
198
204
|
|
|
199
205
|
For a register other than the house one (plain, persuasive, warm, literary),
|
|
200
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
|
|
|
@@ -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",
|
|
@@ -693,7 +754,12 @@ var FALSE_RANGE = [
|
|
|
693
754
|
];
|
|
694
755
|
var RESIDUE = [
|
|
695
756
|
/\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
|
|
696
|
-
|
|
757
|
+
// At the start of a line, or after a sentence that ends in a full stop or
|
|
758
|
+
// an exclamation mark: "I hope this email finds you well! Certainly, here
|
|
759
|
+
// is the handover request." slipped through when only a line start counted
|
|
760
|
+
// (found live, 2026-10-19). Not after a question mark: an FAQ answer may
|
|
761
|
+
// honestly open "Do you work weekends? Absolutely, here are our hours."
|
|
762
|
+
/(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
|
|
697
763
|
/\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
|
|
698
764
|
/\blet me know if you(?:['’]d| would) like me to\b/i,
|
|
699
765
|
/\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
|
|
@@ -709,6 +775,13 @@ var RESIDUE = [
|
|
|
709
775
|
/[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
|
|
710
776
|
/【\d+(?:[:†][^】]{0,40})?】/
|
|
711
777
|
];
|
|
778
|
+
var STOCK_EMAIL = [
|
|
779
|
+
/\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
|
|
780
|
+
/\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
|
|
781
|
+
/\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
|
|
782
|
+
/\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
|
|
783
|
+
/\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
|
|
784
|
+
];
|
|
712
785
|
var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
|
|
713
786
|
function residueHits(ctx) {
|
|
714
787
|
const hits = phrases(RESIDUE, quoted, allText)(ctx);
|
|
@@ -790,15 +863,39 @@ var RESEARCH_TELLS = [
|
|
|
790
863
|
f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
|
|
791
864
|
f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
|
|
792
865
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
|
|
793
|
-
f("content/home.md", "Here's a more concise version for the hero.")
|
|
866
|
+
f("content/home.md", "Here's a more concise version for the hero."),
|
|
867
|
+
// The client handover email that a clean copy-check let through, 2026-10-19.
|
|
868
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions.")
|
|
794
869
|
],
|
|
795
870
|
pass: [
|
|
796
871
|
f("content/home.md", "Certainly the busiest week of the year: book early."),
|
|
872
|
+
f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
|
|
797
873
|
f("content/home.md", "Here's the price list for 2026."),
|
|
798
874
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
|
|
799
875
|
]
|
|
800
876
|
}
|
|
801
877
|
},
|
|
878
|
+
{
|
|
879
|
+
id: "stock-email-line",
|
|
880
|
+
name: "Stock email line",
|
|
881
|
+
generation: 1,
|
|
882
|
+
severity: "warn",
|
|
883
|
+
surface: "copy",
|
|
884
|
+
why: "'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time.",
|
|
885
|
+
fix: "Cut the opener and start with the reason for writing. Replace the closer with the next step and who to ask: 'Ring me on 07700 900123 if the date does not work.'",
|
|
886
|
+
detect: phrases(STOCK_EMAIL, quoted),
|
|
887
|
+
fixtures: {
|
|
888
|
+
flag: [
|
|
889
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions."),
|
|
890
|
+
f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
|
|
891
|
+
f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
|
|
892
|
+
],
|
|
893
|
+
pass: [
|
|
894
|
+
f("emails/handover.md", "Hi Sam,\n\nThe handover pack is attached: certificates, the board schedule and the warranty. Ring me on 07700 900123 if anything is missing."),
|
|
895
|
+
f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
|
|
896
|
+
]
|
|
897
|
+
}
|
|
898
|
+
},
|
|
802
899
|
{
|
|
803
900
|
id: "prompt-context",
|
|
804
901
|
name: "Prompt context",
|
|
@@ -3071,6 +3168,10 @@ var HOUSE = {
|
|
|
3071
3168
|
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
3072
3169
|
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
3073
3170
|
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
3171
|
+
// New 2026-10-19: review first, like every new tell, until estate hits are read.
|
|
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" },
|
|
3074
3175
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3075
3176
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3076
3177
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3812,7 +3913,7 @@ var IMAGERY_TELLS = [
|
|
|
3812
3913
|
];
|
|
3813
3914
|
|
|
3814
3915
|
// src/character/check.ts
|
|
3815
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
3916
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
3816
3917
|
var CATALOGUE = [
|
|
3817
3918
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3818
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",
|
|
@@ -658,7 +719,12 @@ var FALSE_RANGE = [
|
|
|
658
719
|
];
|
|
659
720
|
var RESIDUE = [
|
|
660
721
|
/\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
|
|
661
|
-
|
|
722
|
+
// At the start of a line, or after a sentence that ends in a full stop or
|
|
723
|
+
// an exclamation mark: "I hope this email finds you well! Certainly, here
|
|
724
|
+
// is the handover request." slipped through when only a line start counted
|
|
725
|
+
// (found live, 2026-10-19). Not after a question mark: an FAQ answer may
|
|
726
|
+
// honestly open "Do you work weekends? Absolutely, here are our hours."
|
|
727
|
+
/(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
|
|
662
728
|
/\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
|
|
663
729
|
/\blet me know if you(?:['’]d| would) like me to\b/i,
|
|
664
730
|
/\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
|
|
@@ -674,6 +740,13 @@ var RESIDUE = [
|
|
|
674
740
|
/[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
|
|
675
741
|
/【\d+(?:[:†][^】]{0,40})?】/
|
|
676
742
|
];
|
|
743
|
+
var STOCK_EMAIL = [
|
|
744
|
+
/\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
|
|
745
|
+
/\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
|
|
746
|
+
/\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
|
|
747
|
+
/\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
|
|
748
|
+
/\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
|
|
749
|
+
];
|
|
677
750
|
var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
|
|
678
751
|
function residueHits(ctx) {
|
|
679
752
|
const hits = phrases(RESIDUE, quoted, allText)(ctx);
|
|
@@ -755,15 +828,39 @@ var RESEARCH_TELLS = [
|
|
|
755
828
|
f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
|
|
756
829
|
f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
|
|
757
830
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
|
|
758
|
-
f("content/home.md", "Here's a more concise version for the hero.")
|
|
831
|
+
f("content/home.md", "Here's a more concise version for the hero."),
|
|
832
|
+
// The client handover email that a clean copy-check let through, 2026-10-19.
|
|
833
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions.")
|
|
759
834
|
],
|
|
760
835
|
pass: [
|
|
761
836
|
f("content/home.md", "Certainly the busiest week of the year: book early."),
|
|
837
|
+
f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
|
|
762
838
|
f("content/home.md", "Here's the price list for 2026."),
|
|
763
839
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
|
|
764
840
|
]
|
|
765
841
|
}
|
|
766
842
|
},
|
|
843
|
+
{
|
|
844
|
+
id: "stock-email-line",
|
|
845
|
+
name: "Stock email line",
|
|
846
|
+
generation: 1,
|
|
847
|
+
severity: "warn",
|
|
848
|
+
surface: "copy",
|
|
849
|
+
why: "'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time.",
|
|
850
|
+
fix: "Cut the opener and start with the reason for writing. Replace the closer with the next step and who to ask: 'Ring me on 07700 900123 if the date does not work.'",
|
|
851
|
+
detect: phrases(STOCK_EMAIL, quoted),
|
|
852
|
+
fixtures: {
|
|
853
|
+
flag: [
|
|
854
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions."),
|
|
855
|
+
f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
|
|
856
|
+
f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
|
|
857
|
+
],
|
|
858
|
+
pass: [
|
|
859
|
+
f("emails/handover.md", "Hi Sam,\n\nThe handover pack is attached: certificates, the board schedule and the warranty. Ring me on 07700 900123 if anything is missing."),
|
|
860
|
+
f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
|
|
861
|
+
]
|
|
862
|
+
}
|
|
863
|
+
},
|
|
767
864
|
{
|
|
768
865
|
id: "prompt-context",
|
|
769
866
|
name: "Prompt context",
|
|
@@ -3036,6 +3133,10 @@ var HOUSE = {
|
|
|
3036
3133
|
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
3037
3134
|
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
3038
3135
|
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
3136
|
+
// New 2026-10-19: review first, like every new tell, until estate hits are read.
|
|
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" },
|
|
3039
3140
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3040
3141
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3041
3142
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3777,7 +3878,7 @@ var IMAGERY_TELLS = [
|
|
|
3777
3878
|
];
|
|
3778
3879
|
|
|
3779
3880
|
// src/character/check.ts
|
|
3780
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
3881
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
3781
3882
|
var CATALOGUE = [
|
|
3782
3883
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3783
3884
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
holdHouseBlocks,
|
|
11
11
|
houseRule,
|
|
12
12
|
scanSource
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
665
|
+
return import("./cli-YSGPGQ7R.js").then((m) => m.runReport(command, rest, io));
|
|
666
666
|
}
|
|
667
667
|
if (command === "register") {
|
|
668
|
-
return import("./cli-
|
|
668
|
+
return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
|
|
669
669
|
}
|
|
670
670
|
if (command === "estate") {
|
|
671
|
-
return import("./cli-
|
|
671
|
+
return import("./cli-5QI6NIZN.js").then((m) => m.runEstate(rest, io));
|
|
672
672
|
}
|
|
673
673
|
if (command === "null") {
|
|
674
|
-
return import("./cli-
|
|
674
|
+
return import("./cli-PBCU7Z64.js").then((m) => m.runNull(rest, io));
|
|
675
675
|
}
|
|
676
676
|
if (command === "calibrate") {
|
|
677
|
-
return import("./cli-
|
|
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-
|
|
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-
|
|
686
|
+
return import("./cli-EYHRJJAX.js").then((m) => m.runPhoneFirst(rest, io));
|
|
687
687
|
}
|
|
688
688
|
if (command === "viewports") {
|
|
689
|
-
return import("./viewports-cli-
|
|
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-
|
|
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);
|