@domandigital/craft 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6545c2e: Calibration tooling. `craft null import <dir> --builder <name> --brief "<text>"` builds a null model from pages another builder made (v0, Lovable), each an HTML file or a folder holding a built `index.html`, served on loopback so root-relative assets resolve. `craft null prompt` prints the prompt to give that builder. `craft calibrate <labels.json>` measures a labelled set (`ai`, `human`, `ai-looking`) and reports precision and recall per tell and CHARACTER.md's first three targets; a page it cannot measure is listed and never counts as a pass. `NullModel` gains an optional `builder`. The blind-test protocol for target 5 is in `docs/blind-test.md`.
8
+ - cb7317e: Lexical and specificity measurement. New exports `lexicalOf`, `vocabulary`, `vocabularyCounts`, `LEXICAL_FEATURES`, `STUDY_WORDS` and the `LexicalFeatures` and `StudyWord` types compute how often a block uses craft's own tell words and the published excess-use words, and how particular it is (names, places, figures, trade nouns). Every study word carries its source paper. Measured against the frozen human baselines and the Claude set, no feature met the ship rule, so no tell is added and `CATALOGUE_VERSION` is unchanged; the result is in `calibration/copy-shape/report-lexical.md`.
9
+
3
10
  ## 0.15.0
4
11
 
5
12
  ### Minor Changes
package/CHARACTER.md CHANGED
@@ -826,6 +826,40 @@ generated-looking site" and is left out by definition. Snapshots and
826
826
  - Both house demos had expired or gated previews on the day. They are not
827
827
  measured, not clean.
828
828
 
829
+ ### Calibration tooling, 2026-09-24
830
+
831
+ `craft calibrate calibration/labels.json` now does what the `score.mjs`
832
+ scripts did by hand. `labels.json` lists the set:
833
+
834
+ - the 20 AI-set pages;
835
+ - the seven measured human references, with the two gated previews listed as
836
+ skipped;
837
+ - wiredge.studio as the first `ai-looking` page: made by people, and it reads
838
+ as generated.
839
+
840
+ `calibrate` reports precision and recall per tell and checks targets 1 to 3.
841
+ Its rules:
842
+
843
+ - A hit on an `ai` or `ai-looking` page counts for a tell, and a hit on a
844
+ `human` page counts against it, because a tell detects a look, not a maker.
845
+ - Flagged keeps the rule fixed on 2026-09-23: typical against the pooled null,
846
+ or three or more distinct design tells.
847
+ - A page it cannot measure is listed by name and counted neither way.
848
+
849
+ `craft null import` builds a null model from another builder's pages, so the
850
+ AI set and the null can include more than Claude. Give each builder the prompt
851
+ `craft null prompt` prints, so its pages answer the same brief.
852
+
853
+ Target 5, the blind test, is a protocol for people, not a command: see
854
+ `docs/blind-test.md`.
855
+
856
+ **No run recorded yet.** The first run needs a browser that can reach live
857
+ pages: wiredge.studio has no snapshot, and `--fresh` re-snapshots every page on
858
+ snapshot v2, which is the recalibration Phase K still owes. The pinned
859
+ 2026-09-23 snapshots are version 1, so the rendered tells added in phases K to
860
+ N mostly cannot fire on them. Record the run here, with its date, alongside the
861
+ 2026-09-23 numbers.
862
+
829
863
  ### Character report first read, 2026-09-23
830
864
 
831
865
  `craft report` on each live snapshot with its repo, its own brief's null and
@@ -9,9 +9,9 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-OUQ25LY2.js";
12
+ } from "./chunk-YK2FFL6Z.js";
13
13
  import "./chunk-JYNXGCYZ.js";
14
- import "./chunk-LFNSB6W7.js";
14
+ import "./chunk-B2MJLGF6.js";
15
15
  import "./chunk-QYBAVU3P.js";
16
16
  export {
17
17
  PROVENANCE_BUDGET_MS,
@@ -4633,6 +4633,7 @@ export {
4633
4633
  NULL_VERSION,
4634
4634
  nullPrompt,
4635
4635
  extractHtml,
4636
+ MIN_RUNS,
4636
4637
  typicality,
4637
4638
  hueName,
4638
4639
  groundName,
@@ -4,14 +4,14 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-YDURAOYR.js";
7
+ } from "./chunk-WAV6AKR4.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-RXXRXV3V.js";
10
+ } from "./chunk-LWVGIJ5A.js";
11
11
  import {
12
12
  parseFlags,
13
13
  toJson
14
- } from "./chunk-MUVFECFC.js";
14
+ } from "./chunk-LXDNIJFG.js";
15
15
  import {
16
16
  readSnapshot
17
17
  } from "./chunk-WUACK2HX.js";
@@ -26,7 +26,7 @@ import {
26
26
  groundName,
27
27
  hueName,
28
28
  shapeName
29
- } from "./chunk-LFNSB6W7.js";
29
+ } from "./chunk-B2MJLGF6.js";
30
30
 
31
31
  // src/estate/cli.ts
32
32
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -103,7 +103,7 @@ async function snapshotOf(target, cwd) {
103
103
  return snap;
104
104
  }
105
105
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
106
- const { snapshotUrl } = await import("./audit-MTUC24XU.js");
106
+ const { snapshotUrl } = await import("./audit-UJCUWORL.js");
107
107
  return snapshotUrl(target);
108
108
  }
109
109
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -4,12 +4,13 @@ import {
4
4
  import {
5
5
  parseFlags,
6
6
  toJson
7
- } from "./chunk-MUVFECFC.js";
7
+ } from "./chunk-LXDNIJFG.js";
8
8
  import {
9
9
  readSnapshot
10
10
  } from "./chunk-WUACK2HX.js";
11
11
  import {
12
12
  CATALOGUE_VERSION,
13
+ MIN_RUNS,
13
14
  NULL_VERSION,
14
15
  auditSnapshot,
15
16
  checkCopy,
@@ -20,13 +21,19 @@ import {
20
21
  hueSwatch,
21
22
  nullPrompt,
22
23
  scanSource
23
- } from "./chunk-LFNSB6W7.js";
24
+ } from "./chunk-B2MJLGF6.js";
24
25
 
25
26
  // src/null/cli.ts
26
27
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
27
- import { join, relative, resolve } from "path";
28
+ import { createServer } from "http";
29
+ import { extname, join, relative, resolve, sep } from "path";
28
30
  import { pathToFileURL } from "url";
29
31
  var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
32
+ var NULL_USAGE = [
33
+ 'usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]',
34
+ ' craft null import <dir> --builder <name> --brief "<text>" [--out <dir>] [--model <name>]',
35
+ ' craft null prompt --brief "<text>"'
36
+ ].join("\n");
30
37
  var pad = (n) => String(n).padStart(2, "0");
31
38
  async function pool(items, size, work) {
32
39
  const queue = [...items];
@@ -38,6 +45,15 @@ function nullPath(target, cwd) {
38
45
  const abs = resolve(cwd, target);
39
46
  return existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "null.json") : abs;
40
47
  }
48
+ function expandNullTargets(targets, cwd) {
49
+ return targets.flatMap((t) => {
50
+ const abs = resolve(cwd, t);
51
+ if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
52
+ return readdirSync(abs).sort().map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
53
+ }
54
+ return [t];
55
+ });
56
+ }
41
57
  function loadNull(target, cwd) {
42
58
  const path = nullPath(target, cwd);
43
59
  if (!existsSync(path)) throw new Error(`no null model at ${target}. Build one with craft null build.`);
@@ -45,10 +61,11 @@ function loadNull(target, cwd) {
45
61
  if (model.version !== NULL_VERSION) throw new Error(`${target} is null model version ${String(model.version)}; this craft reads ${NULL_VERSION}`);
46
62
  return model;
47
63
  }
48
- function measureRun(id, html, snapshot) {
64
+ function measureRun(id, html, snapshot, styles = []) {
49
65
  const file = { path: `${id}.html`, text: html };
50
- const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file]).findings].map((f) => f.tell));
51
- const copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
66
+ const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file, ...styles]).findings].map((f) => f.tell));
67
+ let copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
68
+ if (copy.length === 0) copy = [...new Set(snapshot.sections.flatMap((s) => s.text ?? []).map((t) => t.replace(/\s+/g, " ").trim()).filter(Boolean))];
52
69
  return { id, fingerprint: fingerprint(snapshot), tells: [...tells].sort(), copy };
53
70
  }
54
71
  async function build(flags, io) {
@@ -98,7 +115,7 @@ async function build(flags, io) {
98
115
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
99
116
  if (toShoot.length) {
100
117
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
101
- const { snapshotUrls } = await import("./audit-MTUC24XU.js");
118
+ const { snapshotUrls } = await import("./audit-UJCUWORL.js");
102
119
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
103
120
  results.forEach((r, i) => {
104
121
  const id = toShoot[i];
@@ -127,6 +144,142 @@ async function build(flags, io) {
127
144
  }
128
145
  return 0;
129
146
  }
147
+ function findImportPages(dir) {
148
+ const pages = [];
149
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
150
+ if (e.isFile() && /\.html?$/i.test(e.name)) pages.push({ id: e.name.replace(/\.html?$/i, ""), root: dir, entry: e.name });
151
+ else if (e.isDirectory() && existsSync(join(dir, e.name, "index.html"))) pages.push({ id: e.name, root: join(dir, e.name), entry: "index.html" });
152
+ }
153
+ const ids = pages.map((p) => p.id);
154
+ const twice = ids.find((id, i) => ids.indexOf(id) !== i);
155
+ if (twice) throw new Error(`"${twice}" is both a file and a folder; rename one`);
156
+ return pages.sort((a, b) => a.id.localeCompare(b.id));
157
+ }
158
+ var TYPES = {
159
+ ".html": "text/html",
160
+ ".htm": "text/html",
161
+ ".css": "text/css",
162
+ ".js": "text/javascript",
163
+ ".mjs": "text/javascript",
164
+ ".json": "application/json",
165
+ ".svg": "image/svg+xml",
166
+ ".png": "image/png",
167
+ ".jpg": "image/jpeg",
168
+ ".jpeg": "image/jpeg",
169
+ ".webp": "image/webp",
170
+ ".avif": "image/avif",
171
+ ".gif": "image/gif",
172
+ ".ico": "image/x-icon",
173
+ ".woff": "font/woff",
174
+ ".woff2": "font/woff2",
175
+ ".ttf": "font/ttf",
176
+ ".otf": "font/otf"
177
+ };
178
+ function serveDir(root) {
179
+ const server = createServer((req, res) => {
180
+ let file;
181
+ try {
182
+ file = resolve(root, `.${decodeURIComponent(new URL(req.url ?? "/", "http://local").pathname)}`);
183
+ } catch {
184
+ res.statusCode = 400;
185
+ return void res.end();
186
+ }
187
+ if (file !== root && !file.startsWith(root + sep)) {
188
+ res.statusCode = 403;
189
+ return void res.end();
190
+ }
191
+ if (existsSync(file) && statSync(file).isDirectory()) file = join(file, "index.html");
192
+ if (!existsSync(file)) {
193
+ res.statusCode = 404;
194
+ return void res.end();
195
+ }
196
+ res.setHeader("content-type", TYPES[extname(file).toLowerCase()] ?? "application/octet-stream");
197
+ res.end(readFileSync(file));
198
+ });
199
+ return new Promise((done, fail) => {
200
+ server.once("error", fail);
201
+ server.listen(
202
+ 0,
203
+ "127.0.0.1",
204
+ () => done({
205
+ base: `http://127.0.0.1:${server.address().port}`,
206
+ close: () => new Promise((closed) => server.close(() => closed()))
207
+ })
208
+ );
209
+ });
210
+ }
211
+ function stylesheets(root, id) {
212
+ const found = [];
213
+ const walk = (dir) => {
214
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
215
+ const path = join(dir, e.name);
216
+ if (e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")) walk(path);
217
+ else if (e.isFile() && e.name.endsWith(".css") && statSync(path).size <= 1e6) found.push({ path: `${id}/${relative(root, path)}`, text: readFileSync(path, "utf8") });
218
+ }
219
+ };
220
+ walk(root);
221
+ return found;
222
+ }
223
+ async function importPages(flags, io) {
224
+ const usage = 'usage: craft null import <dir> --builder <name> --brief "<who, where, what they do>" [--out <dir>] [--model <name>]';
225
+ const from = flags.positional[1];
226
+ const brief = flags.brief?.trim();
227
+ if (!from || flags.positional.length > 2 || !flags.builder || !brief || brief.split(/\s+/).length < 5) throw new Error(usage);
228
+ const dir = resolve(io.cwd, from);
229
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`no directory at ${from}`);
230
+ const pages = findImportPages(dir);
231
+ if (pages.length < MIN_RUNS) throw new Error(`${from} holds ${pages.length} page${pages.length === 1 ? "" : "s"}; a null model needs at least ${MIN_RUNS}`);
232
+ const out = resolve(io.cwd, flags.out ?? from);
233
+ const shots = join(out, "snapshots");
234
+ mkdirSync(shots, { recursive: true });
235
+ const failed = [];
236
+ const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
237
+ if (toShoot.length) {
238
+ io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
239
+ const { snapshotUrls } = await import("./audit-UJCUWORL.js");
240
+ const servers = /* @__PURE__ */ new Map();
241
+ try {
242
+ for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
243
+ const urls = toShoot.map((p) => `${servers.get(p.root)?.base}/${p.entry === "index.html" ? "" : encodeURIComponent(p.entry)}`);
244
+ const results = await snapshotUrls(urls);
245
+ results.forEach((r, i) => {
246
+ const p = toShoot[i];
247
+ if (r.snapshot) writeFileSync(join(shots, `${p.id}.json`), `${JSON.stringify({ ...r.snapshot, url: relative(out, join(p.root, p.entry)) }, null, 2)}
248
+ `);
249
+ else failed.push(`${p.id}: the page would not render: ${r.error}`);
250
+ });
251
+ } finally {
252
+ await Promise.all([...servers.values()].map((s) => s.close()));
253
+ }
254
+ }
255
+ const measured = pages.filter((p) => existsSync(join(shots, `${p.id}.json`))).map(
256
+ (p) => measureRun(
257
+ p.id,
258
+ readFileSync(join(p.root, p.entry), "utf8"),
259
+ readSnapshot(readJson(join(shots, `${p.id}.json`)), `${p.id}.json`),
260
+ p.root === dir ? [] : stylesheets(p.root, p.id)
261
+ )
262
+ );
263
+ if (measured.length < MIN_RUNS) {
264
+ for (const f of failed) io.err(` ${f}`);
265
+ throw new Error(`only ${measured.length} of ${pages.length} pages rendered; a null model needs at least ${MIN_RUNS}`);
266
+ }
267
+ const model = {
268
+ version: NULL_VERSION,
269
+ brief,
270
+ prompt: nullPrompt(brief),
271
+ model: flags.model ?? "unknown",
272
+ builder: flags.builder,
273
+ catalogueVersion: CATALOGUE_VERSION,
274
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
275
+ runs: measured
276
+ };
277
+ writeFileSync(join(out, "null.json"), `${JSON.stringify(model, null, 2)}
278
+ `);
279
+ io.out(`craft null: ${measured.length} of ${pages.length} pages from ${flags.builder} in ${relative(io.cwd, join(out, "null.json")) || "null.json"}`);
280
+ for (const f of failed) io.err(` ${f}`);
281
+ return measured.length < pages.length ? 1 : 0;
282
+ }
130
283
  function copyTell(phrase) {
131
284
  const text = `We found that ${phrase} was there.`;
132
285
  const hit = checkCopy([{ path: "phrase.md", text }]).findings[0];
@@ -162,7 +315,13 @@ async function runNull(args, io) {
162
315
  try {
163
316
  const flags = parseFlags(args);
164
317
  if (typeof flags === "string") throw new Error(flags);
165
- if (flags.positional[0] !== "build") throw new Error('usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]');
318
+ if (flags.positional[0] === "import") return await importPages(flags, io);
319
+ if (flags.positional[0] === "prompt") {
320
+ if (!flags.brief || flags.brief.trim().split(/\s+/).length < 5) throw new Error('usage: craft null prompt --brief "<who, where, what they do>"');
321
+ io.out(nullPrompt(flags.brief));
322
+ return 0;
323
+ }
324
+ if (flags.positional[0] !== "build") throw new Error(NULL_USAGE);
166
325
  return await build(flags, io);
167
326
  } catch (error) {
168
327
  io.err(`craft: ${error.message}`);
@@ -174,14 +333,7 @@ function runHarvest(args, io) {
174
333
  if (typeof flags === "string") throw new Error(flags);
175
334
  const targets = flags.positional.slice(1);
176
335
  if (targets.length === 0) throw new Error("usage: craft tells harvest <null.json | dir>... [--share 0.25] [--json]");
177
- const paths = targets.flatMap((t) => {
178
- const abs = resolve(io.cwd, t);
179
- if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
180
- return readdirSync(abs).map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
181
- }
182
- return [t];
183
- });
184
- const models = paths.map((p) => loadNull(p, io.cwd));
336
+ const models = expandNullTargets(targets, io.cwd).map((p) => loadNull(p, io.cwd));
185
337
  const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
186
338
  if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
187
339
  const candidates = harvest(models, { minShare, copyTell });
@@ -191,8 +343,11 @@ function runHarvest(args, io) {
191
343
 
192
344
  export {
193
345
  nullPath,
346
+ expandNullTargets,
194
347
  loadNull,
195
348
  measureRun,
349
+ findImportPages,
350
+ serveDir,
196
351
  formatHarvest,
197
352
  describeTypicality,
198
353
  runNull,
@@ -16,7 +16,7 @@ import {
16
16
  protectedFacts,
17
17
  scanSource,
18
18
  visibleText
19
- } from "./chunk-LFNSB6W7.js";
19
+ } from "./chunk-B2MJLGF6.js";
20
20
 
21
21
  // src/character/config.ts
22
22
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -382,10 +382,13 @@ Usage
382
382
  craft direction research [--brief "<who, where, what>"] [--visual]
383
383
  craft direction propose [--snapshot <file>] [--estate <estate.json | dir>] [--out <file>]
384
384
  craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]
385
+ craft null import <dir> --builder <name> --brief "<text>" [--out <dir>] [--model <name>]
386
+ craft null prompt --brief "<text>"
385
387
  craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] [--estate <file>]
386
388
  craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--directions] [--json] [--strict]
387
389
  craft report <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <file>] [--direction <file>] [--json] [--strict]
388
390
  craft retrofit <url | snapshot.json> [the same] [--out RETROFIT.md]
391
+ craft calibrate <labels.json> [--out <dir>] [--fresh] [--json]
389
392
 
390
393
  scan Markup, component code and stylesheets (source and compiled CSS).
391
394
  --staged reads the git index, for a pre-commit hook.
@@ -419,6 +422,10 @@ direction The site's art-direction.json: every choice with a reason from the
419
422
  sources' PNG photos and drafts choices for a person to confirm.
420
423
  null The counterfactual: about 20 pages \`claude -p\` builds from the brief
421
424
  alone, snapshotted and fingerprinted into null.json. Resumable.
425
+ import does the same for pages another builder made (v0, Lovable):
426
+ each an HTML file, or a folder holding a built index.html, served
427
+ on loopback so its asset paths resolve. prompt prints what to give
428
+ the builder, so its pages answer the same brief. Resumable.
422
429
  estate The register of shipped sites (estate.json). add fingerprints a site;
423
430
  compare lists the nearest, and marks siblings: two sites closer than
424
431
  two pages Claude builds for one brief. --strict exits 1 on a sibling.
@@ -427,6 +434,11 @@ report Every signal for one site: tells, typicality, the estate, the reasons.
427
434
  Anything not given is "not measured", and never counts as a pass.
428
435
  retrofit The report as a checklist: decide, then change type, colour, shape,
429
436
  effects, motion and copy. Never the page grammar.
437
+ calibrate How well the catalogue separates a labelled set (ai, human,
438
+ ai-looking): precision and recall per tell, and CHARACTER.md's
439
+ first three targets. --out keeps results.json and any snapshots it
440
+ took; --fresh takes every snapshot again. A page not measured is
441
+ listed, and never counts as a pass. A measure: always exit 0.
430
442
 
431
443
  ci options (scan, copy, audit)
432
444
  --baseline <file> Report only findings the baseline does not already
@@ -463,10 +475,11 @@ var VALUE_FLAGS = {
463
475
  "--viewport": "viewport",
464
476
  "--component": "component",
465
477
  "--competitor": "competitor",
466
- "--page": "page"
478
+ "--page": "page",
479
+ "--builder": "builder"
467
480
  };
468
481
  function parseFlags(args) {
469
- const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false };
482
+ const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false, fresh: false };
470
483
  for (let i = 0; i < args.length; i += 1) {
471
484
  const a = args[i];
472
485
  if (a === "--json") flags.json = true;
@@ -476,6 +489,7 @@ function parseFlags(args) {
476
489
  else if (a === "--update-baseline") flags.updateBaseline = true;
477
490
  else if (a === "--directions") flags.directions = true;
478
491
  else if (a === "--visual") flags.visual = true;
492
+ else if (a === "--fresh") flags.fresh = true;
479
493
  else if (a in VALUE_FLAGS) {
480
494
  const value = args[i + 1];
481
495
  i += 1;
@@ -580,7 +594,7 @@ async function pageSnapshot(target, cwd) {
580
594
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
581
595
  }
582
596
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
583
- const { snapshotUrl } = await import("./audit-MTUC24XU.js");
597
+ const { snapshotUrl } = await import("./audit-UJCUWORL.js");
584
598
  return snapshotUrl(target);
585
599
  }
586
600
  async function briefFor(flags, cwd) {
@@ -591,7 +605,7 @@ async function briefFor(flags, cwd) {
591
605
  if (typeof brief === "string" && brief.trim()) return brief;
592
606
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
593
607
  if (flags.null) {
594
- const { loadNull } = await import("./cli-FLG3ZCAF.js");
608
+ const { loadNull } = await import("./cli-AVYNHBFJ.js");
595
609
  return loadNull(flags.null, cwd).brief;
596
610
  }
597
611
  return void 0;
@@ -617,25 +631,28 @@ async function compareWithCompetitor(flags, io) {
617
631
  function run(argv, io) {
618
632
  const [command, ...rest] = argv;
619
633
  if (command === "direction") {
620
- return import("./cli-7OZCYOQY.js").then((m) => m.runDirection(rest, io));
634
+ return import("./cli-K4LSRYBE.js").then((m) => m.runDirection(rest, io));
621
635
  }
622
636
  if (command === "report" || command === "retrofit") {
623
- return import("./cli-FCKHYAVZ.js").then((m) => m.runReport(command, rest, io));
637
+ return import("./cli-CWORJYOU.js").then((m) => m.runReport(command, rest, io));
624
638
  }
625
639
  if (command === "estate") {
626
- return import("./cli-22TSWAX6.js").then((m) => m.runEstate(rest, io));
640
+ return import("./cli-PEATEW57.js").then((m) => m.runEstate(rest, io));
627
641
  }
628
642
  if (command === "null") {
629
- return import("./cli-FLG3ZCAF.js").then((m) => m.runNull(rest, io));
643
+ return import("./cli-AVYNHBFJ.js").then((m) => m.runNull(rest, io));
644
+ }
645
+ if (command === "calibrate") {
646
+ return import("./cli-3GQROFCB.js").then((m) => m.runCalibrate(rest, io));
630
647
  }
631
648
  if (command === "tells" && rest[0] === "harvest") {
632
- return import("./cli-FLG3ZCAF.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
649
+ return import("./cli-AVYNHBFJ.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
633
650
  io.err(`craft: ${error.message}`);
634
651
  return 2;
635
652
  });
636
653
  }
637
654
  if (command === "snapshot" || command === "audit") {
638
- return import("./cli-5ACKICBO.js").then((m) => m.runAudit(command, rest, io));
655
+ return import("./cli-52M62QBN.js").then((m) => m.runAudit(command, rest, io));
639
656
  }
640
657
  if (!command || command === "help" || command === "--help" || command === "-h") {
641
658
  io.out(HELP);