@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.
@@ -1,49 +1,3 @@
1
- import {
2
- merge
3
- } from "./chunk-2IQ5OXFR.js";
4
- import "./chunk-OUQ25LY2.js";
5
- import "./chunk-JYNXGCYZ.js";
6
- import {
7
- SIBLING_AT,
8
- compareToEstate,
9
- loadEstate
10
- } from "./chunk-QU6Q2QZZ.js";
11
- import {
12
- PAGE_TYPES,
13
- validateDirection
14
- } from "./chunk-YDURAOYR.js";
15
- import {
16
- loadNull
17
- } from "./chunk-RXXRXV3V.js";
18
- import "./chunk-OQXTQQAU.js";
19
- import {
20
- COPY_FILE,
21
- NOT_COPY_DIR,
22
- SOURCE_FILE,
23
- applySeverity,
24
- loadConfig,
25
- loadExceptions,
26
- parseFlags,
27
- readPaths,
28
- toJson
29
- } from "./chunk-MUVFECFC.js";
30
- import {
31
- readSnapshot
32
- } from "./chunk-WUACK2HX.js";
33
- import {
34
- CATALOGUE,
35
- auditSnapshot,
36
- checkCopy,
37
- fingerprint,
38
- scanSource,
39
- typicality
40
- } from "./chunk-LFNSB6W7.js";
41
- import "./chunk-QYBAVU3P.js";
42
-
43
- // src/report/cli.ts
44
- import { existsSync, readFileSync, writeFileSync } from "fs";
45
- import { dirname, resolve } from "path";
46
-
47
1
  // src/report/index.ts
48
2
  var TELL_HEAVY = 3;
49
3
  var MIN_DECIDED = 5;
@@ -199,144 +153,7 @@ function choiceOfShared(part) {
199
153
  return /running order/.test(part) ? void 0 : "motif";
200
154
  }
201
155
 
202
- // src/report/retrofit.ts
203
- var HEADINGS = {
204
- direction: "Decide first",
205
- type: "Type",
206
- colour: "Colour",
207
- shape: "Shape",
208
- effects: "Effects",
209
- motion: "Motion",
210
- layout: "How sections look (not where they sit)",
211
- copy: "Copy"
212
- };
213
- var ORDER = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
214
- function target(a, direction) {
215
- if (!a.choice || a.area === "direction") return "";
216
- const c = direction?.choices?.[a.choice];
217
- if (c && c.because && !c.because.startsWith("PROPOSED:")) return `
218
- - Use: **${c.value}** for ${a.choice}. ${c.because}`;
219
- return `
220
- - Waits on: deciding **${a.choice}** in art-direction.json.`;
221
- }
222
- function retrofitPlan(report, direction) {
223
- const lines = [
224
- `# Retrofit: ${report.site}`,
225
- "",
226
- `**Now:** ${report.verdict}. ${report.summary}`,
227
- "",
228
- "| Signal | Raised | Reading |",
229
- "|---|---|---|",
230
- ...report.signals.map((s) => `| ${s.id} | ${s.raised === null ? "not measured" : s.raised ? "yes" : "no"} | ${s.detail} |`),
231
- "",
232
- "**Done when** `craft report` says *decided*: fewer than three design tells, typicality under 0.10 against this brief's null, no sibling in the estate, and at least five of the seven choices decided with a reason.",
233
- ""
234
- ];
235
- let n = 0;
236
- for (const area of ORDER) {
237
- const list = report.actions.filter((a) => a.area === area);
238
- if (list.length === 0) continue;
239
- n += 1;
240
- lines.push(`## ${n}. ${HEADINGS[area]}`, "");
241
- for (const a of list) lines.push(`- [ ] ${a.what} *(${a.why})*${target(a, direction)}`);
242
- lines.push("");
243
- }
244
- if (n === 0) lines.push("Nothing to change.", "");
245
- lines.push(
246
- "## Leave alone",
247
- "",
248
- "Navigation, the order of the sections, where the call to action sits, reading order and legibility. Change the look, never the page grammar. A layout tell above is about how a section looks: the icon tiles, the chip, the strip. The section stays where it is.",
249
- ""
250
- );
251
- return lines.join("\n");
252
- }
253
-
254
- // src/report/cli.ts
255
- var USAGE = "usage: craft report|retrofit <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <estate.json>] [--direction <file>] [--json] [--out <file>]";
256
- var sameUrl = (a, b) => a.replace(/\/+$/, "").toLowerCase() === b.replace(/\/+$/, "").toLowerCase();
257
- function formatCharacter(r) {
258
- const lines = [`craft report ${r.site}`, "", `Verdict: ${r.verdict.toUpperCase()}. ${r.summary}`, ""];
259
- for (const s of r.signals) lines.push(` ${s.id.padEnd(11)} ${s.raised === null ? "\xB7" : s.raised ? "RAISED" : "ok"} ${s.detail}`);
260
- lines.push("", "What to do, in order:");
261
- r.actions.slice(0, 12).forEach((a, i) => lines.push(` ${String(i + 1).padStart(2)}. [${a.area}] ${a.what}`, ` ${a.why}`));
262
- if (r.actions.length > 12) lines.push(` ...and ${r.actions.length - 12} more: craft retrofit writes them all.`);
263
- return lines.join("\n");
264
- }
265
- async function runReport(command, args, io) {
266
- try {
267
- const flags = parseFlags(args);
268
- if (typeof flags === "string") throw new Error(flags);
269
- const target2 = flags.positional[0];
270
- if (!target2) throw new Error(USAGE);
271
- const page = flags.page ?? "home";
272
- if (!PAGE_TYPES.includes(page)) throw new Error(`--page takes one of ${PAGE_TYPES.join(", ")}`);
273
- const local = resolve(io.cwd, target2);
274
- let snap;
275
- if (/\.json$/i.test(target2) && existsSync(local)) {
276
- snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
277
- } else {
278
- const { snapshotUrl } = await import("./audit-MTUC24XU.js");
279
- snap = await snapshotUrl(target2);
280
- }
281
- const fp = fingerprint(snap);
282
- const repo = flags.repo ? resolve(io.cwd, flags.repo) : null;
283
- const exceptions = loadExceptions(flags, repo ?? io.cwd);
284
- const config = loadConfig(flags, repo ?? io.cwd);
285
- let findings = auditSnapshot(snap, { exceptions });
286
- if (repo) {
287
- findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE, void 0, config.ignore), { exceptions }), findings);
288
- const copyPaths = config.copyPaths ?? ["."];
289
- findings = merge(findings, checkCopy(readPaths(copyPaths, repo, COPY_FILE, (d) => NOT_COPY_DIR(d) || d === "docs", config.ignore), { exceptions }));
290
- }
291
- findings = applySeverity(findings, config);
292
- const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
293
- const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
294
- let estate = null;
295
- if (flags.estate) {
296
- const register = loadEstate(resolve(io.cwd, flags.estate));
297
- const self = register.sites.find((s) => sameUrl(s.url, snap.url));
298
- estate = compareToEstate(fp, register, { exclude: self?.id, siblingAt: SIBLING_AT });
299
- }
300
- const directionFile = resolve(repo ?? io.cwd, flags.direction ?? "art-direction.json");
301
- let direction = null;
302
- let directionReport = null;
303
- if (existsSync(directionFile)) {
304
- direction = JSON.parse(readFileSync(directionFile, "utf8"));
305
- directionReport = validateDirection(direction, {
306
- fingerprint: fp,
307
- snapshot: snap,
308
- page,
309
- strict: flags.strict,
310
- pathExists: (p) => existsSync(resolve(dirname(directionFile), p)),
311
- readSource: (s) => {
312
- if (!s.path || /^https?:/.test(s.path) || !/\.(?:txt|md|markdown|csv|json|html?)$/i.test(s.path)) return void 0;
313
- const path = resolve(dirname(directionFile), s.path);
314
- return existsSync(path) ? readFileSync(path, "utf8") : void 0;
315
- }
316
- });
317
- } else if (flags.direction) {
318
- throw new Error(`no such file: ${flags.direction}`);
319
- }
320
- const copyTells = new Set(CATALOGUE.filter((t) => t.surface === "copy").map((t) => t.id));
321
- const report = characterReport({ site: snap.url, findings, typicality: typical, estate, direction: directionReport }, copyTells);
322
- if (command === "retrofit") {
323
- const plan = retrofitPlan(report, direction);
324
- if (flags.out) {
325
- writeFileSync(resolve(io.cwd, flags.out), plan);
326
- io.out(`craft retrofit: ${report.actions.length} step${report.actions.length === 1 ? "" : "s"} for ${snap.url} in ${flags.out}`);
327
- } else io.out(plan);
328
- return 0;
329
- }
330
- io.out(flags.json ? toJson(report) : formatCharacter(report));
331
- if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${toJson(report)}
332
- `);
333
- return flags.strict && report.verdict !== "decided" ? 1 : 0;
334
- } catch (error) {
335
- io.err(`craft: ${error.message}`);
336
- return 2;
337
- }
338
- }
339
156
  export {
340
- formatCharacter,
341
- runReport
157
+ TELL_HEAVY,
158
+ characterReport
342
159
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-OUQ25LY2.js";
4
+ } from "./chunk-YK2FFL6Z.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-RXXRXV3V.js";
8
+ } from "./chunk-LWVGIJ5A.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -15,7 +15,7 @@ import {
15
15
  prepareReport,
16
16
  readPaths,
17
17
  toJson
18
- } from "./chunk-MUVFECFC.js";
18
+ } from "./chunk-LXDNIJFG.js";
19
19
  import {
20
20
  readSnapshot
21
21
  } from "./chunk-WUACK2HX.js";
@@ -25,7 +25,7 @@ import {
25
25
  fingerprint,
26
26
  scanSource,
27
27
  typicality
28
- } from "./chunk-LFNSB6W7.js";
28
+ } from "./chunk-B2MJLGF6.js";
29
29
 
30
30
  // src/audit/cli.ts
31
31
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -11,7 +11,7 @@ import {
11
11
  levenshtein,
12
12
  normaliseFamily,
13
13
  parseColour
14
- } from "./chunk-LFNSB6W7.js";
14
+ } from "./chunk-B2MJLGF6.js";
15
15
 
16
16
  // src/direction/types.ts
17
17
  var DIRECTION_VERSION = 2;
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-LFNSB6W7.js";
7
+ } from "./chunk-B2MJLGF6.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -0,0 +1,241 @@
1
+ import {
2
+ TELL_HEAVY
3
+ } from "./chunk-NTIPJ5U7.js";
4
+ import {
5
+ expandNullTargets,
6
+ loadNull
7
+ } from "./chunk-LWVGIJ5A.js";
8
+ import "./chunk-OQXTQQAU.js";
9
+ import {
10
+ parseFlags,
11
+ toJson
12
+ } from "./chunk-LXDNIJFG.js";
13
+ import {
14
+ readSnapshot
15
+ } from "./chunk-WUACK2HX.js";
16
+ import {
17
+ CATALOGUE,
18
+ auditSnapshot,
19
+ fingerprint,
20
+ scanSource,
21
+ typicality
22
+ } from "./chunk-B2MJLGF6.js";
23
+ import {
24
+ SNAPSHOT_VERSION
25
+ } from "./chunk-QYBAVU3P.js";
26
+
27
+ // src/calibrate/cli.ts
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
29
+ import { dirname, join, relative, resolve } from "path";
30
+ import { pathToFileURL } from "url";
31
+
32
+ // src/calibrate/index.ts
33
+ var LABELS = ["ai", "ai-looking", "human"];
34
+ var FLAGGED_TARGET = 0.9;
35
+ var HUMAN_TYPICAL_TARGET = 0.1;
36
+ var SURFACE = new Map(CATALOGUE.map((t) => [t.id, t.surface]));
37
+ function designTells(tells) {
38
+ return tells.filter((t) => SURFACE.has(t) && SURFACE.get(t) !== "copy");
39
+ }
40
+ function isFlagged(page) {
41
+ return Boolean(page.typicality?.typical) || designTells(page.tells).length >= TELL_HEAVY;
42
+ }
43
+ var positive = (label) => label !== "human";
44
+ var round = (n) => Math.round(n * 100) / 100;
45
+ var share = (n, of) => `${n} of ${of} (${Math.round(100 * n / of)}%)`;
46
+ function summarise(label, pages, typicality2) {
47
+ const mine = pages.filter((p) => p.label === label);
48
+ return {
49
+ label,
50
+ pages: mine.length,
51
+ // A set measured partly against the null would count the same page two ways.
52
+ flagged: mine.filter((p) => typicality2 ? isFlagged(p) : designTells(p.tells).length >= TELL_HEAVY).length,
53
+ typical: typicality2 ? mine.filter((p) => p.typicality?.typical).length : null,
54
+ tellHeavy: mine.filter((p) => designTells(p.tells).length >= TELL_HEAVY).length,
55
+ blocking: mine.reduce((s, p) => s + p.blocking, 0)
56
+ };
57
+ }
58
+ function flaggedTarget(target, s, missing) {
59
+ const note = missing ? `; ${missing} not measured, counted neither way` : "";
60
+ if (s.pages === 0) return { target, met: null, detail: `no page measured${note}` };
61
+ return { target, met: s.flagged / s.pages >= FLAGGED_TARGET, detail: `${share(s.flagged, s.pages)} flagged, target ${FLAGGED_TARGET * 100}% or more${note}` };
62
+ }
63
+ function calibrate(pages, unmeasured = []) {
64
+ const typicality2 = pages.length > 0 && pages.every((p) => p.typicality !== void 0);
65
+ const labels = LABELS.map((l) => summarise(l, pages, typicality2));
66
+ const byLabel = Object.fromEntries(labels.map((s) => [s.label, s]));
67
+ const missing = (l) => unmeasured.filter((u) => u.label === l).length;
68
+ const positives = pages.filter((p) => positive(p.label)).length;
69
+ const fired = new Set(pages.flatMap((p) => p.tells));
70
+ const tells = [...fired].map((tell) => {
71
+ const hits = Object.fromEntries(LABELS.map((l) => [l, pages.filter((p) => p.label === l && p.tells.includes(tell)).length]));
72
+ const on = hits.ai + hits["ai-looking"];
73
+ return { tell, hits, precision: round(on / (on + hits.human)), recall: positives ? round(on / positives) : null };
74
+ }).sort((a, b) => b.hits.ai + b.hits["ai-looking"] + b.hits.human - (a.hits.ai + a.hits["ai-looking"] + a.hits.human) || a.tell.localeCompare(b.tell));
75
+ const human = byLabel.human;
76
+ const humanNote = missing("human") ? `; ${missing("human")} not measured, counted neither way` : "";
77
+ const targets = [
78
+ flaggedTarget("1. Generated pages get caught", byLabel.ai, missing("ai")),
79
+ human.pages === 0 ? { target: "2. Good human sites don't get caught", met: null, detail: `no page measured${humanNote}` } : {
80
+ target: "2. Good human sites don't get caught",
81
+ // A block hit fails it outright; without null models the other half is not known.
82
+ met: human.blocking > 0 ? false : human.typical === null ? null : human.typical / human.pages <= HUMAN_TYPICAL_TARGET,
83
+ detail: `${human.blocking} block hits, target 0; ${human.typical === null ? "typicality not measured (no null models)" : `${share(human.typical, human.pages)} typical, target ${HUMAN_TYPICAL_TARGET * 100}% or fewer`}${humanNote}`
84
+ },
85
+ flaggedTarget("3. Sites a person labelled AI-looking get caught", byLabel["ai-looking"], missing("ai-looking"))
86
+ ];
87
+ return {
88
+ pages: pages.length,
89
+ typicality: typicality2,
90
+ labels,
91
+ tells,
92
+ silent: CATALOGUE.map((t) => t.id).filter((id) => !fired.has(id)),
93
+ targets,
94
+ unmeasured
95
+ };
96
+ }
97
+ function formatCalibration(c, notes = []) {
98
+ const counts = c.labels.map((s) => `${s.pages} ${s.label}`).join(", ");
99
+ const lines = [`craft calibrate: ${c.pages} pages measured (${counts})`, ...notes];
100
+ if (!c.typicality) lines.push("No null models: flagged means tell-heavy only.");
101
+ if (c.unmeasured.length) {
102
+ lines.push("", `Not measured: ${c.unmeasured.length}. A page not measured never counts as a pass.`);
103
+ for (const u of c.unmeasured) lines.push(` ${u.id} (${u.label}): ${u.reason}`);
104
+ }
105
+ lines.push("", "Targets");
106
+ for (const t of c.targets) lines.push(` ${t.target.padEnd(50)} ${t.met === null ? "not known" : t.met ? "met" : "NOT MET"}`, ` ${t.detail}`);
107
+ lines.push("", ` ${"label".padEnd(11)} ${"pages".padStart(5)} ${"flagged".padStart(8)} ${"typical".padStart(8)} ${"tell-heavy".padStart(11)} ${"blocking".padStart(9)}`);
108
+ for (const s of c.labels) {
109
+ lines.push(` ${s.label.padEnd(11)} ${String(s.pages).padStart(5)} ${String(s.flagged).padStart(8)} ${String(s.typical ?? "-").padStart(8)} ${String(s.tellHeavy).padStart(11)} ${String(s.blocking).padStart(9)}`);
110
+ }
111
+ lines.push("", "Per tell. A hit on ai or ai-looking counts for the tell; a hit on human counts against it.");
112
+ lines.push(` ${"tell".padEnd(28)} ${"ai".padStart(7)} ${"ai-looking".padStart(10)} ${"human".padStart(7)} ${"precision".padStart(9)} ${"recall".padStart(7)}`);
113
+ const of = (l) => c.labels.find((s) => s.label === l)?.pages ?? 0;
114
+ for (const t of c.tells) {
115
+ lines.push(
116
+ ` ${t.tell.padEnd(28)} ${`${t.hits.ai}/${of("ai")}`.padStart(7)} ${`${t.hits["ai-looking"]}/${of("ai-looking")}`.padStart(10)} ${`${t.hits.human}/${of("human")}`.padStart(7)} ${t.precision.toFixed(2).padStart(9)} ${(t.recall === null ? "-" : t.recall.toFixed(2)).padStart(7)}`
117
+ );
118
+ }
119
+ if (!c.tells.length) lines.push(" no tell fired");
120
+ lines.push("", `Never fired on this set: ${c.silent.length} of ${CATALOGUE.length} catalogue tells.`);
121
+ return lines.join("\n");
122
+ }
123
+
124
+ // src/calibrate/cli.ts
125
+ var LABELS_VERSION = 1;
126
+ var USAGE = "usage: craft calibrate <labels.json> [--out <dir>] [--fresh] [--json]";
127
+ function parseLabels(raw, name) {
128
+ const file = raw;
129
+ if (!file || typeof file !== "object" || file.version !== LABELS_VERSION) throw new Error(`${name}: needs "version": ${LABELS_VERSION}`);
130
+ if (!Array.isArray(file.pages) || file.pages.length === 0) throw new Error(`${name}: "pages" needs at least one page`);
131
+ const seen = /* @__PURE__ */ new Set();
132
+ for (const [i, p] of file.pages.entries()) {
133
+ const at = `${name}: pages[${i}]`;
134
+ if (typeof p?.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(p.id)) throw new Error(`${at}: "id" needs lower case, digits and hyphens`);
135
+ if (seen.has(p.id)) throw new Error(`${at}: "${p.id}" is listed twice`);
136
+ seen.add(p.id);
137
+ if (!LABELS.includes(p.label)) throw new Error(`${at} (${p.id}): "label" is one of ${LABELS.join(", ")}`);
138
+ if (!p.skip && !p.snapshot && !p.source && !p.url) throw new Error(`${at} (${p.id}): needs a snapshot, a source or a url, or a skip saying why not`);
139
+ }
140
+ if (file.null !== void 0 && (!Array.isArray(file.null) || file.null.some((n) => typeof n !== "string"))) throw new Error(`${name}: "null" is a list of paths`);
141
+ return file;
142
+ }
143
+ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
144
+ async function runCalibrate(args, io) {
145
+ try {
146
+ const flags = parseFlags(args);
147
+ if (typeof flags === "string") throw new Error(flags);
148
+ const [target] = flags.positional;
149
+ if (!target || flags.positional.length > 1) throw new Error(USAGE);
150
+ const path = resolve(io.cwd, target);
151
+ if (!existsSync(path)) throw new Error(`no labels file at ${target}`);
152
+ const labels = parseLabels(readJson(path), target);
153
+ const base = dirname(path);
154
+ const out = flags.out ? resolve(io.cwd, flags.out) : void 0;
155
+ const models = expandNullTargets(labels.null ?? [], base).map((p) => loadNull(p, base));
156
+ const pool = models.flatMap((m, i) => m.runs.map((r) => ({ ...r, id: `${i}/${r.id}` })));
157
+ const snapshots = /* @__PURE__ */ new Map();
158
+ const unmeasured = [];
159
+ const toTake = [];
160
+ for (const entry of labels.pages) {
161
+ if (entry.skip) {
162
+ unmeasured.push({ id: entry.id, label: entry.label, reason: entry.skip });
163
+ continue;
164
+ }
165
+ const pinned = entry.snapshot ? resolve(base, entry.snapshot) : void 0;
166
+ if (pinned && existsSync(pinned) && !flags.fresh) {
167
+ snapshots.set(entry.id, readSnapshot(readJson(pinned), entry.snapshot ?? entry.id));
168
+ continue;
169
+ }
170
+ const cached = out ? join(out, "snapshots", `${entry.id}.json`) : void 0;
171
+ if (cached && existsSync(cached)) {
172
+ snapshots.set(entry.id, readSnapshot(readJson(cached), `${entry.id}.json`));
173
+ continue;
174
+ }
175
+ const url = entry.source ? pathToFileURL(resolve(base, entry.source)).href : entry.url;
176
+ if (!url) {
177
+ const why = flags.fresh ? "--fresh" : `no snapshot at ${entry.snapshot}`;
178
+ unmeasured.push({ id: entry.id, label: entry.label, reason: `${why}, and no source or url to take one from` });
179
+ continue;
180
+ }
181
+ toTake.push({ entry, url, keep: flags.fresh ? cached : pinned ?? cached });
182
+ }
183
+ if (toTake.length) {
184
+ io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
185
+ try {
186
+ const { snapshotUrls } = await import("./audit-UJCUWORL.js");
187
+ const results = await snapshotUrls(toTake.map((t) => t.url));
188
+ results.forEach((r, i) => {
189
+ const { entry, keep } = toTake[i];
190
+ if (!r.snapshot) return void unmeasured.push({ id: entry.id, label: entry.label, reason: `would not render: ${r.error}` });
191
+ snapshots.set(entry.id, r.snapshot);
192
+ if (keep) {
193
+ mkdirSync(dirname(keep), { recursive: true });
194
+ writeFileSync(keep, `${JSON.stringify(r.snapshot, null, 2)}
195
+ `);
196
+ }
197
+ });
198
+ } catch (error) {
199
+ for (const { entry } of toTake) unmeasured.push({ id: entry.id, label: entry.label, reason: `no browser: ${error.message}` });
200
+ }
201
+ }
202
+ const measured = labels.pages.filter((e) => snapshots.has(e.id)).map((entry) => {
203
+ const snap = snapshots.get(entry.id);
204
+ const rendered = auditSnapshot(snap);
205
+ const source = entry.source && existsSync(resolve(base, entry.source)) ? scanSource([{ path: entry.source, text: readFileSync(resolve(base, entry.source), "utf8") }]) : void 0;
206
+ const tells = new Set([...rendered.findings, ...source?.findings ?? []].map((f) => f.tell));
207
+ const t = pool.length ? typicality(fingerprint(snap), pool) : void 0;
208
+ return {
209
+ id: entry.id,
210
+ label: entry.label,
211
+ tells: [...tells].sort(),
212
+ blocking: rendered.summary.blocking + (source?.summary.blocking ?? 0),
213
+ ...t ? { typicality: { score: t.score, typical: t.typical } } : {}
214
+ };
215
+ });
216
+ const result = calibrate(measured, unmeasured);
217
+ const older = [...snapshots.values()].filter((s) => s.migratedFrom !== void 0).length;
218
+ const notes = [
219
+ ...models.length ? [`Typicality against ${pool.length} null pages from ${models.length} brief${models.length === 1 ? "" : "s"}, pooled.`] : [],
220
+ ...older ? [`${older} of ${snapshots.size} snapshots are older than version ${SNAPSHOT_VERSION}: tells that read newer fields cannot fire on them. --fresh takes them again.`] : []
221
+ ];
222
+ const pages = Object.fromEntries(measured.map((m) => [m.id, m]));
223
+ if (out) {
224
+ mkdirSync(out, { recursive: true });
225
+ writeFileSync(join(out, "results.json"), `${toJson({ labelsFile: relative(out, path), notes, ...result, pages })}
226
+ `);
227
+ }
228
+ io.out(flags.json ? toJson({ notes, ...result, pages }) : formatCalibration(result, notes));
229
+ if (out && !flags.json) io.out(`
230
+ Written to ${relative(io.cwd, join(out, "results.json")) || "results.json"}`);
231
+ return 0;
232
+ } catch (error) {
233
+ io.err(`craft: ${error.message}`);
234
+ return 2;
235
+ }
236
+ }
237
+ export {
238
+ LABELS_VERSION,
239
+ parseLabels,
240
+ runCalibrate
241
+ };
@@ -4,14 +4,14 @@ import {
4
4
  merge,
5
5
  resolvePages,
6
6
  runAudit
7
- } from "./chunk-2IQ5OXFR.js";
8
- import "./chunk-OUQ25LY2.js";
7
+ } from "./chunk-TQKDCA4V.js";
8
+ import "./chunk-YK2FFL6Z.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-RXXRXV3V.js";
10
+ import "./chunk-LWVGIJ5A.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-MUVFECFC.js";
12
+ import "./chunk-LXDNIJFG.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-LFNSB6W7.js";
14
+ import "./chunk-B2MJLGF6.js";
15
15
  import "./chunk-QYBAVU3P.js";
16
16
  export {
17
17
  describeComponents,
@@ -1,23 +1,29 @@
1
1
  import {
2
2
  describeTypicality,
3
+ expandNullTargets,
4
+ findImportPages,
3
5
  formatHarvest,
4
6
  loadNull,
5
7
  measureRun,
6
8
  nullPath,
7
9
  runHarvest,
8
- runNull
9
- } from "./chunk-RXXRXV3V.js";
10
+ runNull,
11
+ serveDir
12
+ } from "./chunk-LWVGIJ5A.js";
10
13
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-MUVFECFC.js";
14
+ import "./chunk-LXDNIJFG.js";
12
15
  import "./chunk-WUACK2HX.js";
13
- import "./chunk-LFNSB6W7.js";
16
+ import "./chunk-B2MJLGF6.js";
14
17
  import "./chunk-QYBAVU3P.js";
15
18
  export {
16
19
  describeTypicality,
20
+ expandNullTargets,
21
+ findImportPages,
17
22
  formatHarvest,
18
23
  loadNull,
19
24
  measureRun,
20
25
  nullPath,
21
26
  runHarvest,
22
- runNull
27
+ runNull,
28
+ serveDir
23
29
  };