@domandigital/craft 0.12.0 → 0.13.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.
@@ -10,7 +10,7 @@ import {
10
10
  holdHouseBlocks,
11
11
  houseRule,
12
12
  scanSource
13
- } from "./chunk-AL6MRG6V.js";
13
+ } from "./chunk-GMVXADJ6.js";
14
14
 
15
15
  // src/character/config.ts
16
16
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -528,7 +528,8 @@ var VALUE_FLAGS = {
528
528
  "--baseline": "baseline",
529
529
  "--sarif": "sarif",
530
530
  "--pages": "pages",
531
- "--viewport": "viewport"
531
+ "--viewport": "viewport",
532
+ "--component": "component"
532
533
  };
533
534
  function parseFlags(args) {
534
535
  const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
@@ -639,25 +640,25 @@ var NOT_COPY_DIR = (name) => name.startsWith("_");
639
640
  function run(argv, io) {
640
641
  const [command, ...rest] = argv;
641
642
  if (command === "direction") {
642
- return import("./cli-ZPJIWVEP.js").then((m) => m.runDirection(rest, io));
643
+ return import("./cli-DRKAMFAH.js").then((m) => m.runDirection(rest, io));
643
644
  }
644
645
  if (command === "report" || command === "retrofit") {
645
- return import("./cli-ZB2VJ7CY.js").then((m) => m.runReport(command, rest, io));
646
+ return import("./cli-J5ECRCD4.js").then((m) => m.runReport(command, rest, io));
646
647
  }
647
648
  if (command === "estate") {
648
- return import("./cli-2DY3V3SF.js").then((m) => m.runEstate(rest, io));
649
+ return import("./cli-BKJMLD3H.js").then((m) => m.runEstate(rest, io));
649
650
  }
650
651
  if (command === "null") {
651
- return import("./cli-C3JLFXMG.js").then((m) => m.runNull(rest, io));
652
+ return import("./cli-ILH7OO5G.js").then((m) => m.runNull(rest, io));
652
653
  }
653
654
  if (command === "tells" && rest[0] === "harvest") {
654
- return import("./cli-C3JLFXMG.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
655
+ return import("./cli-ILH7OO5G.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
655
656
  io.err(`craft: ${error.message}`);
656
657
  return 2;
657
658
  });
658
659
  }
659
660
  if (command === "snapshot" || command === "audit") {
660
- return import("./cli-QM2AQQMF.js").then((m) => m.runAudit(command, rest, io));
661
+ return import("./cli-N2IDXH7W.js").then((m) => m.runAudit(command, rest, io));
661
662
  }
662
663
  if (!command || command === "help" || command === "--help" || command === "-h") {
663
664
  io.out(HELP);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SNAPSHOT_VERSION
3
- } from "./chunk-AL6MRG6V.js";
3
+ } from "./chunk-GMVXADJ6.js";
4
4
 
5
5
  // src/snapshot/migrate.ts
6
6
  var READABLE_SNAPSHOT_VERSIONS = [1, 2];
@@ -0,0 +1,217 @@
1
+ import {
2
+ readSnapshot
3
+ } from "./chunk-CZDOMIVS.js";
4
+ import {
5
+ parseFlags,
6
+ toJson
7
+ } from "./chunk-3VTPI7BU.js";
8
+ import {
9
+ CATALOGUE_VERSION,
10
+ NULL_VERSION,
11
+ auditSnapshot,
12
+ checkCopy,
13
+ extractHtml,
14
+ extractStrings,
15
+ fingerprint,
16
+ harvest,
17
+ hueSwatch,
18
+ nullPrompt,
19
+ scanSource
20
+ } from "./chunk-GMVXADJ6.js";
21
+
22
+ // src/null/cli.ts
23
+ import { spawn } from "child_process";
24
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
25
+ import { tmpdir } from "os";
26
+ import { join, relative, resolve } from "path";
27
+ import { pathToFileURL } from "url";
28
+ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
29
+ var pad = (n) => String(n).padStart(2, "0");
30
+ function generate(prompt, model) {
31
+ const bin = process.env.CRAFT_CLAUDE ?? "claude";
32
+ const args = ["-p", "--tools", "", "--no-session-persistence", "--disable-slash-commands", "--setting-sources", "", "--strict-mcp-config", ...model ? ["--model", model] : [], prompt];
33
+ const cwd = mkdtempSync(join(tmpdir(), "craft-null-"));
34
+ return new Promise((done, fail) => {
35
+ const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
36
+ let out = "";
37
+ let err = "";
38
+ child.stdout.on("data", (d) => out += d.toString());
39
+ child.stderr.on("data", (d) => err += d.toString());
40
+ child.on("error", (e) => fail(new Error(`${bin}: ${e.message}. Install Claude Code, or set CRAFT_CLAUDE.`)));
41
+ child.on("close", (code) => {
42
+ rmSync(cwd, { recursive: true, force: true });
43
+ if (code === 0) done(out);
44
+ else fail(new Error(`${bin} exited ${code}: ${(err || out).trim().slice(0, 200)}`));
45
+ });
46
+ });
47
+ }
48
+ async function pool(items, size, work) {
49
+ const queue = [...items];
50
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(size, queue.length)) }, async () => {
51
+ for (let item = queue.shift(); item !== void 0; item = queue.shift()) await work(item);
52
+ }));
53
+ }
54
+ function nullPath(target, cwd) {
55
+ const abs = resolve(cwd, target);
56
+ return existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "null.json") : abs;
57
+ }
58
+ function loadNull(target, cwd) {
59
+ const path = nullPath(target, cwd);
60
+ if (!existsSync(path)) throw new Error(`no null model at ${target}. Build one with craft null build.`);
61
+ const model = readJson(path);
62
+ if (model.version !== NULL_VERSION) throw new Error(`${target} is null model version ${String(model.version)}; this craft reads ${NULL_VERSION}`);
63
+ return model;
64
+ }
65
+ function measureRun(id, html, snapshot) {
66
+ const file = { path: `${id}.html`, text: html };
67
+ const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file]).findings].map((f) => f.tell));
68
+ const copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
69
+ return { id, fingerprint: fingerprint(snapshot), tells: [...tells].sort(), copy };
70
+ }
71
+ async function build(flags, io) {
72
+ const brief = flags.brief ?? (flags.direction ? readJson(resolve(io.cwd, flags.direction)).brief : void 0);
73
+ if (!brief || brief.trim().split(/\s+/).length < 5) throw new Error('usage: craft null build --brief "<who, where, what they do>" --out <dir> [--runs 20] [--parallel 4]');
74
+ if (!flags.out) throw new Error("--out <dir> is where the pages and null.json go");
75
+ const runs = Number(flags.runs ?? 20);
76
+ const parallel = Number(flags.parallel ?? 4);
77
+ if (!Number.isInteger(runs) || runs < 5) throw new Error("--runs needs a whole number, 5 or more");
78
+ const dir = resolve(io.cwd, flags.out);
79
+ const pages = join(dir, "pages");
80
+ const shots = join(dir, "snapshots");
81
+ mkdirSync(pages, { recursive: true });
82
+ mkdirSync(shots, { recursive: true });
83
+ const prompt = nullPrompt(brief);
84
+ const ids = Array.from({ length: runs }, (_, i) => pad(i + 1));
85
+ for (const id of ids) {
86
+ const path = join(pages, `${id}.html`);
87
+ if (!existsSync(path)) continue;
88
+ const raw = readFileSync(path, "utf8");
89
+ const html = extractHtml(raw);
90
+ if (!html) {
91
+ writeFileSync(join(pages, `${id}.reply.txt`), raw);
92
+ rmSync(path);
93
+ rmSync(join(shots, `${id}.json`), { force: true });
94
+ io.out(` ${id} was a reply, not a page: kept as ${id}.reply.txt and generated again`);
95
+ } else if (html !== raw) writeFileSync(path, html);
96
+ }
97
+ const missing = ids.filter((id) => !existsSync(join(pages, `${id}.html`)));
98
+ const failed = [];
99
+ if (missing.length) io.out(`craft null: generating ${missing.length} page${missing.length === 1 ? "" : "s"}, ${parallel} at a time`);
100
+ await pool(missing, parallel, async (id) => {
101
+ try {
102
+ const reply = await generate(prompt, flags.model);
103
+ const html = extractHtml(reply);
104
+ if (!html) {
105
+ writeFileSync(join(pages, `${id}.reply.txt`), reply);
106
+ failed.push(`${id}: the reply held no HTML page (kept as ${id}.reply.txt)`);
107
+ return;
108
+ }
109
+ writeFileSync(join(pages, `${id}.html`), html);
110
+ io.out(` ${id} written`);
111
+ } catch (error) {
112
+ failed.push(`${id}: ${error.message}`);
113
+ }
114
+ });
115
+ const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
116
+ if (toShoot.length) {
117
+ io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
118
+ const { snapshotUrls } = await import("./audit-5TYDYRXV.js");
119
+ const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
120
+ results.forEach((r, i) => {
121
+ const id = toShoot[i];
122
+ if (r.snapshot) writeFileSync(join(shots, `${id}.json`), `${JSON.stringify({ ...r.snapshot, url: `pages/${id}.html` }, null, 2)}
123
+ `);
124
+ else failed.push(`${id}: the page would not render: ${r.error}`);
125
+ });
126
+ }
127
+ const measured = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && existsSync(join(shots, `${id}.json`))).map((id) => measureRun(id, readFileSync(join(pages, `${id}.html`), "utf8"), readSnapshot(readJson(join(shots, `${id}.json`)), `${id}.json`)));
128
+ const model = {
129
+ version: NULL_VERSION,
130
+ brief: brief.trim(),
131
+ prompt,
132
+ model: flags.model ?? "default",
133
+ catalogueVersion: CATALOGUE_VERSION,
134
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
135
+ runs: measured
136
+ };
137
+ writeFileSync(join(dir, "null.json"), `${JSON.stringify(model, null, 2)}
138
+ `);
139
+ io.out(`craft null: ${measured.length} of ${runs} pages in ${relative(io.cwd, join(dir, "null.json")) || "null.json"}`);
140
+ for (const f of failed) io.err(` ${f}`);
141
+ if (measured.length < runs) {
142
+ io.err(`craft null: ${runs - measured.length} missing. Run the same command again to fill them in.`);
143
+ return 1;
144
+ }
145
+ return 0;
146
+ }
147
+ function copyTell(phrase) {
148
+ const text = `We found that ${phrase} was there.`;
149
+ const hit = checkCopy([{ path: "phrase.md", text }]).findings[0];
150
+ return hit ? hit.tell : null;
151
+ }
152
+ function formatHarvest(candidates, models) {
153
+ const lines = [`craft tells harvest: ${models.reduce((s, m) => s + m.runs.length, 0)} null pages from ${models.length} brief${models.length === 1 ? "" : "s"}`];
154
+ const fresh = candidates.filter((c) => !c.known);
155
+ const known = candidates.filter((c) => c.known);
156
+ const runs = models.flatMap((m) => m.runs);
157
+ const line = (c) => {
158
+ const swatch = c.kind === "accent" ? hueSwatch(runs, c.value) : null;
159
+ const where = models.length > 1 ? `, ${c.briefs} brief${c.briefs === 1 ? "" : "s"}` : "";
160
+ return ` ${c.kind.padEnd(8)} ${`${c.value}${swatch ? ` (${swatch})` : ""}`.padEnd(44)} ${c.runs} of ${c.of}${where}${c.known ? ` caught by ${c.known}` : ""}`;
161
+ };
162
+ lines.push("", "Not on the catalogue: candidates for the next generation. A person decides.");
163
+ lines.push(...fresh.length ? fresh.map(line) : [" none"]);
164
+ lines.push("", "Already caught: the catalogue still describes what the model builds.");
165
+ lines.push(...known.length ? known.map(line) : [" none"]);
166
+ return lines.join("\n");
167
+ }
168
+ function describeTypicality(t, pages, briefs) {
169
+ const pct = Math.round(t.score * 100);
170
+ return [
171
+ `Typicality against ${pages} null pages from ${briefs} brief${briefs === 1 ? "" : "s"}`,
172
+ ` score ${t.score.toFixed(2)}: ${t.typical ? "typical" : "not typical"}. ${pct}% of the model's own pages sit further from the rest than this one.`,
173
+ ` distance ${t.distance.toFixed(3)} to its nearest null pages; the null's own median is ${t.baseline.toFixed(3)}`,
174
+ ` nearest ${t.nearest.map((n) => `${n.id} ${n.distance.toFixed(2)}`).join(", ")}`,
175
+ ` shares ${t.shared.map((s) => `${s.part} ${s.value} (${s.runs} of ${s.of})`).join(", ") || "no choice with a quarter of the null"}`
176
+ ].join("\n");
177
+ }
178
+ async function runNull(args, io) {
179
+ try {
180
+ const flags = parseFlags(args);
181
+ if (typeof flags === "string") throw new Error(flags);
182
+ if (flags.positional[0] !== "build") throw new Error('usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]');
183
+ return await build(flags, io);
184
+ } catch (error) {
185
+ io.err(`craft: ${error.message}`);
186
+ return 2;
187
+ }
188
+ }
189
+ function runHarvest(args, io) {
190
+ const flags = parseFlags(args);
191
+ if (typeof flags === "string") throw new Error(flags);
192
+ const targets = flags.positional.slice(1);
193
+ if (targets.length === 0) throw new Error("usage: craft tells harvest <null.json | dir>... [--share 0.25] [--json]");
194
+ const paths = targets.flatMap((t) => {
195
+ const abs = resolve(io.cwd, t);
196
+ if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
197
+ return readdirSync(abs).map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
198
+ }
199
+ return [t];
200
+ });
201
+ const models = paths.map((p) => loadNull(p, io.cwd));
202
+ const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
203
+ if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
204
+ const candidates = harvest(models, { minShare, copyTell });
205
+ io.out(flags.json ? toJson({ briefs: models.map((m) => m.brief), candidates }) : formatHarvest(candidates, models));
206
+ return 0;
207
+ }
208
+
209
+ export {
210
+ nullPath,
211
+ loadNull,
212
+ measureRun,
213
+ formatHarvest,
214
+ describeTypicality,
215
+ runNull,
216
+ runHarvest
217
+ };