@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 +7 -0
- package/CHARACTER.md +34 -0
- package/dist/{audit-MTUC24XU.js → audit-UJCUWORL.js} +2 -2
- package/dist/{chunk-LFNSB6W7.js → chunk-B2MJLGF6.js} +1 -0
- package/dist/{chunk-QU6Q2QZZ.js → chunk-CVG5GSMN.js} +5 -5
- package/dist/{chunk-RXXRXV3V.js → chunk-LWVGIJ5A.js} +171 -16
- package/dist/{chunk-MUVFECFC.js → chunk-LXDNIJFG.js} +28 -11
- package/dist/{cli-FCKHYAVZ.js → chunk-NTIPJ5U7.js} +2 -185
- package/dist/{chunk-2IQ5OXFR.js → chunk-TQKDCA4V.js} +4 -4
- package/dist/{chunk-YDURAOYR.js → chunk-WAV6AKR4.js} +1 -1
- package/dist/{chunk-OUQ25LY2.js → chunk-YK2FFL6Z.js} +1 -1
- package/dist/cli-3GQROFCB.js +241 -0
- package/dist/{cli-5ACKICBO.js → cli-52M62QBN.js} +5 -5
- package/dist/{cli-FLG3ZCAF.js → cli-AVYNHBFJ.js} +11 -5
- package/dist/cli-CWORJYOU.js +190 -0
- package/dist/{cli-7OZCYOQY.js → cli-K4LSRYBE.js} +4 -4
- package/dist/{cli-22TSWAX6.js → cli-PEATEW57.js} +5 -5
- package/dist/cli.js +2 -2
- package/dist/index.cjs +299 -100
- package/dist/index.d.cts +149 -2
- package/dist/index.d.ts +149 -2
- package/dist/index.js +287 -100
- package/package.json +1 -1
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-
|
|
12
|
+
} from "./chunk-YK2FFL6Z.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-B2MJLGF6.js";
|
|
15
15
|
import "./chunk-QYBAVU3P.js";
|
|
16
16
|
export {
|
|
17
17
|
PROVENANCE_BUDGET_MS,
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WAV6AKR4.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-LWVGIJ5A.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags,
|
|
13
13
|
toJson
|
|
14
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
-
|
|
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-
|
|
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]
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
637
|
+
return import("./cli-CWORJYOU.js").then((m) => m.runReport(command, rest, io));
|
|
624
638
|
}
|
|
625
639
|
if (command === "estate") {
|
|
626
|
-
return import("./cli-
|
|
640
|
+
return import("./cli-PEATEW57.js").then((m) => m.runEstate(rest, io));
|
|
627
641
|
}
|
|
628
642
|
if (command === "null") {
|
|
629
|
-
return import("./cli-
|
|
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-
|
|
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-
|
|
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);
|