@domandigital/craft 0.20.5 → 0.22.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CHARACTER.md +2 -1
  3. package/COPY.md +6 -2
  4. package/README.md +87 -4
  5. package/dist/{audit-5I2WWTL6.js → audit-LQ2TDISV.js} +3 -3
  6. package/dist/audit.cjs +64 -1
  7. package/dist/audit.js +64 -1
  8. package/dist/{chunk-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
  9. package/dist/{chunk-BBJOLR3R.js → chunk-6PZG4MJS.js} +5 -5
  10. package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
  11. package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
  12. package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
  13. package/dist/{chunk-TSO5W32N.js → chunk-OVWVKMPS.js} +14 -14
  14. package/dist/{chunk-N32UUN3S.js → chunk-RBQCGDQQ.js} +5 -5
  15. package/dist/chunk-XSKLVEI6.js +368 -0
  16. package/dist/{chunk-DMAMGFV7.js → chunk-ZS7PQNJU.js} +4 -4
  17. package/dist/{cli-OH6427MC.js → cli-7DL4BKA7.js} +6 -6
  18. package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
  19. package/dist/{cli-BI6XCMCL.js → cli-AM4FLXGM.js} +5 -5
  20. package/dist/{cli-JQYZXFRH.js → cli-R2HBESHI.js} +5 -5
  21. package/dist/{cli-V4QXTBWR.js → cli-SZ4SZLUN.js} +4 -4
  22. package/dist/{cli-W6FNSHRN.js → cli-T7A3BU5J.js} +9 -9
  23. package/dist/{cli-SQC27VYC.js → cli-WZA5IXSR.js} +6 -6
  24. package/dist/cli-YNIJOEJK.js +244 -0
  25. package/dist/cli.js +3 -3
  26. package/dist/index.cjs +64 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +64 -1
  30. package/dist/phone.cjs +194 -18
  31. package/dist/phone.d.cts +113 -13
  32. package/dist/phone.d.ts +113 -13
  33. package/dist/phone.js +182 -18
  34. package/dist/{viewports-cli-C4DWICYI.js → viewports-cli-F7KJIVZL.js} +51 -86
  35. package/package.json +1 -1
  36. package/word-lists.json +1 -1
  37. package/dist/chunk-IJUZ7RV4.js +0 -151
  38. package/dist/cli-56H5IPWT.js +0 -122
@@ -10,7 +10,7 @@ import {
10
10
  holdHouseBlocks,
11
11
  houseRule,
12
12
  scanSource
13
- } from "./chunk-PFVSKIPM.js";
13
+ } from "./chunk-DE2ZAV4G.js";
14
14
  import {
15
15
  compareCompetitor,
16
16
  compareFacts,
@@ -21,7 +21,7 @@ import {
21
21
  pageCopy,
22
22
  protectedFacts,
23
23
  visibleText
24
- } from "./chunk-FBWBDBW4.js";
24
+ } from "./chunk-ONA73THJ.js";
25
25
 
26
26
  // src/character/config.ts
27
27
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -622,7 +622,7 @@ async function pageSnapshot(target, cwd) {
622
622
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
623
623
  }
624
624
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
625
- const { snapshotUrl } = await import("./audit-5I2WWTL6.js");
625
+ const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
626
626
  return snapshotUrl(target);
627
627
  }
628
628
  async function briefFor(flags, cwd) {
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
633
633
  if (typeof brief === "string" && brief.trim()) return brief;
634
634
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
635
635
  if (flags.null) {
636
- const { loadNull } = await import("./cli-V4QXTBWR.js");
636
+ const { loadNull } = await import("./cli-SZ4SZLUN.js");
637
637
  return loadNull(flags.null, cwd).brief;
638
638
  }
639
639
  return void 0;
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
659
659
  function run(argv, io) {
660
660
  const [command, ...rest] = argv;
661
661
  if (command === "direction") {
662
- return import("./cli-JQYZXFRH.js").then((m) => m.runDirection(rest, io));
662
+ return import("./cli-R2HBESHI.js").then((m) => m.runDirection(rest, io));
663
663
  }
664
664
  if (command === "report" || command === "retrofit") {
665
- return import("./cli-W6FNSHRN.js").then((m) => m.runReport(command, rest, io));
665
+ return import("./cli-T7A3BU5J.js").then((m) => m.runReport(command, rest, io));
666
666
  }
667
667
  if (command === "register") {
668
- return import("./cli-NIH3WTNE.js").then((m) => m.runRegister(rest, io));
668
+ return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
669
669
  }
670
670
  if (command === "estate") {
671
- return import("./cli-SQC27VYC.js").then((m) => m.runEstate(rest, io));
671
+ return import("./cli-WZA5IXSR.js").then((m) => m.runEstate(rest, io));
672
672
  }
673
673
  if (command === "null") {
674
- return import("./cli-V4QXTBWR.js").then((m) => m.runNull(rest, io));
674
+ return import("./cli-SZ4SZLUN.js").then((m) => m.runNull(rest, io));
675
675
  }
676
676
  if (command === "calibrate") {
677
- return import("./cli-BI6XCMCL.js").then((m) => m.runCalibrate(rest, io));
677
+ return import("./cli-AM4FLXGM.js").then((m) => m.runCalibrate(rest, io));
678
678
  }
679
679
  if (command === "tells" && rest[0] === "harvest") {
680
- return import("./cli-V4QXTBWR.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
680
+ return import("./cli-SZ4SZLUN.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
681
681
  io.err(`craft: ${error.message}`);
682
682
  return 2;
683
683
  });
684
684
  }
685
685
  if (command === "phone-first") {
686
- return import("./cli-56H5IPWT.js").then((m) => m.runPhoneFirst(rest, io));
686
+ return import("./cli-YNIJOEJK.js").then((m) => m.runPhoneFirst(rest, io));
687
687
  }
688
688
  if (command === "viewports") {
689
- return import("./viewports-cli-C4DWICYI.js").then((m) => m.runViewports(rest, io));
689
+ return import("./viewports-cli-F7KJIVZL.js").then((m) => m.runViewports(rest, io));
690
690
  }
691
691
  if (command === "snapshot" || command === "audit") {
692
- return import("./cli-OH6427MC.js").then((m) => m.runAudit(command, rest, io));
692
+ return import("./cli-7DL4BKA7.js").then((m) => m.runAudit(command, rest, io));
693
693
  }
694
694
  if (!command || command === "help" || command === "--help" || command === "-h") {
695
695
  io.out(HELP);
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-GJY2VMPD.js";
7
+ } from "./chunk-G47ZCBD5.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-BBJOLR3R.js";
10
+ } from "./chunk-6PZG4MJS.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-TSO5W32N.js";
13
+ } from "./chunk-OVWVKMPS.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-PFVSKIPM.js";
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-5I2WWTL6.js");
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(", ")}` : ""}`;
@@ -0,0 +1,368 @@
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
+ screens: "page too long for a phone"
152
+ };
153
+ function writeBaseline(findings, routes, note, previous) {
154
+ const byFile = tally(findings, (f) => f.file);
155
+ const measured = routes.flatMap(
156
+ ({ 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 }))
157
+ );
158
+ const staticReasons = new Map((previous?.static ?? []).filter(hasReason).map((b) => [b.file, b.reason]));
159
+ const measuredReasons = new Map((previous?.measured ?? []).filter(hasReason).map((b) => [measuredKey(b.route, b.check, b.element), b.reason]));
160
+ return {
161
+ ...note ? { note } : {},
162
+ 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)),
163
+ 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))
164
+ };
165
+ }
166
+ function judgeGrowth(current, base, baseName = "main") {
167
+ const out = [];
168
+ const baseStatic = new Map(base.static.map((b) => [b.file, b.count]));
169
+ for (const b of current.static) {
170
+ const was = baseStatic.get(b.file) ?? 0;
171
+ 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`}`);
172
+ }
173
+ const baseMeasured = new Map(base.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
174
+ for (const b of current.measured) {
175
+ const was = baseMeasured.get(measuredKey(b.route, b.check, b.element)) ?? 0;
176
+ 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})`}`);
177
+ }
178
+ return out;
179
+ }
180
+ var hasUnexcusedEntries = (b) => [...b.static, ...b.measured].some((e) => !hasReason(e));
181
+ var PAGE_FILE = /(?:^|\/)page\.(?:tsx|ts|jsx|js|mdx)$/;
182
+ var INTERCEPTING = /^\(\.{1,3}\)/;
183
+ function routesFromPages(files) {
184
+ const routes = /* @__PURE__ */ new Set();
185
+ for (const file of files) {
186
+ const rel = file.replace(/\\/g, "/");
187
+ if (!PAGE_FILE.test(rel)) continue;
188
+ const dirs = rel.split("/").slice(0, -1);
189
+ if (dirs.some((d) => INTERCEPTING.test(d) || d.startsWith("_"))) continue;
190
+ const url = dirs.filter((d) => !/^\(.*\)$/.test(d) && !d.startsWith("@"));
191
+ routes.add(`/${url.join("/")}`);
192
+ }
193
+ return [...routes].sort();
194
+ }
195
+ function routesFromManifest(manifest) {
196
+ const routes = /* @__PURE__ */ new Set();
197
+ for (const [key, route] of Object.entries(manifest ?? {})) {
198
+ if (!/(?:^|\/)page$/.test(key) || typeof route !== "string") continue;
199
+ if (route.split("/").some((d) => d.startsWith("_"))) continue;
200
+ routes.add(route);
201
+ }
202
+ return [...routes].sort();
203
+ }
204
+ var segmentsOf = (route) => route.split("/").filter(Boolean);
205
+ var rank = (seg) => seg.startsWith("[[...") ? 3 : seg.startsWith("[...") ? 2 : seg.startsWith("[") ? 1 : 0;
206
+ var cleanRoute = (route) => {
207
+ const path = route.replace(/[?#].*$/, "").replace(/\/+$/, "");
208
+ return path === "" ? "/" : path;
209
+ };
210
+ function routeMatches(pattern, route) {
211
+ if (pattern === route) return true;
212
+ const want = segmentsOf(pattern);
213
+ const have = segmentsOf(route);
214
+ for (let i = 0; i < want.length; i++) {
215
+ const seg = want[i];
216
+ if (seg.startsWith("[[...")) return true;
217
+ if (seg.startsWith("[...")) return have.length > i;
218
+ if (have[i] === void 0) return false;
219
+ if (!seg.startsWith("[") && seg !== have[i]) return false;
220
+ }
221
+ return want.length === have.length;
222
+ }
223
+ var moreSpecific = (a, b) => {
224
+ const x = segmentsOf(a).map(rank);
225
+ const y = segmentsOf(b).map(rank);
226
+ for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i];
227
+ return x.length > y.length;
228
+ };
229
+ var excludes = (entry, page) => entry.endsWith("/*") ? page === entry.slice(0, -2) || page.startsWith(entry.slice(0, -1)) : entry === page;
230
+ function judgeRoutes(pages, measuredRoutes, excluded = []) {
231
+ const problems = [];
232
+ excluded.forEach((e, i) => {
233
+ const at = `excludeRoutes[${i}]`;
234
+ const x = e;
235
+ if (typeof x?.route !== "string" || !x.route.startsWith("/")) problems.push(`${at}: names no route (start it with /)`);
236
+ 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.`);
237
+ 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.`);
238
+ });
239
+ const named = excluded.filter((e) => typeof e?.route === "string" && typeof e.reason === "string" && e.reason.trim());
240
+ const covered = /* @__PURE__ */ new Set();
241
+ for (const route of measuredRoutes.map(cleanRoute)) {
242
+ const hit = pages.filter((p) => routeMatches(p, route)).sort((a, b) => moreSpecific(a, b) ? -1 : moreSpecific(b, a) ? 1 : 0)[0];
243
+ if (hit) covered.add(hit);
244
+ }
245
+ for (const page of pages) if (named.some((e) => excludes(e.route, page))) covered.add(page);
246
+ return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
247
+ }
248
+
249
+ // src/phone/viewports.ts
250
+ var LADDER = [
251
+ { name: "320", width: 320, height: 568, phone: true },
252
+ { name: "375", width: 375, height: 812, phone: true },
253
+ { name: "390", width: 390, height: 844, phone: true },
254
+ { name: "430", width: 430, height: 932, phone: true },
255
+ { name: "844x390", width: 844, height: 390, phone: true, landscape: true },
256
+ { name: "768", width: 768, height: 1024, phone: false },
257
+ { name: "1024", width: 1024, height: 768, phone: false },
258
+ { name: "1280", width: 1280, height: 800, phone: false },
259
+ { name: "1440", width: 1440, height: 900, phone: false },
260
+ { name: "1920", width: 1920, height: 1080, phone: false }
261
+ ];
262
+ var SHORT_PHONE = { name: "375x667", width: 375, height: 667, phone: true };
263
+ var DEFAULT_SWEEP = { from: 320, to: 1920, step: 16, wideStep: 32, wideFrom: 1024, height: 800 };
264
+ function sweepWidths(sweep = DEFAULT_SWEEP) {
265
+ const out = [];
266
+ for (let w = sweep.from; w <= sweep.to; w += w < sweep.wideFrom ? sweep.step : sweep.wideStep) out.push(w);
267
+ if (out.at(-1) !== sweep.to) out.push(sweep.to);
268
+ return out;
269
+ }
270
+ var DEFAULT_THRESHOLDS = {
271
+ maxLayerShare: 0.25,
272
+ landscapeMaxLayerShare: 0.5,
273
+ oversizeRatio: 2.5,
274
+ upscaleRatio: 1,
275
+ maxLineCh: 80,
276
+ overlapShare: 0.4,
277
+ minImagePx: 32
278
+ };
279
+ var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
280
+ function screenBudgetFor(budgets, route) {
281
+ if (!budgets) return void 0;
282
+ const path = cleanRoute(route);
283
+ let best;
284
+ for (const key of Object.keys(budgets)) {
285
+ const prefix = key.endsWith("/*") ? key.slice(0, -1) : null;
286
+ const hit = key === "*" ? 0 : prefix ? path === key.slice(0, -2) || path.startsWith(prefix) ? 1 + prefix.length : -1 : routeMatches(key, path) ? key === path ? 1e4 : 5e3 + key.length : -1;
287
+ if (hit >= 0 && (!best || hit > best.score)) best = { key, score: hit };
288
+ }
289
+ return best ? budgets[best.key] : void 0;
290
+ }
291
+ function checkScreenBudgets(budgets, where = "screens") {
292
+ if (budgets === void 0) return void 0;
293
+ if (!budgets || typeof budgets !== "object" || Array.isArray(budgets)) throw new Error(`${where} should map a route to its budget in phone screens, such as { "/": 6.5, "/services/*": 6 }`);
294
+ for (const [route, value] of Object.entries(budgets)) {
295
+ if (route !== "*" && !route.startsWith("/")) throw new Error(`${where}: "${route}" should start with / (or be *)`);
296
+ if (typeof value !== "number" || !(value > 0)) throw new Error(`${where}: "${route}" should be a number of phone screens above 0, not ${JSON.stringify(value)}`);
297
+ }
298
+ return budgets;
299
+ }
300
+ function formatScreens(routes, budgets) {
301
+ return routes.filter((r) => r.screens !== void 0).map((r) => {
302
+ const budget = screenBudgetFor(budgets, r.route);
303
+ return `${r.route} ${r.screens}${budget === void 0 ? "" : ` of ${budget}`}`;
304
+ }).join(", ");
305
+ }
306
+ function formatWidths(failing, tested) {
307
+ const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
308
+ const rank2 = new Map(plain.map((name, i) => [name, i]));
309
+ const names = [...new Set(failing)];
310
+ const runs = [];
311
+ for (const name of names.filter((n) => rank2.has(n)).sort((a, b) => rank2.get(a) - rank2.get(b))) {
312
+ const last = runs.at(-1);
313
+ if (last && rank2.get(name) === rank2.get(last.at(-1)) + 1) last.push(name);
314
+ else runs.push([name]);
315
+ }
316
+ const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
317
+ return [...out, ...names.filter((n) => !rank2.has(n))].join(", ");
318
+ }
319
+ var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
320
+ function mergeViewportFindings(perViewport, tested) {
321
+ const merged = /* @__PURE__ */ new Map();
322
+ for (const { viewport, findings } of perViewport) {
323
+ const counts = /* @__PURE__ */ new Map();
324
+ for (const f of findings) {
325
+ const k = keyOf(f);
326
+ const had = counts.get(k);
327
+ if (had) had.n++;
328
+ else counts.set(k, { first: f, n: 1 });
329
+ }
330
+ for (const [k, { first, n }] of counts) {
331
+ const had = merged.get(k);
332
+ if (had) {
333
+ had.at.push(viewport);
334
+ had.most = Math.max(had.most, n);
335
+ } else merged.set(k, { first, at: [viewport], most: n });
336
+ }
337
+ }
338
+ const out = [];
339
+ for (const { first, at, most } of merged.values()) {
340
+ for (let i = 0; i < most; i++) out.push({ ...first, detail: `${first.detail} (at ${formatWidths(at, tested)})` });
341
+ }
342
+ return out;
343
+ }
344
+
345
+ export {
346
+ parseBaseline,
347
+ EMPTY_BASELINE,
348
+ scanPhoneFirst,
349
+ checkExceptions,
350
+ judgeStatic,
351
+ judgeMeasured,
352
+ writeBaseline,
353
+ judgeGrowth,
354
+ hasUnexcusedEntries,
355
+ routesFromPages,
356
+ routesFromManifest,
357
+ judgeRoutes,
358
+ LADDER,
359
+ SHORT_PHONE,
360
+ DEFAULT_SWEEP,
361
+ sweepWidths,
362
+ DEFAULT_THRESHOLDS,
363
+ mainActionFor,
364
+ screenBudgetFor,
365
+ checkScreenBudgets,
366
+ formatScreens,
367
+ mergeViewportFindings
368
+ };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-YFS32BZ2.js";
4
+ } from "./chunk-6DRBFDWT.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-BBJOLR3R.js";
8
+ } from "./chunk-6PZG4MJS.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-TSO5W32N.js";
17
+ } from "./chunk-OVWVKMPS.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-PFVSKIPM.js";
31
+ } from "./chunk-DE2ZAV4G.js";
32
32
 
33
33
  // src/audit/cli.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -4,16 +4,16 @@ import {
4
4
  merge,
5
5
  resolvePages,
6
6
  runAudit
7
- } from "./chunk-DMAMGFV7.js";
8
- import "./chunk-YFS32BZ2.js";
7
+ } from "./chunk-ZS7PQNJU.js";
8
+ import "./chunk-6DRBFDWT.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-BBJOLR3R.js";
10
+ import "./chunk-6PZG4MJS.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-TSO5W32N.js";
12
+ import "./chunk-OVWVKMPS.js";
13
13
  import "./chunk-QIEDBL4J.js";
14
14
  import "./chunk-WUACK2HX.js";
15
- import "./chunk-PFVSKIPM.js";
16
- import "./chunk-FBWBDBW4.js";
15
+ import "./chunk-DE2ZAV4G.js";
16
+ import "./chunk-ONA73THJ.js";
17
17
  import "./chunk-QYBAVU3P.js";
18
18
  export {
19
19
  describeComponents,
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-FBWBDBW4.js";
12
+ } from "./chunk-ONA73THJ.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-BBJOLR3R.js";
7
+ } from "./chunk-6PZG4MJS.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-TSO5W32N.js";
11
+ } from "./chunk-OVWVKMPS.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-PFVSKIPM.js";
25
- import "./chunk-FBWBDBW4.js";
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-5I2WWTL6.js");
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];
@@ -19,10 +19,10 @@ import {
19
19
  checkReason,
20
20
  researchPrompt,
21
21
  validateDirection
22
- } from "./chunk-GJY2VMPD.js";
22
+ } from "./chunk-G47ZCBD5.js";
23
23
  import {
24
24
  parseFlags
25
- } from "./chunk-TSO5W32N.js";
25
+ } from "./chunk-OVWVKMPS.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-PFVSKIPM.js";
42
- import "./chunk-FBWBDBW4.js";
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-5I2WWTL6.js");
487
+ const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
488
488
  return snapshotUrl(target);
489
489
  }
490
490
  return readSnapshot(readJson(resolve(cwd, target)), target);
@@ -9,13 +9,13 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-BBJOLR3R.js";
12
+ } from "./chunk-6PZG4MJS.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-TSO5W32N.js";
14
+ import "./chunk-OVWVKMPS.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-PFVSKIPM.js";
18
- import "./chunk-FBWBDBW4.js";
17
+ import "./chunk-DE2ZAV4G.js";
18
+ import "./chunk-ONA73THJ.js";
19
19
  import "./chunk-QYBAVU3P.js";
20
20
  export {
21
21
  describeTypicality,