@domandigital/craft 0.13.0 → 0.15.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 +24 -0
- package/CHARACTER.md +272 -39
- package/COPY.md +5 -0
- package/README.md +19 -1
- package/art-direction.schema.json +428 -25
- package/dist/{audit-5TYDYRXV.js → audit-MTUC24XU.js} +3 -2
- package/dist/audit.cjs +674 -92
- package/dist/audit.d.cts +15 -1
- package/dist/audit.d.ts +15 -1
- package/dist/audit.js +674 -92
- package/dist/{chunk-IMB4V34S.js → chunk-2IQ5OXFR.js} +7 -7
- package/dist/{chunk-GMVXADJ6.js → chunk-LFNSB6W7.js} +711 -92
- package/dist/{chunk-3VTPI7BU.js → chunk-MUVFECFC.js} +74 -96
- package/dist/chunk-OQXTQQAU.js +27 -0
- package/dist/{chunk-ZLDA2SYB.js → chunk-OUQ25LY2.js} +47 -6
- package/dist/{chunk-K2BZN4GA.js → chunk-QU6Q2QZZ.js} +49 -14
- package/dist/chunk-QYBAVU3P.js +6 -0
- package/dist/{chunk-ESCVTKPN.js → chunk-RXXRXV3V.js} +9 -26
- package/dist/{chunk-CZDOMIVS.js → chunk-WUACK2HX.js} +2 -1
- package/dist/chunk-YDURAOYR.js +711 -0
- package/dist/cli-22TSWAX6.js +21 -0
- package/dist/cli-5ACKICBO.js +22 -0
- package/dist/{cli-DRKAMFAH.js → cli-7OZCYOQY.js} +89 -22
- package/dist/{cli-J5ECRCD4.js → cli-FCKHYAVZ.js} +40 -16
- package/dist/{cli-ILH7OO5G.js → cli-FLG3ZCAF.js} +6 -4
- package/dist/cli.js +3 -2
- package/dist/generate.js +6 -0
- package/dist/index.cjs +1618 -328
- package/dist/index.d.cts +619 -58
- package/dist/index.d.ts +619 -58
- package/dist/index.js +1555 -328
- package/dist/migrate-VIFAYIBM.js +9 -0
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-ZZ5CKOJP.js +0 -295
- package/dist/cli-BKJMLD3H.js +0 -18
- package/dist/cli-N2IDXH7W.js +0 -20
|
@@ -4,13 +4,19 @@ import {
|
|
|
4
4
|
HOUSE_LOCKED,
|
|
5
5
|
applyHouseGate,
|
|
6
6
|
checkCopy,
|
|
7
|
+
compareCompetitor,
|
|
8
|
+
compareFacts,
|
|
7
9
|
extractCopy,
|
|
8
|
-
extractStrings,
|
|
9
10
|
fileKind,
|
|
11
|
+
formatComparison,
|
|
12
|
+
formatCompetitor,
|
|
10
13
|
holdHouseBlocks,
|
|
11
14
|
houseRule,
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
pageCopy,
|
|
16
|
+
protectedFacts,
|
|
17
|
+
scanSource,
|
|
18
|
+
visibleText
|
|
19
|
+
} from "./chunk-LFNSB6W7.js";
|
|
14
20
|
|
|
15
21
|
// src/character/config.ts
|
|
16
22
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -205,87 +211,6 @@ function toSarif(report, options) {
|
|
|
205
211
|
};
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
// src/character/facts.ts
|
|
209
|
-
var MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)";
|
|
210
|
-
var PATTERNS = [
|
|
211
|
-
["url", /\bhttps?:\/\/[^\s<>"'`)\]]+|\bwww\.[^\s<>"'`)\]]+/gi],
|
|
212
|
-
["email", /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/g],
|
|
213
|
-
["phone", /(?:\+44\s?\(?0?\)?\s?|\b0)\d(?:[\s-]?\d){8,9}\b/g],
|
|
214
|
-
["postcode", /\b[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}\b/g],
|
|
215
|
-
["money", /[£$€]\s?\d[\d,]*(?:\.\d{1,2})?(?:\s?(?:k|m|bn)\b)?|\b\d[\d,]*(?:\.\d{1,2})?\s?(?:pounds|GBP)\b/gi],
|
|
216
|
-
["date", new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}\\b(?:\\s+\\d{4})?|\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?\\b(?:,?\\s+\\d{4})?|\\b${MONTH}\\s+\\d{4}\\b|\\b\\d{1,2}\\/\\d{1,2}\\/\\d{2,4}\\b|\\b(?:19|20)\\d{2}\\b`, "g")],
|
|
217
|
-
["time", /\b\d{1,2}(?:[:.]\d{2})?\s?(?:am|pm)\b|\b\d{1,2}:\d{2}\b/gi],
|
|
218
|
-
["number", /\b\d[\d,]*(?:\.\d+)?%?|\b\d+(?:\.\d+)?\s?(?:per ?cent)\b/gi]
|
|
219
|
-
];
|
|
220
|
-
var NAME = /[A-Z][a-zA-Z’'&-]*[a-zA-Z](?:\s+(?:of|and|&|the)?\s*[A-Z][a-zA-Z’'&-]*[a-zA-Z])*/g;
|
|
221
|
-
var normalise = (kind, value) => {
|
|
222
|
-
const v = value.trim();
|
|
223
|
-
if (kind === "phone") return v.replace(/[^\d+]/g, "").replace(/^\+44\(?0?\)?/, "0").replace(/^\+44/, "0");
|
|
224
|
-
if (kind === "money" || kind === "number") return v.replace(/[\s,]/g, "").toLowerCase();
|
|
225
|
-
if (kind === "postcode") return v.replace(/\s/g, "").toUpperCase();
|
|
226
|
-
if (kind === "url") return v.replace(/[.,;:]+$/, "").toLowerCase();
|
|
227
|
-
return v.replace(/\s+/g, " ").toLowerCase();
|
|
228
|
-
};
|
|
229
|
-
function visibleText(file) {
|
|
230
|
-
const prose = /\.(md|markdown|mdx|txt)$/i.test(file.path);
|
|
231
|
-
return (prose ? extractCopy(file) : extractStrings(file)).map((b) => b.text).join("\n");
|
|
232
|
-
}
|
|
233
|
-
function protectedFacts(text) {
|
|
234
|
-
const facts = [];
|
|
235
|
-
let rest = text;
|
|
236
|
-
const mask = (start, length) => {
|
|
237
|
-
rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
|
|
238
|
-
};
|
|
239
|
-
for (const [kind, pattern] of PATTERNS) {
|
|
240
|
-
for (const m of [...rest.matchAll(pattern)]) {
|
|
241
|
-
const value = m[0].replace(/[.,;:]+$/, "");
|
|
242
|
-
facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
|
|
243
|
-
mask(m.index ?? 0, m[0].length);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const lines = rest.split("\n");
|
|
247
|
-
for (const line of lines) {
|
|
248
|
-
if (/^\s{0,3}#{1,6}\s/.test(line)) continue;
|
|
249
|
-
for (const m of line.matchAll(NAME)) {
|
|
250
|
-
const at = m.index ?? 0;
|
|
251
|
-
const before = line.slice(0, at).trimEnd();
|
|
252
|
-
const opensSentence = before === "" || /[.!?:"“(>*-]$/.test(before) || /^\s*(?:[-*+]|\d+\.)$/.test(before);
|
|
253
|
-
const word = m[0];
|
|
254
|
-
const acronym = /^[A-Z]{2,}$/.test(word);
|
|
255
|
-
if (opensSentence && !acronym && !/\s/.test(word)) continue;
|
|
256
|
-
if (word === "I") continue;
|
|
257
|
-
facts.push({ kind: "name", value: word, key: `name:${normalise("name", word)}` });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return facts;
|
|
261
|
-
}
|
|
262
|
-
var unique = (facts) => [...new Map(facts.map((f) => [f.key, f])).values()];
|
|
263
|
-
function compareFacts(before, after) {
|
|
264
|
-
const a = unique(protectedFacts(before));
|
|
265
|
-
const b = unique(protectedFacts(after));
|
|
266
|
-
const inA = new Set(a.map((f) => f.key));
|
|
267
|
-
const inB = new Set(b.map((f) => f.key));
|
|
268
|
-
return {
|
|
269
|
-
before: a.length,
|
|
270
|
-
after: b.length,
|
|
271
|
-
lost: a.filter((f) => !inB.has(f.key)),
|
|
272
|
-
added: b.filter((f) => !inA.has(f.key))
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
function formatComparison(result, beforePath, afterPath) {
|
|
276
|
-
const lines = [`craft copy compare: ${beforePath} -> ${afterPath}`, ` ${result.before} protected facts before, ${result.after} after`];
|
|
277
|
-
if (result.lost.length) {
|
|
278
|
-
lines.push("", ` Lost (${result.lost.length}): restore each, or say why it went`);
|
|
279
|
-
for (const f of result.lost) lines.push(` ${f.kind.padEnd(9)} ${f.value}`);
|
|
280
|
-
}
|
|
281
|
-
if (result.added.length) {
|
|
282
|
-
lines.push("", ` Added (${result.added.length}): each needs a source, or it is invented`);
|
|
283
|
-
for (const f of result.added) lines.push(` ${f.kind.padEnd(9)} ${f.value}`);
|
|
284
|
-
}
|
|
285
|
-
if (!result.lost.length && !result.added.length) lines.push("", " Every fact kept, nothing added.");
|
|
286
|
-
return lines.join("\n");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
214
|
// src/character/claims.ts
|
|
290
215
|
var CLAIM_KINDS = /* @__PURE__ */ new Set(["money", "number", "date"]);
|
|
291
216
|
var PROPER = "[A-Z][\\w&.\u2019'-]*(?:\\s+(?:of|and|&|the|for)?\\s*[A-Z][\\w&.\u2019'-]*)*";
|
|
@@ -445,18 +370,20 @@ Usage
|
|
|
445
370
|
craft scan [paths...] [--staged] [--json] [--strict] [--direction <file>] [ci options]
|
|
446
371
|
craft copy [paths...] [--gate] [--json] [--strict] [--direction <file>] [ci options]
|
|
447
372
|
craft copy compare <before> <after> [--json]
|
|
373
|
+
craft copy compare <url | snapshot.json> --competitor <url | snapshot.json> [--brief "<text>"] [--json]
|
|
448
374
|
craft copy claims <paths...> [--json]
|
|
449
375
|
craft tells list [--json]
|
|
450
376
|
craft tells harvest <null.json | dir>... [--share 0.25] [--json]
|
|
451
377
|
craft snapshot <url> [--out <file>] [--width <px>] [--height <px>]
|
|
452
378
|
craft audit <url | snapshot.json> [--repo <dir>] [--null <null.json>] [--out <file>] [--json] [--strict] [ci options]
|
|
453
379
|
craft audit --pages <sitemap.xml | urls.txt> [--viewport 390,768,1440] [the same]
|
|
454
|
-
craft direction init [--snapshot <file>] [--client <name>] [--out <file>]
|
|
455
|
-
craft direction validate [--snapshot <file>] [--direction <file>] [--json]
|
|
380
|
+
craft direction init [--snapshot <file | url>] [--client <name>] [--brief "<text>"] [--out <file>]
|
|
381
|
+
craft direction validate [--snapshot <file | url>] [--page home] [--estate <estate.json>] [--direction <file>] [--strict] [--json]
|
|
382
|
+
craft direction research [--brief "<who, where, what>"] [--visual]
|
|
456
383
|
craft direction propose [--snapshot <file>] [--estate <estate.json | dir>] [--out <file>]
|
|
457
384
|
craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]
|
|
458
|
-
craft estate add <url | snapshot.json> --id <id> [--client <name>] [--estate <file>]
|
|
459
|
-
craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--json] [--strict]
|
|
385
|
+
craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] [--estate <file>]
|
|
386
|
+
craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--directions] [--json] [--strict]
|
|
460
387
|
craft report <url | snapshot.json> [--repo <dir>] [--null <dir>] [--estate <file>] [--direction <file>] [--json] [--strict]
|
|
461
388
|
craft retrofit <url | snapshot.json> [the same] [--out RETROFIT.md]
|
|
462
389
|
|
|
@@ -468,6 +395,11 @@ copy The prose in Markdown, markup and content files.
|
|
|
468
395
|
compare lists the protected facts (prices, numbers, dates, times,
|
|
469
396
|
phones, emails, links, postcodes, names) a rewrite lost or added.
|
|
470
397
|
Exit 1 if any: restore it, source it, or say why.
|
|
398
|
+
With --competitor, reads both rendered pages instead and lists the
|
|
399
|
+
hero and service sentences holding no specific (place, name,
|
|
400
|
+
number, price, job) the other page does not also say. The brief's
|
|
401
|
+
terms come from --brief, else art-direction.json, else --null's
|
|
402
|
+
null.json. A measure, not a gate: always exit 0.
|
|
471
403
|
claims lists every sentence holding a price, figure, date or named
|
|
472
404
|
source, marked sourced or UNSOURCED, for a person to check against
|
|
473
405
|
the primary source. It cannot tell true from false. Always exit 0.
|
|
@@ -529,10 +461,12 @@ var VALUE_FLAGS = {
|
|
|
529
461
|
"--sarif": "sarif",
|
|
530
462
|
"--pages": "pages",
|
|
531
463
|
"--viewport": "viewport",
|
|
532
|
-
"--component": "component"
|
|
464
|
+
"--component": "component",
|
|
465
|
+
"--competitor": "competitor",
|
|
466
|
+
"--page": "page"
|
|
533
467
|
};
|
|
534
468
|
function parseFlags(args) {
|
|
535
|
-
const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
|
|
469
|
+
const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false, directions: false, visual: false };
|
|
536
470
|
for (let i = 0; i < args.length; i += 1) {
|
|
537
471
|
const a = args[i];
|
|
538
472
|
if (a === "--json") flags.json = true;
|
|
@@ -540,6 +474,8 @@ function parseFlags(args) {
|
|
|
540
474
|
else if (a === "--staged") flags.staged = true;
|
|
541
475
|
else if (a === "--gate") flags.gate = true;
|
|
542
476
|
else if (a === "--update-baseline") flags.updateBaseline = true;
|
|
477
|
+
else if (a === "--directions") flags.directions = true;
|
|
478
|
+
else if (a === "--visual") flags.visual = true;
|
|
543
479
|
else if (a in VALUE_FLAGS) {
|
|
544
480
|
const value = args[i + 1];
|
|
545
481
|
i += 1;
|
|
@@ -637,28 +573,69 @@ function finish(report, flags, title, io) {
|
|
|
637
573
|
var SOURCE_FILE = (p) => ["markup", "script", "css"].includes(fileKind(p)) && !/\.d\.ts$|\.min\.js$/.test(p);
|
|
638
574
|
var COPY_FILE = (p) => (["markup", "prose", "script"].includes(fileKind(p)) || /\.jsonl?$/i.test(p)) && !NOT_SITE_COPY.test(p.split("/").pop() ?? p) && !/(?:^|\/)(?:package(?:-lock)?|tsconfig[\w.-]*|art-direction)\.json$/.test(p);
|
|
639
575
|
var NOT_COPY_DIR = (name) => name.startsWith("_");
|
|
576
|
+
async function pageSnapshot(target, cwd) {
|
|
577
|
+
const local = resolve(cwd, target);
|
|
578
|
+
if (/\.json$/i.test(target) && existsSync(local)) {
|
|
579
|
+
const { readSnapshot } = await import("./migrate-VIFAYIBM.js");
|
|
580
|
+
return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
581
|
+
}
|
|
582
|
+
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
|
|
583
|
+
const { snapshotUrl } = await import("./audit-MTUC24XU.js");
|
|
584
|
+
return snapshotUrl(target);
|
|
585
|
+
}
|
|
586
|
+
async function briefFor(flags, cwd) {
|
|
587
|
+
if (flags.brief) return flags.brief;
|
|
588
|
+
const direction = resolve(cwd, flags.direction ?? "art-direction.json");
|
|
589
|
+
if (existsSync(direction)) {
|
|
590
|
+
const brief = JSON.parse(readFileSync(direction, "utf8")).brief;
|
|
591
|
+
if (typeof brief === "string" && brief.trim()) return brief;
|
|
592
|
+
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
593
|
+
if (flags.null) {
|
|
594
|
+
const { loadNull } = await import("./cli-FLG3ZCAF.js");
|
|
595
|
+
return loadNull(flags.null, cwd).brief;
|
|
596
|
+
}
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
async function compareWithCompetitor(flags, io) {
|
|
600
|
+
try {
|
|
601
|
+
if (flags.positional.length !== 1 || !flags.competitor) throw new Error('usage: craft copy compare <url | snapshot.json> --competitor <url | snapshot.json> [--brief "<text>"] [--json]');
|
|
602
|
+
const brief = await briefFor(flags, io.cwd);
|
|
603
|
+
const [ours, theirs] = await Promise.all([pageSnapshot(flags.positional[0], io.cwd), pageSnapshot(flags.competitor, io.cwd)]);
|
|
604
|
+
const copy = [ours, theirs].map((snap, i) => {
|
|
605
|
+
const page = pageCopy(snap);
|
|
606
|
+
if (!page) throw new Error(`${[flags.positional[0], flags.competitor][i]} has no page text: snapshot it again with this craft`);
|
|
607
|
+
return page;
|
|
608
|
+
});
|
|
609
|
+
const result = compareCompetitor(copy[0], copy[1], brief);
|
|
610
|
+
io.out(flags.json ? toJson(result) : formatCompetitor(result));
|
|
611
|
+
return 0;
|
|
612
|
+
} catch (error) {
|
|
613
|
+
io.err(`craft: ${error.message}`);
|
|
614
|
+
return 2;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
640
617
|
function run(argv, io) {
|
|
641
618
|
const [command, ...rest] = argv;
|
|
642
619
|
if (command === "direction") {
|
|
643
|
-
return import("./cli-
|
|
620
|
+
return import("./cli-7OZCYOQY.js").then((m) => m.runDirection(rest, io));
|
|
644
621
|
}
|
|
645
622
|
if (command === "report" || command === "retrofit") {
|
|
646
|
-
return import("./cli-
|
|
623
|
+
return import("./cli-FCKHYAVZ.js").then((m) => m.runReport(command, rest, io));
|
|
647
624
|
}
|
|
648
625
|
if (command === "estate") {
|
|
649
|
-
return import("./cli-
|
|
626
|
+
return import("./cli-22TSWAX6.js").then((m) => m.runEstate(rest, io));
|
|
650
627
|
}
|
|
651
628
|
if (command === "null") {
|
|
652
|
-
return import("./cli-
|
|
629
|
+
return import("./cli-FLG3ZCAF.js").then((m) => m.runNull(rest, io));
|
|
653
630
|
}
|
|
654
631
|
if (command === "tells" && rest[0] === "harvest") {
|
|
655
|
-
return import("./cli-
|
|
632
|
+
return import("./cli-FLG3ZCAF.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
656
633
|
io.err(`craft: ${error.message}`);
|
|
657
634
|
return 2;
|
|
658
635
|
});
|
|
659
636
|
}
|
|
660
637
|
if (command === "snapshot" || command === "audit") {
|
|
661
|
-
return import("./cli-
|
|
638
|
+
return import("./cli-5ACKICBO.js").then((m) => m.runAudit(command, rest, io));
|
|
662
639
|
}
|
|
663
640
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
664
641
|
io.out(HELP);
|
|
@@ -690,6 +667,7 @@ function run(argv, io) {
|
|
|
690
667
|
if (command === "copy" && rest[0] === "compare") {
|
|
691
668
|
const flags = parseFlags(rest.slice(1));
|
|
692
669
|
if (typeof flags === "string") throw new Error(flags);
|
|
670
|
+
if (flags.competitor) return compareWithCompetitor(flags, io);
|
|
693
671
|
if (flags.positional.length !== 2) throw new Error("usage: craft copy compare <before> <after> [--json]");
|
|
694
672
|
const [before, after] = flags.positional.map((p) => {
|
|
695
673
|
const abs = resolve(io.cwd, p);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/null/generate.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function generate(prompt, model) {
|
|
7
|
+
const bin = process.env.CRAFT_CLAUDE ?? "claude";
|
|
8
|
+
const args = ["-p", "--tools", "", "--no-session-persistence", "--disable-slash-commands", "--setting-sources", "", "--strict-mcp-config", ...model ? ["--model", model] : [], prompt];
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), "craft-null-"));
|
|
10
|
+
return new Promise((done, fail) => {
|
|
11
|
+
const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
12
|
+
let out = "";
|
|
13
|
+
let err = "";
|
|
14
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
15
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
16
|
+
child.on("error", (e) => fail(new Error(`${bin}: ${e.message}. Install Claude Code, or set CRAFT_CLAUDE.`)));
|
|
17
|
+
child.on("close", (code) => {
|
|
18
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
19
|
+
if (code === 0) done(out);
|
|
20
|
+
else fail(new Error(`${bin} exited ${code}: ${(err || out).trim().slice(0, 200)}`));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
generate
|
|
27
|
+
};
|
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
decodePng
|
|
3
3
|
} from "./chunk-JYNXGCYZ.js";
|
|
4
4
|
import {
|
|
5
|
-
SNAPSHOT_VERSION,
|
|
6
5
|
auditSnapshot,
|
|
7
6
|
fingerprint
|
|
8
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LFNSB6W7.js";
|
|
8
|
+
import {
|
|
9
|
+
SNAPSHOT_VERSION
|
|
10
|
+
} from "./chunk-QYBAVU3P.js";
|
|
9
11
|
|
|
10
12
|
// src/audit/collect.ts
|
|
11
13
|
function collectInPage() {
|
|
@@ -218,7 +220,7 @@ function collectInPage() {
|
|
|
218
220
|
let covered = 0;
|
|
219
221
|
let minX = Infinity;
|
|
220
222
|
let maxX = -Infinity;
|
|
221
|
-
let
|
|
223
|
+
let textBlocks2 = 0;
|
|
222
224
|
let centred = 0;
|
|
223
225
|
for (const n of leaves) {
|
|
224
226
|
const r = box(n);
|
|
@@ -230,14 +232,14 @@ function collectInPage() {
|
|
|
230
232
|
left += split * r.height;
|
|
231
233
|
right += (r.width - split) * r.height;
|
|
232
234
|
if (ownText(n).length > 1) {
|
|
233
|
-
|
|
235
|
+
textBlocks2 += 1;
|
|
234
236
|
const mid = r.left + r.width / 2;
|
|
235
237
|
if (style(n).textAlign === "center" || Math.abs(mid - centreX) < vw * 0.03 && r.width < b.width * 0.8) centred += 1;
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
const round = (v) => Math.round(v * 1e3) / 1e3;
|
|
239
241
|
return {
|
|
240
|
-
centredShare:
|
|
242
|
+
centredShare: textBlocks2 ? round(centred / textBlocks2) : 0,
|
|
241
243
|
mirrorSymmetry: left + right > 0 ? round(1 - Math.abs(left - right) / (left + right)) : 1,
|
|
242
244
|
whitespaceRatio: round(Math.max(0, 1 - Math.min(1, covered / Math.max(1, b.width * b.height)))),
|
|
243
245
|
contentWidthRatio: leaves.length ? round(Math.min(1, (maxX - minX) / vw)) : 0,
|
|
@@ -302,6 +304,43 @@ function collectInPage() {
|
|
|
302
304
|
});
|
|
303
305
|
return { badges, avatars, carousel };
|
|
304
306
|
};
|
|
307
|
+
const TEXT_BLOCK = "h1, h2, h3, h4, h5, h6, p, li, blockquote, figcaption, dt, dd, td, th, summary, cite, q";
|
|
308
|
+
const CHROME = "nav, [role=navigation], footer, [role=contentinfo], button, [role=button], [aria-hidden=true], script, style, noscript, template";
|
|
309
|
+
const readable = (el) => {
|
|
310
|
+
if (!shown(el)) return false;
|
|
311
|
+
const r = el.getBoundingClientRect();
|
|
312
|
+
return r.width > 2 && r.height > 2;
|
|
313
|
+
};
|
|
314
|
+
const textBlocks = (scope, limit, keep = () => true) => {
|
|
315
|
+
const out = [];
|
|
316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
317
|
+
const add = (el) => {
|
|
318
|
+
const t = (el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
319
|
+
if (t.length < 2 || seen.has(t)) return;
|
|
320
|
+
seen.add(t);
|
|
321
|
+
out.push(t);
|
|
322
|
+
};
|
|
323
|
+
for (const el of Array.from(scope.querySelectorAll(TEXT_BLOCK))) {
|
|
324
|
+
if (out.length >= limit) break;
|
|
325
|
+
if (el.parentElement?.closest(TEXT_BLOCK) || el.closest(CHROME) || !readable(el) || !keep(el)) continue;
|
|
326
|
+
add(el);
|
|
327
|
+
}
|
|
328
|
+
for (const el of Array.from(scope.querySelectorAll("div, span"))) {
|
|
329
|
+
if (out.length >= limit) break;
|
|
330
|
+
if (ownText(el).split(" ").length < 3) continue;
|
|
331
|
+
if (el.closest(TEXT_BLOCK) || el.closest(CHROME) || el.closest("a") || !readable(el) || !keep(el)) continue;
|
|
332
|
+
add(el);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
};
|
|
336
|
+
const inChromeHeader = (el) => {
|
|
337
|
+
const h = el.closest("header");
|
|
338
|
+
return Boolean(h && !h.querySelector("h1"));
|
|
339
|
+
};
|
|
340
|
+
const firstScreenText = textBlocks(document.body, 12, (el) => {
|
|
341
|
+
const b = box(el);
|
|
342
|
+
return b.top < vh && b.top + b.height > 0 && !inChromeHeader(el) && !el.closest("a");
|
|
343
|
+
});
|
|
305
344
|
const kept = blocks.slice(0, 40);
|
|
306
345
|
const sections = kept.map((el, index) => {
|
|
307
346
|
const b = box(el);
|
|
@@ -330,7 +369,8 @@ function collectInPage() {
|
|
|
330
369
|
role: roleOf(el, kind, geometry, b, cards, iconCards, index === kept.length - 1),
|
|
331
370
|
geometry,
|
|
332
371
|
...parts,
|
|
333
|
-
figures: numbers
|
|
372
|
+
figures: numbers,
|
|
373
|
+
text: textBlocks(el, 40)
|
|
334
374
|
};
|
|
335
375
|
});
|
|
336
376
|
if (header && sections.length === 0) {
|
|
@@ -507,6 +547,7 @@ function collectInPage() {
|
|
|
507
547
|
})(),
|
|
508
548
|
effects,
|
|
509
549
|
images: images.slice(0, 60),
|
|
550
|
+
firstScreenText,
|
|
510
551
|
motion: {
|
|
511
552
|
hiddenSections: sections.filter((s) => s.hiddenAtLoad).length,
|
|
512
553
|
sections: sections.length,
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
compareDirections,
|
|
3
|
+
compareToDeclared,
|
|
4
|
+
formatDirectionMatches,
|
|
5
|
+
summariseDirection,
|
|
6
|
+
validateDirection
|
|
7
|
+
} from "./chunk-YDURAOYR.js";
|
|
4
8
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
loadNull
|
|
10
|
+
} from "./chunk-RXXRXV3V.js";
|
|
7
11
|
import {
|
|
8
12
|
parseFlags,
|
|
9
13
|
toJson
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-MUVFECFC.js";
|
|
15
|
+
import {
|
|
16
|
+
readSnapshot
|
|
17
|
+
} from "./chunk-WUACK2HX.js";
|
|
11
18
|
import {
|
|
12
19
|
COMPONENT_ROLES,
|
|
20
|
+
SHARED_PART,
|
|
13
21
|
auditSnapshot,
|
|
14
22
|
closestComponents,
|
|
15
23
|
estateComponentPairs,
|
|
@@ -18,7 +26,7 @@ import {
|
|
|
18
26
|
groundName,
|
|
19
27
|
hueName,
|
|
20
28
|
shapeName
|
|
21
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-LFNSB6W7.js";
|
|
22
30
|
|
|
23
31
|
// src/estate/cli.ts
|
|
24
32
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -37,7 +45,7 @@ function addSite(register, site) {
|
|
|
37
45
|
}
|
|
38
46
|
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
39
47
|
const out = [];
|
|
40
|
-
const close = (k) => d.parts[k] <=
|
|
48
|
+
const close = (k) => d.parts[k] <= SHARED_PART;
|
|
41
49
|
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
42
50
|
if (close("type")) {
|
|
43
51
|
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
@@ -52,14 +60,14 @@ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
|
52
60
|
}
|
|
53
61
|
function compareToEstate(target, register, options = {}) {
|
|
54
62
|
const at = options.siblingAt ?? SIBLING_AT;
|
|
55
|
-
return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
|
|
63
|
+
return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
|
|
56
64
|
const d = fingerprintDistance(target, s.fingerprint);
|
|
57
65
|
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target, s.fingerprint, d) };
|
|
58
66
|
}).sort((a, b) => a.distance - b.distance);
|
|
59
67
|
}
|
|
60
68
|
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
61
69
|
const out = [];
|
|
62
|
-
const s = register.sites;
|
|
70
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
63
71
|
for (let i = 0; i < s.length; i += 1) {
|
|
64
72
|
for (let j = i + 1; j < s.length; j += 1) {
|
|
65
73
|
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
@@ -81,7 +89,7 @@ function siblingLine(briefs) {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// src/estate/cli.ts
|
|
84
|
-
var USAGE = "usage: craft estate add <url | snapshot.json> --id <id> [--client <name>] | craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--component <role>] [--json] [--strict]";
|
|
92
|
+
var USAGE = "usage: craft estate add [<url | snapshot.json>] --id <id> [--client <name>] [--direction <art-direction.json>] | craft estate compare [<url | snapshot.json | id>] [--null <dir>] [--component <role>] [--directions] [--json] [--strict]";
|
|
85
93
|
function loadEstate(path) {
|
|
86
94
|
if (!existsSync(path)) return emptyEstate();
|
|
87
95
|
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -95,7 +103,7 @@ async function snapshotOf(target, cwd) {
|
|
|
95
103
|
return snap;
|
|
96
104
|
}
|
|
97
105
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
98
|
-
const { snapshotUrl } = await import("./audit-
|
|
106
|
+
const { snapshotUrl } = await import("./audit-MTUC24XU.js");
|
|
99
107
|
return snapshotUrl(target);
|
|
100
108
|
}
|
|
101
109
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -115,7 +123,7 @@ function componentRole(name) {
|
|
|
115
123
|
if (!COMPONENT_ROLES.includes(name)) throw new Error(`--component takes one of ${COMPONENT_ROLES.join(", ")}`);
|
|
116
124
|
return name;
|
|
117
125
|
}
|
|
118
|
-
var withRole = (register, role) => register.sites.filter((s) => (s.fingerprint
|
|
126
|
+
var withRole = (register, role) => register.sites.filter((s) => (s.fingerprint?.sections ?? []).some((x) => x.role === role)).length;
|
|
119
127
|
function formatComponentPairs(role, pairs, measured, sites) {
|
|
120
128
|
const lines = [`craft estate: ${role} components, ${measured} of ${sites} sites have one, closest pairs first (no sibling line yet: ranked, not judged)`];
|
|
121
129
|
for (const p of pairs) lines.push(` ${`${p.a} + ${p.b}`.padEnd(34)} ${p.distance.toFixed(2)}${p.shared.length ? ` shares ${p.shared.join(", ")}` : ""}`);
|
|
@@ -134,6 +142,19 @@ async function runEstate(args, io) {
|
|
|
134
142
|
const file = resolve(io.cwd, flags.estate ?? "estate.json");
|
|
135
143
|
const register = loadEstate(file);
|
|
136
144
|
const at = flags.null ? siblingLine(flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd).runs.map((r) => r.fingerprint))) : SIBLING_AT;
|
|
145
|
+
const directionFile = flags.direction ? JSON.parse(readFileSync(resolve(io.cwd, flags.direction), "utf8")) : void 0;
|
|
146
|
+
if (directionFile && !validateDirection(directionFile).valid) throw new Error("the direction has errors: run craft direction validate before registering it");
|
|
147
|
+
const declared = () => summariseDirection(directionFile);
|
|
148
|
+
if (sub === "add" && !target && flags.id && flags.direction) {
|
|
149
|
+
const existing = register.sites.find((s) => s.id === flags.id);
|
|
150
|
+
const direction = declared();
|
|
151
|
+
const next = addSite(register, { ...existing ?? { id: flags.id, client: flags.client ?? directionFile.client, url: "", tells: [], addedAt: (/* @__PURE__ */ new Date()).toISOString() }, direction });
|
|
152
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}
|
|
153
|
+
`);
|
|
154
|
+
io.out(`craft estate: ${flags.id} direction recorded.`);
|
|
155
|
+
io.out(formatDirectionMatches(flags.id, compareToDeclared(direction, next.sites, flags.id)));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
137
158
|
if (sub === "add") {
|
|
138
159
|
if (!target || !flags.id) throw new Error(USAGE);
|
|
139
160
|
const snap = await snapshotOf(target, io.cwd);
|
|
@@ -144,7 +165,8 @@ async function runEstate(args, io) {
|
|
|
144
165
|
url: snap.url,
|
|
145
166
|
fingerprint: fp,
|
|
146
167
|
tells: [...new Set(auditSnapshot(snap).findings.map((f) => f.tell))].sort(),
|
|
147
|
-
addedAt: snap.capturedAt
|
|
168
|
+
addedAt: snap.capturedAt,
|
|
169
|
+
...flags.direction ? { direction: declared() } : register.sites.find((s) => s.id === flags.id)?.direction ? { direction: register.sites.find((s) => s.id === flags.id).direction } : {}
|
|
148
170
|
});
|
|
149
171
|
writeFileSync(file, `${JSON.stringify(next, null, 2)}
|
|
150
172
|
`);
|
|
@@ -152,6 +174,17 @@ async function runEstate(args, io) {
|
|
|
152
174
|
io.out(formatMatches(flags.id, compareToEstate(fp, next, { exclude: flags.id, siblingAt: at }).slice(0, 3), at));
|
|
153
175
|
return 0;
|
|
154
176
|
}
|
|
177
|
+
if (sub === "compare" && flags.directions) {
|
|
178
|
+
const withDirection = register.sites.filter((s) => s.direction);
|
|
179
|
+
const pairs = withDirection.flatMap((a, i) => withDirection.slice(i + 1).map((b) => ({ a: a.id, b: b.id, ...compareDirections(a.direction, b.direction) })));
|
|
180
|
+
pairs.sort((x, y) => y.shared.length - x.shared.length);
|
|
181
|
+
if (flags.json) io.out(toJson({ sites: withDirection.length, pairs }));
|
|
182
|
+
else {
|
|
183
|
+
io.out(`craft estate: ${withDirection.length} of ${register.sites.length} sites have a declared direction, ${pairs.filter((p) => p.flagged).length} pair(s) worth a look (ranked, not judged)`);
|
|
184
|
+
for (const p of pairs) io.out(` ${`${p.a} + ${p.b}`.padEnd(34)} ${p.flagged ? "LOOK " : " "}${p.shared.length ? `shares ${p.shared.join(", ")}` : "nothing shared"}`);
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
155
188
|
if (sub === "compare" && flags.component) {
|
|
156
189
|
const role = componentRole(flags.component);
|
|
157
190
|
if (!target) {
|
|
@@ -161,9 +194,10 @@ async function runEstate(args, io) {
|
|
|
161
194
|
return 0;
|
|
162
195
|
}
|
|
163
196
|
const known = register.sites.find((s) => s.id === target);
|
|
197
|
+
if (known && !known.fingerprint) throw new Error(`${target} has a declared direction only; add a snapshot to compare rendered components`);
|
|
164
198
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
165
199
|
if (!(fp.sections ?? []).some((x) => x.role === role)) throw new Error(`${known?.id ?? target} has no ${role} section measured (roles need snapshot version 2)`);
|
|
166
|
-
const pairs = register.sites.filter((s) => s.id !== known?.id).flatMap((s) => {
|
|
200
|
+
const pairs = register.sites.filter((s) => s.id !== known?.id && s.fingerprint).flatMap((s) => {
|
|
167
201
|
const c = closestComponents(fp, s.fingerprint, role);
|
|
168
202
|
return c ? [{ a: known?.id ?? target, b: s.id, ...c }] : [];
|
|
169
203
|
}).sort((x, y) => x.distance - y.distance);
|
|
@@ -177,6 +211,7 @@ async function runEstate(args, io) {
|
|
|
177
211
|
return flags.strict && pairs.some((p) => p.sibling) ? 1 : 0;
|
|
178
212
|
}
|
|
179
213
|
const known = register.sites.find((s) => s.id === target);
|
|
214
|
+
if (known && !known.fingerprint) throw new Error(`${target} has a declared direction only; add a snapshot to compare rendered sites`);
|
|
180
215
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
181
216
|
const matches = compareToEstate(fp, register, { exclude: known?.id, siblingAt: at });
|
|
182
217
|
io.out(flags.json ? toJson({ siblingAt: at, matches }) : formatMatches(known?.id ?? target, matches, at));
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
generate
|
|
3
|
+
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags,
|
|
6
6
|
toJson
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MUVFECFC.js";
|
|
8
|
+
import {
|
|
9
|
+
readSnapshot
|
|
10
|
+
} from "./chunk-WUACK2HX.js";
|
|
8
11
|
import {
|
|
9
12
|
CATALOGUE_VERSION,
|
|
10
13
|
NULL_VERSION,
|
|
@@ -17,34 +20,14 @@ import {
|
|
|
17
20
|
hueSwatch,
|
|
18
21
|
nullPrompt,
|
|
19
22
|
scanSource
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-LFNSB6W7.js";
|
|
21
24
|
|
|
22
25
|
// src/null/cli.ts
|
|
23
|
-
import {
|
|
24
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
25
|
-
import { tmpdir } from "os";
|
|
26
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
26
27
|
import { join, relative, resolve } from "path";
|
|
27
28
|
import { pathToFileURL } from "url";
|
|
28
29
|
var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
29
30
|
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
31
|
async function pool(items, size, work) {
|
|
49
32
|
const queue = [...items];
|
|
50
33
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(size, queue.length)) }, async () => {
|
|
@@ -115,7 +98,7 @@ async function build(flags, io) {
|
|
|
115
98
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
116
99
|
if (toShoot.length) {
|
|
117
100
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
118
|
-
const { snapshotUrls } = await import("./audit-
|
|
101
|
+
const { snapshotUrls } = await import("./audit-MTUC24XU.js");
|
|
119
102
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
120
103
|
results.forEach((r, i) => {
|
|
121
104
|
const id = toShoot[i];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SNAPSHOT_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QYBAVU3P.js";
|
|
4
4
|
|
|
5
5
|
// src/snapshot/migrate.ts
|
|
6
6
|
var READABLE_SNAPSHOT_VERSIONS = [1, 2];
|
|
@@ -15,5 +15,6 @@ function readSnapshot(data, source = "snapshot") {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export {
|
|
18
|
+
READABLE_SNAPSHOT_VERSIONS,
|
|
18
19
|
readSnapshot
|
|
19
20
|
};
|