@domandigital/craft 0.20.4 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/CHARACTER.md +3 -1
- package/COPY.md +9 -3
- package/README.md +73 -3
- package/dist/{audit-BN2GSFKT.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +104 -3
- package/dist/audit.js +104 -3
- package/dist/{chunk-NQAYRK2K.js → chunk-4WAIOZ4W.js} +14 -14
- package/dist/{chunk-YOZLPNJS.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/chunk-BQ7H23QP.js +261 -0
- package/dist/{chunk-2276FJVN.js → chunk-BYXDX66D.js} +4 -4
- package/dist/{chunk-JGWOA4LI.js → chunk-DE2ZAV4G.js} +6 -2
- package/dist/{chunk-KQF44RI3.js → chunk-FNB3NP57.js} +5 -5
- package/dist/{chunk-S3L7TEJF.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-7DQDF3VL.js → chunk-ONA73THJ.js} +99 -2
- package/dist/{chunk-GBDPNNC2.js → chunk-STWH5EYU.js} +5 -5
- package/dist/{cli-QGJB6OUB.js → cli-5QI6NIZN.js} +6 -6
- package/dist/{cli-AK4EQHUF.js → cli-5VHX5EJF.js} +5 -5
- package/dist/{cli-ZXDGVDYU.js → cli-7QL26YPR.js} +1 -1
- package/dist/cli-EYHRJJAX.js +241 -0
- package/dist/{cli-AFKXDGUH.js → cli-PBCU7Z64.js} +4 -4
- package/dist/{cli-6AZVLNWL.js → cli-RHDNHL4U.js} +5 -5
- package/dist/{cli-OYVZOHHV.js → cli-XAWJWK7I.js} +6 -6
- package/dist/{cli-7HKVVL3P.js → cli-YSGPGQ7R.js} +9 -9
- package/dist/cli.js +3 -3
- package/dist/index.cjs +104 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +104 -3
- package/dist/phone.cjs +137 -11
- package/dist/phone.d.cts +86 -12
- package/dist/phone.d.ts +86 -12
- package/dist/phone.js +128 -11
- package/dist/{viewports-cli-WWWWEBKI.js → viewports-cli-XYYX4SKV.js} +12 -8
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-IJUZ7RV4.js +0 -151
- package/dist/cli-6INEFJYN.js +0 -122
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// src/phone/judge.ts
|
|
2
|
+
var hasReason = (entry) => typeof entry.reason === "string" && entry.reason.trim() !== "";
|
|
3
|
+
function parseBaseline(raw, where) {
|
|
4
|
+
const b = raw ?? {};
|
|
5
|
+
const entries = [...b.static ?? [], ...b.measured ?? []];
|
|
6
|
+
const bad = entries.find((e) => e.reason !== void 0 && typeof e.reason !== "string");
|
|
7
|
+
if (bad) throw new Error(`${where}: an entry has a "reason" that is not text: ${JSON.stringify(bad)}`);
|
|
8
|
+
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
9
|
+
}
|
|
10
|
+
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
11
|
+
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
12
|
+
var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
|
|
13
|
+
var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
|
|
14
|
+
var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
|
|
15
|
+
var OK = /mobile-first-ok\b:?(.*)/;
|
|
16
|
+
var VAR_OPACITY = /[^\s"'`]*var\([^)]*\)\]\/[0-9]+/g;
|
|
17
|
+
function reasonOf(line) {
|
|
18
|
+
const m = line?.match(OK);
|
|
19
|
+
return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
|
|
20
|
+
}
|
|
21
|
+
function scanPhoneFirst(files, exceptions = [], options = {}) {
|
|
22
|
+
const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
|
|
23
|
+
const findings = [];
|
|
24
|
+
let excused = 0;
|
|
25
|
+
for (const { path, text } of files) {
|
|
26
|
+
if (excusedFiles.has(path)) continue;
|
|
27
|
+
const lines = text.split("\n");
|
|
28
|
+
const lineOf = (index) => text.slice(0, index).split("\n").length;
|
|
29
|
+
const found = [];
|
|
30
|
+
for (const m of text.matchAll(MEDIA)) {
|
|
31
|
+
if (CAPS_WIDTH.test(m[1])) found.push({ line: lineOf(m.index), what: `${m[0].slice(0, m[0].length - m[1].length)}${m[1].replace(/\s+/g, " ").trimEnd()}` });
|
|
32
|
+
}
|
|
33
|
+
for (const m of text.matchAll(VARIANT)) {
|
|
34
|
+
if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
|
|
35
|
+
}
|
|
36
|
+
const silent = /* @__PURE__ */ new Set();
|
|
37
|
+
if (options.tailwind3?.(path)) {
|
|
38
|
+
for (const m of text.matchAll(VAR_OPACITY)) {
|
|
39
|
+
silent.add(found.push({ line: lineOf(m.index), what: `${m[0]} (Tailwind 3 emits no CSS for opacity on a var() colour)` }) - 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const order = found.map((f, i) => ({ ...f, silent: silent.has(i) })).sort((a, b) => a.line - b.line);
|
|
43
|
+
for (const { line, what, silent: cannotExcuse } of order) {
|
|
44
|
+
if (cannotExcuse) {
|
|
45
|
+
findings.push({ file: path, line, what });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const here = reasonOf(lines[line - 1]);
|
|
49
|
+
const above = reasonOf(lines[line - 2]);
|
|
50
|
+
if (here || above) {
|
|
51
|
+
excused++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
findings.push({ file: path, line, what: here === "" || above === "" ? `${what} (mobile-first-ok with no reason)` : what });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { findings, excused };
|
|
58
|
+
}
|
|
59
|
+
function checkExceptions(exceptions, exists) {
|
|
60
|
+
const problems = [];
|
|
61
|
+
exceptions.forEach((e, i) => {
|
|
62
|
+
const at = `exceptions[${i}]`;
|
|
63
|
+
const x = e;
|
|
64
|
+
if (typeof x?.file !== "string" || !x.file.trim()) problems.push(`${at}: names no file`);
|
|
65
|
+
else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.file}): no reason. Say why this file needs a max-width query, or remove the exception.`);
|
|
66
|
+
else if (!exists(x.file)) problems.push(`${at}: ${x.file} is not there. Remove the exception.`);
|
|
67
|
+
});
|
|
68
|
+
return problems;
|
|
69
|
+
}
|
|
70
|
+
var normaliseElement = (element) => element.replace(/("[^]*)$/, (text) => text.replace(/\d+/g, "#"));
|
|
71
|
+
var tally = (items, key) => {
|
|
72
|
+
const m = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
const k = key(item);
|
|
75
|
+
const had = m.get(k);
|
|
76
|
+
if (had) had.count++;
|
|
77
|
+
else m.set(k, { item, count: 1 });
|
|
78
|
+
}
|
|
79
|
+
return m;
|
|
80
|
+
};
|
|
81
|
+
function judgeStatic(findings, baseline) {
|
|
82
|
+
const byFile = tally(findings, (f) => f.file);
|
|
83
|
+
const allowed = new Map(baseline.static.map((b) => [b.file, b.count]));
|
|
84
|
+
const fresh = [];
|
|
85
|
+
const stale = [];
|
|
86
|
+
let known = 0;
|
|
87
|
+
for (const [file, { count }] of byFile) {
|
|
88
|
+
const allow = allowed.get(file) ?? 0;
|
|
89
|
+
known += Math.min(count, allow);
|
|
90
|
+
if (count > allow) {
|
|
91
|
+
const here = findings.filter((f) => f.file === file);
|
|
92
|
+
const lead = allow ? `${file}: ${count} desktop-first rules, the baseline allows ${allow}. One of these is new:` : `${file}:`;
|
|
93
|
+
fresh.push(`${lead}
|
|
94
|
+
${here.map((f) => ` line ${f.line}: ${f.what}`).join("\n")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const { file, count } of baseline.static) {
|
|
98
|
+
const now = byFile.get(file)?.count ?? 0;
|
|
99
|
+
if (now < count) stale.push(`${file}: the baseline allows ${count}, there ${now === 1 ? "is" : "are"} ${now} now. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
|
|
100
|
+
}
|
|
101
|
+
return { fresh, stale, known };
|
|
102
|
+
}
|
|
103
|
+
var measuredKey = (route, check, element) => `${route}\0${check}\0${element}`;
|
|
104
|
+
function judgeMeasured(routes, baseline) {
|
|
105
|
+
const fresh = [];
|
|
106
|
+
const stale = [];
|
|
107
|
+
let known = 0;
|
|
108
|
+
const allowed = new Map(baseline.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
109
|
+
const seen = /* @__PURE__ */ new Map();
|
|
110
|
+
const measured = new Set(routes.map((r) => r.route));
|
|
111
|
+
for (const { route, findings } of routes) {
|
|
112
|
+
const counted = tally(findings, (f) => measuredKey(route, f.check, normaliseElement(f.element)));
|
|
113
|
+
for (const [key, { item, count }] of counted) {
|
|
114
|
+
seen.set(key, count);
|
|
115
|
+
const allow = allowed.get(key) ?? 0;
|
|
116
|
+
known += Math.min(count, allow);
|
|
117
|
+
if (count > allow) {
|
|
118
|
+
const times = allow ? ` (${count} found, the baseline allows ${allow})` : count > 1 ? ` (${count} of them)` : "";
|
|
119
|
+
fresh.push(`${route}: ${LABEL[item.check]} ${item.element}: ${item.detail}${times}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const missing = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const b of baseline.measured) {
|
|
125
|
+
const key = measuredKey(b.route, b.check, b.element);
|
|
126
|
+
if (!measured.has(b.route)) {
|
|
127
|
+
if (!missing.has(b.route)) stale.push(`${b.route}: in the baseline but not measured this run. Measure it, or remove its lines.`);
|
|
128
|
+
missing.add(b.route);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const now = seen.get(key) ?? 0;
|
|
132
|
+
if (now < b.count) stale.push(`${b.route}: ${LABEL[b.check]} ${b.element}: the baseline allows ${b.count}, ${now} found. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
|
|
133
|
+
}
|
|
134
|
+
return { fresh, stale, known };
|
|
135
|
+
}
|
|
136
|
+
var LABEL = {
|
|
137
|
+
scroll: "scrolls sideways",
|
|
138
|
+
target: "small target",
|
|
139
|
+
field: "small field",
|
|
140
|
+
text: "small text",
|
|
141
|
+
title: "title cut off",
|
|
142
|
+
overlap: "text overlaps text",
|
|
143
|
+
action: "main action out of reach",
|
|
144
|
+
layers: "fixed layers cover too much",
|
|
145
|
+
landscape: "landscape screen eaten by fixed layers",
|
|
146
|
+
image: "image not sized for its place",
|
|
147
|
+
input: "wrong keyboard",
|
|
148
|
+
autocomplete: "field the phone cannot fill in",
|
|
149
|
+
tel: "phone number is not a link",
|
|
150
|
+
linelength: "line too long"
|
|
151
|
+
};
|
|
152
|
+
function writeBaseline(findings, routes, note, previous) {
|
|
153
|
+
const byFile = tally(findings, (f) => f.file);
|
|
154
|
+
const measured = routes.flatMap(
|
|
155
|
+
({ route, findings: findings2 }) => [...tally(findings2, (f) => measuredKey(route, f.check, normaliseElement(f.element))).values()].map(({ item, count }) => ({ route, check: item.check, element: normaliseElement(item.element), count }))
|
|
156
|
+
);
|
|
157
|
+
const staticReasons = new Map((previous?.static ?? []).filter(hasReason).map((b) => [b.file, b.reason]));
|
|
158
|
+
const measuredReasons = new Map((previous?.measured ?? []).filter(hasReason).map((b) => [measuredKey(b.route, b.check, b.element), b.reason]));
|
|
159
|
+
return {
|
|
160
|
+
...note ? { note } : {},
|
|
161
|
+
static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what), ...staticReasons.has(item.file) ? { reason: staticReasons.get(item.file) } : {} })).sort((a, b) => a.file.localeCompare(b.file)),
|
|
162
|
+
measured: measured.map((m) => measuredReasons.has(measuredKey(m.route, m.check, m.element)) ? { ...m, reason: measuredReasons.get(measuredKey(m.route, m.check, m.element)) } : m).sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function judgeGrowth(current, base, baseName = "main") {
|
|
166
|
+
const out = [];
|
|
167
|
+
const baseStatic = new Map(base.static.map((b) => [b.file, b.count]));
|
|
168
|
+
for (const b of current.static) {
|
|
169
|
+
const was = baseStatic.get(b.file) ?? 0;
|
|
170
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.file}: ${was ? `${b.count} desktop-first rules, ${baseName} allows ${was}` : `${b.count} desktop-first rule${b.count === 1 ? "" : "s"} added to the baseline`}`);
|
|
171
|
+
}
|
|
172
|
+
const baseMeasured = new Map(base.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
173
|
+
for (const b of current.measured) {
|
|
174
|
+
const was = baseMeasured.get(measuredKey(b.route, b.check, b.element)) ?? 0;
|
|
175
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.route}: ${LABEL[b.check]} ${b.element}: ${was ? `${b.count} in the baseline, ${baseName} allows ${was}` : `added to the baseline (${b.count})`}`);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
var hasUnexcusedEntries = (b) => [...b.static, ...b.measured].some((e) => !hasReason(e));
|
|
180
|
+
var PAGE_FILE = /(?:^|\/)page\.(?:tsx|ts|jsx|js|mdx)$/;
|
|
181
|
+
var INTERCEPTING = /^\(\.{1,3}\)/;
|
|
182
|
+
function routesFromPages(files) {
|
|
183
|
+
const routes = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const rel = file.replace(/\\/g, "/");
|
|
186
|
+
if (!PAGE_FILE.test(rel)) continue;
|
|
187
|
+
const dirs = rel.split("/").slice(0, -1);
|
|
188
|
+
if (dirs.some((d) => INTERCEPTING.test(d) || d.startsWith("_"))) continue;
|
|
189
|
+
const url = dirs.filter((d) => !/^\(.*\)$/.test(d) && !d.startsWith("@"));
|
|
190
|
+
routes.add(`/${url.join("/")}`);
|
|
191
|
+
}
|
|
192
|
+
return [...routes].sort();
|
|
193
|
+
}
|
|
194
|
+
function routesFromManifest(manifest) {
|
|
195
|
+
const routes = /* @__PURE__ */ new Set();
|
|
196
|
+
for (const [key, route] of Object.entries(manifest ?? {})) {
|
|
197
|
+
if (!/(?:^|\/)page$/.test(key) || typeof route !== "string") continue;
|
|
198
|
+
if (route.split("/").some((d) => d.startsWith("_"))) continue;
|
|
199
|
+
routes.add(route);
|
|
200
|
+
}
|
|
201
|
+
return [...routes].sort();
|
|
202
|
+
}
|
|
203
|
+
var segmentsOf = (route) => route.split("/").filter(Boolean);
|
|
204
|
+
var rank = (seg) => seg.startsWith("[[...") ? 3 : seg.startsWith("[...") ? 2 : seg.startsWith("[") ? 1 : 0;
|
|
205
|
+
var cleanRoute = (route) => {
|
|
206
|
+
const path = route.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
207
|
+
return path === "" ? "/" : path;
|
|
208
|
+
};
|
|
209
|
+
function routeMatches(pattern, route) {
|
|
210
|
+
if (pattern === route) return true;
|
|
211
|
+
const want = segmentsOf(pattern);
|
|
212
|
+
const have = segmentsOf(route);
|
|
213
|
+
for (let i = 0; i < want.length; i++) {
|
|
214
|
+
const seg = want[i];
|
|
215
|
+
if (seg.startsWith("[[...")) return true;
|
|
216
|
+
if (seg.startsWith("[...")) return have.length > i;
|
|
217
|
+
if (have[i] === void 0) return false;
|
|
218
|
+
if (!seg.startsWith("[") && seg !== have[i]) return false;
|
|
219
|
+
}
|
|
220
|
+
return want.length === have.length;
|
|
221
|
+
}
|
|
222
|
+
var moreSpecific = (a, b) => {
|
|
223
|
+
const x = segmentsOf(a).map(rank);
|
|
224
|
+
const y = segmentsOf(b).map(rank);
|
|
225
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
226
|
+
return x.length > y.length;
|
|
227
|
+
};
|
|
228
|
+
var excludes = (entry, page) => entry.endsWith("/*") ? page === entry.slice(0, -2) || page.startsWith(entry.slice(0, -1)) : entry === page;
|
|
229
|
+
function judgeRoutes(pages, measuredRoutes, excluded = []) {
|
|
230
|
+
const problems = [];
|
|
231
|
+
excluded.forEach((e, i) => {
|
|
232
|
+
const at = `excludeRoutes[${i}]`;
|
|
233
|
+
const x = e;
|
|
234
|
+
if (typeof x?.route !== "string" || !x.route.startsWith("/")) problems.push(`${at}: names no route (start it with /)`);
|
|
235
|
+
else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.route}): no reason. Say why the gate cannot measure this page, or remove the entry.`);
|
|
236
|
+
else if (!pages.some((p) => excludes(x.route, p))) problems.push(`${at}: ${x.route} is not a page route of the app. Remove the entry.`);
|
|
237
|
+
});
|
|
238
|
+
const named = excluded.filter((e) => typeof e?.route === "string" && typeof e.reason === "string" && e.reason.trim());
|
|
239
|
+
const covered = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const route of measuredRoutes.map(cleanRoute)) {
|
|
241
|
+
const hit = pages.filter((p) => routeMatches(p, route)).sort((a, b) => moreSpecific(a, b) ? -1 : moreSpecific(b, a) ? 1 : 0)[0];
|
|
242
|
+
if (hit) covered.add(hit);
|
|
243
|
+
}
|
|
244
|
+
for (const page of pages) if (named.some((e) => excludes(e.route, page))) covered.add(page);
|
|
245
|
+
return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export {
|
|
249
|
+
parseBaseline,
|
|
250
|
+
EMPTY_BASELINE,
|
|
251
|
+
scanPhoneFirst,
|
|
252
|
+
checkExceptions,
|
|
253
|
+
judgeStatic,
|
|
254
|
+
judgeMeasured,
|
|
255
|
+
writeBaseline,
|
|
256
|
+
judgeGrowth,
|
|
257
|
+
hasUnexcusedEntries,
|
|
258
|
+
routesFromPages,
|
|
259
|
+
routesFromManifest,
|
|
260
|
+
judgeRoutes
|
|
261
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
snapshotUrl,
|
|
3
3
|
snapshotUrls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6DRBFDWT.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FNB3NP57.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-
|
|
17
|
+
} from "./chunk-4WAIOZ4W.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-
|
|
31
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
32
32
|
|
|
33
33
|
// src/audit/cli.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
parseFile,
|
|
10
10
|
sentencesOf,
|
|
11
11
|
specificity
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ONA73THJ.js";
|
|
13
13
|
import {
|
|
14
14
|
SNAPSHOT_VERSION
|
|
15
15
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -2428,6 +2428,10 @@ 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" },
|
|
2433
|
+
// New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
|
|
2434
|
+
"matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
|
|
2431
2435
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2432
2436
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
2433
2437
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3023,7 +3027,7 @@ var IMAGERY_TELLS = [
|
|
|
3023
3027
|
];
|
|
3024
3028
|
|
|
3025
3029
|
// src/character/check.ts
|
|
3026
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
3030
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
3027
3031
|
var CATALOGUE = [
|
|
3028
3032
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3029
3033
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-4WAIOZ4W.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-DE2ZAV4G.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ONA73THJ.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-LQ2TDISV.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-LQ2TDISV.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));
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
levenshtein,
|
|
11
11
|
normaliseFamily,
|
|
12
12
|
parseColour
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
14
14
|
import {
|
|
15
15
|
TRADE_NOUNS
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-ONA73THJ.js";
|
|
17
17
|
|
|
18
18
|
// src/direction/types.ts
|
|
19
19
|
var DIRECTION_VERSION = 2;
|
|
@@ -645,6 +645,33 @@ var OUTCOME_PROMISES = [
|
|
|
645
645
|
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
646
646
|
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")
|
|
647
647
|
];
|
|
648
|
+
var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
|
|
649
|
+
var MATEY = [
|
|
650
|
+
// "Alright Carlo,", "Oi Dave", "Alright, mate"
|
|
651
|
+
/\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
|
|
652
|
+
// "mate" as a form of address, not "a mate who knows"
|
|
653
|
+
/,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
|
|
654
|
+
/\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
|
|
655
|
+
/\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
|
|
656
|
+
// "Right then," opening a sentence, not "the right then left turn"
|
|
657
|
+
new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
|
|
658
|
+
// "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
|
|
659
|
+
/\b(?:all|get(?:ting)?\s+(?:it|that|this|you|them|everything)|got\s+(?:it|that|you)|you['’]re)\s+sorted\b(?!\s+(?:by|in|into|alphabetically)\b)/i,
|
|
660
|
+
new RegExp(`${SENTENCE_START}Sorted[.!]`),
|
|
661
|
+
// "proper" as an intensifier before an adjective, not "proper planning"
|
|
662
|
+
/\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
|
|
663
|
+
/\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
|
|
664
|
+
/\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
|
|
665
|
+
/\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
|
|
666
|
+
/\bcheck\s+out\s+(?:our|my|this|these)\b/i
|
|
667
|
+
];
|
|
668
|
+
var CLIPPED = [
|
|
669
|
+
/\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
|
|
670
|
+
/\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
|
|
671
|
+
new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
|
|
672
|
+
// "Alright Carlo, built you a proper site": the "I" left out
|
|
673
|
+
/(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
|
|
674
|
+
];
|
|
648
675
|
var NEGATION = [
|
|
649
676
|
// "It's not just a haircut, it's an experience."
|
|
650
677
|
/\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,
|
|
@@ -881,6 +908,40 @@ var COPY_TELLS = [
|
|
|
881
908
|
]
|
|
882
909
|
}
|
|
883
910
|
},
|
|
911
|
+
{
|
|
912
|
+
id: "matey-register",
|
|
913
|
+
name: "Matey register",
|
|
914
|
+
generation: 2,
|
|
915
|
+
severity: "warn",
|
|
916
|
+
surface: "copy",
|
|
917
|
+
why: "'Alright Dave,', 'no worries, we'll get you sorted', 'Quick one:', 'have look at': a put-on, over-familiar British voice, with the small words clipped out. A model asked to sound British writes it; the UK is broad, and a client reads it as cringe and a little rude.",
|
|
918
|
+
fix: "Write plain, complete British English. 'Alright Dave, built you a site' becomes 'Hi Dave, I have built you a site'; 'No worries, we'll get you sorted' becomes 'We can fit you in on Thursday'.",
|
|
919
|
+
detect: phrases([...MATEY, ...CLIPPED], quoted),
|
|
920
|
+
fixtures: {
|
|
921
|
+
flag: [
|
|
922
|
+
// From the portal's seeded prospect follow-ups, 2026-10-19.
|
|
923
|
+
f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
|
|
924
|
+
f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
|
|
925
|
+
f("emails/followup.md", "Carlo, built the shop a proper site."),
|
|
926
|
+
f("content/contact.md", "No worries, we'll get you sorted."),
|
|
927
|
+
f("emails/quote.md", "Take look on these photos and give it go."),
|
|
928
|
+
f("emails/quote.md", "Quick one: is Thursday still good?"),
|
|
929
|
+
f("emails/quote.md", "Right then, here is the plan."),
|
|
930
|
+
f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
|
|
931
|
+
],
|
|
932
|
+
pass: [
|
|
933
|
+
f("content/home.md", "Proper planning saves a second visit."),
|
|
934
|
+
f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
|
|
935
|
+
f("content/directions.md", "At the junction, take the right then left turn."),
|
|
936
|
+
// Chair and Blade's own approved line, and "mate" as a noun.
|
|
937
|
+
f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
|
|
938
|
+
f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
|
|
939
|
+
f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
|
|
940
|
+
f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
|
|
941
|
+
f("content/home.md", "Built to last, and made for older houses.")
|
|
942
|
+
]
|
|
943
|
+
}
|
|
944
|
+
},
|
|
884
945
|
{
|
|
885
946
|
id: "hollow-imperative",
|
|
886
947
|
name: "Hollow imperative",
|
|
@@ -1081,7 +1142,12 @@ var FALSE_RANGE = [
|
|
|
1081
1142
|
];
|
|
1082
1143
|
var RESIDUE = [
|
|
1083
1144
|
/\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
|
|
1084
|
-
|
|
1145
|
+
// At the start of a line, or after a sentence that ends in a full stop or
|
|
1146
|
+
// an exclamation mark: "I hope this email finds you well! Certainly, here
|
|
1147
|
+
// is the handover request." slipped through when only a line start counted
|
|
1148
|
+
// (found live, 2026-10-19). Not after a question mark: an FAQ answer may
|
|
1149
|
+
// honestly open "Do you work weekends? Absolutely, here are our hours."
|
|
1150
|
+
/(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
|
|
1085
1151
|
/\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
1152
|
/\blet me know if you(?:['’]d| would) like me to\b/i,
|
|
1087
1153
|
/\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
|
|
@@ -1097,6 +1163,13 @@ var RESIDUE = [
|
|
|
1097
1163
|
/[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
|
|
1098
1164
|
/【\d+(?:[:†][^】]{0,40})?】/
|
|
1099
1165
|
];
|
|
1166
|
+
var STOCK_EMAIL = [
|
|
1167
|
+
/\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
|
|
1168
|
+
/\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
|
|
1169
|
+
/\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
|
|
1170
|
+
/\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
|
|
1171
|
+
/\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
|
|
1172
|
+
];
|
|
1100
1173
|
var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
|
|
1101
1174
|
function residueHits(ctx) {
|
|
1102
1175
|
const hits = phrases(RESIDUE, quoted, allText)(ctx);
|
|
@@ -1178,15 +1251,39 @@ var RESEARCH_TELLS = [
|
|
|
1178
1251
|
f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
|
|
1179
1252
|
f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
|
|
1180
1253
|
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.")
|
|
1254
|
+
f("content/home.md", "Here's a more concise version for the hero."),
|
|
1255
|
+
// The client handover email that a clean copy-check let through, 2026-10-19.
|
|
1256
|
+
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
1257
|
],
|
|
1183
1258
|
pass: [
|
|
1184
1259
|
f("content/home.md", "Certainly the busiest week of the year: book early."),
|
|
1260
|
+
f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
|
|
1185
1261
|
f("content/home.md", "Here's the price list for 2026."),
|
|
1186
1262
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
|
|
1187
1263
|
]
|
|
1188
1264
|
}
|
|
1189
1265
|
},
|
|
1266
|
+
{
|
|
1267
|
+
id: "stock-email-line",
|
|
1268
|
+
name: "Stock email line",
|
|
1269
|
+
generation: 1,
|
|
1270
|
+
severity: "warn",
|
|
1271
|
+
surface: "copy",
|
|
1272
|
+
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.",
|
|
1273
|
+
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.'",
|
|
1274
|
+
detect: phrases(STOCK_EMAIL, quoted),
|
|
1275
|
+
fixtures: {
|
|
1276
|
+
flag: [
|
|
1277
|
+
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."),
|
|
1278
|
+
f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
|
|
1279
|
+
f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
|
|
1280
|
+
],
|
|
1281
|
+
pass: [
|
|
1282
|
+
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."),
|
|
1283
|
+
f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
|
|
1284
|
+
]
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1190
1287
|
{
|
|
1191
1288
|
id: "prompt-context",
|
|
1192
1289
|
name: "Prompt context",
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-G47ZCBD5.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FNB3NP57.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-4WAIOZ4W.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-
|
|
33
|
+
} from "./chunk-DE2ZAV4G.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-
|
|
136
|
+
const { snapshotUrl } = await import("./audit-LQ2TDISV.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(", ")}` : ""}`;
|
|
@@ -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-STWH5EYU.js";
|
|
8
|
+
import "./chunk-G47ZCBD5.js";
|
|
9
|
+
import "./chunk-FNB3NP57.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-4WAIOZ4W.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-DE2ZAV4G.js";
|
|
15
|
+
import "./chunk-ONA73THJ.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
formatComponentPairs,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FNB3NP57.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-4WAIOZ4W.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-DE2ZAV4G.js";
|
|
25
|
+
import "./chunk-ONA73THJ.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-LQ2TDISV.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];
|