@domandigital/craft 0.19.3 → 0.20.1

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 (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CHARACTER.md +4 -2
  3. package/COPY.md +3 -1
  4. package/README.md +51 -0
  5. package/STANDARD.md +86 -3
  6. package/dist/{audit-43PC4LMS.js → audit-BN2GSFKT.js} +3 -3
  7. package/dist/audit.cjs +80 -5
  8. package/dist/audit.js +80 -5
  9. package/dist/{chunk-4LJFZH2I.js → chunk-2OBDCIVW.js} +2 -2
  10. package/dist/{chunk-BK4AS2RU.js → chunk-6NXPDCUO.js} +26 -13
  11. package/dist/{chunk-CZ7KT452.js → chunk-7DQDF3VL.js} +76 -4
  12. package/dist/{chunk-6M2LK7QE.js → chunk-FQYKKKHI.js} +5 -5
  13. package/dist/{chunk-XVMKZ4SM.js → chunk-GOPHZI6O.js} +4 -4
  14. package/dist/{cli-6LCOSHMW.js → chunk-IJUZ7RV4.js} +16 -114
  15. package/dist/{chunk-D2FTTM4F.js → chunk-IZ6GTU54.js} +5 -5
  16. package/dist/{chunk-WK2WMWTH.js → chunk-JGWOA4LI.js} +5 -2
  17. package/dist/{chunk-VDUIIWG7.js → chunk-YOZLPNJS.js} +1 -1
  18. package/dist/{cli-Q5X3W6I4.js → cli-DVYLDCEA.js} +5 -5
  19. package/dist/{cli-QAYH2OX5.js → cli-HUN5ZTH5.js} +6 -6
  20. package/dist/{cli-WN2GHYPF.js → cli-IREKGVSM.js} +6 -6
  21. package/dist/cli-LSJI7QRD.js +122 -0
  22. package/dist/{cli-APYEOHFV.js → cli-MKZJMZUJ.js} +5 -5
  23. package/dist/{cli-TMLHMNWU.js → cli-NTNUZUFP.js} +4 -4
  24. package/dist/{cli-4LEAEOOV.js → cli-VFKKFRB4.js} +9 -9
  25. package/dist/{cli-VPARW6FY.js → cli-ZXDGVDYU.js} +1 -1
  26. package/dist/cli.js +3 -3
  27. package/dist/index.cjs +80 -5
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +80 -5
  31. package/dist/phone.cjs +546 -19
  32. package/dist/phone.d.cts +265 -28
  33. package/dist/phone.d.ts +265 -28
  34. package/dist/phone.js +532 -19
  35. package/dist/viewports-cli-C4VXMPKK.js +683 -0
  36. package/package.json +1 -1
  37. package/word-lists.json +3 -1
@@ -3,21 +3,21 @@ import {
3
3
  } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-XVMKZ4SM.js";
7
- import "./chunk-VDUIIWG7.js";
6
+ } from "./chunk-GOPHZI6O.js";
7
+ import "./chunk-YOZLPNJS.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-6M2LK7QE.js";
13
+ } from "./chunk-FQYKKKHI.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
- } from "./chunk-4LJFZH2I.js";
17
+ } from "./chunk-2OBDCIVW.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-D2FTTM4F.js";
20
+ } from "./chunk-IZ6GTU54.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-BK4AS2RU.js";
31
+ } from "./chunk-6NXPDCUO.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-WK2WMWTH.js";
46
- import "./chunk-CZ7KT452.js";
45
+ } from "./chunk-JGWOA4LI.js";
46
+ import "./chunk-7DQDF3VL.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-43PC4LMS.js");
129
+ const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
130
130
  snap = await snapshotUrl(target2);
131
131
  }
132
132
  const fp = fingerprint(snap);
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-CZ7KT452.js";
12
+ } from "./chunk-7DQDF3VL.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-BK4AS2RU.js";
4
+ } from "./chunk-6NXPDCUO.js";
5
5
  import "./chunk-QIEDBL4J.js";
6
- import "./chunk-WK2WMWTH.js";
7
- import "./chunk-CZ7KT452.js";
6
+ import "./chunk-JGWOA4LI.js";
7
+ import "./chunk-7DQDF3VL.js";
8
8
  import "./chunk-QYBAVU3P.js";
9
9
 
10
10
  // src/cli.ts
package/dist/index.cjs CHANGED
@@ -2003,6 +2003,8 @@ var REVIEW_PHRASES = [
2003
2003
  "truly",
2004
2004
  "genuinely",
2005
2005
  "deeply",
2006
+ "clear",
2007
+ "clearly",
2006
2008
  "ultimately",
2007
2009
  "one-size-fits-all",
2008
2010
  "one size fits all",
@@ -2013,6 +2015,33 @@ var REVIEW_PHRASES = [
2013
2015
  "arguably the best",
2014
2016
  "bar none"
2015
2017
  ];
2018
+ var SALES_TAGLINES = [
2019
+ /\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
2020
+ /\breal,? (?:measurable |tangible )?results\b/i,
2021
+ /\bresults that (?:matter|count|speak|last)\b/i,
2022
+ /\bresults you can (?:see|trust|measure)\b/i,
2023
+ /\bwork(?:s|ing)? harder for you\b/i,
2024
+ /\bmake (?:every|your) (?:penny|pound) count\b/i,
2025
+ /\bevery (?:penny|pound) (?:counts|works)\b/i,
2026
+ /\bbang for your buck\b/i,
2027
+ /\bstand out from the (?:crowd|competition)\b/i,
2028
+ /\b(?:pages?|websites?|sites?|copy|content|ads?) that (?:actually |really )?(?:converts?|sells?)\b/i
2029
+ ];
2030
+ var SALES_RESULTS = "(?:enquiries|leads|customers|clients|sales|jobs|bookings|revenue|calls|traffic|profit)";
2031
+ var OUTCOME_PROMISES = [
2032
+ // "We'll turn more of that into rewiring enquiries"
2033
+ new RegExp(`\\bturn(?:s|ing)?\\b[^.!?\\n]{0,50}?\\binto\\s+(?:more\\s+|real\\s+|paying\\s+)?(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i"),
2034
+ // "once the first areas are working"
2035
+ /\bonce\b[^.!?\n]{0,40}?\b(?:is|are|starts?|begins?)\s+(?:working|paying off|performing|ranking|delivering|bringing in)\b/i,
2036
+ // "competitors you can outdo"
2037
+ /\b(?:competitors?|competition|rivals?)\b[^.!?\n]{0,25}?\b(?:you|we)\s*(?:can|will|could|['’]ll)\s+(?:easily\s+)?(?:outdo|beat|outrank|overtake|outperform|outshine)\b/i,
2038
+ /\b(?:outdo|outrank|overtake|outperform|outshine|beat)\s+(?:your|the|local|bigger)\s+(?:[\w-]+\s+)?(?:competitors|competition|rivals)\b/i,
2039
+ // "exactly what convinces someone spending £8k"
2040
+ /\bexactly what\b[^.!?\n]{0,15}?\b(?:convinces|persuades|wins|sells)\b/i,
2041
+ new RegExp(`\\bguarantee[sd]?\\b[^.!?\\n]{0,25}?\\b(?:results|rankings?|first page|page one|top spot|${SALES_RESULTS.slice(3, -1)})\\b`, "i"),
2042
+ /\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
2043
+ 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")
2044
+ ];
2016
2045
  var NEGATION = [
2017
2046
  // "It's not just a haircut, it's an experience."
2018
2047
  /\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,
@@ -2198,12 +2227,55 @@ var COPY_TELLS = [
2198
2227
  generation: 1,
2199
2228
  severity: "warn",
2200
2229
  surface: "copy",
2201
- why: "Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
2230
+ why: "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.",
2202
2231
  fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
2203
- detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1")).join("|")})`, "i")], quoted),
2232
+ detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
2233
+ fixtures: {
2234
+ flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
2235
+ pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
2236
+ }
2237
+ },
2238
+ {
2239
+ id: "sales-tagline",
2240
+ name: "Sales tagline",
2241
+ generation: 1,
2242
+ severity: "warn",
2243
+ surface: "copy",
2244
+ why: "'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.",
2245
+ fix: "Say what you will actually do and why. 'So your budget goes where it counts' becomes 'Oxford first, because that is where you want the work'.",
2246
+ detect: phrases(SALES_TAGLINES, quoted),
2247
+ fixtures: {
2248
+ flag: [
2249
+ f("content/home.md", "Strongest opportunities first, so your budget goes where it counts."),
2250
+ f("content/home.md", "A clearer online presence. More enquiries. Real results.")
2251
+ ],
2252
+ pass: [
2253
+ f("content/home.md", "We start with Oxford, then expand based on search demand and results."),
2254
+ f("content/home.md", "We report the results every month.")
2255
+ ]
2256
+ }
2257
+ },
2258
+ {
2259
+ id: "outcome-promise",
2260
+ name: "Outcome promise",
2261
+ generation: 1,
2262
+ severity: "warn",
2263
+ surface: "copy",
2264
+ why: "'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.",
2265
+ fix: "Promise the work, not the outcome. 'We'll turn that into enquiries' becomes 'We'll rewrite the page around the questions homeowners ask'. Results are measured and reported, never promised.",
2266
+ detect: phrases(OUTCOME_PROMISES, quoted),
2204
2267
  fixtures: {
2205
- flag: [f("content/home.md", "We are truly proud of it.")],
2206
- pass: [f("content/home.md", "We have fitted 400 boilers since 2009.")]
2268
+ flag: [
2269
+ f("content/home.md", "We'll turn more of that into rewiring enquiries."),
2270
+ f("content/home.md", "We add new towns once the first areas are working."),
2271
+ f("content/home.md", "We target the competitors you can outdo."),
2272
+ f("content/home.md", "We write exactly what convinces someone spending \xA38k.")
2273
+ ],
2274
+ pass: [
2275
+ f("content/home.md", "We'll rewrite your rewiring page around homeowners' questions."),
2276
+ f("content/home.md", "We record every rewiring enquiry, quotation and won job."),
2277
+ f("content/home.md", "Once the fuse board is fitted, we test every circuit.")
2278
+ ]
2207
2279
  }
2208
2280
  },
2209
2281
  {
@@ -5118,6 +5190,9 @@ var HOUSE = {
5118
5190
  // on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
5119
5191
  "chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
5120
5192
  "review-phrase": { tier: "review", label: "review tier" },
5193
+ // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
5194
+ "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
5195
+ "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
5121
5196
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
5122
5197
  "stock-phrase": { tier: "review", label: "stock phrase" },
5123
5198
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -6323,7 +6398,7 @@ var IMAGERY_TELLS = [
6323
6398
  ];
6324
6399
 
6325
6400
  // src/character/check.ts
6326
- var CATALOGUE_VERSION = "2026.09.12";
6401
+ var CATALOGUE_VERSION = "2026.10.2";
6327
6402
  var CATALOGUE = [
6328
6403
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
6329
6404
  ...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.09.12";
1255
+ declare const CATALOGUE_VERSION = "2026.10.2";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
@@ -3013,7 +3013,7 @@ declare const NEGATIVE_REASSURANCE: readonly ["no risk", "never locked in", "not
3013
3013
  /** Banned only when vague. Reported for review, never blocking. */
3014
3014
  declare const VAGUE_WORDS: readonly ["innovative", "scalable", "end-to-end", "streamline", "solutions"];
3015
3015
  /** The review tier of copy-rules.md: normal English once, a tell when stacked. */
3016
- declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolutely!", "absolutely,", "truly", "genuinely", "deeply", "ultimately", "one-size-fits-all", "one size fits all", "let me know if you'd", "the single most", "by far the most", "hands down the", "arguably the best", "bar none"];
3016
+ declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolutely!", "absolutely,", "truly", "genuinely", "deeply", "clear", "clearly", "ultimately", "one-size-fits-all", "one size fits all", "let me know if you'd", "the single most", "by far the most", "hands down the", "arguably the best", "bar none"];
3017
3017
 
3018
3018
  /**
3019
3019
  * The reason rule, one standard for every layer of an art direction.
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.09.12";
1255
+ declare const CATALOGUE_VERSION = "2026.10.2";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
@@ -3013,7 +3013,7 @@ declare const NEGATIVE_REASSURANCE: readonly ["no risk", "never locked in", "not
3013
3013
  /** Banned only when vague. Reported for review, never blocking. */
3014
3014
  declare const VAGUE_WORDS: readonly ["innovative", "scalable", "end-to-end", "streamline", "solutions"];
3015
3015
  /** The review tier of copy-rules.md: normal English once, a tell when stacked. */
3016
- declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolutely!", "absolutely,", "truly", "genuinely", "deeply", "ultimately", "one-size-fits-all", "one size fits all", "let me know if you'd", "the single most", "by far the most", "hands down the", "arguably the best", "bar none"];
3016
+ declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolutely!", "absolutely,", "truly", "genuinely", "deeply", "clear", "clearly", "ultimately", "one-size-fits-all", "one size fits all", "let me know if you'd", "the single most", "by far the most", "hands down the", "arguably the best", "bar none"];
3017
3017
 
3018
3018
  /**
3019
3019
  * The reason rule, one standard for every layer of an art direction.
package/dist/index.js CHANGED
@@ -1701,6 +1701,8 @@ var REVIEW_PHRASES = [
1701
1701
  "truly",
1702
1702
  "genuinely",
1703
1703
  "deeply",
1704
+ "clear",
1705
+ "clearly",
1704
1706
  "ultimately",
1705
1707
  "one-size-fits-all",
1706
1708
  "one size fits all",
@@ -1711,6 +1713,33 @@ var REVIEW_PHRASES = [
1711
1713
  "arguably the best",
1712
1714
  "bar none"
1713
1715
  ];
1716
+ var SALES_TAGLINES = [
1717
+ /\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
1718
+ /\breal,? (?:measurable |tangible )?results\b/i,
1719
+ /\bresults that (?:matter|count|speak|last)\b/i,
1720
+ /\bresults you can (?:see|trust|measure)\b/i,
1721
+ /\bwork(?:s|ing)? harder for you\b/i,
1722
+ /\bmake (?:every|your) (?:penny|pound) count\b/i,
1723
+ /\bevery (?:penny|pound) (?:counts|works)\b/i,
1724
+ /\bbang for your buck\b/i,
1725
+ /\bstand out from the (?:crowd|competition)\b/i,
1726
+ /\b(?:pages?|websites?|sites?|copy|content|ads?) that (?:actually |really )?(?:converts?|sells?)\b/i
1727
+ ];
1728
+ var SALES_RESULTS = "(?:enquiries|leads|customers|clients|sales|jobs|bookings|revenue|calls|traffic|profit)";
1729
+ var OUTCOME_PROMISES = [
1730
+ // "We'll turn more of that into rewiring enquiries"
1731
+ new RegExp(`\\bturn(?:s|ing)?\\b[^.!?\\n]{0,50}?\\binto\\s+(?:more\\s+|real\\s+|paying\\s+)?(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i"),
1732
+ // "once the first areas are working"
1733
+ /\bonce\b[^.!?\n]{0,40}?\b(?:is|are|starts?|begins?)\s+(?:working|paying off|performing|ranking|delivering|bringing in)\b/i,
1734
+ // "competitors you can outdo"
1735
+ /\b(?:competitors?|competition|rivals?)\b[^.!?\n]{0,25}?\b(?:you|we)\s*(?:can|will|could|['’]ll)\s+(?:easily\s+)?(?:outdo|beat|outrank|overtake|outperform|outshine)\b/i,
1736
+ /\b(?:outdo|outrank|overtake|outperform|outshine|beat)\s+(?:your|the|local|bigger)\s+(?:[\w-]+\s+)?(?:competitors|competition|rivals)\b/i,
1737
+ // "exactly what convinces someone spending £8k"
1738
+ /\bexactly what\b[^.!?\n]{0,15}?\b(?:convinces|persuades|wins|sells)\b/i,
1739
+ new RegExp(`\\bguarantee[sd]?\\b[^.!?\\n]{0,25}?\\b(?:results|rankings?|first page|page one|top spot|${SALES_RESULTS.slice(3, -1)})\\b`, "i"),
1740
+ /\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
1741
+ 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")
1742
+ ];
1714
1743
  var NEGATION = [
1715
1744
  // "It's not just a haircut, it's an experience."
1716
1745
  /\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,
@@ -1896,12 +1925,55 @@ var COPY_TELLS = [
1896
1925
  generation: 1,
1897
1926
  severity: "warn",
1898
1927
  surface: "copy",
1899
- why: "Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
1928
+ why: "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.",
1900
1929
  fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
1901
- detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1")).join("|")})`, "i")], quoted),
1930
+ detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
1931
+ fixtures: {
1932
+ flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
1933
+ pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
1934
+ }
1935
+ },
1936
+ {
1937
+ id: "sales-tagline",
1938
+ name: "Sales tagline",
1939
+ generation: 1,
1940
+ severity: "warn",
1941
+ surface: "copy",
1942
+ why: "'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.",
1943
+ fix: "Say what you will actually do and why. 'So your budget goes where it counts' becomes 'Oxford first, because that is where you want the work'.",
1944
+ detect: phrases(SALES_TAGLINES, quoted),
1945
+ fixtures: {
1946
+ flag: [
1947
+ f("content/home.md", "Strongest opportunities first, so your budget goes where it counts."),
1948
+ f("content/home.md", "A clearer online presence. More enquiries. Real results.")
1949
+ ],
1950
+ pass: [
1951
+ f("content/home.md", "We start with Oxford, then expand based on search demand and results."),
1952
+ f("content/home.md", "We report the results every month.")
1953
+ ]
1954
+ }
1955
+ },
1956
+ {
1957
+ id: "outcome-promise",
1958
+ name: "Outcome promise",
1959
+ generation: 1,
1960
+ severity: "warn",
1961
+ surface: "copy",
1962
+ why: "'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.",
1963
+ fix: "Promise the work, not the outcome. 'We'll turn that into enquiries' becomes 'We'll rewrite the page around the questions homeowners ask'. Results are measured and reported, never promised.",
1964
+ detect: phrases(OUTCOME_PROMISES, quoted),
1902
1965
  fixtures: {
1903
- flag: [f("content/home.md", "We are truly proud of it.")],
1904
- pass: [f("content/home.md", "We have fitted 400 boilers since 2009.")]
1966
+ flag: [
1967
+ f("content/home.md", "We'll turn more of that into rewiring enquiries."),
1968
+ f("content/home.md", "We add new towns once the first areas are working."),
1969
+ f("content/home.md", "We target the competitors you can outdo."),
1970
+ f("content/home.md", "We write exactly what convinces someone spending \xA38k.")
1971
+ ],
1972
+ pass: [
1973
+ f("content/home.md", "We'll rewrite your rewiring page around homeowners' questions."),
1974
+ f("content/home.md", "We record every rewiring enquiry, quotation and won job."),
1975
+ f("content/home.md", "Once the fuse board is fitted, we test every circuit.")
1976
+ ]
1905
1977
  }
1906
1978
  },
1907
1979
  {
@@ -4816,6 +4888,9 @@ var HOUSE = {
4816
4888
  // on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
4817
4889
  "chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
4818
4890
  "review-phrase": { tier: "review", label: "review tier" },
4891
+ // New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
4892
+ "sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
4893
+ "outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
4819
4894
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
4820
4895
  "stock-phrase": { tier: "review", label: "stock phrase" },
4821
4896
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -6021,7 +6096,7 @@ var IMAGERY_TELLS = [
6021
6096
  ];
6022
6097
 
6023
6098
  // src/character/check.ts
6024
- var CATALOGUE_VERSION = "2026.09.12";
6099
+ var CATALOGUE_VERSION = "2026.10.2";
6025
6100
  var CATALOGUE = [
6026
6101
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
6027
6102
  ...SPECIFICITY_COPY_TELLS,