@domandigital/craft 0.20.3 → 0.20.5

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.20.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ ## 0.20.4
10
+
11
+ ### Patch Changes
12
+
13
+ - cc6c0b4: Document opening-photo selection using the Bellerose follow-up: compare photographic strength, crop and sequence before service variety or slide count, with stakeholder preference kept separate from unmeasured customer outcomes.
14
+
3
15
  ## 0.20.3
4
16
 
5
17
  ### Patch Changes
package/CHARACTER.md CHANGED
@@ -589,6 +589,33 @@ before reading stakeholder recommendations when possible; if feedback is already
589
589
  known, record that limitation. A preferred example is useful calibration, not a
590
590
  universal order, a conversion claim or an exemption from future scrutiny.
591
591
 
592
+ #### Select photographs for their position in the sequence
593
+
594
+ Review every proposed opening photograph at the intended size and crop, in order.
595
+ Service coverage alone does not make a photograph suitable for the opening.
596
+ Compare clarity, framing, visible completion, distracting surroundings and the
597
+ visual standard set by the lead image. A technically sound carousel can still
598
+ weaken the page when its later slides are noticeably poorer.
599
+
600
+ On 18 October 2026, after approving Bellerose's mobile flow, Dmitri pointed out
601
+ that the radiator and kitchen-tap slides did not compare with the first bathroom
602
+ photograph. The initial selection had favoured showing different services. A
603
+ review of the available photographs found that the radiator's unfinished edge
604
+ and dust sheet, and the tap's busy surroundings, weakened the opening sequence.
605
+ The approved revision kept the bath-and-shower photograph and used the twin-basins
606
+ photograph second. The lower captioned project gallery retained its service
607
+ coverage. The two bathroom views are not evidence of two separate jobs.
608
+
609
+ The reusable lesson is to choose the strongest coherent sequence the available
610
+ assets support, then let that determine the count. Two strong images can serve
611
+ an opening better than three padded for variety; this is not a rule to always
612
+ use two images, bathrooms or a carousel. Useful job-record photographs can still
613
+ belong beside a service or further down the page. Do not fabricate or retouch
614
+ work into completion to fill a slot. Record rejected candidates, the intended
615
+ crop, the reason for the choice and the stakeholder response with the rendered
616
+ comparison. Here the evidence is visual judgement and an approved preference;
617
+ customer behaviour and enquiry effects remain unmeasured.
618
+
592
619
  #### Using it
593
620
 
594
621
  Generating a page: run `craft direction research`, which asks where the
@@ -1314,7 +1341,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
1314
1341
  after changing an entry; a test fails until you do.
1315
1342
 
1316
1343
  <!-- craft:catalogue:start -->
1317
- Catalogue version `2026.10.2`, 70 tells.
1344
+ Catalogue version `2026.10.3`, 71 tells.
1318
1345
 
1319
1346
  | Id | Gen | Surface | Severity | Tell | Why it is a default |
1320
1347
  | --- | --- | --- | --- | --- | --- |
@@ -1361,6 +1388,7 @@ Catalogue version `2026.10.2`, 70 tells.
1361
1388
  | `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. |
1362
1389
  | `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. |
1363
1390
  | `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. |
1391
+ | `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. |
1364
1392
  | `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. |
1365
1393
  | `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. |
1366
1394
  | `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
@@ -90,6 +91,7 @@ person reading decides which instance earns its place.
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" |
91
92
  | `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
93
  | `ai-vocabulary` | tapestry, testament, realm, elevate, nestled, meticulous, unparalleled, moreover; and underscore, showcasing, pivotal, measured 10 to 14 times over-used in post-2022 text |
94
+ | `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
95
  | `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
96
  | `hollow-imperative` | Discover, Experience, Transform, Elevate as the opening verb of a heading or button |
95
97
  | `rhetorical-opener` | "Looking for...?", "Tired of...?", "Ready to...?" |
package/README.md CHANGED
@@ -364,3 +364,5 @@ widget name no longer proves duplication. A smaller implementation does not
364
364
  select a better design automatically. Protected staging reviews may reserve
365
365
  explicitly marked publication assets while reporting `publicationReady: false`.
366
366
  See CHARACTER.md, “Context and learning from a later result”.
367
+
368
+ Opening photographs are selected as a rendered sequence: compare their quality and crop before filling a slide count or covering every service. See [Select photographs for their position in the sequence](./CHARACTER.md#select-photographs-for-their-position-in-the-sequence) for the Bellerose follow-up and its limits.
@@ -9,10 +9,10 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-YOZLPNJS.js";
12
+ } from "./chunk-YFS32BZ2.js";
13
13
  import "./chunk-JYNXGCYZ.js";
14
- import "./chunk-JGWOA4LI.js";
15
- import "./chunk-7DQDF3VL.js";
14
+ import "./chunk-PFVSKIPM.js";
15
+ import "./chunk-FBWBDBW4.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
18
18
  PROVENANCE_BUDGET_MS,
package/dist/audit.cjs CHANGED
@@ -693,7 +693,12 @@ var FALSE_RANGE = [
693
693
  ];
694
694
  var RESIDUE = [
695
695
  /\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,
696
+ // At the start of a line, or after a sentence that ends in a full stop or
697
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
698
+ // is the handover request." slipped through when only a line start counted
699
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
700
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
701
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
697
702
  /\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
703
  /\blet me know if you(?:['’]d| would) like me to\b/i,
699
704
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -709,6 +714,13 @@ var RESIDUE = [
709
714
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
710
715
  /【\d+(?:[:†][^】]{0,40})?】/
711
716
  ];
717
+ var STOCK_EMAIL = [
718
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
719
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
720
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
721
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
722
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
723
+ ];
712
724
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
713
725
  function residueHits(ctx) {
714
726
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -790,15 +802,39 @@ var RESEARCH_TELLS = [
790
802
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
791
803
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
792
804
  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.")
805
+ f("content/home.md", "Here's a more concise version for the hero."),
806
+ // The client handover email that a clean copy-check let through, 2026-10-19.
807
+ 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
808
  ],
795
809
  pass: [
796
810
  f("content/home.md", "Certainly the busiest week of the year: book early."),
811
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
797
812
  f("content/home.md", "Here's the price list for 2026."),
798
813
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
799
814
  ]
800
815
  }
801
816
  },
817
+ {
818
+ id: "stock-email-line",
819
+ name: "Stock email line",
820
+ generation: 1,
821
+ severity: "warn",
822
+ surface: "copy",
823
+ 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.",
824
+ 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.'",
825
+ detect: phrases(STOCK_EMAIL, quoted),
826
+ fixtures: {
827
+ flag: [
828
+ 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."),
829
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
830
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
831
+ ],
832
+ pass: [
833
+ 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."),
834
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
835
+ ]
836
+ }
837
+ },
802
838
  {
803
839
  id: "prompt-context",
804
840
  name: "Prompt context",
@@ -3071,6 +3107,8 @@ var HOUSE = {
3071
3107
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
3072
3108
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
3073
3109
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
3110
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
3111
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
3074
3112
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
3075
3113
  "stock-phrase": { tier: "review", label: "stock phrase" },
3076
3114
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3812,7 +3850,7 @@ var IMAGERY_TELLS = [
3812
3850
  ];
3813
3851
 
3814
3852
  // src/character/check.ts
3815
- var CATALOGUE_VERSION = "2026.10.2";
3853
+ var CATALOGUE_VERSION = "2026.10.3";
3816
3854
  var CATALOGUE = [
3817
3855
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3818
3856
  ...SPECIFICITY_COPY_TELLS,
package/dist/audit.js CHANGED
@@ -658,7 +658,12 @@ var FALSE_RANGE = [
658
658
  ];
659
659
  var RESIDUE = [
660
660
  /\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,
661
+ // At the start of a line, or after a sentence that ends in a full stop or
662
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
663
+ // is the handover request." slipped through when only a line start counted
664
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
665
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
666
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
662
667
  /\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
668
  /\blet me know if you(?:['’]d| would) like me to\b/i,
664
669
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -674,6 +679,13 @@ var RESIDUE = [
674
679
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
675
680
  /【\d+(?:[:†][^】]{0,40})?】/
676
681
  ];
682
+ var STOCK_EMAIL = [
683
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
684
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
685
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
686
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
687
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
688
+ ];
677
689
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
678
690
  function residueHits(ctx) {
679
691
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -755,15 +767,39 @@ var RESEARCH_TELLS = [
755
767
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
756
768
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
757
769
  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.")
770
+ f("content/home.md", "Here's a more concise version for the hero."),
771
+ // The client handover email that a clean copy-check let through, 2026-10-19.
772
+ 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
773
  ],
760
774
  pass: [
761
775
  f("content/home.md", "Certainly the busiest week of the year: book early."),
776
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
762
777
  f("content/home.md", "Here's the price list for 2026."),
763
778
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
764
779
  ]
765
780
  }
766
781
  },
782
+ {
783
+ id: "stock-email-line",
784
+ name: "Stock email line",
785
+ generation: 1,
786
+ severity: "warn",
787
+ surface: "copy",
788
+ 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.",
789
+ 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.'",
790
+ detect: phrases(STOCK_EMAIL, quoted),
791
+ fixtures: {
792
+ flag: [
793
+ 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
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
795
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
796
+ ],
797
+ pass: [
798
+ 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."),
799
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
800
+ ]
801
+ }
802
+ },
767
803
  {
768
804
  id: "prompt-context",
769
805
  name: "Prompt context",
@@ -3036,6 +3072,8 @@ var HOUSE = {
3036
3072
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
3037
3073
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
3038
3074
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
3075
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
3076
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
3039
3077
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
3040
3078
  "stock-phrase": { tier: "review", label: "stock phrase" },
3041
3079
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3777,7 +3815,7 @@ var IMAGERY_TELLS = [
3777
3815
  ];
3778
3816
 
3779
3817
  // src/character/check.ts
3780
- var CATALOGUE_VERSION = "2026.10.2";
3818
+ var CATALOGUE_VERSION = "2026.10.3";
3781
3819
  var CATALOGUE = [
3782
3820
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3783
3821
  ...SPECIFICITY_COPY_TELLS,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-NQAYRK2K.js";
6
+ } from "./chunk-TSO5W32N.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -22,10 +22,10 @@ import {
22
22
  hueSwatch,
23
23
  nullPrompt,
24
24
  scanSource
25
- } from "./chunk-JGWOA4LI.js";
25
+ } from "./chunk-PFVSKIPM.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-7DQDF3VL.js";
28
+ } from "./chunk-FBWBDBW4.js";
29
29
 
30
30
  // src/null/cli.ts
31
31
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
@@ -120,7 +120,7 @@ async function build(flags, io) {
120
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
121
121
  if (toShoot.length) {
122
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
123
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
123
+ const { snapshotUrls } = await import("./audit-5I2WWTL6.js");
124
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
125
125
  results.forEach((r, i) => {
126
126
  const id = toShoot[i];
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
241
241
  const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
242
  if (toShoot.length) {
243
243
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
244
+ const { snapshotUrls } = await import("./audit-5I2WWTL6.js");
245
245
  const servers = /* @__PURE__ */ new Map();
246
246
  try {
247
247
  for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-YOZLPNJS.js";
4
+ } from "./chunk-YFS32BZ2.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-KQF44RI3.js";
8
+ } from "./chunk-BBJOLR3R.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-NQAYRK2K.js";
17
+ } from "./chunk-TSO5W32N.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -28,7 +28,7 @@ import {
28
28
  rhythmOf,
29
29
  scanSource,
30
30
  typicality
31
- } from "./chunk-JGWOA4LI.js";
31
+ } from "./chunk-PFVSKIPM.js";
32
32
 
33
33
  // src/audit/cli.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -1081,7 +1081,12 @@ var FALSE_RANGE = [
1081
1081
  ];
1082
1082
  var RESIDUE = [
1083
1083
  /\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
1084
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
1084
+ // At the start of a line, or after a sentence that ends in a full stop or
1085
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
1086
+ // is the handover request." slipped through when only a line start counted
1087
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
1088
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
1089
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
1085
1090
  /\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
1086
1091
  /\blet me know if you(?:['’]d| would) like me to\b/i,
1087
1092
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -1097,6 +1102,13 @@ var RESIDUE = [
1097
1102
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
1098
1103
  /【\d+(?:[:†][^】]{0,40})?】/
1099
1104
  ];
1105
+ var STOCK_EMAIL = [
1106
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
1107
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
1108
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
1109
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
1110
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
1111
+ ];
1100
1112
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
1101
1113
  function residueHits(ctx) {
1102
1114
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -1178,15 +1190,39 @@ var RESEARCH_TELLS = [
1178
1190
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
1179
1191
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
1180
1192
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
1181
- f("content/home.md", "Here's a more concise version for the hero.")
1193
+ f("content/home.md", "Here's a more concise version for the hero."),
1194
+ // The client handover email that a clean copy-check let through, 2026-10-19.
1195
+ 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.")
1182
1196
  ],
1183
1197
  pass: [
1184
1198
  f("content/home.md", "Certainly the busiest week of the year: book early."),
1199
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
1185
1200
  f("content/home.md", "Here's the price list for 2026."),
1186
1201
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
1187
1202
  ]
1188
1203
  }
1189
1204
  },
1205
+ {
1206
+ id: "stock-email-line",
1207
+ name: "Stock email line",
1208
+ generation: 1,
1209
+ severity: "warn",
1210
+ surface: "copy",
1211
+ 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.",
1212
+ 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.'",
1213
+ detect: phrases(STOCK_EMAIL, quoted),
1214
+ fixtures: {
1215
+ flag: [
1216
+ 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."),
1217
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
1218
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
1219
+ ],
1220
+ pass: [
1221
+ 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."),
1222
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
1223
+ ]
1224
+ }
1225
+ },
1190
1226
  {
1191
1227
  id: "prompt-context",
1192
1228
  name: "Prompt context",
@@ -10,10 +10,10 @@ import {
10
10
  levenshtein,
11
11
  normaliseFamily,
12
12
  parseColour
13
- } from "./chunk-JGWOA4LI.js";
13
+ } from "./chunk-PFVSKIPM.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
- } from "./chunk-7DQDF3VL.js";
16
+ } from "./chunk-FBWBDBW4.js";
17
17
 
18
18
  // src/direction/types.ts
19
19
  var DIRECTION_VERSION = 2;
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-S3L7TEJF.js";
7
+ } from "./chunk-GJY2VMPD.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-KQF44RI3.js";
10
+ } from "./chunk-BBJOLR3R.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-NQAYRK2K.js";
13
+ } from "./chunk-TSO5W32N.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -30,7 +30,7 @@ import {
30
30
  rhythmOf,
31
31
  shapeName,
32
32
  temperatureName
33
- } from "./chunk-JGWOA4LI.js";
33
+ } from "./chunk-PFVSKIPM.js";
34
34
 
35
35
  // src/estate/cli.ts
36
36
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -133,7 +133,7 @@ async function snapshotOf(target, cwd) {
133
133
  return snap;
134
134
  }
135
135
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
136
- const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
136
+ const { snapshotUrl } = await import("./audit-5I2WWTL6.js");
137
137
  return snapshotUrl(target);
138
138
  }
139
139
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -9,7 +9,7 @@ import {
9
9
  parseFile,
10
10
  sentencesOf,
11
11
  specificity
12
- } from "./chunk-7DQDF3VL.js";
12
+ } from "./chunk-FBWBDBW4.js";
13
13
  import {
14
14
  SNAPSHOT_VERSION
15
15
  } from "./chunk-QYBAVU3P.js";
@@ -2428,6 +2428,8 @@ var HOUSE = {
2428
2428
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
2429
2429
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
2430
2430
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
2431
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
2432
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
2431
2433
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
2432
2434
  "stock-phrase": { tier: "review", label: "stock phrase" },
2433
2435
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -3023,7 +3025,7 @@ var IMAGERY_TELLS = [
3023
3025
  ];
3024
3026
 
3025
3027
  // src/character/check.ts
3026
- var CATALOGUE_VERSION = "2026.10.2";
3028
+ var CATALOGUE_VERSION = "2026.10.3";
3027
3029
  var CATALOGUE = [
3028
3030
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
3029
3031
  ...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-PFVSKIPM.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-FBWBDBW4.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-5I2WWTL6.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-V4QXTBWR.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-JQYZXFRH.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-W6FNSHRN.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-NIH3WTNE.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-SQC27VYC.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-V4QXTBWR.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-BI6XCMCL.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-V4QXTBWR.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-56H5IPWT.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-C4DWICYI.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-OH6427MC.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-PFVSKIPM.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -9,12 +9,12 @@ import {
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  readPaths
12
- } from "./chunk-NQAYRK2K.js";
12
+ } from "./chunk-TSO5W32N.js";
13
13
  import {
14
14
  toJson
15
15
  } from "./chunk-QIEDBL4J.js";
16
- import "./chunk-JGWOA4LI.js";
17
- import "./chunk-7DQDF3VL.js";
16
+ import "./chunk-PFVSKIPM.js";
17
+ import "./chunk-FBWBDBW4.js";
18
18
  import "./chunk-QYBAVU3P.js";
19
19
 
20
20
  // src/phone/cli.ts
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-KQF44RI3.js";
7
+ } from "./chunk-BBJOLR3R.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-NQAYRK2K.js";
11
+ } from "./chunk-TSO5W32N.js";
12
12
  import {
13
13
  toJson
14
14
  } from "./chunk-QIEDBL4J.js";
@@ -21,8 +21,8 @@ import {
21
21
  fingerprint,
22
22
  scanSource,
23
23
  typicality
24
- } from "./chunk-JGWOA4LI.js";
25
- import "./chunk-7DQDF3VL.js";
24
+ } from "./chunk-PFVSKIPM.js";
25
+ import "./chunk-FBWBDBW4.js";
26
26
  import {
27
27
  SNAPSHOT_VERSION
28
28
  } from "./chunk-QYBAVU3P.js";
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
188
188
  if (toTake.length) {
189
189
  io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
190
190
  try {
191
- const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
191
+ const { snapshotUrls } = await import("./audit-5I2WWTL6.js");
192
192
  const results = await snapshotUrls(toTake.map((t) => t.url));
193
193
  results.forEach((r, i) => {
194
194
  const { entry, keep } = toTake[i];
@@ -19,10 +19,10 @@ import {
19
19
  checkReason,
20
20
  researchPrompt,
21
21
  validateDirection
22
- } from "./chunk-S3L7TEJF.js";
22
+ } from "./chunk-GJY2VMPD.js";
23
23
  import {
24
24
  parseFlags
25
- } from "./chunk-NQAYRK2K.js";
25
+ } from "./chunk-TSO5W32N.js";
26
26
  import {
27
27
  toJson
28
28
  } from "./chunk-QIEDBL4J.js";
@@ -38,8 +38,8 @@ import {
38
38
  isCream,
39
39
  oklchToRgb,
40
40
  parseColour
41
- } from "./chunk-JGWOA4LI.js";
42
- import "./chunk-7DQDF3VL.js";
41
+ } from "./chunk-PFVSKIPM.js";
42
+ import "./chunk-FBWBDBW4.js";
43
43
  import "./chunk-QYBAVU3P.js";
44
44
 
45
45
  // src/direction/cli.ts
@@ -484,7 +484,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
484
484
  async function snapshotFrom(target, cwd) {
485
485
  if (!target) return void 0;
486
486
  if (/^https?:\/\//.test(target)) {
487
- const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
487
+ const { snapshotUrl } = await import("./audit-5I2WWTL6.js");
488
488
  return snapshotUrl(target);
489
489
  }
490
490
  return readSnapshot(readJson(resolve(cwd, target)), target);
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-7DQDF3VL.js";
12
+ } from "./chunk-FBWBDBW4.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";
@@ -4,16 +4,16 @@ import {
4
4
  merge,
5
5
  resolvePages,
6
6
  runAudit
7
- } from "./chunk-2276FJVN.js";
8
- import "./chunk-YOZLPNJS.js";
7
+ } from "./chunk-DMAMGFV7.js";
8
+ import "./chunk-YFS32BZ2.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-KQF44RI3.js";
10
+ import "./chunk-BBJOLR3R.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-NQAYRK2K.js";
12
+ import "./chunk-TSO5W32N.js";
13
13
  import "./chunk-QIEDBL4J.js";
14
14
  import "./chunk-WUACK2HX.js";
15
- import "./chunk-JGWOA4LI.js";
16
- import "./chunk-7DQDF3VL.js";
15
+ import "./chunk-PFVSKIPM.js";
16
+ import "./chunk-FBWBDBW4.js";
17
17
  import "./chunk-QYBAVU3P.js";
18
18
  export {
19
19
  describeComponents,
@@ -4,15 +4,15 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-GBDPNNC2.js";
8
- import "./chunk-S3L7TEJF.js";
9
- import "./chunk-KQF44RI3.js";
7
+ } from "./chunk-N32UUN3S.js";
8
+ import "./chunk-GJY2VMPD.js";
9
+ import "./chunk-BBJOLR3R.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-NQAYRK2K.js";
11
+ import "./chunk-TSO5W32N.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-JGWOA4LI.js";
15
- import "./chunk-7DQDF3VL.js";
14
+ import "./chunk-PFVSKIPM.js";
15
+ import "./chunk-FBWBDBW4.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
18
18
  formatComponentPairs,
@@ -9,13 +9,13 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-KQF44RI3.js";
12
+ } from "./chunk-BBJOLR3R.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-NQAYRK2K.js";
14
+ import "./chunk-TSO5W32N.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-JGWOA4LI.js";
18
- import "./chunk-7DQDF3VL.js";
17
+ import "./chunk-PFVSKIPM.js";
18
+ import "./chunk-FBWBDBW4.js";
19
19
  import "./chunk-QYBAVU3P.js";
20
20
  export {
21
21
  describeTypicality,
@@ -3,21 +3,21 @@ import {
3
3
  } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-2276FJVN.js";
7
- import "./chunk-YOZLPNJS.js";
6
+ } from "./chunk-DMAMGFV7.js";
7
+ import "./chunk-YFS32BZ2.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-GBDPNNC2.js";
13
+ } from "./chunk-N32UUN3S.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
- } from "./chunk-S3L7TEJF.js";
17
+ } from "./chunk-GJY2VMPD.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-KQF44RI3.js";
20
+ } from "./chunk-BBJOLR3R.js";
21
21
  import "./chunk-OQXTQQAU.js";
22
22
  import {
23
23
  COPY_FILE,
@@ -28,7 +28,7 @@ import {
28
28
  loadExceptions,
29
29
  parseFlags,
30
30
  readPaths
31
- } from "./chunk-NQAYRK2K.js";
31
+ } from "./chunk-TSO5W32N.js";
32
32
  import {
33
33
  toJson
34
34
  } from "./chunk-QIEDBL4J.js";
@@ -42,8 +42,8 @@ import {
42
42
  fingerprint,
43
43
  scanSource,
44
44
  typicality
45
- } from "./chunk-JGWOA4LI.js";
46
- import "./chunk-7DQDF3VL.js";
45
+ } from "./chunk-PFVSKIPM.js";
46
+ import "./chunk-FBWBDBW4.js";
47
47
  import "./chunk-QYBAVU3P.js";
48
48
 
49
49
  // src/report/cli.ts
@@ -126,7 +126,7 @@ async function runReport(command, args, io) {
126
126
  if (/\.json$/i.test(target2) && existsSync(local)) {
127
127
  snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
128
128
  } else {
129
- const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
129
+ const { snapshotUrl } = await import("./audit-5I2WWTL6.js");
130
130
  snap = await snapshotUrl(target2);
131
131
  }
132
132
  const fp = fingerprint(snap);
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-NQAYRK2K.js";
4
+ } from "./chunk-TSO5W32N.js";
5
5
  import "./chunk-QIEDBL4J.js";
6
- import "./chunk-JGWOA4LI.js";
7
- import "./chunk-7DQDF3VL.js";
6
+ import "./chunk-PFVSKIPM.js";
7
+ import "./chunk-FBWBDBW4.js";
8
8
  import "./chunk-QYBAVU3P.js";
9
9
 
10
10
  // src/cli.ts
package/dist/index.cjs CHANGED
@@ -2494,7 +2494,12 @@ var FALSE_RANGE = [
2494
2494
  ];
2495
2495
  var RESIDUE = [
2496
2496
  /\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
2497
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
2497
+ // At the start of a line, or after a sentence that ends in a full stop or
2498
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
2499
+ // is the handover request." slipped through when only a line start counted
2500
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
2501
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
2502
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
2498
2503
  /\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
2499
2504
  /\blet me know if you(?:['’]d| would) like me to\b/i,
2500
2505
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -2510,6 +2515,13 @@ var RESIDUE = [
2510
2515
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
2511
2516
  /【\d+(?:[:†][^】]{0,40})?】/
2512
2517
  ];
2518
+ var STOCK_EMAIL = [
2519
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
2520
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
2521
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
2522
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
2523
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
2524
+ ];
2513
2525
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
2514
2526
  function residueHits(ctx) {
2515
2527
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -2591,15 +2603,39 @@ var RESEARCH_TELLS = [
2591
2603
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
2592
2604
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
2593
2605
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
2594
- f("content/home.md", "Here's a more concise version for the hero.")
2606
+ f("content/home.md", "Here's a more concise version for the hero."),
2607
+ // The client handover email that a clean copy-check let through, 2026-10-19.
2608
+ 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.")
2595
2609
  ],
2596
2610
  pass: [
2597
2611
  f("content/home.md", "Certainly the busiest week of the year: book early."),
2612
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
2598
2613
  f("content/home.md", "Here's the price list for 2026."),
2599
2614
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
2600
2615
  ]
2601
2616
  }
2602
2617
  },
2618
+ {
2619
+ id: "stock-email-line",
2620
+ name: "Stock email line",
2621
+ generation: 1,
2622
+ severity: "warn",
2623
+ surface: "copy",
2624
+ 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.",
2625
+ 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.'",
2626
+ detect: phrases(STOCK_EMAIL, quoted),
2627
+ fixtures: {
2628
+ flag: [
2629
+ 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."),
2630
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
2631
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
2632
+ ],
2633
+ pass: [
2634
+ 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."),
2635
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
2636
+ ]
2637
+ }
2638
+ },
2603
2639
  {
2604
2640
  id: "prompt-context",
2605
2641
  name: "Prompt context",
@@ -5209,6 +5245,8 @@ var HOUSE = {
5209
5245
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
5210
5246
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
5211
5247
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
5248
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
5249
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
5212
5250
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
5213
5251
  "stock-phrase": { tier: "review", label: "stock phrase" },
5214
5252
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -6414,7 +6452,7 @@ var IMAGERY_TELLS = [
6414
6452
  ];
6415
6453
 
6416
6454
  // src/character/check.ts
6417
- var CATALOGUE_VERSION = "2026.10.2";
6455
+ var CATALOGUE_VERSION = "2026.10.3";
6418
6456
  var CATALOGUE = [
6419
6457
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
6420
6458
  ...SPECIFICITY_COPY_TELLS,
package/dist/index.d.cts CHANGED
@@ -1252,7 +1252,7 @@ interface CheckReport {
1252
1252
  * Bumped whenever an entry is added, removed or its detection changes, so a
1253
1253
  * report can say which list it was judged against.
1254
1254
  */
1255
- declare const CATALOGUE_VERSION = "2026.10.2";
1255
+ declare const CATALOGUE_VERSION = "2026.10.3";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
package/dist/index.d.ts CHANGED
@@ -1252,7 +1252,7 @@ interface CheckReport {
1252
1252
  * Bumped whenever an entry is added, removed or its detection changes, so a
1253
1253
  * report can say which list it was judged against.
1254
1254
  */
1255
- declare const CATALOGUE_VERSION = "2026.10.2";
1255
+ declare const CATALOGUE_VERSION = "2026.10.3";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
package/dist/index.js CHANGED
@@ -2176,7 +2176,12 @@ var FALSE_RANGE = [
2176
2176
  ];
2177
2177
  var RESIDUE = [
2178
2178
  /\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
2179
- /(?:^|\n|>)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
2179
+ // At the start of a line, or after a sentence that ends in a full stop or
2180
+ // an exclamation mark: "I hope this email finds you well! Certainly, here
2181
+ // is the handover request." slipped through when only a line start counted
2182
+ // (found live, 2026-10-19). Not after a question mark: an FAQ answer may
2183
+ // honestly open "Do you work weekends? Absolutely, here are our hours."
2184
+ /(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
2180
2185
  /\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
2181
2186
  /\blet me know if you(?:['’]d| would) like me to\b/i,
2182
2187
  /\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
@@ -2192,6 +2197,13 @@ var RESIDUE = [
2192
2197
  /[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
2193
2198
  /【\d+(?:[:†][^】]{0,40})?】/
2194
2199
  ];
2200
+ var STOCK_EMAIL = [
2201
+ /\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
2202
+ /\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
2203
+ /\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
2204
+ /\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
2205
+ /\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
2206
+ ];
2195
2207
  var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
2196
2208
  function residueHits(ctx) {
2197
2209
  const hits = phrases(RESIDUE, quoted, allText)(ctx);
@@ -2273,15 +2285,39 @@ var RESEARCH_TELLS = [
2273
2285
  f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
2274
2286
  f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
2275
2287
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
2276
- f("content/home.md", "Here's a more concise version for the hero.")
2288
+ f("content/home.md", "Here's a more concise version for the hero."),
2289
+ // The client handover email that a clean copy-check let through, 2026-10-19.
2290
+ 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.")
2277
2291
  ],
2278
2292
  pass: [
2279
2293
  f("content/home.md", "Certainly the busiest week of the year: book early."),
2294
+ f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
2280
2295
  f("content/home.md", "Here's the price list for 2026."),
2281
2296
  f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
2282
2297
  ]
2283
2298
  }
2284
2299
  },
2300
+ {
2301
+ id: "stock-email-line",
2302
+ name: "Stock email line",
2303
+ generation: 1,
2304
+ severity: "warn",
2305
+ surface: "copy",
2306
+ 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.",
2307
+ 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.'",
2308
+ detect: phrases(STOCK_EMAIL, quoted),
2309
+ fixtures: {
2310
+ flag: [
2311
+ 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."),
2312
+ f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
2313
+ f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
2314
+ ],
2315
+ pass: [
2316
+ 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."),
2317
+ f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
2318
+ ]
2319
+ }
2320
+ },
2285
2321
  {
2286
2322
  id: "prompt-context",
2287
2323
  name: "Prompt context",
@@ -4891,6 +4927,8 @@ var HOUSE = {
4891
4927
  // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
4892
4928
  "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
4893
4929
  "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
4930
+ // New 2026-10-19: review first, like every new tell, until estate hits are read.
4931
+ "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
4894
4932
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
4895
4933
  "stock-phrase": { tier: "review", label: "stock phrase" },
4896
4934
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -6096,7 +6134,7 @@ var IMAGERY_TELLS = [
6096
6134
  ];
6097
6135
 
6098
6136
  // src/character/check.ts
6099
- var CATALOGUE_VERSION = "2026.10.2";
6137
+ var CATALOGUE_VERSION = "2026.10.3";
6100
6138
  var CATALOGUE = [
6101
6139
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
6102
6140
  ...SPECIFICITY_COPY_TELLS,
@@ -5,13 +5,13 @@ import {
5
5
  } from "./chunk-IJUZ7RV4.js";
6
6
  import {
7
7
  loadPlaywright
8
- } from "./chunk-YOZLPNJS.js";
8
+ } from "./chunk-YFS32BZ2.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
10
  import {
11
11
  toJson
12
12
  } from "./chunk-QIEDBL4J.js";
13
- import "./chunk-JGWOA4LI.js";
14
- import "./chunk-7DQDF3VL.js";
13
+ import "./chunk-PFVSKIPM.js";
14
+ import "./chunk-FBWBDBW4.js";
15
15
  import "./chunk-QYBAVU3P.js";
16
16
 
17
17
  // src/phone/viewports-cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",
package/word-lists.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "catalogueVersion": "2026.10.2",
2
+ "catalogueVersion": "2026.10.3",
3
3
  "lists": {
4
4
  "aiWords": [
5
5
  "tapestry",