@domandigital/craft 0.19.3 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/CHARACTER.md +4 -2
- package/COPY.md +3 -1
- package/README.md +51 -0
- package/STANDARD.md +86 -3
- package/dist/{audit-43PC4LMS.js → audit-BN2GSFKT.js} +3 -3
- package/dist/audit.cjs +80 -5
- package/dist/audit.js +80 -5
- package/dist/{chunk-4LJFZH2I.js → chunk-2OBDCIVW.js} +2 -2
- package/dist/{chunk-BK4AS2RU.js → chunk-6NXPDCUO.js} +26 -13
- package/dist/{chunk-CZ7KT452.js → chunk-7DQDF3VL.js} +76 -4
- package/dist/{chunk-6M2LK7QE.js → chunk-FQYKKKHI.js} +5 -5
- package/dist/{chunk-XVMKZ4SM.js → chunk-GOPHZI6O.js} +4 -4
- package/dist/{cli-6LCOSHMW.js → chunk-IJUZ7RV4.js} +16 -114
- package/dist/{chunk-D2FTTM4F.js → chunk-IZ6GTU54.js} +5 -5
- package/dist/{chunk-WK2WMWTH.js → chunk-JGWOA4LI.js} +5 -2
- package/dist/{chunk-VDUIIWG7.js → chunk-YOZLPNJS.js} +1 -1
- package/dist/{cli-Q5X3W6I4.js → cli-DVYLDCEA.js} +5 -5
- package/dist/{cli-QAYH2OX5.js → cli-HUN5ZTH5.js} +6 -6
- package/dist/{cli-WN2GHYPF.js → cli-IREKGVSM.js} +6 -6
- package/dist/cli-LSJI7QRD.js +122 -0
- package/dist/{cli-APYEOHFV.js → cli-MKZJMZUJ.js} +5 -5
- package/dist/{cli-TMLHMNWU.js → cli-NTNUZUFP.js} +4 -4
- package/dist/{cli-4LEAEOOV.js → cli-VFKKFRB4.js} +9 -9
- package/dist/{cli-VPARW6FY.js → cli-ZXDGVDYU.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.cjs +80 -5
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +80 -5
- package/dist/phone.cjs +546 -19
- package/dist/phone.d.cts +265 -28
- package/dist/phone.d.ts +265 -28
- package/dist/phone.js +532 -19
- package/dist/viewports-cli-C4VXMPKK.js +683 -0
- package/package.json +1 -1
- package/word-lists.json +3 -1
|
@@ -606,6 +606,8 @@ var REVIEW_PHRASES = [
|
|
|
606
606
|
"truly",
|
|
607
607
|
"genuinely",
|
|
608
608
|
"deeply",
|
|
609
|
+
"clear",
|
|
610
|
+
"clearly",
|
|
609
611
|
"ultimately",
|
|
610
612
|
"one-size-fits-all",
|
|
611
613
|
"one size fits all",
|
|
@@ -616,6 +618,33 @@ var REVIEW_PHRASES = [
|
|
|
616
618
|
"arguably the best",
|
|
617
619
|
"bar none"
|
|
618
620
|
];
|
|
621
|
+
var SALES_TAGLINES = [
|
|
622
|
+
/\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
|
|
623
|
+
/\breal,? (?:measurable |tangible )?results\b/i,
|
|
624
|
+
/\bresults that (?:matter|count|speak|last)\b/i,
|
|
625
|
+
/\bresults you can (?:see|trust|measure)\b/i,
|
|
626
|
+
/\bwork(?:s|ing)? harder for you\b/i,
|
|
627
|
+
/\bmake (?:every|your) (?:penny|pound) count\b/i,
|
|
628
|
+
/\bevery (?:penny|pound) (?:counts|works)\b/i,
|
|
629
|
+
/\bbang for your buck\b/i,
|
|
630
|
+
/\bstand out from the (?:crowd|competition)\b/i,
|
|
631
|
+
/\b(?:pages?|websites?|sites?|copy|content|ads?) that (?:actually |really )?(?:converts?|sells?)\b/i
|
|
632
|
+
];
|
|
633
|
+
var SALES_RESULTS = "(?:enquiries|leads|customers|clients|sales|jobs|bookings|revenue|calls|traffic|profit)";
|
|
634
|
+
var OUTCOME_PROMISES = [
|
|
635
|
+
// "We'll turn more of that into rewiring enquiries"
|
|
636
|
+
new RegExp(`\\bturn(?:s|ing)?\\b[^.!?\\n]{0,50}?\\binto\\s+(?:more\\s+|real\\s+|paying\\s+)?(?:[\\w-]+\\s+)?${SALES_RESULTS}\\b`, "i"),
|
|
637
|
+
// "once the first areas are working"
|
|
638
|
+
/\bonce\b[^.!?\n]{0,40}?\b(?:is|are|starts?|begins?)\s+(?:working|paying off|performing|ranking|delivering|bringing in)\b/i,
|
|
639
|
+
// "competitors you can outdo"
|
|
640
|
+
/\b(?:competitors?|competition|rivals?)\b[^.!?\n]{0,25}?\b(?:you|we)\s*(?:can|will|could|['’]ll)\s+(?:easily\s+)?(?:outdo|beat|outrank|overtake|outperform|outshine)\b/i,
|
|
641
|
+
/\b(?:outdo|outrank|overtake|outperform|outshine|beat)\s+(?:your|the|local|bigger)\s+(?:[\w-]+\s+)?(?:competitors|competition|rivals)\b/i,
|
|
642
|
+
// "exactly what convinces someone spending £8k"
|
|
643
|
+
/\bexactly what\b[^.!?\n]{0,15}?\b(?:convinces|persuades|wins|sells)\b/i,
|
|
644
|
+
new RegExp(`\\bguarantee[sd]?\\b[^.!?\\n]{0,25}?\\b(?:results|rankings?|first page|page one|top spot|${SALES_RESULTS.slice(3, -1)})\\b`, "i"),
|
|
645
|
+
/\byou(?:['’]ll| will)\s+(?:get|see|win|receive|start getting)\s+more\b/i,
|
|
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
|
+
];
|
|
619
648
|
var NEGATION = [
|
|
620
649
|
// "It's not just a haircut, it's an experience."
|
|
621
650
|
/\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,
|
|
@@ -801,12 +830,55 @@ var COPY_TELLS = [
|
|
|
801
830
|
generation: 1,
|
|
802
831
|
severity: "warn",
|
|
803
832
|
surface: "copy",
|
|
804
|
-
why: "Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
833
|
+
why: "Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
|
|
805
834
|
fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
|
|
806
|
-
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1")).join("|")})`, "i")], quoted),
|
|
835
|
+
detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
|
|
836
|
+
fixtures: {
|
|
837
|
+
flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
|
|
838
|
+
pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
id: "sales-tagline",
|
|
843
|
+
name: "Sales tagline",
|
|
844
|
+
generation: 1,
|
|
845
|
+
severity: "warn",
|
|
846
|
+
surface: "copy",
|
|
847
|
+
why: "'So your budget goes where it counts', 'Real results', 'pages that convert': advert lines that sound like a pitch and say nothing a reader can check.",
|
|
848
|
+
fix: "Say what you will actually do and why. 'So your budget goes where it counts' becomes 'Oxford first, because that is where you want the work'.",
|
|
849
|
+
detect: phrases(SALES_TAGLINES, quoted),
|
|
850
|
+
fixtures: {
|
|
851
|
+
flag: [
|
|
852
|
+
f("content/home.md", "Strongest opportunities first, so your budget goes where it counts."),
|
|
853
|
+
f("content/home.md", "A clearer online presence. More enquiries. Real results.")
|
|
854
|
+
],
|
|
855
|
+
pass: [
|
|
856
|
+
f("content/home.md", "We start with Oxford, then expand based on search demand and results."),
|
|
857
|
+
f("content/home.md", "We report the results every month.")
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
id: "outcome-promise",
|
|
863
|
+
name: "Outcome promise",
|
|
864
|
+
generation: 1,
|
|
865
|
+
severity: "warn",
|
|
866
|
+
surface: "copy",
|
|
867
|
+
why: "'We'll turn that into enquiries', 'once the first areas are working', 'competitors you can outdo': a hoped-for result written as if it were delivered. Rankings and enquiries depend on things nobody controls.",
|
|
868
|
+
fix: "Promise the work, not the outcome. 'We'll turn that into enquiries' becomes 'We'll rewrite the page around the questions homeowners ask'. Results are measured and reported, never promised.",
|
|
869
|
+
detect: phrases(OUTCOME_PROMISES, quoted),
|
|
807
870
|
fixtures: {
|
|
808
|
-
flag: [
|
|
809
|
-
|
|
871
|
+
flag: [
|
|
872
|
+
f("content/home.md", "We'll turn more of that into rewiring enquiries."),
|
|
873
|
+
f("content/home.md", "We add new towns once the first areas are working."),
|
|
874
|
+
f("content/home.md", "We target the competitors you can outdo."),
|
|
875
|
+
f("content/home.md", "We write exactly what convinces someone spending \xA38k.")
|
|
876
|
+
],
|
|
877
|
+
pass: [
|
|
878
|
+
f("content/home.md", "We'll rewrite your rewiring page around homeowners' questions."),
|
|
879
|
+
f("content/home.md", "We record every rewiring enquiry, quotation and won job."),
|
|
880
|
+
f("content/home.md", "Once the fuse board is fitted, we test every circuit.")
|
|
881
|
+
]
|
|
810
882
|
}
|
|
811
883
|
},
|
|
812
884
|
{
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2OBDCIVW.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IZ6GTU54.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-6NXPDCUO.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-JGWOA4LI.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-BN2GSFKT.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(", ")}` : ""}`;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
snapshotUrl,
|
|
3
3
|
snapshotUrls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-YOZLPNJS.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IZ6GTU54.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-6NXPDCUO.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-JGWOA4LI.js";
|
|
32
32
|
|
|
33
33
|
// src/audit/cli.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SOURCE_FILE,
|
|
3
|
-
readPaths
|
|
4
|
-
} from "./chunk-BK4AS2RU.js";
|
|
5
|
-
import {
|
|
6
|
-
toJson
|
|
7
|
-
} from "./chunk-QIEDBL4J.js";
|
|
8
|
-
import "./chunk-WK2WMWTH.js";
|
|
9
|
-
import "./chunk-CZ7KT452.js";
|
|
10
|
-
import "./chunk-QYBAVU3P.js";
|
|
11
|
-
|
|
12
|
-
// src/phone/cli.ts
|
|
13
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
-
import { join, resolve } from "path";
|
|
15
|
-
|
|
16
1
|
// src/phone/judge.ts
|
|
17
2
|
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
18
3
|
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
@@ -133,7 +118,16 @@ var LABEL = {
|
|
|
133
118
|
target: "small target",
|
|
134
119
|
field: "small field",
|
|
135
120
|
text: "small text",
|
|
136
|
-
title: "title cut off"
|
|
121
|
+
title: "title cut off",
|
|
122
|
+
overlap: "text overlaps text",
|
|
123
|
+
action: "main action out of reach",
|
|
124
|
+
layers: "fixed layers cover too much",
|
|
125
|
+
landscape: "landscape screen eaten by fixed layers",
|
|
126
|
+
image: "image not sized for its place",
|
|
127
|
+
input: "wrong keyboard",
|
|
128
|
+
autocomplete: "field the phone cannot fill in",
|
|
129
|
+
tel: "phone number is not a link",
|
|
130
|
+
linelength: "line too long"
|
|
137
131
|
};
|
|
138
132
|
function writeBaseline(findings, routes, note) {
|
|
139
133
|
const byFile = tally(findings, (f) => f.file);
|
|
@@ -147,103 +141,11 @@ function writeBaseline(findings, routes, note) {
|
|
|
147
141
|
};
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
// src/phone/cli.ts
|
|
151
|
-
var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]";
|
|
152
|
-
function parse(args) {
|
|
153
|
-
const flags = { config: "phone-first.json", update: false, json: false };
|
|
154
|
-
for (let i = 0; i < args.length; i++) {
|
|
155
|
-
const a = args[i];
|
|
156
|
-
if (a === "--update-baseline") flags.update = true;
|
|
157
|
-
else if (a === "--json") flags.json = true;
|
|
158
|
-
else if (a === "--config" || a === "--measured") {
|
|
159
|
-
const v = args[++i];
|
|
160
|
-
if (!v || v.startsWith("--")) return `${a} needs a value`;
|
|
161
|
-
if (a === "--config") flags.config = v;
|
|
162
|
-
else flags.measured = v;
|
|
163
|
-
} else return `unknown argument ${a}`;
|
|
164
|
-
}
|
|
165
|
-
return flags;
|
|
166
|
-
}
|
|
167
|
-
function loadConfig(path) {
|
|
168
|
-
if (!existsSync(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
|
|
169
|
-
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
170
|
-
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`);
|
|
171
|
-
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`);
|
|
172
|
-
if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
|
|
173
|
-
return { source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
|
|
174
|
-
}
|
|
175
|
-
function loadBaseline(path) {
|
|
176
|
-
if (!existsSync(path)) return EMPTY_BASELINE;
|
|
177
|
-
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
178
|
-
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
179
|
-
}
|
|
180
|
-
function loadMeasured(dir) {
|
|
181
|
-
if (!existsSync(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
|
|
182
|
-
const routes = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
|
|
183
|
-
if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
|
|
184
|
-
return routes;
|
|
185
|
-
}
|
|
186
|
-
function runPhoneFirst(args, io) {
|
|
187
|
-
const flags = parse(args);
|
|
188
|
-
if (typeof flags === "string") {
|
|
189
|
-
io.err(`craft phone-first: ${flags}
|
|
190
|
-
${USAGE}`);
|
|
191
|
-
return 2;
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
const config = loadConfig(resolve(io.cwd, flags.config));
|
|
195
|
-
const baselinePath = resolve(io.cwd, config.baseline);
|
|
196
|
-
const baseline = loadBaseline(baselinePath);
|
|
197
|
-
const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync(resolve(io.cwd, f)));
|
|
198
|
-
const sources = config.source.filter((s2) => existsSync(resolve(io.cwd, s2)));
|
|
199
|
-
const missing = config.source.filter((s2) => !sources.includes(s2));
|
|
200
|
-
if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
|
|
201
|
-
const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
|
|
202
|
-
const { findings, excused } = scanPhoneFirst(files, config.exceptions);
|
|
203
|
-
const routes = flags.measured ? loadMeasured(resolve(io.cwd, flags.measured)) : null;
|
|
204
|
-
if (flags.update) {
|
|
205
|
-
const next = writeBaseline(findings, routes ?? [], baseline.note);
|
|
206
|
-
if (!routes) next.measured = baseline.measured;
|
|
207
|
-
writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
208
|
-
`);
|
|
209
|
-
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)`);
|
|
210
|
-
return exceptionProblems.length ? 1 : 0;
|
|
211
|
-
}
|
|
212
|
-
const s = judgeStatic(findings, baseline);
|
|
213
|
-
const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
|
|
214
|
-
const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length > 0;
|
|
215
|
-
if (flags.json) {
|
|
216
|
-
io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m }));
|
|
217
|
-
return failed ? 1 : 0;
|
|
218
|
-
}
|
|
219
|
-
const section = (title, lines) => lines.length ? `
|
|
220
|
-
${title}
|
|
221
|
-
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
222
|
-
const known = s.known + m.known;
|
|
223
|
-
const summary = [
|
|
224
|
-
`${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at 375px` : ""}`,
|
|
225
|
-
excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
|
|
226
|
-
known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null
|
|
227
|
-
].filter(Boolean).join(", ");
|
|
228
|
-
if (!failed) {
|
|
229
|
-
io.out(`craft phone-first: pass. ${summary}.`);
|
|
230
|
-
return 0;
|
|
231
|
-
}
|
|
232
|
-
io.err(
|
|
233
|
-
[
|
|
234
|
-
`craft phone-first: fail. ${summary}.`,
|
|
235
|
-
section("The exceptions:", exceptionProblems),
|
|
236
|
-
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),
|
|
237
|
-
section("Measured at 375px:", m.fresh),
|
|
238
|
-
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
|
|
239
|
-
].join("")
|
|
240
|
-
);
|
|
241
|
-
return 1;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
io.err(`craft phone-first: ${error.message}`);
|
|
244
|
-
return 2;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
144
|
export {
|
|
248
|
-
|
|
145
|
+
EMPTY_BASELINE,
|
|
146
|
+
scanPhoneFirst,
|
|
147
|
+
checkExceptions,
|
|
148
|
+
judgeStatic,
|
|
149
|
+
judgeMeasured,
|
|
150
|
+
writeBaseline
|
|
249
151
|
};
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6NXPDCUO.js";
|
|
7
7
|
import {
|
|
8
8
|
toJson
|
|
9
9
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
hueSwatch,
|
|
23
23
|
nullPrompt,
|
|
24
24
|
scanSource
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-JGWOA4LI.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-7DQDF3VL.js";
|
|
29
29
|
|
|
30
30
|
// src/null/cli.ts
|
|
31
31
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
@@ -120,7 +120,7 @@ async function build(flags, io) {
|
|
|
120
120
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
121
121
|
if (toShoot.length) {
|
|
122
122
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
123
|
-
const { snapshotUrls } = await import("./audit-
|
|
123
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
|
|
124
124
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
125
125
|
results.forEach((r, i) => {
|
|
126
126
|
const id = toShoot[i];
|
|
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
|
|
|
241
241
|
const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
|
|
242
242
|
if (toShoot.length) {
|
|
243
243
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
|
|
244
|
-
const { snapshotUrls } = await import("./audit-
|
|
244
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
|
|
245
245
|
const servers = /* @__PURE__ */ new Map();
|
|
246
246
|
try {
|
|
247
247
|
for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
parseFile,
|
|
10
10
|
sentencesOf,
|
|
11
11
|
specificity
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7DQDF3VL.js";
|
|
13
13
|
import {
|
|
14
14
|
SNAPSHOT_VERSION
|
|
15
15
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -2425,6 +2425,9 @@ var HOUSE = {
|
|
|
2425
2425
|
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2426
2426
|
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2427
2427
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
2428
|
+
// New 2026-10-14 (founder): review first, like every new tell, until estate hits are read.
|
|
2429
|
+
"sales-tagline": { tier: "review", label: "sales tagline, say what you will actually do" },
|
|
2430
|
+
"outcome-promise": { tier: "review", label: "outcome promise, promise the work, not the result" },
|
|
2428
2431
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2429
2432
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
2430
2433
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3020,7 +3023,7 @@ var IMAGERY_TELLS = [
|
|
|
3020
3023
|
];
|
|
3021
3024
|
|
|
3022
3025
|
// src/character/check.ts
|
|
3023
|
-
var CATALOGUE_VERSION = "2026.
|
|
3026
|
+
var CATALOGUE_VERSION = "2026.10.2";
|
|
3024
3027
|
var CATALOGUE = [
|
|
3025
3028
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3026
3029
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IZ6GTU54.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-6NXPDCUO.js";
|
|
12
12
|
import {
|
|
13
13
|
toJson
|
|
14
14
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
fingerprint,
|
|
22
22
|
scanSource,
|
|
23
23
|
typicality
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-JGWOA4LI.js";
|
|
25
|
+
import "./chunk-7DQDF3VL.js";
|
|
26
26
|
import {
|
|
27
27
|
SNAPSHOT_VERSION
|
|
28
28
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
|
|
|
188
188
|
if (toTake.length) {
|
|
189
189
|
io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
|
|
190
190
|
try {
|
|
191
|
-
const { snapshotUrls } = await import("./audit-
|
|
191
|
+
const { snapshotUrls } = await import("./audit-BN2GSFKT.js");
|
|
192
192
|
const results = await snapshotUrls(toTake.map((t) => t.url));
|
|
193
193
|
results.forEach((r, i) => {
|
|
194
194
|
const { entry, keep } = toTake[i];
|
|
@@ -4,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-FQYKKKHI.js";
|
|
8
|
+
import "./chunk-2OBDCIVW.js";
|
|
9
|
+
import "./chunk-IZ6GTU54.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-6NXPDCUO.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-JGWOA4LI.js";
|
|
15
|
+
import "./chunk-7DQDF3VL.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
formatComponentPairs,
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-GOPHZI6O.js";
|
|
8
|
+
import "./chunk-YOZLPNJS.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-IZ6GTU54.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-6NXPDCUO.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-JGWOA4LI.js";
|
|
16
|
+
import "./chunk-7DQDF3VL.js";
|
|
17
17
|
import "./chunk-QYBAVU3P.js";
|
|
18
18
|
export {
|
|
19
19
|
describeComponents,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_BASELINE,
|
|
3
|
+
checkExceptions,
|
|
4
|
+
judgeMeasured,
|
|
5
|
+
judgeStatic,
|
|
6
|
+
scanPhoneFirst,
|
|
7
|
+
writeBaseline
|
|
8
|
+
} from "./chunk-IJUZ7RV4.js";
|
|
9
|
+
import {
|
|
10
|
+
SOURCE_FILE,
|
|
11
|
+
readPaths
|
|
12
|
+
} from "./chunk-6NXPDCUO.js";
|
|
13
|
+
import {
|
|
14
|
+
toJson
|
|
15
|
+
} from "./chunk-QIEDBL4J.js";
|
|
16
|
+
import "./chunk-JGWOA4LI.js";
|
|
17
|
+
import "./chunk-7DQDF3VL.js";
|
|
18
|
+
import "./chunk-QYBAVU3P.js";
|
|
19
|
+
|
|
20
|
+
// src/phone/cli.ts
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
22
|
+
import { join, resolve } from "path";
|
|
23
|
+
var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]";
|
|
24
|
+
function parse(args) {
|
|
25
|
+
const flags = { config: "phone-first.json", update: false, json: false };
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (a === "--update-baseline") flags.update = true;
|
|
29
|
+
else if (a === "--json") flags.json = true;
|
|
30
|
+
else if (a === "--config" || a === "--measured") {
|
|
31
|
+
const v = args[++i];
|
|
32
|
+
if (!v || v.startsWith("--")) return `${a} needs a value`;
|
|
33
|
+
if (a === "--config") flags.config = v;
|
|
34
|
+
else flags.measured = v;
|
|
35
|
+
} else return `unknown argument ${a}`;
|
|
36
|
+
}
|
|
37
|
+
return flags;
|
|
38
|
+
}
|
|
39
|
+
function loadConfig(path) {
|
|
40
|
+
if (!existsSync(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
|
|
41
|
+
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
42
|
+
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`);
|
|
43
|
+
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`);
|
|
44
|
+
if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
|
|
45
|
+
return { source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
|
|
46
|
+
}
|
|
47
|
+
function loadBaseline(path) {
|
|
48
|
+
if (!existsSync(path)) return EMPTY_BASELINE;
|
|
49
|
+
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
+
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
51
|
+
}
|
|
52
|
+
function loadMeasured(dir) {
|
|
53
|
+
if (!existsSync(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
|
|
54
|
+
const routes = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
|
|
55
|
+
if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
|
|
56
|
+
return routes;
|
|
57
|
+
}
|
|
58
|
+
function runPhoneFirst(args, io) {
|
|
59
|
+
const flags = parse(args);
|
|
60
|
+
if (typeof flags === "string") {
|
|
61
|
+
io.err(`craft phone-first: ${flags}
|
|
62
|
+
${USAGE}`);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const config = loadConfig(resolve(io.cwd, flags.config));
|
|
67
|
+
const baselinePath = resolve(io.cwd, config.baseline);
|
|
68
|
+
const baseline = loadBaseline(baselinePath);
|
|
69
|
+
const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync(resolve(io.cwd, f)));
|
|
70
|
+
const sources = config.source.filter((s2) => existsSync(resolve(io.cwd, s2)));
|
|
71
|
+
const missing = config.source.filter((s2) => !sources.includes(s2));
|
|
72
|
+
if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
|
|
73
|
+
const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
|
|
74
|
+
const { findings, excused } = scanPhoneFirst(files, config.exceptions);
|
|
75
|
+
const routes = flags.measured ? loadMeasured(resolve(io.cwd, flags.measured)) : null;
|
|
76
|
+
if (flags.update) {
|
|
77
|
+
const next = writeBaseline(findings, routes ?? [], baseline.note);
|
|
78
|
+
if (!routes) next.measured = baseline.measured;
|
|
79
|
+
writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
80
|
+
`);
|
|
81
|
+
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)`);
|
|
82
|
+
return exceptionProblems.length ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
const s = judgeStatic(findings, baseline);
|
|
85
|
+
const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
|
|
86
|
+
const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length > 0;
|
|
87
|
+
if (flags.json) {
|
|
88
|
+
io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m }));
|
|
89
|
+
return failed ? 1 : 0;
|
|
90
|
+
}
|
|
91
|
+
const wide = routes?.some((r) => r.viewports) ?? false;
|
|
92
|
+
const section = (title, lines) => lines.length ? `
|
|
93
|
+
${title}
|
|
94
|
+
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
95
|
+
const known = s.known + m.known;
|
|
96
|
+
const summary = [
|
|
97
|
+
`${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at ${wide ? "every viewport" : "375px"}` : ""}`,
|
|
98
|
+
excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
|
|
99
|
+
known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null
|
|
100
|
+
].filter(Boolean).join(", ");
|
|
101
|
+
if (!failed) {
|
|
102
|
+
io.out(`craft phone-first: pass. ${summary}.`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
io.err(
|
|
106
|
+
[
|
|
107
|
+
`craft phone-first: fail. ${summary}.`,
|
|
108
|
+
section("The exceptions:", exceptionProblems),
|
|
109
|
+
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),
|
|
110
|
+
section(wide ? "Measured across the viewports, phone first:" : "Measured at 375px:", m.fresh),
|
|
111
|
+
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
|
|
112
|
+
].join("")
|
|
113
|
+
);
|
|
114
|
+
return 1;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
io.err(`craft phone-first: ${error.message}`);
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
runPhoneFirst
|
|
122
|
+
};
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
PROPOSED,
|
|
8
8
|
researchPrompt,
|
|
9
9
|
validateDirection
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2OBDCIVW.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-6NXPDCUO.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
isCream,
|
|
27
27
|
oklchToRgb,
|
|
28
28
|
parseColour
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import "./chunk-
|
|
29
|
+
} from "./chunk-JGWOA4LI.js";
|
|
30
|
+
import "./chunk-7DQDF3VL.js";
|
|
31
31
|
import "./chunk-QYBAVU3P.js";
|
|
32
32
|
|
|
33
33
|
// src/direction/cli.ts
|
|
@@ -208,7 +208,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
|
208
208
|
async function snapshotFrom(target, cwd) {
|
|
209
209
|
if (!target) return void 0;
|
|
210
210
|
if (/^https?:\/\//.test(target)) {
|
|
211
|
-
const { snapshotUrl } = await import("./audit-
|
|
211
|
+
const { snapshotUrl } = await import("./audit-BN2GSFKT.js");
|
|
212
212
|
return snapshotUrl(target);
|
|
213
213
|
}
|
|
214
214
|
return readSnapshot(readJson(resolve(cwd, target)), target);
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-IZ6GTU54.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-6NXPDCUO.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-JGWOA4LI.js";
|
|
18
|
+
import "./chunk-7DQDF3VL.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|