@domandigital/craft 0.11.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.
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  CATALOGUE,
3
3
  CATALOGUE_VERSION,
4
+ HOUSE_LOCKED,
4
5
  applyHouseGate,
5
6
  checkCopy,
6
7
  extractCopy,
7
8
  extractStrings,
8
9
  fileKind,
10
+ holdHouseBlocks,
9
11
  houseRule,
10
12
  scanSource
11
- } from "./chunk-3NGWXZZE.js";
13
+ } from "./chunk-GMVXADJ6.js";
12
14
 
13
15
  // src/character/config.ts
14
16
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -526,7 +528,8 @@ var VALUE_FLAGS = {
526
528
  "--baseline": "baseline",
527
529
  "--sarif": "sarif",
528
530
  "--pages": "pages",
529
- "--viewport": "viewport"
531
+ "--viewport": "viewport",
532
+ "--component": "component"
530
533
  };
531
534
  function parseFlags(args) {
532
535
  const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
@@ -637,25 +640,25 @@ var NOT_COPY_DIR = (name) => name.startsWith("_");
637
640
  function run(argv, io) {
638
641
  const [command, ...rest] = argv;
639
642
  if (command === "direction") {
640
- return import("./cli-U3SZVHSK.js").then((m) => m.runDirection(rest, io));
643
+ return import("./cli-DRKAMFAH.js").then((m) => m.runDirection(rest, io));
641
644
  }
642
645
  if (command === "report" || command === "retrofit") {
643
- return import("./cli-DBZMVYYV.js").then((m) => m.runReport(command, rest, io));
646
+ return import("./cli-J5ECRCD4.js").then((m) => m.runReport(command, rest, io));
644
647
  }
645
648
  if (command === "estate") {
646
- return import("./cli-VREYWCT4.js").then((m) => m.runEstate(rest, io));
649
+ return import("./cli-BKJMLD3H.js").then((m) => m.runEstate(rest, io));
647
650
  }
648
651
  if (command === "null") {
649
- return import("./cli-4ESXOLKV.js").then((m) => m.runNull(rest, io));
652
+ return import("./cli-ILH7OO5G.js").then((m) => m.runNull(rest, io));
650
653
  }
651
654
  if (command === "tells" && rest[0] === "harvest") {
652
- return import("./cli-4ESXOLKV.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
655
+ return import("./cli-ILH7OO5G.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
653
656
  io.err(`craft: ${error.message}`);
654
657
  return 2;
655
658
  });
656
659
  }
657
660
  if (command === "snapshot" || command === "audit") {
658
- return import("./cli-T5RD2BTK.js").then((m) => m.runAudit(command, rest, io));
661
+ return import("./cli-N2IDXH7W.js").then((m) => m.runAudit(command, rest, io));
659
662
  }
660
663
  if (!command || command === "help" || command === "--help" || command === "-h") {
661
664
  io.out(HELP);
@@ -713,10 +716,22 @@ function run(argv, io) {
713
716
  const paths = flags.positional.length > 0 ? flags.positional : command === "copy" ? config.copyPaths ?? [] : ["."];
714
717
  if (command === "copy" && paths.length === 0 && !flags.staged) throw new Error("usage: craft copy <paths...> (or copyPaths in craft.config.json)");
715
718
  const files = flags.staged ? readStaged(io.cwd, wanted, config.ignore) : readPaths(paths, io.cwd, wanted, command === "copy" ? NOT_COPY_DIR : void 0, config.ignore);
716
- const exceptions = loadExceptions(flags, io.cwd);
719
+ let exceptions = loadExceptions(flags, io.cwd);
720
+ let effective = config;
721
+ let refused = [];
722
+ if (command === "copy" && flags.gate) {
723
+ const held = holdHouseBlocks(exceptions, config.severity);
724
+ exceptions = held.exceptions;
725
+ effective = { ...config, severity: held.severity };
726
+ const warned = held.refusedSeverity.filter((tell) => config.severity[tell].level === "warn").map((tell) => ({ tell, because: `craft.config.json: ${config.severity[tell].because}` }));
727
+ refused = [...held.refused, ...warned];
728
+ }
717
729
  let report = command === "scan" ? scanSource(files, { exceptions }) : checkCopy(files, { exceptions });
718
- if (command === "copy" && flags.gate) report = applyHouseGate(report);
719
- const prepared = prepareReport(report, flags, io, config);
730
+ if (command === "copy" && flags.gate) {
731
+ report = applyHouseGate(report);
732
+ report = { ...report, rejectedExceptions: [...report.rejectedExceptions, ...refused.map((exception) => ({ exception, reason: HOUSE_LOCKED }))] };
733
+ }
734
+ const prepared = prepareReport(report, flags, io, effective);
720
735
  if (!prepared) return 0;
721
736
  return finish(prepared, flags, command === "scan" ? "craft scan" : "craft copy", io);
722
737
  }
@@ -0,0 +1,19 @@
1
+ import {
2
+ SNAPSHOT_VERSION
3
+ } from "./chunk-GMVXADJ6.js";
4
+
5
+ // src/snapshot/migrate.ts
6
+ var READABLE_SNAPSHOT_VERSIONS = [1, 2];
7
+ function readSnapshot(data, source = "snapshot") {
8
+ const v = data?.version;
9
+ if (typeof data !== "object" || data === null || !READABLE_SNAPSHOT_VERSIONS.includes(v)) {
10
+ throw new Error(`${source} is not a snapshot this craft reads (version ${String(v)}; reads ${READABLE_SNAPSHOT_VERSIONS.join(" and ")})`);
11
+ }
12
+ const snap = data;
13
+ if (snap.version === 1) return { ...snap, version: SNAPSHOT_VERSION, migratedFrom: 1 };
14
+ return snap;
15
+ }
16
+
17
+ export {
18
+ readSnapshot
19
+ };
@@ -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
+ };