@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,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_BASELINE,
|
|
3
|
+
checkExceptions,
|
|
4
|
+
hasUnexcusedEntries,
|
|
5
|
+
judgeGrowth,
|
|
6
|
+
judgeMeasured,
|
|
7
|
+
judgeRoutes,
|
|
8
|
+
judgeStatic,
|
|
9
|
+
parseBaseline,
|
|
10
|
+
routesFromManifest,
|
|
11
|
+
routesFromPages,
|
|
12
|
+
scanPhoneFirst,
|
|
13
|
+
writeBaseline
|
|
14
|
+
} from "./chunk-BQ7H23QP.js";
|
|
15
|
+
import {
|
|
16
|
+
SOURCE_FILE,
|
|
17
|
+
readPaths
|
|
18
|
+
} from "./chunk-4WAIOZ4W.js";
|
|
19
|
+
import {
|
|
20
|
+
toJson
|
|
21
|
+
} from "./chunk-QIEDBL4J.js";
|
|
22
|
+
import "./chunk-DE2ZAV4G.js";
|
|
23
|
+
import "./chunk-ONA73THJ.js";
|
|
24
|
+
import "./chunk-QYBAVU3P.js";
|
|
25
|
+
|
|
26
|
+
// src/phone/cli.ts
|
|
27
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
28
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
29
|
+
|
|
30
|
+
// src/phone/discover.ts
|
|
31
|
+
import { execFileSync } from "child_process";
|
|
32
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
33
|
+
import { basename, dirname, join, relative, resolve, sep } from "path";
|
|
34
|
+
var PAGE_FILE = /^page\.(?:tsx|ts|jsx|js|mdx)$/;
|
|
35
|
+
function pageFiles(dir, base = dir, into = []) {
|
|
36
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) pageFiles(join(dir, entry.name), base, into);
|
|
39
|
+
} else if (entry.isFile() && PAGE_FILE.test(entry.name)) into.push(relative(base, join(dir, entry.name)));
|
|
40
|
+
}
|
|
41
|
+
return into;
|
|
42
|
+
}
|
|
43
|
+
function discoverRoutes(cwd, source, appDir, routesManifest) {
|
|
44
|
+
const explicit = appDir === void 0 ? void 0 : Array.isArray(appDir) ? appDir : [appDir];
|
|
45
|
+
const candidates = explicit ?? source.flatMap((s) => [s, join(s, "app"), join(s, "src", "app")]).filter((s) => basename(resolve(cwd, s)) === "app");
|
|
46
|
+
const routes = /* @__PURE__ */ new Set();
|
|
47
|
+
const from = [];
|
|
48
|
+
for (const dir of candidates) {
|
|
49
|
+
const abs = resolve(cwd, dir);
|
|
50
|
+
if (!existsSync(abs)) {
|
|
51
|
+
if (explicit) throw new Error(`appDir ${dir} is not there`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const pages = pageFiles(abs);
|
|
55
|
+
if (pages.length === 0) continue;
|
|
56
|
+
from.push(dir);
|
|
57
|
+
for (const r of routesFromPages(pages)) routes.add(r);
|
|
58
|
+
}
|
|
59
|
+
if (routesManifest) {
|
|
60
|
+
const file = resolve(cwd, routesManifest);
|
|
61
|
+
if (!existsSync(file)) throw new Error(`routesManifest ${routesManifest} is not there. Build the app first, or use appDir.`);
|
|
62
|
+
from.push(routesManifest);
|
|
63
|
+
for (const r of routesFromManifest(JSON.parse(readFileSync(file, "utf8")))) routes.add(r);
|
|
64
|
+
}
|
|
65
|
+
return { routes: [...routes].sort(), from };
|
|
66
|
+
}
|
|
67
|
+
var majorOf = (range) => {
|
|
68
|
+
if (typeof range !== "string" || /^(?:catalog|workspace|link|file):/.test(range)) return null;
|
|
69
|
+
const m = range.match(/\d+/);
|
|
70
|
+
return m ? Number(m[0]) : null;
|
|
71
|
+
};
|
|
72
|
+
var readJson = (file) => {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function tailwindMajor(dir) {
|
|
80
|
+
for (let at = resolve(dir); ; at = dirname(at)) {
|
|
81
|
+
const installed = majorOf(readJson(join(at, "node_modules", "tailwindcss", "package.json"))?.version);
|
|
82
|
+
if (installed !== null) return installed;
|
|
83
|
+
const pkg = readJson(join(at, "package.json"));
|
|
84
|
+
const declared = majorOf(pkg?.dependencies?.tailwindcss) ?? majorOf(pkg?.devDependencies?.tailwindcss);
|
|
85
|
+
if (declared !== null) return declared;
|
|
86
|
+
if (dirname(at) === at) return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function tailwind3Files(cwd, source, forced) {
|
|
90
|
+
const dirs = source.map((s) => resolve(cwd, s));
|
|
91
|
+
const versions = Object.fromEntries(source.map((s, i) => [s, forced ?? tailwindMajor(dirs[i])]));
|
|
92
|
+
return {
|
|
93
|
+
versions,
|
|
94
|
+
isV3: (path) => {
|
|
95
|
+
const abs = resolve(cwd, path);
|
|
96
|
+
return source.some((s, i) => (abs === dirs[i] || abs.startsWith(dirs[i] + sep)) && versions[s] === 3);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
var git = (cwd, args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
101
|
+
var defaultBase = (flag, env = process.env) => flag ?? (env.GITHUB_BASE_REF ? `origin/${env.GITHUB_BASE_REF}` : "origin/main");
|
|
102
|
+
function readBaseCopy(cwd, baselinePath, ref) {
|
|
103
|
+
try {
|
|
104
|
+
git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
105
|
+
} catch {
|
|
106
|
+
return { kind: "not-git" };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
110
|
+
} catch {
|
|
111
|
+
return { kind: "no-ref", ref };
|
|
112
|
+
}
|
|
113
|
+
const rel = relative(cwd, baselinePath).split(sep).join("/");
|
|
114
|
+
let text;
|
|
115
|
+
try {
|
|
116
|
+
text = git(cwd, ["show", `${ref}:${rel.startsWith("..") ? rel : `./${rel}`}`]);
|
|
117
|
+
} catch {
|
|
118
|
+
return { kind: "no-file", ref };
|
|
119
|
+
}
|
|
120
|
+
return { kind: "found", ref, baseline: parseBaseline(JSON.parse(text), `${ref}:${rel}`) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/phone/cli.ts
|
|
124
|
+
var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--base <ref>] [--update-baseline] [--json]";
|
|
125
|
+
function parse(args) {
|
|
126
|
+
const flags = { config: "phone-first.json", update: false, json: false };
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const a = args[i];
|
|
129
|
+
if (a === "--update-baseline") flags.update = true;
|
|
130
|
+
else if (a === "--json") flags.json = true;
|
|
131
|
+
else if (a === "--config" || a === "--measured" || a === "--base") {
|
|
132
|
+
const v = args[++i];
|
|
133
|
+
if (!v || v.startsWith("--")) return `${a} needs a value`;
|
|
134
|
+
if (a === "--config") flags.config = v;
|
|
135
|
+
else if (a === "--base") flags.base = v;
|
|
136
|
+
else flags.measured = v;
|
|
137
|
+
} else return `unknown argument ${a}`;
|
|
138
|
+
}
|
|
139
|
+
return flags;
|
|
140
|
+
}
|
|
141
|
+
function loadConfig(path) {
|
|
142
|
+
if (!existsSync2(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
|
|
143
|
+
const c = JSON.parse(readFileSync2(path, "utf8"));
|
|
144
|
+
if (!Array.isArray(c.source) || c.source.length === 0 || c.source.some((s) => typeof s !== "string")) throw new Error(`${path}: "source" should list the folders to scan`);
|
|
145
|
+
if (c.ignore !== void 0 && (!Array.isArray(c.ignore) || c.ignore.some((s) => typeof s !== "string"))) throw new Error(`${path}: "ignore" should be a list of globs`);
|
|
146
|
+
if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
|
|
147
|
+
if (c.excludeRoutes !== void 0 && !Array.isArray(c.excludeRoutes)) throw new Error(`${path}: "excludeRoutes" should be a list of { "route", "reason" }`);
|
|
148
|
+
if (c.appDir !== void 0 && typeof c.appDir !== "string" && !(Array.isArray(c.appDir) && c.appDir.every((d) => typeof d === "string"))) throw new Error(`${path}: "appDir" should be a folder or a list of folders`);
|
|
149
|
+
if (c.tailwind !== void 0 && typeof c.tailwind !== "number") throw new Error(`${path}: "tailwind" should be the major version, 3 or 4`);
|
|
150
|
+
return { ...c, source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], excludeRoutes: c.excludeRoutes ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
|
|
151
|
+
}
|
|
152
|
+
function loadBaseline(path) {
|
|
153
|
+
if (!existsSync2(path)) return EMPTY_BASELINE;
|
|
154
|
+
return parseBaseline(JSON.parse(readFileSync2(path, "utf8")), path);
|
|
155
|
+
}
|
|
156
|
+
function loadMeasured(dir) {
|
|
157
|
+
if (!existsSync2(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
|
|
158
|
+
const routes = readdirSync2(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync2(join2(dir, f), "utf8")));
|
|
159
|
+
if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
|
|
160
|
+
return routes;
|
|
161
|
+
}
|
|
162
|
+
function runPhoneFirst(args, io) {
|
|
163
|
+
const flags = parse(args);
|
|
164
|
+
if (typeof flags === "string") {
|
|
165
|
+
io.err(`craft phone-first: ${flags}
|
|
166
|
+
${USAGE}`);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const config = loadConfig(resolve2(io.cwd, flags.config));
|
|
171
|
+
const baselinePath = resolve2(io.cwd, config.baseline);
|
|
172
|
+
const baseline = loadBaseline(baselinePath);
|
|
173
|
+
const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync2(resolve2(io.cwd, f)));
|
|
174
|
+
const sources = config.source.filter((s2) => existsSync2(resolve2(io.cwd, s2)));
|
|
175
|
+
const missing = config.source.filter((s2) => !sources.includes(s2));
|
|
176
|
+
if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
|
|
177
|
+
const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
|
|
178
|
+
const tailwind = tailwind3Files(io.cwd, sources, config.tailwind);
|
|
179
|
+
const { findings, excused } = scanPhoneFirst(files, config.exceptions, { tailwind3: tailwind.isV3 });
|
|
180
|
+
const routes = flags.measured ? loadMeasured(resolve2(io.cwd, flags.measured)) : null;
|
|
181
|
+
if (flags.update) {
|
|
182
|
+
const next = writeBaseline(findings, routes ?? [], baseline.note, baseline);
|
|
183
|
+
if (!routes) next.measured = baseline.measured;
|
|
184
|
+
writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
185
|
+
`);
|
|
186
|
+
io.out(`craft phone-first: wrote ${config.baseline} (${next.static.reduce((n, s2) => n + s2.count, 0)} static, ${next.measured.reduce((n, m2) => n + m2.count, 0)} measured)`);
|
|
187
|
+
return exceptionProblems.length ? 1 : 0;
|
|
188
|
+
}
|
|
189
|
+
const s = judgeStatic(findings, baseline);
|
|
190
|
+
const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
|
|
191
|
+
const base = defaultBase(flags.base);
|
|
192
|
+
const copy = hasUnexcusedEntries(baseline) ? readBaseCopy(io.cwd, baselinePath, base) : null;
|
|
193
|
+
const grown = copy?.kind === "found" ? judgeGrowth(baseline, copy.baseline, copy.ref) : [];
|
|
194
|
+
const baseProblems = copy?.kind === "no-ref" ? [`cannot see ${copy.ref}, so the baseline cannot be compared with it. Fetch it first (\`git fetch origin main\`, or \`fetch-depth: 0\` on actions/checkout), or name another base with --base <ref>.`] : [];
|
|
195
|
+
const baseNote = copy?.kind === "not-git" ? "baseline growth not checked: not a git checkout" : copy?.kind === "no-file" ? `no ${config.baseline} on ${copy.ref} yet, so this baseline is the first` : null;
|
|
196
|
+
const app = routes ? discoverRoutes(io.cwd, sources, config.appDir, config.routesManifest) : null;
|
|
197
|
+
const covered = app && routes ? judgeRoutes(app.routes, routes.map((r) => r.route), config.excludeRoutes) : null;
|
|
198
|
+
const routeNote = routes && app && app.routes.length === 0 ? "route coverage not checked: no Next app folder with page files in source (set appDir)" : null;
|
|
199
|
+
const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length + grown.length + baseProblems.length + (covered ? covered.missing.length + covered.problems.length : 0) > 0;
|
|
200
|
+
if (flags.json) {
|
|
201
|
+
io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m, baseline: { base, grown, problems: baseProblems, note: baseNote }, pages: covered ? { ...covered, found: app.routes.length } : null, tailwind: tailwind.versions }));
|
|
202
|
+
return failed ? 1 : 0;
|
|
203
|
+
}
|
|
204
|
+
const wide = routes?.some((r) => r.viewports) ?? false;
|
|
205
|
+
const section = (title, lines) => lines.length ? `
|
|
206
|
+
${title}
|
|
207
|
+
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
208
|
+
const known = s.known + m.known;
|
|
209
|
+
const summary = [
|
|
210
|
+
`${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at ${wide ? "every viewport" : "375px"}` : ""}`,
|
|
211
|
+
excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
|
|
212
|
+
known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null,
|
|
213
|
+
covered && app.routes.length ? `${app.routes.length} page route${app.routes.length === 1 ? "" : "s"} covered` : null
|
|
214
|
+
].filter(Boolean).join(", ");
|
|
215
|
+
if (!failed) {
|
|
216
|
+
io.out(`craft phone-first: pass. ${summary}.${[baseNote, routeNote].filter(Boolean).map((n) => `
|
|
217
|
+
note: ${n}`).join("")}`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
io.err(
|
|
221
|
+
[
|
|
222
|
+
`craft phone-first: fail. ${summary}.`,
|
|
223
|
+
section("The exceptions:", exceptionProblems),
|
|
224
|
+
section("Built desktop first (write the phone as the base styles and widen with min-width; a genuine exception takes `mobile-first-ok: <reason>` on its line):", s.fresh),
|
|
225
|
+
section(wide ? "Measured across the viewports, phone first:" : "Measured at 375px:", m.fresh),
|
|
226
|
+
section(`Page routes the gate never measures (add the route to the phone-first specs with seeded data; a page that only redirects goes in "excludeRoutes" with its reason):`, covered?.missing ?? []),
|
|
227
|
+
section("excludeRoutes:", covered?.problems ?? []),
|
|
228
|
+
section(`The baseline holds entries that ${copy?.kind === "found" ? copy.ref : base} does not (the baseline only shrinks: fix the breach where it is, or, for third-party UI or a destructive-only form, add the entry with a "reason". If main shrank the baseline since this branch left it, merge main first):`, grown),
|
|
229
|
+
section("The base branch:", baseProblems),
|
|
230
|
+
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
|
|
231
|
+
].join("")
|
|
232
|
+
);
|
|
233
|
+
return 1;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
io.err(`craft phone-first: ${error.message}`);
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
runPhoneFirst
|
|
241
|
+
};
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FNB3NP57.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-4WAIOZ4W.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-DE2ZAV4G.js";
|
|
18
|
+
import "./chunk-ONA73THJ.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
checkReason,
|
|
20
20
|
researchPrompt,
|
|
21
21
|
validateDirection
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-G47ZCBD5.js";
|
|
23
23
|
import {
|
|
24
24
|
parseFlags
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-4WAIOZ4W.js";
|
|
26
26
|
import {
|
|
27
27
|
toJson
|
|
28
28
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -38,8 +38,8 @@ import {
|
|
|
38
38
|
isCream,
|
|
39
39
|
oklchToRgb,
|
|
40
40
|
parseColour
|
|
41
|
-
} from "./chunk-
|
|
42
|
-
import "./chunk-
|
|
41
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
42
|
+
import "./chunk-ONA73THJ.js";
|
|
43
43
|
import "./chunk-QYBAVU3P.js";
|
|
44
44
|
|
|
45
45
|
// src/direction/cli.ts
|
|
@@ -484,7 +484,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
|
484
484
|
async function snapshotFrom(target, cwd) {
|
|
485
485
|
if (!target) return void 0;
|
|
486
486
|
if (/^https?:\/\//.test(target)) {
|
|
487
|
-
const { snapshotUrl } = await import("./audit-
|
|
487
|
+
const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
|
|
488
488
|
return snapshotUrl(target);
|
|
489
489
|
}
|
|
490
490
|
return readSnapshot(readJson(resolve(cwd, target)), target);
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-BYXDX66D.js";
|
|
8
|
+
import "./chunk-6DRBFDWT.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-FNB3NP57.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-4WAIOZ4W.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-DE2ZAV4G.js";
|
|
16
|
+
import "./chunk-ONA73THJ.js";
|
|
17
17
|
import "./chunk-QYBAVU3P.js";
|
|
18
18
|
export {
|
|
19
19
|
describeComponents,
|
|
@@ -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-BYXDX66D.js";
|
|
7
|
+
import "./chunk-6DRBFDWT.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-STWH5EYU.js";
|
|
14
14
|
import {
|
|
15
15
|
PAGE_TYPES,
|
|
16
16
|
validateDirection
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-G47ZCBD5.js";
|
|
18
18
|
import {
|
|
19
19
|
loadNull
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FNB3NP57.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-4WAIOZ4W.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-DE2ZAV4G.js";
|
|
46
|
+
import "./chunk-ONA73THJ.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-LQ2TDISV.js");
|
|
130
130
|
snap = await snapshotUrl(target2);
|
|
131
131
|
}
|
|
132
132
|
const fp = fingerprint(snap);
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
run
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4WAIOZ4W.js";
|
|
5
5
|
import "./chunk-QIEDBL4J.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-DE2ZAV4G.js";
|
|
7
|
+
import "./chunk-ONA73THJ.js";
|
|
8
8
|
import "./chunk-QYBAVU3P.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
package/dist/index.cjs
CHANGED
|
@@ -2058,6 +2058,33 @@ var OUTCOME_PROMISES = [
|
|
|
2058
2058
|
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
2059
2059
|
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")
|
|
2060
2060
|
];
|
|
2061
|
+
var SENTENCE_START = String.raw`(?<=^|[\n>"“]|[.!?]\s+)`;
|
|
2062
|
+
var MATEY = [
|
|
2063
|
+
// "Alright Carlo,", "Oi Dave", "Alright, mate"
|
|
2064
|
+
/\b(?:Alright|Oi)(?:,\s*|\s+)(?:mate\b|[A-Z][a-z]+\b)/,
|
|
2065
|
+
// "mate" as a form of address, not "a mate who knows"
|
|
2066
|
+
/,\s*mate\b|\b(?:thanks|ta|cheers|alright|oi|fussed|sorry|worries|bother|hiya|hey)\s+mate\b/i,
|
|
2067
|
+
/\b(?:innit|chuffed|lovely jubbly)\b|\bbish,?\s+bash,?\s+bosh\b|\bbob['’]?s\s+your\s+uncle\b/i,
|
|
2068
|
+
/\bno\s+(?:worries|bother)\b|\bnot\s+fussed\b/i,
|
|
2069
|
+
// "Right then," opening a sentence, not "the right then left turn"
|
|
2070
|
+
new RegExp(`${SENTENCE_START}Right,?\\s+then\\b[,!.]`),
|
|
2071
|
+
// "we'll get you sorted", "all sorted", "Sorted!", not "sorted by date"
|
|
2072
|
+
/\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,
|
|
2073
|
+
new RegExp(`${SENTENCE_START}Sorted[.!]`),
|
|
2074
|
+
// "proper" as an intensifier before an adjective, not "proper planning"
|
|
2075
|
+
/\bproper\s+(?:good|nice|quick|tidy|lovely|decent|chuffed|busy|mint|smart|happy|gutted|brilliant|buzzing)\b/i,
|
|
2076
|
+
/\bgive\s+(?:me|us)\s+a\s+(?:shout|bell|buzz|tinkle)\b/i,
|
|
2077
|
+
/\bwhen\s+you\s+(?:get|have|['’]ve\s+got)\s+a\s+(?:sec|min|mo)\b/i,
|
|
2078
|
+
/\bhave\s+a\s+(?:quick\s+)?(?:gander|squiz|nosey|nose(?:\s+(?:round|around))?)\b(?!\s+for\b)/i,
|
|
2079
|
+
/\bcheck\s+out\s+(?:our|my|this|these)\b/i
|
|
2080
|
+
];
|
|
2081
|
+
var CLIPPED = [
|
|
2082
|
+
/\b(?:take|takes|took|taking|have|has|had|having)\s+(?:quick\s+)?look\s+(?:at|on|over|through|in)\b/i,
|
|
2083
|
+
/\bgive\s+(?:it|this|that|them)\s+(?:go|try|look|read|whirl|bash)\b(?![-\w])/i,
|
|
2084
|
+
new RegExp(`${SENTENCE_START}Quick\\s+one\\b(?=\\s*[:,.!?\u2014\u2013-]|\\s+(?:for you|before|on|about)\\b)`),
|
|
2085
|
+
// "Alright Carlo, built you a proper site": the "I" left out
|
|
2086
|
+
/(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
|
|
2087
|
+
];
|
|
2061
2088
|
var NEGATION = [
|
|
2062
2089
|
// "It's not just a haircut, it's an experience."
|
|
2063
2090
|
/\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,
|
|
@@ -2294,6 +2321,40 @@ var COPY_TELLS = [
|
|
|
2294
2321
|
]
|
|
2295
2322
|
}
|
|
2296
2323
|
},
|
|
2324
|
+
{
|
|
2325
|
+
id: "matey-register",
|
|
2326
|
+
name: "Matey register",
|
|
2327
|
+
generation: 2,
|
|
2328
|
+
severity: "warn",
|
|
2329
|
+
surface: "copy",
|
|
2330
|
+
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.",
|
|
2331
|
+
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'.",
|
|
2332
|
+
detect: phrases([...MATEY, ...CLIPPED], quoted),
|
|
2333
|
+
fixtures: {
|
|
2334
|
+
flag: [
|
|
2335
|
+
// From the portal's seeded prospect follow-ups, 2026-10-19.
|
|
2336
|
+
f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
|
|
2337
|
+
f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
|
|
2338
|
+
f("emails/followup.md", "Carlo, built the shop a proper site."),
|
|
2339
|
+
f("content/contact.md", "No worries, we'll get you sorted."),
|
|
2340
|
+
f("emails/quote.md", "Take look on these photos and give it go."),
|
|
2341
|
+
f("emails/quote.md", "Quick one: is Thursday still good?"),
|
|
2342
|
+
f("emails/quote.md", "Right then, here is the plan."),
|
|
2343
|
+
f("content/reviews.md", "The fitters did a proper good job and we were chuffed.")
|
|
2344
|
+
],
|
|
2345
|
+
pass: [
|
|
2346
|
+
f("content/home.md", "Proper planning saves a second visit."),
|
|
2347
|
+
f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
|
|
2348
|
+
f("content/directions.md", "At the junction, take the right then left turn."),
|
|
2349
|
+
// Chair and Blade's own approved line, and "mate" as a noun.
|
|
2350
|
+
f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
|
|
2351
|
+
f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
|
|
2352
|
+
f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
|
|
2353
|
+
f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
|
|
2354
|
+
f("content/home.md", "Built to last, and made for older houses.")
|
|
2355
|
+
]
|
|
2356
|
+
}
|
|
2357
|
+
},
|
|
2297
2358
|
{
|
|
2298
2359
|
id: "hollow-imperative",
|
|
2299
2360
|
name: "Hollow imperative",
|
|
@@ -2494,7 +2555,12 @@ var FALSE_RANGE = [
|
|
|
2494
2555
|
];
|
|
2495
2556
|
var RESIDUE = [
|
|
2496
2557
|
/\bas an AI(?:\s+language)?\s+(?:model|assistant)\b/i,
|
|
2497
|
-
|
|
2558
|
+
// At the start of a line, or after a sentence that ends in a full stop or
|
|
2559
|
+
// an exclamation mark: "I hope this email finds you well! Certainly, here
|
|
2560
|
+
// is the handover request." slipped through when only a line start counted
|
|
2561
|
+
// (found live, 2026-10-19). Not after a question mark: an FAQ answer may
|
|
2562
|
+
// honestly open "Do you work weekends? Absolutely, here are our hours."
|
|
2563
|
+
/(?<=^|[\n>]|[.!]\s+)\s*(?:Certainly|Sure|Absolutely|Of course|Great question)[!,.]\s+(?:Here(?:['’]s| is| are)|Below)\b/i,
|
|
2498
2564
|
/\bhere(?:['’]s| is)\s+(?:a|an|the|your)\s+(?:revised|rewritten|updated|polished|refined|improved|draft|more concise)\s+(?:version|draft|copy)\b/i,
|
|
2499
2565
|
/\blet me know if you(?:['’]d| would) like me to\b/i,
|
|
2500
2566
|
/\b(?:my|the) (?:knowledge|training) cut-?off\b|\bas of my last (?:update|training)\b/i,
|
|
@@ -2510,6 +2576,13 @@ var RESIDUE = [
|
|
|
2510
2576
|
/[\uE200-\uE2FF]*entity[\uE200-\uE2FF]*\["[a-z_]+"\s*,\s*"[^"\n]{1,80}"(?:\s*,\s*"[^"\n]{0,160}")*\][\uE200-\uE2FF]*/,
|
|
2511
2577
|
/【\d+(?:[:†][^】]{0,40})?】/
|
|
2512
2578
|
];
|
|
2579
|
+
var STOCK_EMAIL = [
|
|
2580
|
+
/\bI (?:hope|trust) (?:this|my|the) (?:e-?mail|message|note|letter) finds you(?: all)? (?:well|in good health)\b/i,
|
|
2581
|
+
/\bI hope you(?:['’]re| are)(?: all)? (?:doing |keeping )?well\b/i,
|
|
2582
|
+
/\b(?:I )?hope you(?:['’]re| are) having an? (?:great|good|lovely|wonderful|fantastic) (?:day|week|weekend)\b/i,
|
|
2583
|
+
/\blet (?:me|us) know if you have any (?:further |other |more )?questions\b/i,
|
|
2584
|
+
/\b(?:do not|don['’]t) hesitate to (?:contact|reach out|get in touch|ask|call)\b/i
|
|
2585
|
+
];
|
|
2513
2586
|
var AI_UTM = /utm_source=(?:chatgpt\.com|chat\.openai\.com|openai|perplexity(?:\.ai)?|claude\.ai|copilot(?:\.microsoft\.com)?|gemini(?:\.google\.com)?)\b/gi;
|
|
2514
2587
|
function residueHits(ctx) {
|
|
2515
2588
|
const hits = phrases(RESIDUE, quoted, allText)(ctx);
|
|
@@ -2591,15 +2664,39 @@ var RESEARCH_TELLS = [
|
|
|
2591
2664
|
f("content/home.md", "Prices start at \xA340 :contentReference[oaicite:2]{index=2} for a full set."),
|
|
2592
2665
|
f("content/home.md", "Prices start at \xA340\u30104\u2020source\u3011 for a full set."),
|
|
2593
2666
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=chatgpt.com">Read the guide</a>;`),
|
|
2594
|
-
f("content/home.md", "Here's a more concise version for the hero.")
|
|
2667
|
+
f("content/home.md", "Here's a more concise version for the hero."),
|
|
2668
|
+
// The client handover email that a clean copy-check let through, 2026-10-19.
|
|
2669
|
+
f("emails/handover.md", "Hi,\n\nI hope this email finds you well! Certainly, here is the handover request. Let me know if you have any questions.")
|
|
2595
2670
|
],
|
|
2596
2671
|
pass: [
|
|
2597
2672
|
f("content/home.md", "Certainly the busiest week of the year: book early."),
|
|
2673
|
+
f("content/faq.md", "Do you work weekends? Absolutely, here are our hours."),
|
|
2598
2674
|
f("content/home.md", "Here's the price list for 2026."),
|
|
2599
2675
|
f("app/guide.tsx", `export const Guide = () => <a href="https://example.com/guide?utm_source=newsletter">Read the guide</a>;`)
|
|
2600
2676
|
]
|
|
2601
2677
|
}
|
|
2602
2678
|
},
|
|
2679
|
+
{
|
|
2680
|
+
id: "stock-email-line",
|
|
2681
|
+
name: "Stock email line",
|
|
2682
|
+
generation: 1,
|
|
2683
|
+
severity: "warn",
|
|
2684
|
+
surface: "copy",
|
|
2685
|
+
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.",
|
|
2686
|
+
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.'",
|
|
2687
|
+
detect: phrases(STOCK_EMAIL, quoted),
|
|
2688
|
+
fixtures: {
|
|
2689
|
+
flag: [
|
|
2690
|
+
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."),
|
|
2691
|
+
f("emails/quote.md", "Hope you're having a great week. The quote is attached."),
|
|
2692
|
+
f("content/contact.md", "Please don't hesitate to contact us about a rewire.")
|
|
2693
|
+
],
|
|
2694
|
+
pass: [
|
|
2695
|
+
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."),
|
|
2696
|
+
f("content/contact.md", "If you have any questions, ring Jo on 01280 700123.")
|
|
2697
|
+
]
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2603
2700
|
{
|
|
2604
2701
|
id: "prompt-context",
|
|
2605
2702
|
name: "Prompt context",
|
|
@@ -5209,6 +5306,10 @@ var HOUSE = {
|
|
|
5209
5306
|
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
5210
5307
|
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
5211
5308
|
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
5309
|
+
// New 2026-10-19: review first, like every new tell, until estate hits are read.
|
|
5310
|
+
"stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
|
|
5311
|
+
// New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
|
|
5312
|
+
"matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
|
|
5212
5313
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
5213
5314
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
5214
5315
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6414,7 +6515,7 @@ var IMAGERY_TELLS = [
|
|
|
6414
6515
|
];
|
|
6415
6516
|
|
|
6416
6517
|
// src/character/check.ts
|
|
6417
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6518
|
+
var CATALOGUE_VERSION = "2026.10.4";
|
|
6418
6519
|
var CATALOGUE = [
|
|
6419
6520
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6420
6521
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/index.d.cts
CHANGED
|
@@ -1252,7 +1252,7 @@ interface CheckReport {
|
|
|
1252
1252
|
* Bumped whenever an entry is added, removed or its detection changes, so a
|
|
1253
1253
|
* report can say which list it was judged against.
|
|
1254
1254
|
*/
|
|
1255
|
-
declare const CATALOGUE_VERSION = "2026.10.
|
|
1255
|
+
declare const CATALOGUE_VERSION = "2026.10.4";
|
|
1256
1256
|
declare const CATALOGUE: readonly Tell[];
|
|
1257
1257
|
declare function tellById(id: string): Tell | undefined;
|
|
1258
1258
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1252,7 +1252,7 @@ interface CheckReport {
|
|
|
1252
1252
|
* Bumped whenever an entry is added, removed or its detection changes, so a
|
|
1253
1253
|
* report can say which list it was judged against.
|
|
1254
1254
|
*/
|
|
1255
|
-
declare const CATALOGUE_VERSION = "2026.10.
|
|
1255
|
+
declare const CATALOGUE_VERSION = "2026.10.4";
|
|
1256
1256
|
declare const CATALOGUE: readonly Tell[];
|
|
1257
1257
|
declare function tellById(id: string): Tell | undefined;
|
|
1258
1258
|
/**
|