@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CHARACTER.md +3 -1
  3. package/COPY.md +9 -3
  4. package/README.md +73 -3
  5. package/dist/{audit-BN2GSFKT.js → audit-LQ2TDISV.js} +3 -3
  6. package/dist/audit.cjs +104 -3
  7. package/dist/audit.js +104 -3
  8. package/dist/{chunk-NQAYRK2K.js → chunk-4WAIOZ4W.js} +14 -14
  9. package/dist/{chunk-YOZLPNJS.js → chunk-6DRBFDWT.js} +1 -1
  10. package/dist/chunk-BQ7H23QP.js +261 -0
  11. package/dist/{chunk-2276FJVN.js → chunk-BYXDX66D.js} +4 -4
  12. package/dist/{chunk-JGWOA4LI.js → chunk-DE2ZAV4G.js} +6 -2
  13. package/dist/{chunk-KQF44RI3.js → chunk-FNB3NP57.js} +5 -5
  14. package/dist/{chunk-S3L7TEJF.js → chunk-G47ZCBD5.js} +2 -2
  15. package/dist/{chunk-7DQDF3VL.js → chunk-ONA73THJ.js} +99 -2
  16. package/dist/{chunk-GBDPNNC2.js → chunk-STWH5EYU.js} +5 -5
  17. package/dist/{cli-QGJB6OUB.js → cli-5QI6NIZN.js} +6 -6
  18. package/dist/{cli-AK4EQHUF.js → cli-5VHX5EJF.js} +5 -5
  19. package/dist/{cli-ZXDGVDYU.js → cli-7QL26YPR.js} +1 -1
  20. package/dist/cli-EYHRJJAX.js +241 -0
  21. package/dist/{cli-AFKXDGUH.js → cli-PBCU7Z64.js} +4 -4
  22. package/dist/{cli-6AZVLNWL.js → cli-RHDNHL4U.js} +5 -5
  23. package/dist/{cli-OYVZOHHV.js → cli-XAWJWK7I.js} +6 -6
  24. package/dist/{cli-7HKVVL3P.js → cli-YSGPGQ7R.js} +9 -9
  25. package/dist/cli.js +3 -3
  26. package/dist/index.cjs +104 -3
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +104 -3
  30. package/dist/phone.cjs +137 -11
  31. package/dist/phone.d.cts +86 -12
  32. package/dist/phone.d.ts +86 -12
  33. package/dist/phone.js +128 -11
  34. package/dist/{viewports-cli-WWWWEBKI.js → viewports-cli-XYYX4SKV.js} +12 -8
  35. package/package.json +1 -1
  36. package/word-lists.json +1 -1
  37. package/dist/chunk-IJUZ7RV4.js +0 -151
  38. package/dist/cli-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.2`, 70 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.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", "as an AI
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 American idiom ("shallow wins", "looks busy"), no "honestly" as an
197
- 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.
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 cut-off `h1`.
275
+ counts), a text field under 16px or under 48px tall, text under 12px, a
276
+ cut-off `h1`.
275
277
 
276
278
  `phone-first.baseline.json` lists what a repo already breaks, counted per file
277
279
  and per route (digits folded, so fixture dates do not split one button into
278
280
  many). The gate fails on anything the baseline does not list, and on a
279
281
  baseline that claims more than is there, so it only shrinks: fix a breach and
280
282
  lower its count in the same PR. `--update-baseline` writes the baseline that
281
- makes the current run pass; run it once when the gate goes in.
283
+ makes the current run pass; run it once when the gate goes in, and after that
284
+ only to lower counts. It keeps every entry's `reason`.
285
+
286
+ ### Four checks that hold the portal standard's phone lessons
287
+
288
+ The causes behind the October 2026 phone fixes are in the portal standard
289
+ (`references/phone-faults.md`, DOM-1044). These four make the gate itself
290
+ refuse them (DOM-1053).
291
+
292
+ 1. **The baseline only shrinks.** The run compares the baseline with the base
293
+ branch's copy and fails on an entry main does not have, or on a higher
294
+ count. Fix the breach where it is. The one way in is a named exception: an
295
+ entry with a non-empty `reason` (third-party UI that cannot be restyled, a
296
+ destructive-only form). Old baselines, with no `reason`, read as before.
297
+ A branch that left main before main shrank its baseline looks like it added
298
+ entries: merge main.
299
+
300
+ ```json
301
+ { "route": "/admin/content", "check": "text", "element": "<p> \"Sign in\"", "count": 14,
302
+ "reason": "Sanity Studio's sign-in dialog, third-party UI" }
303
+ ```
304
+
305
+ The base is `origin/main`, or the pull request's target branch when
306
+ `GITHUB_BASE_REF` is set, or `--base <ref>`. The runner has to be able to see
307
+ it: with `actions/checkout`, use `fetch-depth: 0` or run
308
+ `git fetch origin main` first. If the base cannot be seen the gate fails and
309
+ says so; it never skips. It passes without looking when every entry carries a
310
+ `reason`, when the base has no baseline yet (the pull request that brings the
311
+ gate in), and outside a git checkout (it prints a note).
312
+
313
+ 2. **Every route is measured.** With `--measured`, the measured routes are the
314
+ gate's route list. Every page route of the app must be in it. The pages come
315
+ from the Next app folders (`appDir`; left out, any `source` folder called
316
+ `app`) and from `routesManifest`, a built app's
317
+ `app-path-routes-manifest.json`. Route groups, parallel slots, private
318
+ folders and intercepting routes are not pages of their own. A measured route
319
+ covers the page it opens: `/portal/projects/p1?theme=dark` covers
320
+ `/portal/projects/[id]`, and so does the literal label
321
+ `/portal/projects/[id]`; a static sibling such as `/portal/projects/new`
322
+ does not. A page the gate cannot measure (one that only redirects) goes in
323
+ `excludeRoutes`, the only way out, with a reason; `"/admin/*"` takes a prefix
324
+ and everything under it. An entry with no reason, or naming no page, fails.
325
+ When no app folder with pages is found, the run prints that route coverage
326
+ was not checked.
327
+
328
+ ```json
329
+ { "source": ["app", "components"],
330
+ "excludeRoutes": [{ "route": "/portal/projects", "reason": "redirects to /portal; measured there" }] }
331
+ ```
332
+
333
+ 3. **Fields are 48px tall.** A text field, select or textarea under 48px fails
334
+ the `field` check ("44px tall, under 48px (use min-h-12)"), matching portal
335
+ standard rule 5. Until now the gate only failed fields under 44px, as
336
+ targets. Configurable: `fieldHeightPx` in `floors`, for `measurePhone`,
337
+ `measureViewports`, `measureSite` and `viewports.json`; the default is 48.
338
+
339
+ ```ts
340
+ await measureViewports(page, { route, floors: { fieldHeightPx: 44 } }); // the old gate
341
+ ```
342
+
343
+ 4. **Tailwind 3 `var()` opacity.** Tailwind 3 cannot apply `/NN` opacity to a
344
+ `var()` colour and emits no CSS, so `bg-[var(--bg-1)]/95` leaves a fixed bar
345
+ transparent. On a Tailwind 3 app the source scan fails on
346
+ `var\([^)]*\)\]/[0-9]+`. The version is read per `source` folder from the
347
+ installed `node_modules/tailwindcss`, else the declared range in the nearest
348
+ `package.json`; set `"tailwind": 3` or `4` in `phone-first.json` when that
349
+ cannot find it. Tailwind 4 supports the syntax and is not scanned. It is a
350
+ bug rather than a style, so a `mobile-first-ok` comment does not excuse it;
351
+ a file-level `exceptions` entry still does.
282
352
 
283
353
  ## Every viewport
284
354
 
@@ -9,10 +9,10 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-YOZLPNJS.js";
12
+ } from "./chunk-6DRBFDWT.js";
13
13
  import "./chunk-JYNXGCYZ.js";
14
- import "./chunk-JGWOA4LI.js";
15
- import "./chunk-7DQDF3VL.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",
@@ -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
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
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.2";
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
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
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.2";
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-JGWOA4LI.js";
13
+ } from "./chunk-DE2ZAV4G.js";
14
14
  import {
15
15
  compareCompetitor,
16
16
  compareFacts,
@@ -21,7 +21,7 @@ import {
21
21
  pageCopy,
22
22
  protectedFacts,
23
23
  visibleText
24
- } from "./chunk-7DQDF3VL.js";
24
+ } from "./chunk-ONA73THJ.js";
25
25
 
26
26
  // src/character/config.ts
27
27
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -622,7 +622,7 @@ async function pageSnapshot(target, cwd) {
622
622
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
623
623
  }
624
624
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
625
- const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
625
+ const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
626
626
  return snapshotUrl(target);
627
627
  }
628
628
  async function briefFor(flags, cwd) {
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
633
633
  if (typeof brief === "string" && brief.trim()) return brief;
634
634
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
635
635
  if (flags.null) {
636
- const { loadNull } = await import("./cli-AFKXDGUH.js");
636
+ const { loadNull } = await import("./cli-PBCU7Z64.js");
637
637
  return loadNull(flags.null, cwd).brief;
638
638
  }
639
639
  return void 0;
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
659
659
  function run(argv, io) {
660
660
  const [command, ...rest] = argv;
661
661
  if (command === "direction") {
662
- return import("./cli-6AZVLNWL.js").then((m) => m.runDirection(rest, io));
662
+ return import("./cli-RHDNHL4U.js").then((m) => m.runDirection(rest, io));
663
663
  }
664
664
  if (command === "report" || command === "retrofit") {
665
- return import("./cli-7HKVVL3P.js").then((m) => m.runReport(command, rest, io));
665
+ return import("./cli-YSGPGQ7R.js").then((m) => m.runReport(command, rest, io));
666
666
  }
667
667
  if (command === "register") {
668
- return import("./cli-ZXDGVDYU.js").then((m) => m.runRegister(rest, io));
668
+ return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
669
669
  }
670
670
  if (command === "estate") {
671
- return import("./cli-QGJB6OUB.js").then((m) => m.runEstate(rest, io));
671
+ return import("./cli-5QI6NIZN.js").then((m) => m.runEstate(rest, io));
672
672
  }
673
673
  if (command === "null") {
674
- return import("./cli-AFKXDGUH.js").then((m) => m.runNull(rest, io));
674
+ return import("./cli-PBCU7Z64.js").then((m) => m.runNull(rest, io));
675
675
  }
676
676
  if (command === "calibrate") {
677
- return import("./cli-AK4EQHUF.js").then((m) => m.runCalibrate(rest, io));
677
+ return import("./cli-5VHX5EJF.js").then((m) => m.runCalibrate(rest, io));
678
678
  }
679
679
  if (command === "tells" && rest[0] === "harvest") {
680
- return import("./cli-AFKXDGUH.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
680
+ return import("./cli-PBCU7Z64.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
681
681
  io.err(`craft: ${error.message}`);
682
682
  return 2;
683
683
  });
684
684
  }
685
685
  if (command === "phone-first") {
686
- return import("./cli-6INEFJYN.js").then((m) => m.runPhoneFirst(rest, io));
686
+ return import("./cli-EYHRJJAX.js").then((m) => m.runPhoneFirst(rest, io));
687
687
  }
688
688
  if (command === "viewports") {
689
- return import("./viewports-cli-WWWWEBKI.js").then((m) => m.runViewports(rest, io));
689
+ return import("./viewports-cli-XYYX4SKV.js").then((m) => m.runViewports(rest, io));
690
690
  }
691
691
  if (command === "snapshot" || command === "audit") {
692
- return import("./cli-OYVZOHHV.js").then((m) => m.runAudit(command, rest, io));
692
+ return import("./cli-XAWJWK7I.js").then((m) => m.runAudit(command, rest, io));
693
693
  }
694
694
  if (!command || command === "help" || command === "--help" || command === "-h") {
695
695
  io.out(HELP);
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-JGWOA4LI.js";
7
+ } from "./chunk-DE2ZAV4G.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";