@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CHARACTER.md +4 -2
  3. package/COPY.md +3 -1
  4. package/README.md +51 -0
  5. package/STANDARD.md +86 -3
  6. package/dist/{audit-43PC4LMS.js → audit-BN2GSFKT.js} +3 -3
  7. package/dist/audit.cjs +80 -5
  8. package/dist/audit.js +80 -5
  9. package/dist/{chunk-4LJFZH2I.js → chunk-2OBDCIVW.js} +2 -2
  10. package/dist/{chunk-BK4AS2RU.js → chunk-6NXPDCUO.js} +26 -13
  11. package/dist/{chunk-CZ7KT452.js → chunk-7DQDF3VL.js} +76 -4
  12. package/dist/{chunk-6M2LK7QE.js → chunk-FQYKKKHI.js} +5 -5
  13. package/dist/{chunk-XVMKZ4SM.js → chunk-GOPHZI6O.js} +4 -4
  14. package/dist/{cli-6LCOSHMW.js → chunk-IJUZ7RV4.js} +16 -114
  15. package/dist/{chunk-D2FTTM4F.js → chunk-IZ6GTU54.js} +5 -5
  16. package/dist/{chunk-WK2WMWTH.js → chunk-JGWOA4LI.js} +5 -2
  17. package/dist/{chunk-VDUIIWG7.js → chunk-YOZLPNJS.js} +1 -1
  18. package/dist/{cli-Q5X3W6I4.js → cli-DVYLDCEA.js} +5 -5
  19. package/dist/{cli-QAYH2OX5.js → cli-HUN5ZTH5.js} +6 -6
  20. package/dist/{cli-WN2GHYPF.js → cli-IREKGVSM.js} +6 -6
  21. package/dist/cli-LSJI7QRD.js +122 -0
  22. package/dist/{cli-APYEOHFV.js → cli-MKZJMZUJ.js} +5 -5
  23. package/dist/{cli-TMLHMNWU.js → cli-NTNUZUFP.js} +4 -4
  24. package/dist/{cli-4LEAEOOV.js → cli-VFKKFRB4.js} +9 -9
  25. package/dist/{cli-VPARW6FY.js → cli-ZXDGVDYU.js} +1 -1
  26. package/dist/cli.js +3 -3
  27. package/dist/index.cjs +80 -5
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +80 -5
  31. package/dist/phone.cjs +546 -19
  32. package/dist/phone.d.cts +265 -28
  33. package/dist/phone.d.ts +265 -28
  34. package/dist/phone.js +532 -19
  35. package/dist/viewports-cli-C4VXMPKK.js +683 -0
  36. package/package.json +1 -1
  37. 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: [f("content/home.md", "We are truly proud of it.")],
809
- pass: [f("content/home.md", "We have fitted 400 boilers since 2009.")]
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-4LJFZH2I.js";
7
+ } from "./chunk-2OBDCIVW.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-D2FTTM4F.js";
10
+ } from "./chunk-IZ6GTU54.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-BK4AS2RU.js";
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-WK2WMWTH.js";
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-43PC4LMS.js");
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-VDUIIWG7.js";
4
+ } from "./chunk-YOZLPNJS.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-D2FTTM4F.js";
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-BK4AS2RU.js";
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-WK2WMWTH.js";
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
- runPhoneFirst
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-BK4AS2RU.js";
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-WK2WMWTH.js";
25
+ } from "./chunk-JGWOA4LI.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-CZ7KT452.js";
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-43PC4LMS.js");
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-43PC4LMS.js");
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-CZ7KT452.js";
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.09.12";
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,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-WK2WMWTH.js";
7
+ } from "./chunk-JGWOA4LI.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-D2FTTM4F.js";
7
+ } from "./chunk-IZ6GTU54.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-BK4AS2RU.js";
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-WK2WMWTH.js";
25
- import "./chunk-CZ7KT452.js";
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-43PC4LMS.js");
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-6M2LK7QE.js";
8
- import "./chunk-4LJFZH2I.js";
9
- import "./chunk-D2FTTM4F.js";
7
+ } from "./chunk-FQYKKKHI.js";
8
+ import "./chunk-2OBDCIVW.js";
9
+ import "./chunk-IZ6GTU54.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-BK4AS2RU.js";
11
+ import "./chunk-6NXPDCUO.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-WK2WMWTH.js";
15
- import "./chunk-CZ7KT452.js";
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-XVMKZ4SM.js";
8
- import "./chunk-VDUIIWG7.js";
7
+ } from "./chunk-GOPHZI6O.js";
8
+ import "./chunk-YOZLPNJS.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-D2FTTM4F.js";
10
+ import "./chunk-IZ6GTU54.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-BK4AS2RU.js";
12
+ import "./chunk-6NXPDCUO.js";
13
13
  import "./chunk-QIEDBL4J.js";
14
14
  import "./chunk-WUACK2HX.js";
15
- import "./chunk-WK2WMWTH.js";
16
- import "./chunk-CZ7KT452.js";
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-4LJFZH2I.js";
10
+ } from "./chunk-2OBDCIVW.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-BK4AS2RU.js";
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-WK2WMWTH.js";
30
- import "./chunk-CZ7KT452.js";
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-43PC4LMS.js");
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-D2FTTM4F.js";
12
+ } from "./chunk-IZ6GTU54.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-BK4AS2RU.js";
14
+ import "./chunk-6NXPDCUO.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-WK2WMWTH.js";
18
- import "./chunk-CZ7KT452.js";
17
+ import "./chunk-JGWOA4LI.js";
18
+ import "./chunk-7DQDF3VL.js";
19
19
  import "./chunk-QYBAVU3P.js";
20
20
  export {
21
21
  describeTypicality,