@domandigital/craft 0.9.0 → 0.11.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 +26 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +6 -0
- package/README.md +41 -0
- package/dist/{audit-5P65UMFL.js → audit-QIPXL7E4.js} +3 -3
- package/dist/audit.cjs +67 -11
- package/dist/audit.js +67 -11
- package/dist/chunk-2AJB27BJ.js +743 -0
- package/dist/{chunk-ZT3WLN7R.js → chunk-3NGWXZZE.js} +64 -10
- package/dist/{chunk-VKQDOUHO.js → chunk-3OZQ4C5R.js} +7 -6
- package/dist/{chunk-5LI2VFN5.js → chunk-HOHHQB4N.js} +9 -8
- package/dist/{chunk-BYY32NIN.js → chunk-JP75P2DU.js} +5 -3
- package/dist/{chunk-JRMU6GYX.js → chunk-JWHC67ZS.js} +1 -1
- package/dist/chunk-MSQ57PJU.js +206 -0
- package/dist/{chunk-EHNEGKFR.js → chunk-QMYERCBB.js} +1 -1
- package/dist/{cli-4VAOH7ST.js → cli-4ESXOLKV.js} +4 -4
- package/dist/{cli-V55MTIOP.js → cli-DBZMVYYV.js} +20 -14
- package/dist/cli-T5RD2BTK.js +17 -0
- package/dist/{cli-3XPEGBUR.js → cli-U3SZVHSK.js} +7 -6
- package/dist/cli-VREYWCT4.js +16 -0
- package/dist/cli.js +2 -2
- package/dist/index.cjs +400 -10
- package/dist/index.d.cts +227 -2
- package/dist/index.d.ts +227 -2
- package/dist/index.js +383 -10
- package/package.json +13 -6
- package/word-lists.json +1 -1
- package/dist/chunk-DAENYJUO.js +0 -112
- package/dist/chunk-YLQE35PN.js +0 -364
- package/dist/cli-J7C6TI2B.js +0 -16
- package/dist/cli-WDSVVHKG.js +0 -15
|
@@ -866,10 +866,14 @@ var COPY_TELLS = [
|
|
|
866
866
|
surface: "copy",
|
|
867
867
|
why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
|
|
868
868
|
fix: "Write one sentence that says which of the three is true, and how you know.",
|
|
869
|
-
|
|
869
|
+
// The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
|
|
870
|
+
detect: phrases([/(?:\b[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?:[ \t]+|[ \t]*\n(?![ \t]*\n)[ \t]*)){2}[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?=\s|$)/], quoted),
|
|
870
871
|
fixtures: {
|
|
871
|
-
flag: [f("content/home.md", "No fuss. No jargon. Just results.")],
|
|
872
|
-
pass: [
|
|
872
|
+
flag: [f("content/home.md", "No fuss. No jargon. Just results."), f("content/home.md", "No fuss.\nNo jargon.\nJust results.")],
|
|
873
|
+
pass: [
|
|
874
|
+
f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
|
|
875
|
+
f("content/guide.md", "Your competitor appears in the top three. You don't. Again.\n\nIt's frustrating.")
|
|
876
|
+
]
|
|
873
877
|
}
|
|
874
878
|
},
|
|
875
879
|
{
|
|
@@ -888,6 +892,14 @@ var COPY_TELLS = [
|
|
|
888
892
|
}
|
|
889
893
|
];
|
|
890
894
|
var ING_TAIL = /,\s+(?:ensuring|highlighting|underscoring|showcasing|reflecting|demonstrating|emphasi[sz]ing|fostering|cementing|solidifying|reinforcing|symboli[sz]ing|signalling|signaling)\b[^.!?\n]{0,80}/i;
|
|
895
|
+
var PROMPT_CONTEXT = [
|
|
896
|
+
/\b(?:the|this|that)\s+(?:attached|uploaded|provided)\s+(?:(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\s+)?(?:research|research file|research pack|source file|sources)\b/i,
|
|
897
|
+
/\b(?:attached|uploaded)\s+(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\b/i,
|
|
898
|
+
/\bthe\s+(?:attached|uploaded|provided)\s+brief\s+(?:asks|says|wants|requests|suggests|mentions)\b/i
|
|
899
|
+
];
|
|
900
|
+
function gerundList(block, _match, index) {
|
|
901
|
+
return /(?:^|[(,;:]\s*)\w+ing\b[^,.;()!?]{0,40}$/.test(block.text.slice(Math.max(0, index - 60), index));
|
|
902
|
+
}
|
|
891
903
|
var VAGUE_ATTRIBUTION = [
|
|
892
904
|
/\b(?:studies|research|experts|scientists|surveys|statistics|the data)\s+(?:show|shows|suggest|suggests|agree|say|says|confirm|confirms|prove|proves|indicate|indicates)\b/i,
|
|
893
905
|
/\b(?:many|most)\s+(?:experts|people|customers|professionals|homeowners)\s+(?:agree|say|believe)\b/i,
|
|
@@ -973,8 +985,10 @@ function inlineLabelHits(ctx) {
|
|
|
973
985
|
run = [];
|
|
974
986
|
};
|
|
975
987
|
for (const line of lines) {
|
|
976
|
-
const m = /^\s*
|
|
977
|
-
|
|
988
|
+
const m = /^\s*[-*+]\s+\*\*([^*]{1,40}?):?\*\*:?\s+(\S.*)$/.exec(line);
|
|
989
|
+
const data = m && /[\d£$€%`]|https?:|^\*\*/.test(m[2]);
|
|
990
|
+
const named = m && /\S[A-Z]|[./]/.test(m[1]);
|
|
991
|
+
if (m && !data && !named && m[2].trim().split(/\s+/).length <= 6) run.push(offset);
|
|
978
992
|
else flush();
|
|
979
993
|
offset += line.length + 1;
|
|
980
994
|
}
|
|
@@ -1012,6 +1026,29 @@ var RESEARCH_TELLS = [
|
|
|
1012
1026
|
]
|
|
1013
1027
|
}
|
|
1014
1028
|
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "prompt-context",
|
|
1031
|
+
name: "Prompt context",
|
|
1032
|
+
generation: 1,
|
|
1033
|
+
severity: "warn",
|
|
1034
|
+
surface: "copy",
|
|
1035
|
+
why: "'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell.",
|
|
1036
|
+
fix: "Delete the reference to the research or brief. Where it carried a claim, name the real source or cut the claim.",
|
|
1037
|
+
detect: phrases(PROMPT_CONTEXT, quoted),
|
|
1038
|
+
fixtures: {
|
|
1039
|
+
flag: [
|
|
1040
|
+
f("content/guide.md", "This article uses the attached Perplexity research file as the primary source."),
|
|
1041
|
+
f("content/guide.md", "The attached research is right to say these platforms suit early-stage businesses."),
|
|
1042
|
+
f("content/guide.md", "That is exactly how the uploaded research frames the category."),
|
|
1043
|
+
f("content/guide.md", "The attached brief asks for the point that most tradespeople miss.")
|
|
1044
|
+
],
|
|
1045
|
+
pass: [
|
|
1046
|
+
f("content/guide.md", "We research every quote before we send it."),
|
|
1047
|
+
f("app/email.tsx", `export const Invoice = () => <p>Your invoice is attached.</p>;`),
|
|
1048
|
+
f("content/guide.md", "BrightLocal's research shows that reviews drive 20% of local ranking.")
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1015
1052
|
{
|
|
1016
1053
|
id: "placeholder",
|
|
1017
1054
|
name: "Unfilled placeholder",
|
|
@@ -1064,7 +1101,11 @@ var RESEARCH_TELLS = [
|
|
|
1064
1101
|
pass: [
|
|
1065
1102
|
f("content/home.md", "- Pages load in under a second\n- Card details never touch our server\n- Support by phone until 8pm\n"),
|
|
1066
1103
|
f("content/home.md", "- **Speed:** Faster pages\n- **Security:** Safer data\n"),
|
|
1067
|
-
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n")
|
|
1104
|
+
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n"),
|
|
1105
|
+
f("content/guide.md", "- **Per page**: \xA3100-\xA3200 per page\n- **Hourly**: \xA350-\xA375/hour\n- **Day rate**: \xA3350-\xA3500\n"),
|
|
1106
|
+
f("content/guide.md", "- **Law Society Find a Solicitor:** For solicitors\n- **AccountingWEB / ICAEW directories:** For accountants\n- **NHS.uk:** For NHS services\n"),
|
|
1107
|
+
f("content/guide.md", "1. **Visit PageSpeed Insights** and paste the address\n2. **Enter their URL** and run it\n3. **Note their scores** for later\n"),
|
|
1108
|
+
f("content/guide.md", "- **Structural problems:** **REBUILD.**\n- **Messaging issues:** **REFRESH.**\n- **Neither:** **LEAVE IT.**\n")
|
|
1068
1109
|
]
|
|
1069
1110
|
}
|
|
1070
1111
|
},
|
|
@@ -1076,7 +1117,7 @@ var RESEARCH_TELLS = [
|
|
|
1076
1117
|
surface: "copy",
|
|
1077
1118
|
why: "'..., ensuring peace of mind', '..., highlighting our commitment': a participle tacked on the end that claims significance and names no mechanism. One of the most common shapes in the Wikipedia guide to AI writing.",
|
|
1078
1119
|
fix: "Cut the tail, or replace it with the mechanism: 'so you can drop the car off before work'.",
|
|
1079
|
-
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, ""))),
|
|
1120
|
+
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, "")), void 0, gerundList),
|
|
1080
1121
|
fixtures: {
|
|
1081
1122
|
flag: [
|
|
1082
1123
|
f("content/home.md", "Every engineer is Gas Safe registered, ensuring complete peace of mind."),
|
|
@@ -1084,7 +1125,8 @@ var RESEARCH_TELLS = [
|
|
|
1084
1125
|
],
|
|
1085
1126
|
pass: [
|
|
1086
1127
|
f("content/home.md", "We open at 7am, so you can drop the car off before work."),
|
|
1087
|
-
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers.")
|
|
1128
|
+
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers."),
|
|
1129
|
+
f("content/guide.md", "Many items (claiming your GBP, ensuring NAP consistency, writing title tags) are straightforward.")
|
|
1088
1130
|
]
|
|
1089
1131
|
}
|
|
1090
1132
|
},
|
|
@@ -1334,6 +1376,7 @@ function repeatHits(ctx) {
|
|
|
1334
1376
|
for (const s of sentences(doc2)) {
|
|
1335
1377
|
const words = normal(s.text);
|
|
1336
1378
|
if (words.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
1379
|
+
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
1337
1380
|
const key = words.join(" ");
|
|
1338
1381
|
const earlier = whole.get(key);
|
|
1339
1382
|
if (earlier) {
|
|
@@ -1531,7 +1574,15 @@ var DENSITY_TELLS = [
|
|
|
1531
1574
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nMore here.\n\nWe check every valve before the engineer leaves the house.")),
|
|
1532
1575
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nBy then we check every valve before the engineer leaves the site."))
|
|
1533
1576
|
],
|
|
1534
|
-
pass: [
|
|
1577
|
+
pass: [
|
|
1578
|
+
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nThe engineer leaves once the customer has signed the sheet.")),
|
|
1579
|
+
f2(
|
|
1580
|
+
"content/guide.md",
|
|
1581
|
+
doc(
|
|
1582
|
+
"The Independent Healthcare Providers Network Private Patient Insight Report 2024 found that private demand rose.[^1]\n\n[^1]: Independent Healthcare Providers Network, Private Patient Insight Report 2024, published online."
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
]
|
|
1535
1586
|
}
|
|
1536
1587
|
},
|
|
1537
1588
|
{
|
|
@@ -2644,6 +2695,9 @@ var HOUSE = {
|
|
|
2644
2695
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2645
2696
|
"false-range": { tier: "review", label: "false range" },
|
|
2646
2697
|
"placeholder": { tier: "review", label: "unfilled placeholder" },
|
|
2698
|
+
// Every estate hit so far is real (38 in five DD articles, 2026-09-24), but
|
|
2699
|
+
// a proposal can honestly say "the attached research". Review until measured there.
|
|
2700
|
+
"prompt-context": { tier: "review", label: "prompt context: the model naming its own inputs" },
|
|
2647
2701
|
"question-reveal": { tier: "review", label: "staged reveal" },
|
|
2648
2702
|
"inline-label-list": { tier: "review", label: "bold-label bullets" },
|
|
2649
2703
|
"phrase-density": { tier: "review", label: "density" },
|
|
@@ -2992,7 +3046,7 @@ var RENDERED_PATHS = {
|
|
|
2992
3046
|
};
|
|
2993
3047
|
|
|
2994
3048
|
// src/character/check.ts
|
|
2995
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
3049
|
+
var CATALOGUE_VERSION = "2026.09.8";
|
|
2996
3050
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2997
3051
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2998
3052
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
parseFlags
|
|
3
|
-
|
|
2
|
+
parseFlags,
|
|
3
|
+
toJson
|
|
4
|
+
} from "./chunk-2AJB27BJ.js";
|
|
4
5
|
import {
|
|
5
6
|
fingerprint,
|
|
6
7
|
fingerprintDistance
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QMYERCBB.js";
|
|
8
9
|
import {
|
|
9
10
|
CATALOGUE_VERSION,
|
|
10
11
|
REFLEX_FONTS_1,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
isCream,
|
|
19
20
|
oklchToRgb,
|
|
20
21
|
scanSource
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-3NGWXZZE.js";
|
|
22
23
|
|
|
23
24
|
// src/null/cli.ts
|
|
24
25
|
import { spawn } from "child_process";
|
|
@@ -352,7 +353,7 @@ async function build(flags, io) {
|
|
|
352
353
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
353
354
|
if (toShoot.length) {
|
|
354
355
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
355
|
-
const { snapshotUrls } = await import("./audit-
|
|
356
|
+
const { snapshotUrls } = await import("./audit-QIPXL7E4.js");
|
|
356
357
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
357
358
|
results.forEach((r, i) => {
|
|
358
359
|
const id = toShoot[i];
|
|
@@ -439,7 +440,7 @@ function runHarvest(args, io) {
|
|
|
439
440
|
const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
|
|
440
441
|
if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
|
|
441
442
|
const candidates = harvest(models, { minShare, copyTell });
|
|
442
|
-
io.out(flags.json ?
|
|
443
|
+
io.out(flags.json ? toJson({ briefs: models.map((m) => m.brief), candidates }) : formatHarvest(candidates, models));
|
|
443
444
|
return 0;
|
|
444
445
|
}
|
|
445
446
|
|
|
@@ -3,18 +3,19 @@ import {
|
|
|
3
3
|
hueName,
|
|
4
4
|
loadNull,
|
|
5
5
|
shapeName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-3OZQ4C5R.js";
|
|
7
7
|
import {
|
|
8
|
-
parseFlags
|
|
9
|
-
|
|
8
|
+
parseFlags,
|
|
9
|
+
toJson
|
|
10
|
+
} from "./chunk-2AJB27BJ.js";
|
|
10
11
|
import {
|
|
11
12
|
fingerprint,
|
|
12
13
|
fingerprintDistance
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QMYERCBB.js";
|
|
14
15
|
import {
|
|
15
16
|
SNAPSHOT_VERSION,
|
|
16
17
|
auditSnapshot
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-3NGWXZZE.js";
|
|
18
19
|
|
|
19
20
|
// src/estate/cli.ts
|
|
20
21
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -92,7 +93,7 @@ async function snapshotOf(target, cwd) {
|
|
|
92
93
|
return snap;
|
|
93
94
|
}
|
|
94
95
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
95
|
-
const { snapshotUrl } = await import("./audit-
|
|
96
|
+
const { snapshotUrl } = await import("./audit-QIPXL7E4.js");
|
|
96
97
|
return snapshotUrl(target);
|
|
97
98
|
}
|
|
98
99
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -137,13 +138,13 @@ async function runEstate(args, io) {
|
|
|
137
138
|
if (sub === "compare") {
|
|
138
139
|
if (!target) {
|
|
139
140
|
const pairs = estatePairs(register, at);
|
|
140
|
-
io.out(flags.json ?
|
|
141
|
+
io.out(flags.json ? toJson({ siblingAt: at, pairs }) : formatPairs(pairs, at, register.sites.length));
|
|
141
142
|
return flags.strict && pairs.some((p) => p.sibling) ? 1 : 0;
|
|
142
143
|
}
|
|
143
144
|
const known = register.sites.find((s) => s.id === target);
|
|
144
145
|
const fp = known ? known.fingerprint : fingerprint(await snapshotOf(target, io.cwd));
|
|
145
146
|
const matches = compareToEstate(fp, register, { exclude: known?.id, siblingAt: at });
|
|
146
|
-
io.out(flags.json ?
|
|
147
|
+
io.out(flags.json ? toJson({ siblingAt: at, matches }) : formatMatches(known?.id ?? target, matches, at));
|
|
147
148
|
return flags.strict && matches.some((m) => m.sibling) ? 1 : 0;
|
|
148
149
|
}
|
|
149
150
|
throw new Error(USAGE);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fingerprint
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QMYERCBB.js";
|
|
4
4
|
import {
|
|
5
5
|
SNAPSHOT_VERSION,
|
|
6
6
|
auditSnapshot
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3NGWXZZE.js";
|
|
8
8
|
|
|
9
9
|
// src/audit/collect.ts
|
|
10
10
|
function collectInPage() {
|
|
@@ -390,7 +390,9 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
390
390
|
for (const url of urls) {
|
|
391
391
|
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
392
392
|
try {
|
|
393
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
393
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
394
|
+
const status = typeof response?.status === "function" ? response.status() : 200;
|
|
395
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
394
396
|
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
395
397
|
} catch (error) {
|
|
396
398
|
out.push({ url, error: error.message });
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
snapshotUrl,
|
|
3
|
+
snapshotUrls
|
|
4
|
+
} from "./chunk-JP75P2DU.js";
|
|
5
|
+
import {
|
|
6
|
+
describeTypicality,
|
|
7
|
+
loadNull,
|
|
8
|
+
typicality
|
|
9
|
+
} from "./chunk-3OZQ4C5R.js";
|
|
10
|
+
import {
|
|
11
|
+
SOURCE_FILE,
|
|
12
|
+
finish,
|
|
13
|
+
loadConfig,
|
|
14
|
+
loadExceptions,
|
|
15
|
+
parseFlags,
|
|
16
|
+
prepareReport,
|
|
17
|
+
readPaths,
|
|
18
|
+
toJson
|
|
19
|
+
} from "./chunk-2AJB27BJ.js";
|
|
20
|
+
import {
|
|
21
|
+
fingerprint
|
|
22
|
+
} from "./chunk-QMYERCBB.js";
|
|
23
|
+
import {
|
|
24
|
+
SNAPSHOT_VERSION,
|
|
25
|
+
auditSnapshot,
|
|
26
|
+
scanSource
|
|
27
|
+
} from "./chunk-3NGWXZZE.js";
|
|
28
|
+
|
|
29
|
+
// src/audit/cli.ts
|
|
30
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
31
|
+
import { resolve } from "path";
|
|
32
|
+
|
|
33
|
+
// src/audit/pages.ts
|
|
34
|
+
var decode = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
35
|
+
function parseSitemap(xml) {
|
|
36
|
+
const locs = [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)].map((m) => decode(m[1]));
|
|
37
|
+
return /<sitemapindex[\s>]/i.test(xml) ? { urls: [], sitemaps: locs } : { urls: locs, sitemaps: [] };
|
|
38
|
+
}
|
|
39
|
+
function parseUrlList(text) {
|
|
40
|
+
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
41
|
+
}
|
|
42
|
+
function parseViewports(value) {
|
|
43
|
+
const widths = value.split(",").map((w) => w.trim());
|
|
44
|
+
return widths.map((w) => {
|
|
45
|
+
const width = Number(w);
|
|
46
|
+
if (!Number.isInteger(width) || width < 240 || width > 3840) throw new Error(`--viewport: "${w}" is not a width in px between 240 and 3840`);
|
|
47
|
+
return { width, height: width < 600 ? 844 : width < 1024 ? 1024 : 900 };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/audit/cli.ts
|
|
52
|
+
var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
|
|
53
|
+
function describeFingerprint(fp) {
|
|
54
|
+
return [
|
|
55
|
+
"Fingerprint",
|
|
56
|
+
` accent ${fmt(fp.accent)}`,
|
|
57
|
+
` ground ${fmt(fp.ground)}`,
|
|
58
|
+
` display ${fp.display.family} (${fp.display.class})`,
|
|
59
|
+
` body ${fp.body.family} (${fp.body.class})`,
|
|
60
|
+
` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
|
|
61
|
+
` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
|
|
62
|
+
` effects ${fp.effects.join(", ") || "none"}`,
|
|
63
|
+
` layout ${fp.layout.join(" > ") || "none"}`
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function merge(a, b) {
|
|
67
|
+
const byTell = { ...a.summary.byTell };
|
|
68
|
+
for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
|
|
69
|
+
return {
|
|
70
|
+
catalogueVersion: a.catalogueVersion,
|
|
71
|
+
findings: [...a.findings, ...b.findings],
|
|
72
|
+
rejectedExceptions: a.rejectedExceptions,
|
|
73
|
+
excepted: [...a.excepted, ...b.excepted],
|
|
74
|
+
suppressed: [...a.suppressed, ...b.suppressed],
|
|
75
|
+
summary: {
|
|
76
|
+
files: a.summary.files + b.summary.files,
|
|
77
|
+
findings: a.summary.findings + b.summary.findings,
|
|
78
|
+
byGeneration: { 1: a.summary.byGeneration[1] + b.summary.byGeneration[1], 2: a.summary.byGeneration[2] + b.summary.byGeneration[2], 3: a.summary.byGeneration[3] + b.summary.byGeneration[3] },
|
|
79
|
+
byTell,
|
|
80
|
+
blocking: a.summary.blocking + b.summary.blocking
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readSnapshot(path) {
|
|
85
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
86
|
+
if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
async function readText(source, cwd) {
|
|
90
|
+
if (/^https?:\/\//i.test(source)) {
|
|
91
|
+
const res = await fetch(source);
|
|
92
|
+
if (!res.ok) throw new Error(`${source}: HTTP ${res.status}`);
|
|
93
|
+
return res.text();
|
|
94
|
+
}
|
|
95
|
+
const path = resolve(cwd, source);
|
|
96
|
+
if (!existsSync(path)) throw new Error(`no such file: ${source}`);
|
|
97
|
+
return readFileSync(path, "utf8");
|
|
98
|
+
}
|
|
99
|
+
async function resolvePages(source, cwd) {
|
|
100
|
+
const text = await readText(source, cwd);
|
|
101
|
+
if (!/^\s*<(?:\?xml|urlset|sitemapindex)/i.test(text)) return parseUrlList(text);
|
|
102
|
+
const { urls, sitemaps } = parseSitemap(text);
|
|
103
|
+
const nested = await Promise.all(sitemaps.map(async (s) => parseSitemap(await readText(s, cwd)).urls));
|
|
104
|
+
const all = [...urls, ...nested.flat()];
|
|
105
|
+
if (all.length === 0) throw new Error(`${source}: no <loc> entries`);
|
|
106
|
+
return [...new Set(all)];
|
|
107
|
+
}
|
|
108
|
+
function viewportsOf(flags) {
|
|
109
|
+
if (flags.viewport) return parseViewports(flags.viewport);
|
|
110
|
+
if (flags.width || flags.height) return [{ width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) }];
|
|
111
|
+
return [void 0];
|
|
112
|
+
}
|
|
113
|
+
async function runAudit(command, args, io) {
|
|
114
|
+
try {
|
|
115
|
+
const flags = parseFlags(args);
|
|
116
|
+
if (typeof flags === "string") throw new Error(flags);
|
|
117
|
+
const target = flags.positional[0];
|
|
118
|
+
if (command === "snapshot" && (flags.pages || flags.viewport)) throw new Error("craft snapshot takes one page; use --width and --height");
|
|
119
|
+
if (flags.pages && target) throw new Error("give a page or --pages, not both");
|
|
120
|
+
if (!target && !flags.pages) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json> | --pages <sitemap.xml | urls.txt>" : ">"}`);
|
|
121
|
+
const viewports = viewportsOf(flags);
|
|
122
|
+
const audited = [];
|
|
123
|
+
const failed = [];
|
|
124
|
+
const local = target ? resolve(io.cwd, target) : "";
|
|
125
|
+
if (target && command === "audit" && /\.json$/i.test(target) && existsSync(local)) {
|
|
126
|
+
audited.push({ url: target, viewport: null, snapshot: readSnapshot(local) });
|
|
127
|
+
} else if (target && viewports.length === 1) {
|
|
128
|
+
const snapshot = await snapshotUrl(target, { viewport: viewports[0] });
|
|
129
|
+
audited.push({ url: target, viewport: viewports[0] ?? null, snapshot });
|
|
130
|
+
} else {
|
|
131
|
+
const urls = flags.pages ? await resolvePages(flags.pages, io.cwd) : [target];
|
|
132
|
+
for (const viewport of viewports) {
|
|
133
|
+
for (const r of await snapshotUrls(urls, { viewport })) {
|
|
134
|
+
if (r.snapshot) audited.push({ url: r.url, viewport: viewport ?? null, snapshot: r.snapshot });
|
|
135
|
+
else failed.push({ url: r.url, viewport: viewport ?? null, error: r.error ?? "no snapshot" });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (audited.length === 0) throw new Error(`no page could be measured:
|
|
140
|
+
${failed.map((f) => ` ${f.url}: ${f.error}`).join("\n")}`);
|
|
141
|
+
const single = audited.length === 1 && failed.length === 0;
|
|
142
|
+
if (flags.out) {
|
|
143
|
+
if (!single) throw new Error("--out saves one snapshot; drop --pages and --viewport, or snapshot each page");
|
|
144
|
+
writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(audited[0].snapshot, null, 2)}
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
if (command === "snapshot") {
|
|
148
|
+
const { snapshot } = audited[0];
|
|
149
|
+
if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
|
|
150
|
+
else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
const root = flags.repo ? resolve(io.cwd, flags.repo) : io.cwd;
|
|
154
|
+
const exceptions = loadExceptions(flags, root);
|
|
155
|
+
const config = loadConfig(flags, root);
|
|
156
|
+
const label = (a) => viewports.length > 1 && a.viewport ? `${a.snapshot.url} @ ${a.viewport.width}px` : a.snapshot.url;
|
|
157
|
+
let report = audited.map((a) => {
|
|
158
|
+
const r = auditSnapshot(a.snapshot, { exceptions });
|
|
159
|
+
return { ...r, findings: r.findings.map((f) => ({ ...f, path: label(a) })) };
|
|
160
|
+
}).reduce((acc, r) => merge(acc, r));
|
|
161
|
+
if (flags.repo) {
|
|
162
|
+
report = merge(scanSource(readPaths(["."], root, SOURCE_FILE, void 0, config.ignore), { exceptions }), report);
|
|
163
|
+
}
|
|
164
|
+
const prepared = prepareReport(report, flags, io, config);
|
|
165
|
+
if (!prepared) return 0;
|
|
166
|
+
const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
|
|
167
|
+
const pooled = nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r));
|
|
168
|
+
const pages = audited.map((a) => {
|
|
169
|
+
const fp = fingerprint(a.snapshot);
|
|
170
|
+
return { url: a.snapshot.url, viewport: a.viewport, fingerprint: fp, typicality: nulls.length ? typicality(fp, pooled) : null };
|
|
171
|
+
});
|
|
172
|
+
const unmeasured = failed.length > 0 && flags.strict ? 1 : 0;
|
|
173
|
+
if (flags.json) {
|
|
174
|
+
const extra = single ? { fingerprint: pages[0].fingerprint, typicality: pages[0].typicality, snapshot: flags.out ?? null } : {};
|
|
175
|
+
io.out(toJson({ report: prepared, ...extra, pages, failed }));
|
|
176
|
+
return Math.max(prepared.summary.blocking > 0 || flags.strict && prepared.summary.findings > 0 ? 1 : 0, unmeasured);
|
|
177
|
+
}
|
|
178
|
+
const title = single ? `craft audit ${audited[0].snapshot.url}` : `craft audit: ${audited.length} page${audited.length === 1 ? "" : "s"}`;
|
|
179
|
+
const code = finish(prepared, { ...flags, json: false }, title, io);
|
|
180
|
+
for (const page of pages) {
|
|
181
|
+
io.out("");
|
|
182
|
+
if (!single) io.out(`${page.url}${page.viewport ? ` @ ${page.viewport.width}px` : ""}`);
|
|
183
|
+
io.out(describeFingerprint(page.fingerprint));
|
|
184
|
+
if (page.typicality) {
|
|
185
|
+
io.out("");
|
|
186
|
+
io.out(describeTypicality(page.typicality, pooled.length, nulls.length));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (failed.length) {
|
|
190
|
+
io.out("");
|
|
191
|
+
io.out("Not measured (never counts as a pass):");
|
|
192
|
+
for (const f of failed) io.out(` ${f.url}${f.viewport ? ` @ ${f.viewport.width}px` : ""}: ${f.error}`);
|
|
193
|
+
}
|
|
194
|
+
return Math.max(code, unmeasured);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
io.err(`craft: ${error.message}`);
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export {
|
|
202
|
+
describeFingerprint,
|
|
203
|
+
merge,
|
|
204
|
+
resolvePages,
|
|
205
|
+
runAudit
|
|
206
|
+
};
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
nullPath,
|
|
7
7
|
runHarvest,
|
|
8
8
|
runNull
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-3OZQ4C5R.js";
|
|
10
|
+
import "./chunk-2AJB27BJ.js";
|
|
11
|
+
import "./chunk-QMYERCBB.js";
|
|
12
|
+
import "./chunk-3NGWXZZE.js";
|
|
13
13
|
export {
|
|
14
14
|
describeTypicality,
|
|
15
15
|
formatHarvest,
|
|
@@ -1,37 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
2
|
validateDirection
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JWHC67ZS.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-MSQ57PJU.js";
|
|
7
|
+
import "./chunk-JP75P2DU.js";
|
|
8
8
|
import {
|
|
9
9
|
SIBLING_AT,
|
|
10
10
|
compareToEstate,
|
|
11
11
|
loadEstate
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HOHHQB4N.js";
|
|
13
13
|
import {
|
|
14
14
|
loadNull,
|
|
15
15
|
typicality
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-3OZQ4C5R.js";
|
|
17
17
|
import {
|
|
18
18
|
COPY_FILE,
|
|
19
19
|
NOT_COPY_DIR,
|
|
20
20
|
SOURCE_FILE,
|
|
21
|
+
applySeverity,
|
|
22
|
+
loadConfig,
|
|
21
23
|
loadExceptions,
|
|
22
24
|
parseFlags,
|
|
23
|
-
readPaths
|
|
24
|
-
|
|
25
|
+
readPaths,
|
|
26
|
+
toJson
|
|
27
|
+
} from "./chunk-2AJB27BJ.js";
|
|
25
28
|
import {
|
|
26
29
|
fingerprint
|
|
27
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-QMYERCBB.js";
|
|
28
31
|
import {
|
|
29
32
|
CATALOGUE,
|
|
30
33
|
SNAPSHOT_VERSION,
|
|
31
34
|
auditSnapshot,
|
|
32
35
|
checkCopy,
|
|
33
36
|
scanSource
|
|
34
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-3NGWXZZE.js";
|
|
35
38
|
|
|
36
39
|
// src/report/cli.ts
|
|
37
40
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -259,17 +262,20 @@ async function runReport(command, args, io) {
|
|
|
259
262
|
snap = JSON.parse(readFileSync(local, "utf8"));
|
|
260
263
|
if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target2} is not a snapshot`);
|
|
261
264
|
} else {
|
|
262
|
-
const { snapshotUrl } = await import("./audit-
|
|
265
|
+
const { snapshotUrl } = await import("./audit-QIPXL7E4.js");
|
|
263
266
|
snap = await snapshotUrl(target2);
|
|
264
267
|
}
|
|
265
268
|
const fp = fingerprint(snap);
|
|
266
269
|
const repo = flags.repo ? resolve(io.cwd, flags.repo) : null;
|
|
267
270
|
const exceptions = loadExceptions(flags, repo ?? io.cwd);
|
|
271
|
+
const config = loadConfig(flags, repo ?? io.cwd);
|
|
268
272
|
let findings = auditSnapshot(snap, { exceptions });
|
|
269
273
|
if (repo) {
|
|
270
|
-
findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), findings);
|
|
271
|
-
|
|
274
|
+
findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE, void 0, config.ignore), { exceptions }), findings);
|
|
275
|
+
const copyPaths = config.copyPaths ?? ["."];
|
|
276
|
+
findings = merge(findings, checkCopy(readPaths(copyPaths, repo, COPY_FILE, (d) => NOT_COPY_DIR(d) || d === "docs", config.ignore), { exceptions }));
|
|
272
277
|
}
|
|
278
|
+
findings = applySeverity(findings, config);
|
|
273
279
|
const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
|
|
274
280
|
const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
|
|
275
281
|
let estate = null;
|
|
@@ -297,8 +303,8 @@ async function runReport(command, args, io) {
|
|
|
297
303
|
} else io.out(plan);
|
|
298
304
|
return 0;
|
|
299
305
|
}
|
|
300
|
-
io.out(flags.json ?
|
|
301
|
-
if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${
|
|
306
|
+
io.out(flags.json ? toJson(report) : formatCharacter(report));
|
|
307
|
+
if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${toJson(report)}
|
|
302
308
|
`);
|
|
303
309
|
return flags.strict && report.verdict !== "decided" ? 1 : 0;
|
|
304
310
|
} catch (error) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describeFingerprint,
|
|
3
|
+
merge,
|
|
4
|
+
resolvePages,
|
|
5
|
+
runAudit
|
|
6
|
+
} from "./chunk-MSQ57PJU.js";
|
|
7
|
+
import "./chunk-JP75P2DU.js";
|
|
8
|
+
import "./chunk-3OZQ4C5R.js";
|
|
9
|
+
import "./chunk-2AJB27BJ.js";
|
|
10
|
+
import "./chunk-QMYERCBB.js";
|
|
11
|
+
import "./chunk-3NGWXZZE.js";
|
|
12
|
+
export {
|
|
13
|
+
describeFingerprint,
|
|
14
|
+
merge,
|
|
15
|
+
resolvePages,
|
|
16
|
+
runAudit
|
|
17
|
+
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DIRECTION_VERSION,
|
|
3
3
|
validateDirection
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JWHC67ZS.js";
|
|
5
5
|
import {
|
|
6
|
-
parseFlags
|
|
7
|
-
|
|
6
|
+
parseFlags,
|
|
7
|
+
toJson
|
|
8
|
+
} from "./chunk-2AJB27BJ.js";
|
|
8
9
|
import {
|
|
9
10
|
fingerprint
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QMYERCBB.js";
|
|
11
12
|
import {
|
|
12
13
|
deltaEOk,
|
|
13
14
|
formatHex,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
isCream,
|
|
17
18
|
oklchToRgb,
|
|
18
19
|
parseColour
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-3NGWXZZE.js";
|
|
20
21
|
|
|
21
22
|
// src/direction/cli.ts
|
|
22
23
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -287,7 +288,7 @@ function runDirection(args, io) {
|
|
|
287
288
|
fingerprint: current,
|
|
288
289
|
pathExists: (p) => existsSync(resolve(dirname(file), p))
|
|
289
290
|
});
|
|
290
|
-
if (flags.json) io.out(
|
|
291
|
+
if (flags.json) io.out(toJson(report));
|
|
291
292
|
else {
|
|
292
293
|
for (const p of report.problems) io.out(` ${p.severity === "error" ? "error" : "warn "} ${p.at || "(file)"} ${p.message}`);
|
|
293
294
|
io.out(`craft direction: ${report.valid ? "valid" : "not valid"}, ${report.decided} of 7 choices decided with a reason.`);
|