@domandigital/craft 0.20.5 → 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 +17 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +6 -2
- package/README.md +73 -3
- package/dist/{audit-5I2WWTL6.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +64 -1
- package/dist/audit.js +64 -1
- package/dist/{chunk-TSO5W32N.js → chunk-4WAIOZ4W.js} +14 -14
- package/dist/{chunk-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/chunk-BQ7H23QP.js +261 -0
- package/dist/{chunk-DMAMGFV7.js → chunk-BYXDX66D.js} +4 -4
- package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
- package/dist/{chunk-BBJOLR3R.js → chunk-FNB3NP57.js} +5 -5
- package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
- package/dist/{chunk-N32UUN3S.js → chunk-STWH5EYU.js} +5 -5
- package/dist/{cli-SQC27VYC.js → cli-5QI6NIZN.js} +6 -6
- package/dist/{cli-BI6XCMCL.js → cli-5VHX5EJF.js} +5 -5
- package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
- package/dist/cli-EYHRJJAX.js +241 -0
- package/dist/{cli-V4QXTBWR.js → cli-PBCU7Z64.js} +4 -4
- package/dist/{cli-JQYZXFRH.js → cli-RHDNHL4U.js} +5 -5
- package/dist/{cli-OH6427MC.js → cli-XAWJWK7I.js} +6 -6
- package/dist/{cli-W6FNSHRN.js → cli-YSGPGQ7R.js} +9 -9
- package/dist/cli.js +3 -3
- package/dist/index.cjs +64 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -1
- 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-C4DWICYI.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-56H5IPWT.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";
|
|
@@ -2430,6 +2430,8 @@ var HOUSE = {
|
|
|
2430
2430
|
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
2431
2431
|
// New 2026-10-19: review first, like every new tell, until estate hits are read.
|
|
2432
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" },
|
|
2433
2435
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2434
2436
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
2435
2437
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3025,7 +3027,7 @@ var IMAGERY_TELLS = [
|
|
|
3025
3027
|
];
|
|
3026
3028
|
|
|
3027
3029
|
// src/character/check.ts
|
|
3028
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
3030
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
3029
3031
|
var CATALOGUE = [
|
|
3030
3032
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3031
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",
|
|
@@ -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];
|