@domandigital/craft 0.20.0 → 0.20.2
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 +13 -0
- package/CHARACTER.md +4 -2
- package/COPY.md +3 -1
- package/README.md +4 -1
- package/dist/{audit-43PC4LMS.js → audit-BN2GSFKT.js} +3 -3
- package/dist/audit.cjs +80 -5
- package/dist/audit.js +80 -5
- package/dist/{chunk-4LJFZH2I.js → chunk-2OBDCIVW.js} +2 -2
- package/dist/{chunk-CZ7KT452.js → chunk-7DQDF3VL.js} +76 -4
- package/dist/{chunk-RFU5CIST.js → chunk-IJXDBYHH.js} +5 -5
- package/dist/{chunk-WK2WMWTH.js → chunk-JGWOA4LI.js} +5 -2
- package/dist/{chunk-LZVTN2KI.js → chunk-JLLULDVW.js} +14 -14
- package/dist/{chunk-3CD3DTEI.js → chunk-TCMM3PTK.js} +4 -4
- package/dist/{chunk-VDUIIWG7.js → chunk-YOZLPNJS.js} +1 -1
- package/dist/{chunk-H4UF2I7V.js → chunk-YRXMPI3T.js} +5 -5
- package/dist/{cli-5DONIRMR.js → cli-A6SNEMRL.js} +9 -9
- package/dist/{cli-3IAIKGCP.js → cli-DZGQOIBX.js} +6 -6
- package/dist/{cli-YHS2VPWQ.js → cli-FF2YDHA4.js} +3 -3
- package/dist/{cli-EDQNTWDD.js → cli-KJRHO3YO.js} +4 -4
- package/dist/{cli-73JCUXAV.js → cli-LVYTLT2A.js} +5 -5
- package/dist/{cli-EOXIJOJN.js → cli-U7PJQKZT.js} +6 -6
- package/dist/{cli-YUVATZD5.js → cli-YQEVPRL5.js} +5 -5
- package/dist/{cli-VPARW6FY.js → cli-ZXDGVDYU.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.cjs +80 -5
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +80 -5
- package/dist/phone.cjs +20 -1
- package/dist/phone.d.cts +30 -1
- package/dist/phone.d.ts +30 -1
- package/dist/phone.js +18 -1
- package/dist/{viewports-cli-RYV4LDQK.js → viewports-cli-WWWWEBKI.js} +5 -4
- package/package.json +1 -1
- package/word-lists.json +3 -1
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-JLLULDVW.js";
|
|
7
7
|
import {
|
|
8
8
|
toJson
|
|
9
9
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
hueSwatch,
|
|
23
23
|
nullPrompt,
|
|
24
24
|
scanSource
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-JGWOA4LI.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-7DQDF3VL.js";
|
|
29
29
|
|
|
30
30
|
// src/null/cli.ts
|
|
31
31
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
@@ -120,7 +120,7 @@ async function build(flags, io) {
|
|
|
120
120
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
121
121
|
if (toShoot.length) {
|
|
122
122
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
123
|
-
const { snapshotUrls } = await import("./audit-
|
|
123
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
|
|
124
124
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
125
125
|
results.forEach((r, i) => {
|
|
126
126
|
const id = toShoot[i];
|
|
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
|
|
|
241
241
|
const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
|
|
242
242
|
if (toShoot.length) {
|
|
243
243
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
|
|
244
|
-
const { snapshotUrls } = await import("./audit-
|
|
244
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.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));
|
|
@@ -3,21 +3,21 @@ import {
|
|
|
3
3
|
} from "./chunk-OKVHWXYV.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-TCMM3PTK.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-
|
|
13
|
+
} from "./chunk-IJXDBYHH.js";
|
|
14
14
|
import {
|
|
15
15
|
PAGE_TYPES,
|
|
16
16
|
validateDirection
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-2OBDCIVW.js";
|
|
18
18
|
import {
|
|
19
19
|
loadNull
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-YRXMPI3T.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-
|
|
31
|
+
} from "./chunk-JLLULDVW.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-
|
|
46
|
-
import "./chunk-
|
|
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-
|
|
129
|
+
const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
|
|
130
130
|
snap = await snapshotUrl(target2);
|
|
131
131
|
}
|
|
132
132
|
const fp = fingerprint(snap);
|
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
formatPairs,
|
|
5
5
|
loadEstate,
|
|
6
6
|
runEstate
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-IJXDBYHH.js";
|
|
8
|
+
import "./chunk-2OBDCIVW.js";
|
|
9
|
+
import "./chunk-YRXMPI3T.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-JLLULDVW.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-JGWOA4LI.js";
|
|
15
|
+
import "./chunk-7DQDF3VL.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
formatComponentPairs,
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
SOURCE_FILE,
|
|
11
11
|
readPaths
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JLLULDVW.js";
|
|
13
13
|
import {
|
|
14
14
|
toJson
|
|
15
15
|
} from "./chunk-QIEDBL4J.js";
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-JGWOA4LI.js";
|
|
17
|
+
import "./chunk-7DQDF3VL.js";
|
|
18
18
|
import "./chunk-QYBAVU3P.js";
|
|
19
19
|
|
|
20
20
|
// src/phone/cli.ts
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YRXMPI3T.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-JLLULDVW.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-JGWOA4LI.js";
|
|
18
|
+
import "./chunk-7DQDF3VL.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YRXMPI3T.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JLLULDVW.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-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-JGWOA4LI.js";
|
|
25
|
+
import "./chunk-7DQDF3VL.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-
|
|
191
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.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];
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-TCMM3PTK.js";
|
|
8
|
+
import "./chunk-YOZLPNJS.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-YRXMPI3T.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-JLLULDVW.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-JGWOA4LI.js";
|
|
16
|
+
import "./chunk-7DQDF3VL.js";
|
|
17
17
|
import "./chunk-QYBAVU3P.js";
|
|
18
18
|
export {
|
|
19
19
|
describeComponents,
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
PROPOSED,
|
|
8
8
|
researchPrompt,
|
|
9
9
|
validateDirection
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2OBDCIVW.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-JLLULDVW.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
isCream,
|
|
27
27
|
oklchToRgb,
|
|
28
28
|
parseColour
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import "./chunk-
|
|
29
|
+
} from "./chunk-JGWOA4LI.js";
|
|
30
|
+
import "./chunk-7DQDF3VL.js";
|
|
31
31
|
import "./chunk-QYBAVU3P.js";
|
|
32
32
|
|
|
33
33
|
// src/direction/cli.ts
|
|
@@ -208,7 +208,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
|
208
208
|
async function snapshotFrom(target, cwd) {
|
|
209
209
|
if (!target) return void 0;
|
|
210
210
|
if (/^https?:\/\//.test(target)) {
|
|
211
|
-
const { snapshotUrl } = await import("./audit-
|
|
211
|
+
const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
|
|
212
212
|
return snapshotUrl(target);
|
|
213
213
|
}
|
|
214
214
|
return readSnapshot(readJson(resolve(cwd, target)), target);
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
run
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JLLULDVW.js";
|
|
5
5
|
import "./chunk-QIEDBL4J.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
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: [
|
|
2206
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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: [
|
|
1904
|
-
|
|
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.
|
|
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,
|
package/dist/phone.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(phone_exports, {
|
|
|
24
24
|
DEFAULT_MEASURED_DIR: () => DEFAULT_MEASURED_DIR,
|
|
25
25
|
DEFAULT_SWEEP: () => DEFAULT_SWEEP,
|
|
26
26
|
DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
|
|
27
|
+
DESKTOP: () => DESKTOP,
|
|
27
28
|
EMPTY_BASELINE: () => EMPTY_BASELINE,
|
|
28
29
|
LADDER: () => LADDER,
|
|
29
30
|
PHONE: () => PHONE,
|
|
@@ -38,6 +39,7 @@ __export(phone_exports, {
|
|
|
38
39
|
mainActionFor: () => mainActionFor,
|
|
39
40
|
measurePhone: () => measurePhone,
|
|
40
41
|
measurePhoneInPage: () => measurePhoneInPage,
|
|
42
|
+
measureRoute: () => measureRoute,
|
|
41
43
|
measureSite: () => measureSite,
|
|
42
44
|
measureViewportInPage: () => measureViewportInPage,
|
|
43
45
|
measureViewports: () => measureViewports,
|
|
@@ -724,6 +726,21 @@ async function measureViewports(page, options) {
|
|
|
724
726
|
await page.evaluate(() => document.fonts?.ready.then(() => void 0), void 0);
|
|
725
727
|
return finish(options.route, await collect(page, options), options.out);
|
|
726
728
|
}
|
|
729
|
+
var DESKTOP = { viewport: { width: 1024, height: 768 }, deviceScaleFactor: 1 };
|
|
730
|
+
async function measureRoute(open, options) {
|
|
731
|
+
if (!options.route.startsWith("/")) throw new Error(`measureRoute: route "${options.route}" should start with /`);
|
|
732
|
+
const collected = [];
|
|
733
|
+
for (const scope of ["phone", "wide"]) {
|
|
734
|
+
const { page, close } = await open(scope);
|
|
735
|
+
try {
|
|
736
|
+
await page.evaluate(() => document.fonts?.ready.then(() => void 0), void 0);
|
|
737
|
+
collected.push(await collect(page, { ...options, scope }));
|
|
738
|
+
} finally {
|
|
739
|
+
await close?.();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return finish(options.route, { perViewport: collected.flatMap((c) => c.perViewport), tested: collected.flatMap((c) => c.tested) }, options.out);
|
|
743
|
+
}
|
|
727
744
|
async function measureSite(browser, options) {
|
|
728
745
|
const result = { routes: [], errors: [] };
|
|
729
746
|
for (const route of options.routes) {
|
|
@@ -731,7 +748,7 @@ async function measureSite(browser, options) {
|
|
|
731
748
|
let failed = null;
|
|
732
749
|
for (const [scope, context] of [
|
|
733
750
|
["phone", PHONE],
|
|
734
|
-
["wide",
|
|
751
|
+
["wide", DESKTOP]
|
|
735
752
|
]) {
|
|
736
753
|
const ctx = await browser.newContext({ ...context });
|
|
737
754
|
try {
|
|
@@ -775,6 +792,7 @@ async function measurePhone(page, options) {
|
|
|
775
792
|
DEFAULT_MEASURED_DIR,
|
|
776
793
|
DEFAULT_SWEEP,
|
|
777
794
|
DEFAULT_THRESHOLDS,
|
|
795
|
+
DESKTOP,
|
|
778
796
|
EMPTY_BASELINE,
|
|
779
797
|
LADDER,
|
|
780
798
|
PHONE,
|
|
@@ -789,6 +807,7 @@ async function measurePhone(page, options) {
|
|
|
789
807
|
mainActionFor,
|
|
790
808
|
measurePhone,
|
|
791
809
|
measurePhoneInPage,
|
|
810
|
+
measureRoute,
|
|
792
811
|
measureSite,
|
|
793
812
|
measureViewportInPage,
|
|
794
813
|
measureViewports,
|