@domandigital/craft 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/CHARACTER.md +100 -1
- package/COPY.md +6 -2
- package/README.md +5 -0
- package/dist/{audit-QIPXL7E4.js → audit-5TYDYRXV.js} +9 -3
- package/dist/audit.cjs +881 -95
- package/dist/audit.d.cts +161 -4
- package/dist/audit.d.ts +161 -4
- package/dist/audit.js +878 -95
- package/dist/{chunk-2AJB27BJ.js → chunk-3VTPI7BU.js} +26 -11
- package/dist/chunk-CZDOMIVS.js +19 -0
- package/dist/chunk-ESCVTKPN.js +217 -0
- package/dist/{chunk-3NGWXZZE.js → chunk-GMVXADJ6.js} +2929 -2082
- package/dist/{chunk-MSQ57PJU.js → chunk-IMB4V34S.js} +31 -18
- package/dist/chunk-JYNXGCYZ.js +75 -0
- package/dist/{chunk-HOHHQB4N.js → chunk-K2BZN4GA.js} +52 -16
- package/dist/{chunk-JP75P2DU.js → chunk-ZLDA2SYB.js} +342 -16
- package/dist/{chunk-JWHC67ZS.js → chunk-ZZ5CKOJP.js} +121 -1
- package/dist/cli-BKJMLD3H.js +18 -0
- package/dist/{cli-U3SZVHSK.js → cli-DRKAMFAH.js} +12 -80
- package/dist/{cli-4ESXOLKV.js → cli-ILH7OO5G.js} +4 -4
- package/dist/{cli-DBZMVYYV.js → cli-J5ECRCD4.js} +17 -17
- package/dist/cli-N2IDXH7W.js +20 -0
- package/dist/cli.js +2 -2
- package/dist/index.cjs +1291 -502
- package/dist/index.d.cts +586 -200
- package/dist/index.d.ts +586 -200
- package/dist/index.js +1263 -502
- package/licences.json +104 -0
- package/package.json +3 -2
- package/word-lists.json +1 -1
- package/dist/chunk-3OZQ4C5R.js +0 -459
- package/dist/chunk-QMYERCBB.js +0 -105
- package/dist/cli-T5RD2BTK.js +0 -17
- package/dist/cli-VREYWCT4.js +0 -16
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AI_PHRASES: () => AI_PHRASES,
|
|
24
|
+
AI_SOURCE_TYPES: () => AI_SOURCE_TYPES,
|
|
24
25
|
AI_VIOLET: () => AI_VIOLET,
|
|
25
26
|
AI_WORDS: () => AI_WORDS,
|
|
26
27
|
BASELINE_VERSION: () => BASELINE_VERSION,
|
|
@@ -28,6 +29,7 @@ __export(index_exports, {
|
|
|
28
29
|
CATALOGUE: () => CATALOGUE,
|
|
29
30
|
CATALOGUE_VERSION: () => CATALOGUE_VERSION,
|
|
30
31
|
CHOICE_KEYS: () => CHOICE_KEYS,
|
|
32
|
+
COMPONENT_ROLES: () => COMPONENT_ROLES,
|
|
31
33
|
DEFAULT_IGNORE: () => DEFAULT_IGNORE,
|
|
32
34
|
DENSITIES: () => DENSITIES,
|
|
33
35
|
DENSITY_NAMES: () => DENSITY_NAMES,
|
|
@@ -37,13 +39,17 @@ __export(index_exports, {
|
|
|
37
39
|
DURATION_S: () => DURATION_S,
|
|
38
40
|
EASE: () => EASE,
|
|
39
41
|
EASE_TUPLE: () => EASE_TUPLE,
|
|
42
|
+
EDGE_STEP: () => EDGE_STEP,
|
|
40
43
|
ESTATE_VERSION: () => ESTATE_VERSION,
|
|
41
44
|
EXIT_RATIO: () => EXIT_RATIO,
|
|
42
45
|
FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
|
|
43
46
|
HOUSE: () => HOUSE,
|
|
44
47
|
HOUSE_BUDGET: () => HOUSE_BUDGET,
|
|
48
|
+
HOUSE_LOCKED: () => HOUSE_LOCKED,
|
|
45
49
|
HOUSE_TYPE: () => HOUSE_TYPE,
|
|
50
|
+
IMAGERY_TELLS: () => IMAGERY_TELLS,
|
|
46
51
|
JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
|
|
52
|
+
LICENCES: () => LICENCES,
|
|
47
53
|
LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
|
|
48
54
|
MIN_DECIDED: () => MIN_DECIDED,
|
|
49
55
|
MIN_RUNS: () => MIN_RUNS,
|
|
@@ -51,11 +57,15 @@ __export(index_exports, {
|
|
|
51
57
|
NULL_VERSION: () => NULL_VERSION,
|
|
52
58
|
PILL_LIMIT: () => PILL_LIMIT,
|
|
53
59
|
PLAINER_WORDS: () => PLAINER_WORDS,
|
|
60
|
+
PROMO_BADGE: () => PROMO_BADGE,
|
|
54
61
|
PROPOSED: () => PROPOSED,
|
|
62
|
+
PROVENANCE_SCAN_BYTES: () => PROVENANCE_SCAN_BYTES,
|
|
55
63
|
RAMP_STEPS: () => RAMP_STEPS,
|
|
64
|
+
READABLE_SNAPSHOT_VERSIONS: () => READABLE_SNAPSHOT_VERSIONS,
|
|
56
65
|
REFLEX_FONTS_1: () => REFLEX_FONTS_1,
|
|
57
66
|
REFLEX_FONTS_2: () => REFLEX_FONTS_2,
|
|
58
67
|
RENDERED_PATHS: () => RENDERED_PATHS,
|
|
68
|
+
RENDERED_TELLS: () => RENDERED_TELLS,
|
|
59
69
|
REVEAL_LIMIT: () => REVEAL_LIMIT,
|
|
60
70
|
REVIEW_PHRASES: () => REVIEW_PHRASES,
|
|
61
71
|
SCALE: () => SCALE,
|
|
@@ -66,6 +76,8 @@ __export(index_exports, {
|
|
|
66
76
|
SOURCE_KINDS: () => SOURCE_KINDS,
|
|
67
77
|
SPACE_STEPS: () => SPACE_STEPS,
|
|
68
78
|
SPRING: () => SPRING,
|
|
79
|
+
STOCK_AVATAR_HOSTS: () => STOCK_AVATAR_HOSTS,
|
|
80
|
+
STOCK_PHOTO_HOSTS: () => STOCK_PHOTO_HOSTS,
|
|
69
81
|
STOCK_PHRASES: () => STOCK_PHRASES,
|
|
70
82
|
TELL_HEAVY: () => TELL_HEAVY,
|
|
71
83
|
TYPE_STEPS: () => TYPE_STEPS,
|
|
@@ -84,8 +96,14 @@ __export(index_exports, {
|
|
|
84
96
|
checkPair: () => checkPair,
|
|
85
97
|
checkRestraint: () => checkRestraint,
|
|
86
98
|
choicesOf: () => choicesOf,
|
|
99
|
+
closestComponents: () => closestComponents,
|
|
87
100
|
compareFacts: () => compareFacts,
|
|
88
101
|
compareToEstate: () => compareToEstate,
|
|
102
|
+
componentDistance: () => componentDistance,
|
|
103
|
+
componentShared: () => componentShared,
|
|
104
|
+
componentTypicalities: () => componentTypicalities,
|
|
105
|
+
componentTypicality: () => componentTypicality,
|
|
106
|
+
componentsOf: () => componentsOf,
|
|
89
107
|
craftTokens: () => craftTokens,
|
|
90
108
|
createBaseline: () => createBaseline,
|
|
91
109
|
deltaEOk: () => deltaEOk,
|
|
@@ -94,9 +112,11 @@ __export(index_exports, {
|
|
|
94
112
|
densityTokens: () => densityTokens,
|
|
95
113
|
emitCss: () => emitCss,
|
|
96
114
|
emptyEstate: () => emptyEstate,
|
|
115
|
+
estateComponentPairs: () => estateComponentPairs,
|
|
97
116
|
estatePairs: () => estatePairs,
|
|
98
117
|
exitDuration: () => exitDuration,
|
|
99
118
|
extractHtml: () => extractHtml,
|
|
119
|
+
faceLicence: () => faceLicence,
|
|
100
120
|
findClaims: () => findClaims,
|
|
101
121
|
findColours: () => findColours,
|
|
102
122
|
findingKey: () => findingKey,
|
|
@@ -114,6 +134,7 @@ __export(index_exports, {
|
|
|
114
134
|
groundName: () => groundName,
|
|
115
135
|
harvest: () => harvest,
|
|
116
136
|
hexToOklch: () => hexToOklch,
|
|
137
|
+
holdHouseBlocks: () => holdHouseBlocks,
|
|
117
138
|
houseRule: () => houseRule,
|
|
118
139
|
hueName: () => hueName,
|
|
119
140
|
hueSwatch: () => hueSwatch,
|
|
@@ -123,6 +144,8 @@ __export(index_exports, {
|
|
|
123
144
|
initDirection: () => initDirection,
|
|
124
145
|
isAiViolet: () => isAiViolet,
|
|
125
146
|
isCream: () => isCream,
|
|
147
|
+
layoutDistance: () => layoutDistance,
|
|
148
|
+
licencesJson: () => licencesJson,
|
|
126
149
|
makeSnapshot: () => makeSnapshot,
|
|
127
150
|
motionTokens: () => motionTokens,
|
|
128
151
|
namedSources: () => namedSources,
|
|
@@ -140,8 +163,10 @@ __export(index_exports, {
|
|
|
140
163
|
protectedFacts: () => protectedFacts,
|
|
141
164
|
ramp: () => ramp,
|
|
142
165
|
rampFromAnchors: () => rampFromAnchors,
|
|
166
|
+
readSnapshot: () => readSnapshot,
|
|
143
167
|
retrofitPlan: () => retrofitPlan,
|
|
144
168
|
runTell: () => runTell,
|
|
169
|
+
scanProvenance: () => scanProvenance,
|
|
145
170
|
scanSource: () => scanSource,
|
|
146
171
|
sectionRhythm: () => sectionRhythm,
|
|
147
172
|
semantic: () => semantic,
|
|
@@ -150,6 +175,8 @@ __export(index_exports, {
|
|
|
150
175
|
sharedParts: () => sharedParts,
|
|
151
176
|
shouldAnimate: () => shouldAnimate,
|
|
152
177
|
siblingLine: () => siblingLine,
|
|
178
|
+
stockAvatarSource: () => stockAvatarSource,
|
|
179
|
+
stockSource: () => stockSource,
|
|
153
180
|
tailwindV3Preset: () => tailwindV3Preset,
|
|
154
181
|
tailwindV4Theme: () => tailwindV4Theme,
|
|
155
182
|
tellById: () => tellById,
|
|
@@ -160,6 +187,7 @@ __export(index_exports, {
|
|
|
160
187
|
typicality: () => typicality,
|
|
161
188
|
validateDirection: () => validateDirection,
|
|
162
189
|
visibleText: () => visibleText,
|
|
190
|
+
visualMeasures: () => visualMeasures,
|
|
163
191
|
wcagContrast: () => wcagContrast,
|
|
164
192
|
wordListsJson: () => wordListsJson
|
|
165
193
|
});
|
|
@@ -2417,14 +2445,14 @@ function contractionHits(ctx) {
|
|
|
2417
2445
|
for (const doc2 of documents(ctx)) {
|
|
2418
2446
|
if (doc2.words < 800) continue;
|
|
2419
2447
|
const text = doc2.blocks.map((b) => withoutHeadings(b.text)).join("\n");
|
|
2420
|
-
const
|
|
2421
|
-
const rate =
|
|
2448
|
+
const count3 = (text.match(CONTRACTION) ?? []).length;
|
|
2449
|
+
const rate = count3 * 1e3 / doc2.words;
|
|
2422
2450
|
if (rate < 1) {
|
|
2423
2451
|
const first = sentences(doc2)[0];
|
|
2424
2452
|
hits.push({
|
|
2425
2453
|
path: doc2.path,
|
|
2426
2454
|
offset: first?.offset ?? 0,
|
|
2427
|
-
message: `${
|
|
2455
|
+
message: `${count3} contraction${count3 === 1 ? "" : "s"} in ${thousands(doc2.words)} words. Prose with none reads stiff; formal legal pages are the exception`
|
|
2428
2456
|
});
|
|
2429
2457
|
}
|
|
2430
2458
|
}
|
|
@@ -2547,16 +2575,16 @@ function filler(words2, seed = 1, options = {}) {
|
|
|
2547
2575
|
return state % n;
|
|
2548
2576
|
};
|
|
2549
2577
|
const out = [];
|
|
2550
|
-
let
|
|
2578
|
+
let count3 = 0;
|
|
2551
2579
|
let i = 0;
|
|
2552
|
-
while (
|
|
2580
|
+
while (count3 < words2) {
|
|
2553
2581
|
const length = uniform ?? shape[i % shape.length];
|
|
2554
2582
|
const s = [];
|
|
2555
2583
|
for (let w = 0; w < length; w += 1) s.push(FILLER_WORDS[next(FILLER_WORDS.length)]);
|
|
2556
2584
|
if (contractions && i % 6 === 2) s.splice(1, 0, "we're");
|
|
2557
2585
|
s[0] = s[0].charAt(0).toUpperCase() + s[0].slice(1);
|
|
2558
2586
|
out.push(`${s.join(" ")}.`);
|
|
2559
|
-
|
|
2587
|
+
count3 += s.length;
|
|
2560
2588
|
i += 1;
|
|
2561
2589
|
}
|
|
2562
2590
|
const paragraphs = [];
|
|
@@ -2909,6 +2937,7 @@ var REFLEX_FONTS_2 = [
|
|
|
2909
2937
|
"Plus Jakarta Sans",
|
|
2910
2938
|
"Outfit",
|
|
2911
2939
|
"Geist",
|
|
2940
|
+
"Cal Sans",
|
|
2912
2941
|
"Sora",
|
|
2913
2942
|
"Instrument Sans",
|
|
2914
2943
|
"Instrument Serif",
|
|
@@ -3130,7 +3159,7 @@ function iconTiles(file) {
|
|
|
3130
3159
|
return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
|
|
3131
3160
|
});
|
|
3132
3161
|
}
|
|
3133
|
-
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
|
|
3162
|
+
var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons|@iconscout\/react-unicons[^"']*|@mui\/icons-material[^"']*|react-material-symbols[^"']*)["']/;
|
|
3134
3163
|
function iconTileStackHits(ctx) {
|
|
3135
3164
|
const hits = [];
|
|
3136
3165
|
for (const file of ctx.files) {
|
|
@@ -3277,6 +3306,12 @@ function shadcnHits(ctx) {
|
|
|
3277
3306
|
const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
|
|
3278
3307
|
return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
|
|
3279
3308
|
}
|
|
3309
|
+
function shadcnCardHits(ctx) {
|
|
3310
|
+
return eachUnit(
|
|
3311
|
+
ctx,
|
|
3312
|
+
(unit) => has(unit, /^bg-card$/) && has(unit, /^text-card-foreground$/) && has(unit, /^border$/) && has(unit, /^rounded-(?:lg|xl)$/) && has(unit, /^shadow(?:-sm)?$/) ? "the registry Card recipe, unchanged" : null
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3280
3315
|
function marqueeHits(ctx) {
|
|
3281
3316
|
const hits = [];
|
|
3282
3317
|
const pattern = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
|
|
@@ -3494,6 +3529,23 @@ const inter = Inter({ subsets: ["latin"] });`),
|
|
|
3494
3529
|
pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
|
|
3495
3530
|
}
|
|
3496
3531
|
},
|
|
3532
|
+
{
|
|
3533
|
+
id: "shadcn-card-stock",
|
|
3534
|
+
name: "Stock shadcn Card",
|
|
3535
|
+
generation: 1,
|
|
3536
|
+
severity: "warn",
|
|
3537
|
+
surface: "source",
|
|
3538
|
+
why: "The registry Card (bg-card, a plain border, rounded-lg or rounded-xl, shadow-sm) is the container every generated page is built from. Left as shipped, every card on the site looks like every other shadcn site's.",
|
|
3539
|
+
fix: "Give the card this site's radius and shadow from the restraint budget, or drop the shadow and let a visible border carry it.",
|
|
3540
|
+
detect: shadcnCardHits,
|
|
3541
|
+
fixtures: {
|
|
3542
|
+
flag: [
|
|
3543
|
+
f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
|
|
3544
|
+
f3("components/ui/card.tsx", `className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}`)
|
|
3545
|
+
],
|
|
3546
|
+
pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
|
|
3547
|
+
}
|
|
3548
|
+
},
|
|
3497
3549
|
{
|
|
3498
3550
|
id: "cream-palette",
|
|
3499
3551
|
name: "Cream ground",
|
|
@@ -3720,6 +3772,17 @@ var HOUSE = {
|
|
|
3720
3772
|
function houseRule(id, name = id) {
|
|
3721
3773
|
return HOUSE[id] ?? { tier: "review", label: name };
|
|
3722
3774
|
}
|
|
3775
|
+
var HOUSE_LOCKED = "the house blocking tier cannot be lowered by a repo. A genuine one-off takes copy-ok on its line; a rule that is wrong is changed in craft's house.ts and COPY.md";
|
|
3776
|
+
function holdHouseBlocks(exceptions, severity) {
|
|
3777
|
+
const blocks = (tell) => HOUSE[tell]?.tier === "block";
|
|
3778
|
+
const refusedSeverity = Object.entries(severity).filter(([tell, o]) => blocks(tell) && o.level !== "block").map(([tell]) => tell);
|
|
3779
|
+
return {
|
|
3780
|
+
exceptions: exceptions.filter((e) => !blocks(e.tell)),
|
|
3781
|
+
refused: exceptions.filter((e) => blocks(e.tell)),
|
|
3782
|
+
severity: Object.fromEntries(Object.entries(severity).filter(([tell]) => !refusedSeverity.includes(tell))),
|
|
3783
|
+
refusedSeverity
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3723
3786
|
|
|
3724
3787
|
// src/character/parse.ts
|
|
3725
3788
|
var MARKUP = /\.(tsx|jsx|html?|astro|vue|svelte|mdx)$/i;
|
|
@@ -4053,7 +4116,7 @@ function extractStrings(file) {
|
|
|
4053
4116
|
}
|
|
4054
4117
|
|
|
4055
4118
|
// src/snapshot/types.ts
|
|
4056
|
-
var SNAPSHOT_VERSION =
|
|
4119
|
+
var SNAPSHOT_VERSION = 2;
|
|
4057
4120
|
|
|
4058
4121
|
// src/snapshot/fixture.ts
|
|
4059
4122
|
function makeSnapshot(patch = {}) {
|
|
@@ -4386,188 +4449,988 @@ var RENDERED_PATHS = {
|
|
|
4386
4449
|
}
|
|
4387
4450
|
};
|
|
4388
4451
|
|
|
4389
|
-
// src/
|
|
4390
|
-
var
|
|
4391
|
-
var
|
|
4392
|
-
|
|
4393
|
-
)
|
|
4394
|
-
|
|
4395
|
-
return
|
|
4396
|
-
}
|
|
4397
|
-
function
|
|
4398
|
-
const
|
|
4399
|
-
const
|
|
4400
|
-
|
|
4401
|
-
if (
|
|
4402
|
-
rejected.push({ exception, reason: `no tell called "${exception.tell}"` });
|
|
4403
|
-
} else if (typeof exception.because !== "string" || exception.because.trim().length < 12) {
|
|
4404
|
-
rejected.push({ exception, reason: "an exception needs a because of at least a sentence" });
|
|
4405
|
-
} else {
|
|
4406
|
-
applied.push(exception);
|
|
4407
|
-
}
|
|
4452
|
+
// src/fingerprint/index.ts
|
|
4453
|
+
var FINGERPRINT_VERSION = 2;
|
|
4454
|
+
var CHROMATIC = 0.03;
|
|
4455
|
+
var MIN_ACCENT_AREA = 2e4;
|
|
4456
|
+
var oklch = (value) => {
|
|
4457
|
+
const parsed = parseColour(value);
|
|
4458
|
+
return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
|
|
4459
|
+
};
|
|
4460
|
+
function accentOf(s) {
|
|
4461
|
+
const buttons = /* @__PURE__ */ new Map();
|
|
4462
|
+
for (const c of s.controls) {
|
|
4463
|
+
const o = oklch(c.background);
|
|
4464
|
+
if (o && o.c >= CHROMATIC) buttons.set(c.background, (buttons.get(c.background) ?? 0) + 1);
|
|
4408
4465
|
}
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4466
|
+
const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
|
|
4467
|
+
if (topButton) return oklch(topButton[0]);
|
|
4468
|
+
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC && x.w >= MIN_ACCENT_AREA).sort((a, b) => b.w - a.w)[0];
|
|
4469
|
+
if (bg) return bg.o;
|
|
4470
|
+
const text = s.colours.text.map((t) => ({ o: oklch(t.value), w: t.chars })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
4471
|
+
return text ? text.o : null;
|
|
4414
4472
|
}
|
|
4415
|
-
function
|
|
4416
|
-
const
|
|
4473
|
+
function faces(s) {
|
|
4474
|
+
const byChars = [...s.fonts].sort((a, b) => b.chars - a.chars);
|
|
4475
|
+
const byDisplay = [...s.fonts].sort((a, b) => b.displayChars - a.displayChars);
|
|
4476
|
+
const bodyName = normaliseFamily(byChars[0]?.family ?? "system-ui");
|
|
4477
|
+
const displayName = normaliseFamily((byDisplay[0]?.displayChars ? byDisplay[0].family : byChars[0]?.family) ?? "system-ui");
|
|
4417
4478
|
return {
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
generation: tell.generation,
|
|
4421
|
-
severity: tell.severity,
|
|
4422
|
-
path: hit2.path,
|
|
4423
|
-
line: texts.has(hit2.path) ? lineAt(text, hit2.offset) : 0,
|
|
4424
|
-
excerpt: hit2.excerpt ?? excerptAt(text, hit2.offset),
|
|
4425
|
-
message: hit2.message,
|
|
4426
|
-
fix: tell.fix,
|
|
4427
|
-
...tell.surface === "copy" ? { house: houseRule(tell.id, tell.name).tier } : {}
|
|
4479
|
+
display: { family: displayName, class: fontClass(displayName) },
|
|
4480
|
+
body: { family: bodyName, class: fontClass(bodyName) }
|
|
4428
4481
|
};
|
|
4429
4482
|
}
|
|
4430
|
-
function
|
|
4431
|
-
|
|
4432
|
-
const
|
|
4433
|
-
const
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
const
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
excepted.set(exception, (excepted.get(exception) ?? 0) + 1);
|
|
4448
|
-
continue;
|
|
4449
|
-
}
|
|
4450
|
-
const finding = toFinding(tell, hit2, texts);
|
|
4451
|
-
if (marked(linesOf(hit2.path), finding.line)) {
|
|
4452
|
-
suppressed.push({ tell: tell.id, path: finding.path, line: finding.line });
|
|
4453
|
-
continue;
|
|
4454
|
-
}
|
|
4455
|
-
findings.push(finding);
|
|
4456
|
-
}
|
|
4457
|
-
}
|
|
4458
|
-
findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.tell.localeCompare(b.tell));
|
|
4459
|
-
const byGeneration = { 1: 0, 2: 0, 3: 0 };
|
|
4460
|
-
const byTell = {};
|
|
4461
|
-
for (const finding of findings) {
|
|
4462
|
-
byGeneration[finding.generation] += 1;
|
|
4463
|
-
byTell[finding.tell] = (byTell[finding.tell] ?? 0) + 1;
|
|
4464
|
-
}
|
|
4483
|
+
function median(values) {
|
|
4484
|
+
if (values.length === 0) return null;
|
|
4485
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4486
|
+
const mid = Math.floor(sorted.length / 2);
|
|
4487
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
4488
|
+
}
|
|
4489
|
+
function fingerprint(s) {
|
|
4490
|
+
const effects = [];
|
|
4491
|
+
if (s.effects.glass > 0) effects.push("glass-panel");
|
|
4492
|
+
if (s.effects.gradientText > 0) effects.push("gradient-text");
|
|
4493
|
+
if (s.effects.glows > 0 || s.effects.gradients.some((g) => g.radial)) effects.push("radial-spotlight-glow");
|
|
4494
|
+
if (s.effects.gridBackgrounds > 0) effects.push("grid-background");
|
|
4495
|
+
if (s.effects.marquees > 0) effects.push("marquee");
|
|
4496
|
+
if (s.effects.hairlineShadowCards >= 3) effects.push("thin-border-wide-shadow");
|
|
4497
|
+
if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
|
|
4498
|
+
if (s.motion.introOverlay) effects.push("intro-cinematic");
|
|
4499
|
+
const below = s.sections.filter((x) => x.top > s.viewport.height).length;
|
|
4465
4500
|
return {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4501
|
+
version: FINGERPRINT_VERSION,
|
|
4502
|
+
accent: accentOf(s),
|
|
4503
|
+
ground: oklch(s.ground) ?? { l: 1, c: 0, h: 0 },
|
|
4504
|
+
...faces(s),
|
|
4505
|
+
roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
|
|
4506
|
+
motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
|
|
4507
|
+
effects: effects.sort(),
|
|
4508
|
+
layout: s.sections.map((x) => x.kind),
|
|
4509
|
+
...s.sections.length > 0 && s.sections.every((x) => x.role && x.geometry) ? {
|
|
4510
|
+
sections: s.sections.map((x) => ({
|
|
4511
|
+
role: x.role,
|
|
4512
|
+
centred: x.geometry.centredShare,
|
|
4513
|
+
width: x.geometry.contentWidthRatio,
|
|
4514
|
+
symmetry: x.geometry.mirrorSymmetry,
|
|
4515
|
+
controls: x.geometry.controls,
|
|
4516
|
+
cards: x.cards,
|
|
4517
|
+
background: x.geometry.background === s.ground ? null : oklch(x.geometry.background),
|
|
4518
|
+
badges: (x.badges ?? []).map((b) => b.toLowerCase()),
|
|
4519
|
+
avatars: x.avatars ?? 0,
|
|
4520
|
+
carousel: x.carousel ?? false,
|
|
4521
|
+
figures: x.figures ?? 0
|
|
4522
|
+
}))
|
|
4523
|
+
} : {}
|
|
4478
4524
|
};
|
|
4479
4525
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4526
|
+
var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
|
|
4527
|
+
function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
|
|
4528
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
4529
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
4530
|
+
let prev = row[0];
|
|
4531
|
+
row[0] = i;
|
|
4532
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
4533
|
+
const cur = row[j];
|
|
4534
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost(a[i - 1], b[j - 1]));
|
|
4535
|
+
prev = cur;
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
return row[b.length];
|
|
4483
4539
|
}
|
|
4484
|
-
function
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
return report(tells, files, (tell) => tell.surface === "source" ? tell.detect(ctx) : [], options);
|
|
4540
|
+
function sectionCost(x, y) {
|
|
4541
|
+
if (x.role !== y.role) return 1;
|
|
4542
|
+
return Math.min(0.5, (Math.abs(x.centred - y.centred) + Math.abs(x.width - y.width)) / 2);
|
|
4488
4543
|
}
|
|
4489
|
-
function
|
|
4490
|
-
|
|
4544
|
+
function layoutDistance(a, b) {
|
|
4545
|
+
if (a.sections && b.sections) {
|
|
4546
|
+
const longest2 = Math.max(a.sections.length, b.sections.length);
|
|
4547
|
+
return longest2 === 0 ? 0 : levenshtein(a.sections, b.sections, sectionCost) / longest2;
|
|
4548
|
+
}
|
|
4549
|
+
const longest = Math.max(a.layout.length, b.layout.length);
|
|
4550
|
+
return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
|
|
4491
4551
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
return
|
|
4552
|
+
var clamp = (n) => Math.max(0, Math.min(1, n));
|
|
4553
|
+
function faceDistance(a, b) {
|
|
4554
|
+
if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
|
|
4555
|
+
return a.class === b.class ? 0.5 : 1;
|
|
4496
4556
|
}
|
|
4497
|
-
function
|
|
4498
|
-
const
|
|
4499
|
-
const
|
|
4500
|
-
|
|
4501
|
-
|
|
4557
|
+
function fingerprintDistance(a, b) {
|
|
4558
|
+
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / 0.3) : a.accent || b.accent ? 1 : 0;
|
|
4559
|
+
const ground = clamp(deltaEOk(a.ground, b.ground) / 0.15);
|
|
4560
|
+
const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
|
|
4561
|
+
const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
|
|
4562
|
+
const motion = clamp(Math.abs(a.motion - b.motion));
|
|
4563
|
+
const union = /* @__PURE__ */ new Set([...a.effects, ...b.effects]);
|
|
4564
|
+
const shared = a.effects.filter((e) => b.effects.includes(e)).length;
|
|
4565
|
+
const effects = union.size === 0 ? 0 : 1 - shared / union.size;
|
|
4566
|
+
const layout = layoutDistance(a, b);
|
|
4567
|
+
const parts2 = { accent, ground, type, shape, motion, effects, layout };
|
|
4568
|
+
const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
|
|
4569
|
+
return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
|
|
4502
4570
|
}
|
|
4503
|
-
function
|
|
4504
|
-
|
|
4505
|
-
return tell.detect(copyContext(files));
|
|
4571
|
+
function nearest(target2, set, k = set.length) {
|
|
4572
|
+
return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target2, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
|
|
4506
4573
|
}
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4574
|
+
|
|
4575
|
+
// src/null/index.ts
|
|
4576
|
+
var NULL_VERSION = 1;
|
|
4577
|
+
function nullPrompt(brief) {
|
|
4511
4578
|
return [
|
|
4512
|
-
`
|
|
4579
|
+
`Build the home page for this business: ${brief.trim()}`,
|
|
4513
4580
|
"",
|
|
4514
|
-
"
|
|
4515
|
-
"| --- | --- | --- | --- | --- | --- |",
|
|
4516
|
-
...rows
|
|
4581
|
+
"Write it as one complete, self-contained HTML file. You may load fonts and libraries from a CDN. Use real copy, not placeholder text. Reply with the HTML only."
|
|
4517
4582
|
].join("\n");
|
|
4518
4583
|
}
|
|
4519
|
-
function
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
stockPhrases: STOCK_PHRASES,
|
|
4526
|
-
aiPhrases: AI_PHRASES,
|
|
4527
|
-
plainerWords: PLAINER_WORDS,
|
|
4528
|
-
buzzwords: BUZZWORDS,
|
|
4529
|
-
negativeReassurance: NEGATIVE_REASSURANCE,
|
|
4530
|
-
vagueWords: VAGUE_WORDS,
|
|
4531
|
-
reviewPhrases: REVIEW_PHRASES
|
|
4532
|
-
}
|
|
4533
|
-
},
|
|
4534
|
-
null,
|
|
4535
|
-
2
|
|
4536
|
-
) + "\n";
|
|
4584
|
+
function extractHtml(reply) {
|
|
4585
|
+
const start = reply.search(/<!doctype html|<html[\s>]/i);
|
|
4586
|
+
if (start < 0) return null;
|
|
4587
|
+
const end = reply.toLowerCase().lastIndexOf("</html>");
|
|
4588
|
+
const html = end > start ? reply.slice(start, end + "</html>".length) : reply.slice(start).replace(/\s*```\s*$/, "");
|
|
4589
|
+
return html.length > 200 ? html : null;
|
|
4537
4590
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
function
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
}
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4591
|
+
var MIN_RUNS = 5;
|
|
4592
|
+
var TYPICAL_AT = 0.1;
|
|
4593
|
+
function meanNearest(distances, k) {
|
|
4594
|
+
const sorted = [...distances].sort((a, b) => a - b).slice(0, k);
|
|
4595
|
+
return sorted.reduce((s, d) => s + d, 0) / sorted.length;
|
|
4596
|
+
}
|
|
4597
|
+
function median2(values) {
|
|
4598
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4599
|
+
const mid = Math.floor(sorted.length / 2);
|
|
4600
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
4601
|
+
}
|
|
4602
|
+
function typicality(target2, runs, k = 3) {
|
|
4603
|
+
if (runs.length < MIN_RUNS) throw new Error(`a null model needs at least ${MIN_RUNS} runs; this one has ${runs.length}`);
|
|
4604
|
+
const kk = Math.min(k, runs.length - 1);
|
|
4605
|
+
const toTarget = runs.map((r) => ({ id: r.id, distance: fingerprintDistance(target2, r.fingerprint).total }));
|
|
4606
|
+
const distance = meanNearest(
|
|
4607
|
+
toTarget.map((t) => t.distance),
|
|
4608
|
+
kk
|
|
4609
|
+
);
|
|
4610
|
+
const loo = runs.map(
|
|
4611
|
+
(r, i) => meanNearest(
|
|
4612
|
+
runs.filter((_, j) => j !== i).map((o) => fingerprintDistance(r.fingerprint, o.fingerprint).total),
|
|
4613
|
+
kk
|
|
4614
|
+
)
|
|
4615
|
+
);
|
|
4616
|
+
const score = loo.filter((d) => d >= distance).length / loo.length;
|
|
4617
|
+
return {
|
|
4618
|
+
score: Math.round(score * 100) / 100,
|
|
4619
|
+
typical: score >= TYPICAL_AT,
|
|
4620
|
+
distance: Math.round(distance * 1e3) / 1e3,
|
|
4621
|
+
baseline: Math.round(median2(loo) * 1e3) / 1e3,
|
|
4622
|
+
nearest: toTarget.sort((a, b) => a.distance - b.distance).slice(0, kk),
|
|
4623
|
+
shared: sharedChoices(target2, runs)
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
var HUES = [
|
|
4627
|
+
["red", 0, 45],
|
|
4628
|
+
["orange", 45, 75],
|
|
4629
|
+
["yellow", 75, 110],
|
|
4630
|
+
["green", 110, 165],
|
|
4631
|
+
["teal", 165, 215],
|
|
4632
|
+
["blue", 215, 265],
|
|
4633
|
+
["violet", 265, 315],
|
|
4634
|
+
["pink", 315, 360]
|
|
4635
|
+
];
|
|
4636
|
+
function hueName(o) {
|
|
4637
|
+
if (!o) return "none";
|
|
4638
|
+
if (o.c < 0.03) return "neutral";
|
|
4639
|
+
return HUES.find(([, lo, hi]) => o.h >= lo && o.h < hi)?.[0] ?? "red";
|
|
4640
|
+
}
|
|
4641
|
+
function groundName(o) {
|
|
4642
|
+
if (o.l < 0.3) return "dark";
|
|
4643
|
+
if (isCream(o)) return "cream";
|
|
4644
|
+
if (o.l >= 0.97 && o.c < 0.012) return "white";
|
|
4645
|
+
return o.c < 0.02 ? "grey" : "tinted";
|
|
4646
|
+
}
|
|
4647
|
+
function shapeName(roundness) {
|
|
4648
|
+
if (roundness === null) return "no buttons";
|
|
4649
|
+
if (roundness >= 0.45) return "pill";
|
|
4650
|
+
return roundness >= 0.15 ? "soft" : "square";
|
|
4651
|
+
}
|
|
4652
|
+
function choicesOf(fp) {
|
|
4653
|
+
return {
|
|
4654
|
+
display: [fp.display.family],
|
|
4655
|
+
body: [fp.body.family],
|
|
4656
|
+
accent: [hueName(fp.accent)],
|
|
4657
|
+
ground: [groundName(fp.ground)],
|
|
4658
|
+
shape: [shapeName(fp.roundness)],
|
|
4659
|
+
motion: [fp.motion >= 0.6 ? "reveals most sections" : fp.motion > 0 ? "reveals some sections" : "no scroll reveals"],
|
|
4660
|
+
effects: fp.effects,
|
|
4661
|
+
opening: [fp.layout.slice(0, 3).join(" > ") || "none"]
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
4664
|
+
function sharedChoices(target2, runs) {
|
|
4665
|
+
const mine = choicesOf(target2);
|
|
4666
|
+
const all = runs.map((r) => choicesOf(r.fingerprint));
|
|
4667
|
+
const out = [];
|
|
4668
|
+
for (const [part, values] of Object.entries(mine)) {
|
|
4669
|
+
for (const value of values) {
|
|
4670
|
+
const count3 = all.filter((c) => c[part].includes(value)).length;
|
|
4671
|
+
if (count3 / runs.length >= 0.25) out.push({ part, value, runs: count3, of: runs.length });
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
return out.sort((a, b) => b.runs - a.runs);
|
|
4675
|
+
}
|
|
4676
|
+
var REFLEX_1 = new Set(REFLEX_FONTS_1.map((f4) => f4.toLowerCase()));
|
|
4677
|
+
var REFLEX_2 = new Set(REFLEX_FONTS_2.map((f4) => f4.toLowerCase()));
|
|
4678
|
+
function knownFor(kind, value, sample) {
|
|
4679
|
+
if (kind === "face") return REFLEX_1.has(value.toLowerCase()) ? "reflex-font" : REFLEX_2.has(value.toLowerCase()) ? "reflex-font-2" : null;
|
|
4680
|
+
if (kind === "accent" && value === "violet") return sample.some((f4) => f4.accent && isAiViolet(f4.accent)) ? "ai-violet" : null;
|
|
4681
|
+
if (kind === "ground" && value === "cream") return "cream-palette";
|
|
4682
|
+
if (kind === "shape" && value === "pill") return "pill-everything";
|
|
4683
|
+
if (kind === "motion" && value === "reveals most sections") return "reveal-everywhere";
|
|
4684
|
+
if (kind === "tell") return value;
|
|
4685
|
+
if (kind === "opening" && /^hero > (?:logos|stats)/.test(value)) return "hero-then-proof";
|
|
4686
|
+
return null;
|
|
4687
|
+
}
|
|
4688
|
+
var STOP = new Set(
|
|
4689
|
+
"a an the and or but of to in on for with at by from as is are be we our you your us it its this that these those will can all more get has have not no any".split(" ")
|
|
4690
|
+
);
|
|
4691
|
+
function grams(text) {
|
|
4692
|
+
const out = /* @__PURE__ */ new Set();
|
|
4693
|
+
for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
|
|
4694
|
+
const words2 = clause.match(/[a-z][a-z'-]*/g) ?? [];
|
|
4695
|
+
for (let n = 2; n <= 4; n += 1) {
|
|
4696
|
+
for (let i = 0; i + n <= words2.length; i += 1) {
|
|
4697
|
+
const g = words2.slice(i, i + n);
|
|
4698
|
+
if (STOP.has(g[0]) || STOP.has(g[n - 1])) continue;
|
|
4699
|
+
if (g.filter((w) => !STOP.has(w)).length < 2) continue;
|
|
4700
|
+
out.add(g.join(" "));
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
return out;
|
|
4705
|
+
}
|
|
4706
|
+
var GRAMMAR = /* @__PURE__ */ new Set([
|
|
4707
|
+
"privacy policy",
|
|
4708
|
+
"cookie policy",
|
|
4709
|
+
"rights reserved",
|
|
4710
|
+
"all rights reserved",
|
|
4711
|
+
"terms of service",
|
|
4712
|
+
"terms and conditions",
|
|
4713
|
+
"terms conditions",
|
|
4714
|
+
"how it works",
|
|
4715
|
+
"what we do",
|
|
4716
|
+
"about us",
|
|
4717
|
+
"contact us",
|
|
4718
|
+
"get in touch",
|
|
4719
|
+
"opening hours",
|
|
4720
|
+
"read more",
|
|
4721
|
+
"learn more",
|
|
4722
|
+
"book now",
|
|
4723
|
+
"free quote",
|
|
4724
|
+
"our services",
|
|
4725
|
+
"quick links",
|
|
4726
|
+
"email address",
|
|
4727
|
+
"phone number",
|
|
4728
|
+
"full name",
|
|
4729
|
+
"first name",
|
|
4730
|
+
"last name",
|
|
4731
|
+
"your name",
|
|
4732
|
+
"your email",
|
|
4733
|
+
"send message"
|
|
4734
|
+
]);
|
|
4735
|
+
var LEGAL = /©|©|\bregistered (?:in|office|number)\b|\bcompany (?:no|number|reg)|\bvat (?:no|number|reg)/i;
|
|
4736
|
+
var NOISE = /\S+@\S+|https?:\/\/\S+|\b[\w-]+(?:\.[\w-]+)*\.(?:co\.uk|com|org|net|uk|io)\b|\b\d{1,2}(?:[:.]\d{2})?\s*(?:am|pm)\b|\d+/gi;
|
|
4737
|
+
function textGrams(text) {
|
|
4738
|
+
if (LEGAL.test(text)) return /* @__PURE__ */ new Set();
|
|
4739
|
+
const out = grams(text.replace(NOISE, " | "));
|
|
4740
|
+
for (const g of out) if (GRAMMAR.has(g)) out.delete(g);
|
|
4741
|
+
return out;
|
|
4742
|
+
}
|
|
4743
|
+
function properNouns(brief) {
|
|
4744
|
+
return new Set((brief.match(/\b[A-Z][a-z]+/g) ?? []).map((w) => w.toLowerCase()).filter((w) => !STOP.has(w)));
|
|
4745
|
+
}
|
|
4746
|
+
function harvest(models, options = {}) {
|
|
4747
|
+
const minShare = options.minShare ?? 0.25;
|
|
4748
|
+
const runs = models.flatMap((m, b) => m.runs.map((r) => ({ ...r, brief: b })));
|
|
4749
|
+
const of = runs.length;
|
|
4750
|
+
if (of === 0) return [];
|
|
4751
|
+
const multiBrief = models.length > 1;
|
|
4752
|
+
const sample = runs.map((r) => r.fingerprint);
|
|
4753
|
+
const tally = /* @__PURE__ */ new Map();
|
|
4754
|
+
const add = (kind, value, run, brief, example) => {
|
|
4755
|
+
const key = `${kind}\0${value}`;
|
|
4756
|
+
const t = tally.get(key) ?? { kind, value, runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example };
|
|
4757
|
+
t.runs.add(run);
|
|
4758
|
+
t.briefs.add(brief);
|
|
4759
|
+
tally.set(key, t);
|
|
4760
|
+
};
|
|
4761
|
+
runs.forEach((r, i) => {
|
|
4762
|
+
const c = choicesOf(r.fingerprint);
|
|
4763
|
+
for (const f4 of /* @__PURE__ */ new Set([...c.display, ...c.body])) add("face", f4, i, r.brief);
|
|
4764
|
+
add("accent", c.accent[0], i, r.brief);
|
|
4765
|
+
add("ground", c.ground[0], i, r.brief);
|
|
4766
|
+
add("shape", c.shape[0], i, r.brief);
|
|
4767
|
+
add("motion", c.motion[0], i, r.brief);
|
|
4768
|
+
add("opening", c.opening[0], i, r.brief);
|
|
4769
|
+
for (const t of r.tells) add("tell", t, i, r.brief);
|
|
4770
|
+
});
|
|
4771
|
+
const out = [];
|
|
4772
|
+
for (const t of tally.values()) {
|
|
4773
|
+
if (t.runs.size / of < minShare) continue;
|
|
4774
|
+
if (t.kind === "accent" && (t.value === "none" || t.value === "neutral")) continue;
|
|
4775
|
+
if (t.kind === "shape" && t.value === "no buttons") continue;
|
|
4776
|
+
if (t.kind === "ground" && (t.value === "white" || t.value === "grey")) continue;
|
|
4777
|
+
if (t.kind === "motion" && t.value === "no scroll reveals") continue;
|
|
4778
|
+
out.push({ kind: t.kind, value: t.value, runs: t.runs.size, of, briefs: t.briefs.size, known: knownFor(t.kind, t.value, sample) });
|
|
4779
|
+
}
|
|
4780
|
+
const phraseMin = Math.max(3, Math.ceil(of * minShare * 0.4));
|
|
4781
|
+
const names = models.map((m) => properNouns(m.brief));
|
|
4782
|
+
const phrases2 = /* @__PURE__ */ new Map();
|
|
4783
|
+
runs.forEach((r, i) => {
|
|
4784
|
+
for (const text of r.copy) {
|
|
4785
|
+
for (const g of textGrams(text)) {
|
|
4786
|
+
if (g.split(" ").some((w) => names[r.brief].has(w))) continue;
|
|
4787
|
+
const p = phrases2.get(g) ?? { runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example: text.trim().slice(0, 140) };
|
|
4788
|
+
p.runs.add(i);
|
|
4789
|
+
p.briefs.add(r.brief);
|
|
4790
|
+
phrases2.set(g, p);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
});
|
|
4794
|
+
const kept = [...phrases2].filter(([, p]) => p.runs.size >= phraseMin && (!multiBrief || p.briefs.size >= 2)).sort((a, b) => b[1].runs.size - a[1].runs.size || b[0].length - a[0].length);
|
|
4795
|
+
const maximal = kept.filter(([g, p]) => !kept.some(([h, q]) => h !== g && h.includes(g) && q.runs.size >= 0.8 * p.runs.size));
|
|
4796
|
+
for (const [g, p] of maximal.slice(0, 40)) {
|
|
4797
|
+
out.push({ kind: "phrase", value: g, runs: p.runs.size, of, briefs: p.briefs.size, known: options.copyTell?.(g) ?? null, example: p.example });
|
|
4798
|
+
}
|
|
4799
|
+
const order = ["face", "accent", "ground", "shape", "motion", "opening", "tell", "phrase"];
|
|
4800
|
+
return out.sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind) || b.runs - a.runs);
|
|
4801
|
+
}
|
|
4802
|
+
function hueSwatch(runs, hue) {
|
|
4803
|
+
const hits = runs.map((r) => r.fingerprint.accent).filter((o) => !!o && hueName(o) === hue);
|
|
4804
|
+
if (hits.length === 0) return null;
|
|
4805
|
+
const centre = hits.reduce((best, o) => hits.reduce((s, p) => s + deltaEOk(o, p), 0) < hits.reduce((s, p) => s + deltaEOk(best, p), 0) ? o : best);
|
|
4806
|
+
return formatHex(oklchToRgb(centre));
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
// src/fingerprint/component.ts
|
|
4810
|
+
var PROMO_BADGE = /\b(?:most popular|popular|recommended|best value|best seller|bestseller|most chosen)\b/i;
|
|
4811
|
+
function componentsOf(fp, role) {
|
|
4812
|
+
return (fp.sections ?? []).filter((s) => s.role === role);
|
|
4813
|
+
}
|
|
4814
|
+
var clamp2 = (n) => Math.max(0, Math.min(1, n));
|
|
4815
|
+
var diff = (a, b, scale) => a === void 0 || b === void 0 ? null : clamp2(Math.abs(a - b) / scale);
|
|
4816
|
+
function componentDistance(a, b) {
|
|
4817
|
+
if (a.role !== b.role) return 1;
|
|
4818
|
+
const promo = (s) => (s.badges ?? []).some((t) => PROMO_BADGE.test(t));
|
|
4819
|
+
const parts2 = [
|
|
4820
|
+
diff(a.centred, b.centred, 1),
|
|
4821
|
+
diff(a.width, b.width, 0.5),
|
|
4822
|
+
diff(a.symmetry, b.symmetry, 0.5),
|
|
4823
|
+
diff(a.controls, b.controls, 2),
|
|
4824
|
+
diff(a.cards, b.cards, 3),
|
|
4825
|
+
diff(a.figures, b.figures, 3),
|
|
4826
|
+
diff(a.avatars, b.avatars, 3),
|
|
4827
|
+
a.carousel === void 0 || b.carousel === void 0 ? null : a.carousel === b.carousel ? 0 : 1,
|
|
4828
|
+
a.badges === void 0 || b.badges === void 0 ? null : promo(a) === promo(b) ? 0 : 1,
|
|
4829
|
+
a.background === void 0 || b.background === void 0 ? null : a.background === null || b.background === null ? a.background === b.background ? 0 : 1 : clamp2(deltaEOk(a.background, b.background) / 0.3)
|
|
4830
|
+
];
|
|
4831
|
+
const known = parts2.filter((p) => p !== null);
|
|
4832
|
+
return known.length === 0 ? 0 : Math.round(known.reduce((s, p) => s + p, 0) / known.length * 1e3) / 1e3;
|
|
4833
|
+
}
|
|
4834
|
+
function componentShared(a, b) {
|
|
4835
|
+
if (a.role !== b.role) return [];
|
|
4836
|
+
const out = [];
|
|
4837
|
+
const both = (f4) => f4(a) && f4(b);
|
|
4838
|
+
if (both((s) => s.centred >= 0.7)) out.push("centred");
|
|
4839
|
+
else if (both((s) => s.centred < 0.3)) out.push("left-aligned");
|
|
4840
|
+
if (both((s) => s.width <= 0.6)) out.push("narrow column");
|
|
4841
|
+
else if (both((s) => s.width >= 0.8)) out.push("full width");
|
|
4842
|
+
if (a.controls !== void 0 && a.controls === b.controls && a.controls > 0) out.push(`${a.controls} button${a.controls === 1 ? "" : "s"}`);
|
|
4843
|
+
if (a.cards !== void 0 && a.cards === b.cards && a.cards >= 2) out.push(`${a.cards} cards`);
|
|
4844
|
+
if (both((s) => (s.figures ?? 0) >= 3)) out.push("a row of figures");
|
|
4845
|
+
if (both((s) => (s.avatars ?? 0) >= 2)) out.push("avatars");
|
|
4846
|
+
if (both((s) => s.carousel === true)) out.push("carousel");
|
|
4847
|
+
if (both((s) => (s.badges ?? []).some((t) => PROMO_BADGE.test(t)))) out.push("promo badge");
|
|
4848
|
+
if (a.background && b.background && deltaEOk(a.background, b.background) <= 0.06) out.push("same coloured band");
|
|
4849
|
+
else if (a.background === null && b.background === null) out.push("on the page ground");
|
|
4850
|
+
return out;
|
|
4851
|
+
}
|
|
4852
|
+
function closestComponents(a, b, role) {
|
|
4853
|
+
let best = null;
|
|
4854
|
+
for (const x of componentsOf(a, role)) {
|
|
4855
|
+
for (const y of componentsOf(b, role)) {
|
|
4856
|
+
const distance = componentDistance(x, y);
|
|
4857
|
+
if (!best || distance < best.distance) best = { distance, shared: componentShared(x, y) };
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
return best;
|
|
4861
|
+
}
|
|
4862
|
+
function estateComponentPairs(register, role) {
|
|
4863
|
+
const out = [];
|
|
4864
|
+
const s = register.sites;
|
|
4865
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
4866
|
+
for (let j = i + 1; j < s.length; j += 1) {
|
|
4867
|
+
const c = closestComponents(s[i].fingerprint, s[j].fingerprint, role);
|
|
4868
|
+
if (c) out.push({ a: s[i].id, b: s[j].id, ...c });
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
return out.sort((x, y) => x.distance - y.distance);
|
|
4872
|
+
}
|
|
4873
|
+
var meanNearest2 = (d, k) => {
|
|
4874
|
+
const s = [...d].sort((a, b) => a - b).slice(0, k);
|
|
4875
|
+
return s.reduce((t, v) => t + v, 0) / s.length;
|
|
4876
|
+
};
|
|
4877
|
+
function componentTypicality(target2, runs, k = 3) {
|
|
4878
|
+
const pool = runs.flatMap((r) => componentsOf(r.fingerprint, target2.role).slice(0, 1));
|
|
4879
|
+
if (pool.length < MIN_RUNS) return null;
|
|
4880
|
+
const kk = Math.min(k, pool.length - 1);
|
|
4881
|
+
const distance = meanNearest2(pool.map((p) => componentDistance(target2, p)), kk);
|
|
4882
|
+
const loo = pool.map((p, i) => meanNearest2(pool.filter((_, j) => j !== i).map((o) => componentDistance(p, o)), kk));
|
|
4883
|
+
const score = loo.filter((d) => d >= distance).length / loo.length;
|
|
4884
|
+
const sorted = [...loo].sort((a, b) => a - b);
|
|
4885
|
+
const mid = Math.floor(sorted.length / 2);
|
|
4886
|
+
const baseline = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
4887
|
+
return {
|
|
4888
|
+
role: target2.role,
|
|
4889
|
+
score: Math.round(score * 100) / 100,
|
|
4890
|
+
typical: score >= TYPICAL_AT,
|
|
4891
|
+
distance: Math.round(distance * 1e3) / 1e3,
|
|
4892
|
+
baseline: Math.round(baseline * 1e3) / 1e3,
|
|
4893
|
+
runs: pool.length
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4896
|
+
var COMPONENT_ROLES = ["cta-band", "footer-cta", "pricing", "testimonials", "faq", "process", "features", "team", "contact", "stats"];
|
|
4897
|
+
function componentTypicalities(fp, runs, k = 3) {
|
|
4898
|
+
const out = [];
|
|
4899
|
+
for (const role of COMPONENT_ROLES) {
|
|
4900
|
+
const first = componentsOf(fp, role)[0];
|
|
4901
|
+
const t = first ? componentTypicality(first, runs, k) : null;
|
|
4902
|
+
if (t) out.push(t);
|
|
4903
|
+
}
|
|
4904
|
+
return out;
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
// src/snapshot/rendered-tells.ts
|
|
4908
|
+
var hit2 = (s, message, excerpt) => ({ path: s.url, offset: 0, message, excerpt: excerpt ?? message });
|
|
4909
|
+
var GROUND = "rgb(255, 255, 255)";
|
|
4910
|
+
var BAND = "rgb(31, 58, 46)";
|
|
4911
|
+
var geo = (patch = {}) => ({
|
|
4912
|
+
centredShare: 0.1,
|
|
4913
|
+
mirrorSymmetry: 0.6,
|
|
4914
|
+
whitespaceRatio: 0.7,
|
|
4915
|
+
contentWidthRatio: 0.85,
|
|
4916
|
+
background: GROUND,
|
|
4917
|
+
controls: 0,
|
|
4918
|
+
...patch
|
|
4919
|
+
});
|
|
4920
|
+
function withSection(index, patch, roles = []) {
|
|
4921
|
+
const base = makeSnapshot();
|
|
4922
|
+
return {
|
|
4923
|
+
...base,
|
|
4924
|
+
sections: base.sections.map((s, i) => ({ ...s, role: roles[i] ?? s.kind, geometry: geo(), badges: [], avatars: 0, carousel: false, figures: 0, ...i === index ? patch : {} }))
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
var measured = (s) => s.geometry !== void 0 && s.role !== void 0;
|
|
4928
|
+
var RENDERED_TELLS = [
|
|
4929
|
+
{
|
|
4930
|
+
id: "cta-band-stock",
|
|
4931
|
+
name: "Stock call-to-action band",
|
|
4932
|
+
generation: 1,
|
|
4933
|
+
severity: "warn",
|
|
4934
|
+
surface: "rendered",
|
|
4935
|
+
why: "A full-width coloured band with a centred heading, one line and one or two buttons is what a model closes every section run with. It asks for the booking the same way on every site.",
|
|
4936
|
+
fix: "Keep the ask, change the band: put it where the decision is made (next to the prices, after the proof), in the client's own words, laid out like the rest of the page rather than as a centred slab.",
|
|
4937
|
+
rendered: {
|
|
4938
|
+
detect: (s) => s.sections.filter((x) => measured(x) && (x.role === "cta-band" || x.role === "footer-cta")).filter((x) => {
|
|
4939
|
+
const g = x.geometry;
|
|
4940
|
+
return g.centredShare >= 0.8 && g.background !== s.ground && g.controls >= 1 && g.controls <= 2 && g.contentWidthRatio <= 0.7;
|
|
4941
|
+
}).map((x) => hit2(s, `a centred ${x.role === "footer-cta" ? "closing " : ""}band on its own colour with ${x.geometry.controls === 1 ? "one button" : "two buttons"}`, x.label)),
|
|
4942
|
+
fixtures: {
|
|
4943
|
+
flag: [
|
|
4944
|
+
withSection(4, { role: "footer-cta", label: "Boiler out today?", geometry: geo({ centredShare: 1, background: BAND, controls: 1, contentWidthRatio: 0.45 }) }),
|
|
4945
|
+
withSection(3, { role: "cta-band", label: "Ready to book?", geometry: geo({ centredShare: 0.9, background: BAND, controls: 2, contentWidthRatio: 0.5 }) })
|
|
4946
|
+
],
|
|
4947
|
+
pass: [
|
|
4948
|
+
makeSnapshot(),
|
|
4949
|
+
// The same ask, laid out like the page: left-aligned on the ground.
|
|
4950
|
+
withSection(4, { role: "footer-cta", label: "Book a visit", geometry: geo({ centredShare: 0.1, background: GROUND, controls: 1, contentWidthRatio: 0.85 }) })
|
|
4951
|
+
]
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
},
|
|
4955
|
+
{
|
|
4956
|
+
id: "pricing-trio-popular",
|
|
4957
|
+
name: "Three-tier pricing with a popular badge",
|
|
4958
|
+
generation: 1,
|
|
4959
|
+
severity: "warn",
|
|
4960
|
+
surface: "rendered",
|
|
4961
|
+
why: "Three price cards with the middle one badged 'Most popular' is the SaaS pricing page, applied to a plumber or a salon whether or not anyone chose the middle option.",
|
|
4962
|
+
fix: "Keep the prices. Show them the way the trade quotes (a price list, per job, per visit), and drop the badge unless it is true and the client can say how many people chose it.",
|
|
4963
|
+
rendered: {
|
|
4964
|
+
detect: (s) => s.sections.filter((x) => measured(x) && x.role === "pricing" && x.cards === 3).filter((x) => (x.badges ?? []).some((b) => PROMO_BADGE.test(b))).map((x) => hit2(s, `three price cards, one badged "${(x.badges ?? []).find((b) => PROMO_BADGE.test(b))}"`, x.label)),
|
|
4965
|
+
fixtures: {
|
|
4966
|
+
flag: [withSection(3, { role: "pricing", kind: "cards", cards: 3, badges: ["Most popular"], label: "Plans" })],
|
|
4967
|
+
pass: [
|
|
4968
|
+
makeSnapshot(),
|
|
4969
|
+
withSection(3, { role: "pricing", kind: "cards", cards: 3, badges: [], label: "Prices" }),
|
|
4970
|
+
withSection(3, { role: "pricing", kind: "text", cards: 0, badges: [], label: "Price list" })
|
|
4971
|
+
]
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
},
|
|
4975
|
+
{
|
|
4976
|
+
id: "testimonial-avatar-carousel",
|
|
4977
|
+
name: "Avatar testimonial carousel",
|
|
4978
|
+
generation: 1,
|
|
4979
|
+
severity: "warn",
|
|
4980
|
+
surface: "rendered",
|
|
4981
|
+
why: "Round headshots over quotes in a sliding carousel is the stock testimonial block. Most visitors see the first slide only, and stock or AI faces make every quote read as invented.",
|
|
4982
|
+
fix: "Show the reviews all at once, where they come from (the Google rating, the named job, the town), with real photos of the work rather than of the reviewer.",
|
|
4983
|
+
rendered: {
|
|
4984
|
+
detect: (s) => s.sections.filter((x) => measured(x) && x.role === "testimonials" && x.carousel === true && (x.avatars ?? 0) >= 2).map((x) => hit2(s, `${x.avatars} round portraits in a sliding testimonial carousel`, x.label)),
|
|
4985
|
+
fixtures: {
|
|
4986
|
+
flag: [withSection(2, { role: "testimonials", avatars: 3, carousel: true, label: "What our clients say" })],
|
|
4987
|
+
pass: [
|
|
4988
|
+
makeSnapshot(),
|
|
4989
|
+
withSection(2, { role: "testimonials", avatars: 3, carousel: false, label: "Reviews" }),
|
|
4990
|
+
withSection(2, { role: "testimonials", avatars: 0, carousel: true, label: "Reviews" })
|
|
4991
|
+
]
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
},
|
|
4995
|
+
{
|
|
4996
|
+
id: "faq-accordion-closer",
|
|
4997
|
+
name: "FAQ accordion as the closer",
|
|
4998
|
+
generation: 2,
|
|
4999
|
+
severity: "warn",
|
|
5000
|
+
surface: "rendered",
|
|
5001
|
+
why: "Ending the page on a collapsed FAQ, often followed only by a band, is the model's default page ending. The answers a buyer needs are hidden behind clicks at the point they decide.",
|
|
5002
|
+
fix: "Answer the questions that block a booking where they arise (price next to prices, area next to the map), and end on the call to action or the proof.",
|
|
5003
|
+
rendered: {
|
|
5004
|
+
detect: (s) => {
|
|
5005
|
+
const body = s.sections.filter((x) => x.role !== "footer-cta" && x.role !== "cta-band");
|
|
5006
|
+
const last = body[body.length - 1];
|
|
5007
|
+
return last && measured(last) && last.role === "faq" ? [hit2(s, "the page ends on a question-and-answer list", last.label)] : [];
|
|
5008
|
+
},
|
|
5009
|
+
fixtures: {
|
|
5010
|
+
flag: [
|
|
5011
|
+
withSection(4, { role: "faq", label: "Common questions" }),
|
|
5012
|
+
{
|
|
5013
|
+
...withSection(3, { role: "faq", label: "FAQ" }),
|
|
5014
|
+
sections: withSection(3, { role: "faq", label: "FAQ" }).sections.map((x, i) => i === 4 ? { ...x, role: "footer-cta" } : x)
|
|
5015
|
+
}
|
|
5016
|
+
],
|
|
5017
|
+
pass: [makeSnapshot(), withSection(2, { role: "faq", label: "Common questions" })]
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
},
|
|
5021
|
+
{
|
|
5022
|
+
id: "stats-row",
|
|
5023
|
+
name: "Row of big numbers",
|
|
5024
|
+
generation: 1,
|
|
5025
|
+
severity: "warn",
|
|
5026
|
+
surface: "rendered",
|
|
5027
|
+
why: "Three or four large figures in a row (500+ clients, 98% satisfaction, 10 years) is the stock proof block, and the numbers are often round, unsourced or invented.",
|
|
5028
|
+
fix: "Keep a figure only if it is true and sourced, and put it in a sentence next to what it proves. One real number in context beats four in a row.",
|
|
5029
|
+
rendered: {
|
|
5030
|
+
detect: (s) => s.sections.filter((x, i) => measured(x) && (x.role === "stats" || x.kind === "stats") && (x.figures ?? 0) >= 3 && (x.figures ?? 0) <= 6 && !(i === 1 && s.sections[0]?.kind === "hero")).map((x) => hit2(s, `${x.figures} large figures in a row`, x.label)),
|
|
5031
|
+
fixtures: {
|
|
5032
|
+
flag: [withSection(3, { role: "stats", kind: "stats", figures: 4, label: "500+" })],
|
|
5033
|
+
pass: [
|
|
5034
|
+
makeSnapshot(),
|
|
5035
|
+
withSection(3, { role: "text", kind: "text", figures: 1, label: "Since 2009" }),
|
|
5036
|
+
withSection(1, { role: "stats", kind: "stats", figures: 4, label: "500+" })
|
|
5037
|
+
]
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
},
|
|
5041
|
+
{
|
|
5042
|
+
id: "centred-everything",
|
|
5043
|
+
name: "Every section centred",
|
|
5044
|
+
generation: 2,
|
|
5045
|
+
severity: "warn",
|
|
5046
|
+
surface: "rendered",
|
|
5047
|
+
why: "When almost every section stacks a centred heading over centred text, the page has no reading line and every block looks like the one before. It is the layout nothing was decided for.",
|
|
5048
|
+
fix: "Pick an alignment and hold it: left-aligned text with a clear edge, keeping centring for the one moment that earns it.",
|
|
5049
|
+
rendered: {
|
|
5050
|
+
detect: (s) => {
|
|
5051
|
+
const m = s.sections.filter(measured);
|
|
5052
|
+
if (m.length < 4) return [];
|
|
5053
|
+
const centred = m.filter((x) => x.geometry.centredShare >= 0.7).length;
|
|
5054
|
+
return centred / m.length >= 0.8 ? [hit2(s, `${centred} of ${m.length} sections centre their text`)] : [];
|
|
5055
|
+
},
|
|
5056
|
+
fixtures: {
|
|
5057
|
+
flag: [{ ...makeSnapshot(), sections: makeSnapshot().sections.map((x) => ({ ...x, role: x.kind, geometry: geo({ centredShare: 0.9 }) })) }],
|
|
5058
|
+
pass: [
|
|
5059
|
+
makeSnapshot(),
|
|
5060
|
+
{ ...makeSnapshot(), sections: makeSnapshot().sections.map((x, i) => ({ ...x, role: x.kind, geometry: geo({ centredShare: i < 2 ? 0.9 : 0.1 }) })) }
|
|
5061
|
+
]
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
];
|
|
5066
|
+
|
|
5067
|
+
// src/snapshot/imagery-tells.ts
|
|
5068
|
+
var hit3 = (s, message, excerpt) => ({ path: s.url, offset: 0, message, excerpt: excerpt ?? message });
|
|
5069
|
+
var STOCK_PHOTO_HOSTS = {
|
|
5070
|
+
"unsplash.com": "Unsplash",
|
|
5071
|
+
"pexels.com": "Pexels",
|
|
5072
|
+
"pixabay.com": "Pixabay",
|
|
5073
|
+
"istockphoto.com": "iStock",
|
|
5074
|
+
"gettyimages.com": "Getty Images",
|
|
5075
|
+
"shutterstock.com": "Shutterstock",
|
|
5076
|
+
"ftcdn.net": "Adobe Stock",
|
|
5077
|
+
"dreamstime.com": "Dreamstime",
|
|
5078
|
+
"depositphotos.com": "Depositphotos",
|
|
5079
|
+
"freepik.com": "Freepik",
|
|
5080
|
+
"rawpixel.com": "rawpixel",
|
|
5081
|
+
"stocksnap.io": "StockSnap",
|
|
5082
|
+
"burst.shopifycdn.com": "Burst",
|
|
5083
|
+
"picsum.photos": "Lorem Picsum",
|
|
5084
|
+
"loremflickr.com": "LoremFlickr",
|
|
5085
|
+
"placehold.co": "a placeholder service",
|
|
5086
|
+
"placeholder.com": "a placeholder service",
|
|
5087
|
+
"dummyimage.com": "a placeholder service"
|
|
5088
|
+
};
|
|
5089
|
+
var STOCK_AVATAR_HOSTS = {
|
|
5090
|
+
"randomuser.me": "randomuser.me",
|
|
5091
|
+
"pravatar.cc": "Pravatar",
|
|
5092
|
+
"dicebear.com": "DiceBear",
|
|
5093
|
+
"xsgames.co": "a random-face service",
|
|
5094
|
+
"thispersondoesnotexist.com": "a generated face",
|
|
5095
|
+
"generated.photos": "a generated face"
|
|
5096
|
+
};
|
|
5097
|
+
var STOCK_FILENAMES = [
|
|
5098
|
+
[/(?:^|[/_-])shutterstock[_-]?\d{4,}/i, "Shutterstock"],
|
|
5099
|
+
[/(?:^|[/_-])istock(?:photo)?[_-]?\d{4,}/i, "iStock"],
|
|
5100
|
+
[/(?:^|[/_-])adobestock[_-]?\d{4,}/i, "Adobe Stock"],
|
|
5101
|
+
[/(?:^|[/_-])gettyimages[_-]?\d{4,}/i, "Getty Images"],
|
|
5102
|
+
[/(?:^|[/_-])depositphotos[_-]?\d{4,}/i, "Depositphotos"],
|
|
5103
|
+
[/(?:^|[/_-])dreamstime[_-]/i, "Dreamstime"],
|
|
5104
|
+
[/(?:^|[/_-])pexels-[a-z0-9-]*\d{4,}/i, "Pexels"],
|
|
5105
|
+
[/-unsplash\.(?:jpe?g|png|webp|avif)(?:[?#]|$)/i, "Unsplash"],
|
|
5106
|
+
// Unsplash's photo ids: photo-<13-digit timestamp>-<12 hex>.
|
|
5107
|
+
[/photo-1\d{12}-[0-9a-f]{12}/, "Unsplash"]
|
|
5108
|
+
];
|
|
5109
|
+
var byHost = (host, table) => {
|
|
5110
|
+
if (!host) return null;
|
|
5111
|
+
const h = host.toLowerCase();
|
|
5112
|
+
for (const [domain, name] of Object.entries(table)) if (h === domain || h.endsWith(`.${domain}`)) return name;
|
|
5113
|
+
return null;
|
|
5114
|
+
};
|
|
5115
|
+
function stockSource(image) {
|
|
5116
|
+
const host = byHost(image.host, STOCK_PHOTO_HOSTS);
|
|
5117
|
+
if (host) return host;
|
|
5118
|
+
let path = image.src.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/?#]*/i, "").replace(/[?#].*$/, "");
|
|
5119
|
+
try {
|
|
5120
|
+
path = decodeURIComponent(path);
|
|
5121
|
+
} catch {
|
|
5122
|
+
}
|
|
5123
|
+
for (const [re, name] of STOCK_FILENAMES) if (re.test(path)) return name;
|
|
5124
|
+
return null;
|
|
5125
|
+
}
|
|
5126
|
+
function stockAvatarSource(image) {
|
|
5127
|
+
return byHost(image.host, STOCK_AVATAR_HOSTS) ?? stockSource(image);
|
|
5128
|
+
}
|
|
5129
|
+
var picture = (patch = {}) => ({
|
|
5130
|
+
src: "https://example.test/images/boiler-swap-hinton.jpg",
|
|
5131
|
+
host: "example.test",
|
|
5132
|
+
width: 640,
|
|
5133
|
+
height: 480,
|
|
5134
|
+
top: 1800,
|
|
5135
|
+
role: "photo",
|
|
5136
|
+
alt: "The new combi boiler fitted in a kitchen cupboard in Hinton",
|
|
5137
|
+
decorative: false,
|
|
5138
|
+
section: 2,
|
|
5139
|
+
background: false,
|
|
5140
|
+
...patch
|
|
5141
|
+
});
|
|
5142
|
+
var withImages = (images) => ({ ...makeSnapshot(), images });
|
|
5143
|
+
var AI_XMP = { digitalSourceType: "trainedAlgorithmicMedia", source: "xmp", c2pa: false, bytes: 48e3 };
|
|
5144
|
+
var NOTHING = { digitalSourceType: null, source: null, c2pa: false, bytes: 48e3 };
|
|
5145
|
+
var count2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
5146
|
+
var IMAGERY_TELLS = [
|
|
5147
|
+
{
|
|
5148
|
+
id: "stock-photo",
|
|
5149
|
+
name: "Stock photography",
|
|
5150
|
+
generation: 1,
|
|
5151
|
+
severity: "warn",
|
|
5152
|
+
surface: "rendered",
|
|
5153
|
+
why: "A picture from a stock library is the picture every other site in the trade can use. It shows a job the client did not do, in a kitchen that is not theirs, and a visitor who has seen it before stops believing the rest.",
|
|
5154
|
+
fix: "Use a photo of the client's own work, premises or people, even a plain one taken on a phone. Where there is none yet, use fewer pictures rather than a library one.",
|
|
5155
|
+
rendered: {
|
|
5156
|
+
detect: (s) => {
|
|
5157
|
+
const found = (s.images ?? []).filter((i) => i.role === "photo" || i.role === "illustration").map((i) => ({ image: i, from: stockSource(i) })).filter((x) => x.from);
|
|
5158
|
+
if (found.length === 0) return [];
|
|
5159
|
+
const names = [...new Set(found.map((x) => x.from))].join(", ");
|
|
5160
|
+
return [hit3(s, `${count2(found.length, "picture")} from ${names}`, found[0].image.src)];
|
|
5161
|
+
},
|
|
5162
|
+
fixtures: {
|
|
5163
|
+
flag: [
|
|
5164
|
+
withImages([picture({ src: "https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=1600", host: "images.unsplash.com", alt: "Plumber fixing a sink" })]),
|
|
5165
|
+
// Downloaded and self-hosted, found by the library's own file name.
|
|
5166
|
+
withImages([picture({ src: "https://example.test/img/shutterstock_1234567890.jpg" })])
|
|
5167
|
+
],
|
|
5168
|
+
pass: [makeSnapshot(), withImages([picture()]), withImages([picture({ role: "icon", width: 48, height: 48, src: "https://images.unsplash.com/photo-1581578731548-c64695cc6952", host: "images.unsplash.com" })])]
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
},
|
|
5172
|
+
{
|
|
5173
|
+
id: "stock-avatar",
|
|
5174
|
+
name: "Stock or placeholder faces",
|
|
5175
|
+
generation: 1,
|
|
5176
|
+
severity: "warn",
|
|
5177
|
+
surface: "rendered",
|
|
5178
|
+
why: "Round portraits from a placeholder-face service or a stock library next to reviews make every quote read as invented, and some of those faces are generated people who do not exist.",
|
|
5179
|
+
fix: "Drop the portraits. Name the reviewer, the town and the job, and link the review to where it was left.",
|
|
5180
|
+
rendered: {
|
|
5181
|
+
detect: (s) => {
|
|
5182
|
+
const found = (s.images ?? []).filter((i) => i.role === "avatar").map((i) => ({ image: i, from: stockAvatarSource(i) })).filter((x) => x.from);
|
|
5183
|
+
if (found.length === 0) return [];
|
|
5184
|
+
const names = [...new Set(found.map((x) => x.from))].join(", ");
|
|
5185
|
+
return [hit3(s, `${count2(found.length, "portrait")} from ${names}`, found[0].image.src)];
|
|
5186
|
+
},
|
|
5187
|
+
fixtures: {
|
|
5188
|
+
flag: [
|
|
5189
|
+
withImages([picture(), picture({ role: "avatar", width: 56, height: 56, src: "https://i.pravatar.cc/150?img=12", host: "i.pravatar.cc", alt: "Sarah" })]),
|
|
5190
|
+
withImages([picture({ role: "avatar", width: 64, height: 64, src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?crop=faces", host: "images.unsplash.com" })])
|
|
5191
|
+
],
|
|
5192
|
+
pass: [makeSnapshot(), withImages([picture(), picture({ role: "avatar", width: 56, height: 56, src: "https://example.test/team/sam.jpg", alt: "Sam, engineer" })])]
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
},
|
|
5196
|
+
{
|
|
5197
|
+
id: "ai-image",
|
|
5198
|
+
name: "Generated image",
|
|
5199
|
+
generation: 2,
|
|
5200
|
+
severity: "warn",
|
|
5201
|
+
surface: "rendered",
|
|
5202
|
+
why: "The file itself says a model made it (its XMP or Content Credentials name a trained-model source). A generated picture stands in for work the client did not photograph, and anyone can read the label.",
|
|
5203
|
+
fix: "Replace it with a photo of the real thing: the work, the premises, the people. Where there is none yet, use fewer pictures.",
|
|
5204
|
+
rendered: {
|
|
5205
|
+
detect: (s) => {
|
|
5206
|
+
const found = (s.images ?? []).filter((i) => i.provenance?.digitalSourceType);
|
|
5207
|
+
if (found.length === 0) return [];
|
|
5208
|
+
const first = found[0].provenance;
|
|
5209
|
+
const where = first.source === "xmp" ? "its XMP metadata" : first.source === "c2pa" ? "its Content Credentials" : "its metadata";
|
|
5210
|
+
return [hit3(s, `${count2(found.length, "image")} labelled as made by a generative model (${first.digitalSourceType} in ${where})`, found[0].src)];
|
|
5211
|
+
},
|
|
5212
|
+
fixtures: {
|
|
5213
|
+
flag: [
|
|
5214
|
+
withImages([picture({ provenance: AI_XMP })]),
|
|
5215
|
+
withImages([picture({ provenance: { digitalSourceType: "compositeWithTrainedAlgorithmicMedia", source: "c2pa", c2pa: true, bytes: 12e4 } })])
|
|
5216
|
+
],
|
|
5217
|
+
// Credentials that say nothing about a model, and an image never read, are not findings.
|
|
5218
|
+
pass: [makeSnapshot(), withImages([picture({ provenance: NOTHING })]), withImages([picture({ provenance: { ...NOTHING, c2pa: true } })]), withImages([picture()])]
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
},
|
|
5222
|
+
{
|
|
5223
|
+
id: "no-real-imagery",
|
|
5224
|
+
name: "No photograph on the page",
|
|
5225
|
+
generation: 1,
|
|
5226
|
+
severity: "warn",
|
|
5227
|
+
surface: "rendered",
|
|
5228
|
+
why: "A page with no photograph at all, only icons and illustrations, is the page a model builds when it has no assets. Icon tiles stand in for the evidence a buyer looks for: the work, the premises, the people.",
|
|
5229
|
+
fix: "Add one real photo of the work where the claim is made: a finished job next to the service, the team next to the booking. One plain photo beats any number of icons.",
|
|
5230
|
+
rendered: {
|
|
5231
|
+
detect: (s) => {
|
|
5232
|
+
if (!s.images || s.sections.length < 4) return [];
|
|
5233
|
+
if (s.images.some((i) => i.role === "photo")) return [];
|
|
5234
|
+
const icons = s.images.filter((i) => i.role === "icon" || i.role === "illustration").length;
|
|
5235
|
+
return [hit3(s, `no photograph on the page${icons ? `: ${count2(icons, "icon or illustration", "icons and illustrations")} instead` : ""}`)];
|
|
5236
|
+
},
|
|
5237
|
+
fixtures: {
|
|
5238
|
+
flag: [
|
|
5239
|
+
withImages([picture({ role: "icon", width: 48, height: 48, src: "https://example.test/icons/wrench.svg" }), picture({ role: "illustration", width: 480, height: 360, src: "https://example.test/hero.svg" })]),
|
|
5240
|
+
withImages([])
|
|
5241
|
+
],
|
|
5242
|
+
// A snapshot with no image record is never judged; nor is a page too short to be a site.
|
|
5243
|
+
pass: [makeSnapshot(), withImages([picture()]), { ...withImages([]), sections: makeSnapshot().sections.slice(0, 3) }]
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
];
|
|
5248
|
+
|
|
5249
|
+
// src/character/check.ts
|
|
5250
|
+
var CATALOGUE_VERSION = "2026.09.11";
|
|
5251
|
+
var CATALOGUE = [
|
|
5252
|
+
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
5253
|
+
...RENDERED_TELLS,
|
|
5254
|
+
...IMAGERY_TELLS
|
|
5255
|
+
];
|
|
5256
|
+
function tellById(id) {
|
|
5257
|
+
return CATALOGUE.find((t) => t.id === id);
|
|
5258
|
+
}
|
|
5259
|
+
function validateExceptions(exceptions) {
|
|
5260
|
+
const applied = [];
|
|
5261
|
+
const rejected = [];
|
|
5262
|
+
for (const exception of exceptions) {
|
|
5263
|
+
if (!tellById(exception.tell)) {
|
|
5264
|
+
rejected.push({ exception, reason: `no tell called "${exception.tell}"` });
|
|
5265
|
+
} else if (typeof exception.because !== "string" || exception.because.trim().length < 12) {
|
|
5266
|
+
rejected.push({ exception, reason: "an exception needs a because of at least a sentence" });
|
|
5267
|
+
} else {
|
|
5268
|
+
applied.push(exception);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
return { applied, rejected };
|
|
5272
|
+
}
|
|
5273
|
+
var MARKER = /\b(?:copy|craft)-ok\b/;
|
|
5274
|
+
function marked(lines, line) {
|
|
5275
|
+
return MARKER.test(lines[line - 1] ?? "") || MARKER.test(lines[line - 2] ?? "");
|
|
5276
|
+
}
|
|
5277
|
+
function toFinding(tell, hit4, texts) {
|
|
5278
|
+
const text = texts.get(hit4.path) ?? "";
|
|
5279
|
+
return {
|
|
5280
|
+
tell: tell.id,
|
|
5281
|
+
name: tell.name,
|
|
5282
|
+
generation: tell.generation,
|
|
5283
|
+
severity: tell.severity,
|
|
5284
|
+
path: hit4.path,
|
|
5285
|
+
line: texts.has(hit4.path) ? lineAt(text, hit4.offset) : 0,
|
|
5286
|
+
excerpt: hit4.excerpt ?? excerptAt(text, hit4.offset),
|
|
5287
|
+
message: hit4.message,
|
|
5288
|
+
fix: tell.fix,
|
|
5289
|
+
...tell.surface === "copy" ? { house: houseRule(tell.id, tell.name).tier } : {}
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
5292
|
+
function report(tells, files, run, options) {
|
|
5293
|
+
const { applied, rejected } = validateExceptions(options.exceptions ?? []);
|
|
5294
|
+
const texts = new Map(files.map((file) => [file.path, file.text]));
|
|
5295
|
+
const selected = options.only ? tells.filter((t) => options.only.includes(t.id)) : tells;
|
|
5296
|
+
const findings = [];
|
|
5297
|
+
const suppressed = [];
|
|
5298
|
+
const lineCache = /* @__PURE__ */ new Map();
|
|
5299
|
+
const linesOf = (path) => {
|
|
5300
|
+
let lines = lineCache.get(path);
|
|
5301
|
+
if (!lines) lineCache.set(path, lines = (texts.get(path) ?? "").split("\n"));
|
|
5302
|
+
return lines;
|
|
5303
|
+
};
|
|
5304
|
+
const excepted = /* @__PURE__ */ new Map();
|
|
5305
|
+
for (const tell of selected) {
|
|
5306
|
+
for (const hit4 of run(tell)) {
|
|
5307
|
+
const exception = applied.find((e) => e.tell === tell.id && (!e.path || hit4.path.includes(e.path)));
|
|
5308
|
+
if (exception) {
|
|
5309
|
+
excepted.set(exception, (excepted.get(exception) ?? 0) + 1);
|
|
5310
|
+
continue;
|
|
5311
|
+
}
|
|
5312
|
+
const finding = toFinding(tell, hit4, texts);
|
|
5313
|
+
if (marked(linesOf(hit4.path), finding.line)) {
|
|
5314
|
+
suppressed.push({ tell: tell.id, path: finding.path, line: finding.line });
|
|
5315
|
+
continue;
|
|
5316
|
+
}
|
|
5317
|
+
findings.push(finding);
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.tell.localeCompare(b.tell));
|
|
5321
|
+
const byGeneration = { 1: 0, 2: 0, 3: 0 };
|
|
5322
|
+
const byTell = {};
|
|
5323
|
+
for (const finding of findings) {
|
|
5324
|
+
byGeneration[finding.generation] += 1;
|
|
5325
|
+
byTell[finding.tell] = (byTell[finding.tell] ?? 0) + 1;
|
|
5326
|
+
}
|
|
5327
|
+
return {
|
|
5328
|
+
catalogueVersion: CATALOGUE_VERSION,
|
|
5329
|
+
findings,
|
|
5330
|
+
rejectedExceptions: rejected,
|
|
5331
|
+
excepted: [...excepted].map(([e, count3]) => ({ tell: e.tell, because: e.because, count: count3 })),
|
|
5332
|
+
suppressed,
|
|
5333
|
+
summary: {
|
|
5334
|
+
files: files.length,
|
|
5335
|
+
findings: findings.length,
|
|
5336
|
+
byGeneration,
|
|
5337
|
+
byTell,
|
|
5338
|
+
blocking: findings.filter((x) => x.severity === "block").length
|
|
5339
|
+
}
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
function applyHouseGate(result) {
|
|
5343
|
+
const findings = result.findings.map((f4) => f4.house === "block" ? { ...f4, severity: "block" } : f4);
|
|
5344
|
+
return { ...result, findings, summary: { ...result.summary, blocking: findings.filter((f4) => f4.severity === "block").length } };
|
|
5345
|
+
}
|
|
5346
|
+
function scanSource(files, options = {}) {
|
|
5347
|
+
const ctx = { files: files.map(parseFile) };
|
|
5348
|
+
const tells = CATALOGUE.filter((t) => t.surface === "source");
|
|
5349
|
+
return report(tells, files, (tell) => tell.surface === "source" ? tell.detect(ctx) : [], options);
|
|
5350
|
+
}
|
|
5351
|
+
function copyContext(files) {
|
|
5352
|
+
return { files, blocks: files.flatMap(extractCopy), strings: files.flatMap(extractStrings) };
|
|
5353
|
+
}
|
|
5354
|
+
function checkCopy(files, options = {}) {
|
|
5355
|
+
const ctx = copyContext(files);
|
|
5356
|
+
const tells = CATALOGUE.filter((t) => t.surface === "copy");
|
|
5357
|
+
return report(tells, files, (tell) => tell.surface === "copy" ? tell.detect(ctx) : [], options);
|
|
5358
|
+
}
|
|
5359
|
+
function auditSnapshot(snapshot, options = {}) {
|
|
5360
|
+
const tells = CATALOGUE.filter((t) => t.rendered);
|
|
5361
|
+
const result = report(tells, [], (tell) => tell.rendered.detect(snapshot), options);
|
|
5362
|
+
result.summary.files = 1;
|
|
5363
|
+
return result;
|
|
5364
|
+
}
|
|
5365
|
+
function runTell(tell, files) {
|
|
5366
|
+
if (tell.surface === "source") return tell.detect({ files: files.map(parseFile) });
|
|
5367
|
+
if (tell.surface === "rendered") throw new Error(`${tell.id} is seen on a rendered page only; run tell.rendered.detect(snapshot)`);
|
|
5368
|
+
return tell.detect(copyContext(files));
|
|
5369
|
+
}
|
|
5370
|
+
function catalogueTable() {
|
|
5371
|
+
const rows = CATALOGUE.map(
|
|
5372
|
+
(t) => `| \`${t.id}\` | ${t.generation} | ${t.surface}${t.rendered ? " + rendered" : ""} | ${t.severity} | ${t.name} | ${t.why.replace(/\|/g, "\\|")} |`
|
|
5373
|
+
);
|
|
5374
|
+
return [
|
|
5375
|
+
`Catalogue version \`${CATALOGUE_VERSION}\`, ${CATALOGUE.length} tells.`,
|
|
5376
|
+
"",
|
|
5377
|
+
"| Id | Gen | Surface | Severity | Tell | Why it is a default |",
|
|
5378
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
5379
|
+
...rows
|
|
5380
|
+
].join("\n");
|
|
5381
|
+
}
|
|
5382
|
+
function wordListsJson() {
|
|
5383
|
+
return JSON.stringify(
|
|
5384
|
+
{
|
|
5385
|
+
catalogueVersion: CATALOGUE_VERSION,
|
|
5386
|
+
lists: {
|
|
5387
|
+
aiWords: AI_WORDS,
|
|
5388
|
+
stockPhrases: STOCK_PHRASES,
|
|
5389
|
+
aiPhrases: AI_PHRASES,
|
|
5390
|
+
plainerWords: PLAINER_WORDS,
|
|
5391
|
+
buzzwords: BUZZWORDS,
|
|
5392
|
+
negativeReassurance: NEGATIVE_REASSURANCE,
|
|
5393
|
+
vagueWords: VAGUE_WORDS,
|
|
5394
|
+
reviewPhrases: REVIEW_PHRASES
|
|
5395
|
+
}
|
|
5396
|
+
},
|
|
5397
|
+
null,
|
|
5398
|
+
2
|
|
5399
|
+
) + "\n";
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
// src/character/format.ts
|
|
5403
|
+
function formatReport(report2, title) {
|
|
5404
|
+
const lines = [];
|
|
5405
|
+
let current = "";
|
|
5406
|
+
for (const f4 of report2.findings) {
|
|
5407
|
+
if (f4.path !== current) {
|
|
5408
|
+
current = f4.path;
|
|
5409
|
+
lines.push("", f4.path);
|
|
5410
|
+
}
|
|
5411
|
+
lines.push(` ${String(f4.line).padStart(4)} ${f4.tell} (gen ${f4.generation}, ${f4.severity}) ${f4.message}`);
|
|
5412
|
+
lines.push(` ${f4.excerpt}`);
|
|
5413
|
+
}
|
|
5414
|
+
const fixes = new Map(report2.findings.map((f4) => [f4.tell, f4.fix]));
|
|
5415
|
+
if (fixes.size > 0) {
|
|
5416
|
+
lines.push("", "What to do instead:");
|
|
5417
|
+
for (const [tell, fix] of fixes) lines.push(` ${tell}: ${fix}`);
|
|
5418
|
+
}
|
|
5419
|
+
for (const r of report2.rejectedExceptions) {
|
|
5420
|
+
lines.push("", `Exception not applied (${r.exception.tell}): ${r.reason}`);
|
|
5421
|
+
}
|
|
5422
|
+
for (const e of report2.excepted) {
|
|
5423
|
+
lines.push("", `Excepted: ${e.count} \xD7 ${e.tell}, because ${e.because}`);
|
|
5424
|
+
}
|
|
5425
|
+
if (report2.suppressed.length > 0) {
|
|
5426
|
+
lines.push("", `Suppressed: ${report2.suppressed.length} marked copy-ok or craft-ok (${report2.suppressed.map((s) => `${s.path}:${s.line}`).join(", ")})`);
|
|
5427
|
+
}
|
|
5428
|
+
const { summary } = report2;
|
|
5429
|
+
const gens = `gen 1: ${summary.byGeneration[1]}, gen 2: ${summary.byGeneration[2]}, gen 3: ${summary.byGeneration[3]}`;
|
|
5430
|
+
lines.push(
|
|
5431
|
+
"",
|
|
5432
|
+
`${title}: ${summary.findings} finding${summary.findings === 1 ? "" : "s"} in ${summary.files} file${summary.files === 1 ? "" : "s"} (${gens}). Catalogue ${report2.catalogueVersion}.`
|
|
5433
|
+
);
|
|
4571
5434
|
return lines.join("\n").replace(/^\n/, "");
|
|
4572
5435
|
}
|
|
4573
5436
|
|
|
@@ -4954,101 +5817,110 @@ function toJson(data) {
|
|
|
4954
5817
|
return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
|
|
4955
5818
|
}
|
|
4956
5819
|
|
|
4957
|
-
// src/
|
|
4958
|
-
var
|
|
4959
|
-
var
|
|
4960
|
-
var
|
|
4961
|
-
var
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
for (const c of s.controls) {
|
|
4968
|
-
const o = oklch(c.background);
|
|
4969
|
-
if (o && o.c >= CHROMATIC) buttons.set(c.background, (buttons.get(c.background) ?? 0) + 1);
|
|
5820
|
+
// src/snapshot/provenance.ts
|
|
5821
|
+
var PROVENANCE_SCAN_BYTES = 256 * 1024;
|
|
5822
|
+
var AI_SOURCE_TYPES = ["compositeWithTrainedAlgorithmicMedia", "trainedAlgorithmicMedia"];
|
|
5823
|
+
var MARKER2 = new RegExp(`(?:${AI_SOURCE_TYPES.join("|")})`);
|
|
5824
|
+
var C2PA_BOX = /jumd[\s\S]{16,24}c2pa/;
|
|
5825
|
+
var C2PA_PNG = /caBX/;
|
|
5826
|
+
function latin1(bytes) {
|
|
5827
|
+
let out = "";
|
|
5828
|
+
for (let i = 0; i < bytes.length; i += 8192) {
|
|
5829
|
+
out += String.fromCharCode(...bytes.subarray(i, Math.min(bytes.length, i + 8192)));
|
|
4970
5830
|
}
|
|
4971
|
-
|
|
4972
|
-
if (topButton) return oklch(topButton[0]);
|
|
4973
|
-
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC && x.w >= MIN_ACCENT_AREA).sort((a, b) => b.w - a.w)[0];
|
|
4974
|
-
if (bg) return bg.o;
|
|
4975
|
-
const text = s.colours.text.map((t) => ({ o: oklch(t.value), w: t.chars })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
4976
|
-
return text ? text.o : null;
|
|
5831
|
+
return out;
|
|
4977
5832
|
}
|
|
4978
|
-
function
|
|
4979
|
-
const
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5833
|
+
function xmpPackets(text) {
|
|
5834
|
+
const out = [];
|
|
5835
|
+
const open = /<x:xmpmeta\b|<\?xpacket begin/g;
|
|
5836
|
+
let m;
|
|
5837
|
+
while (m = open.exec(text)) {
|
|
5838
|
+
const close = text.indexOf(m[0].startsWith("<x:") ? "</x:xmpmeta>" : "<?xpacket end", m.index);
|
|
5839
|
+
out.push([m.index, close === -1 ? text.length : close]);
|
|
5840
|
+
if (close === -1) break;
|
|
5841
|
+
open.lastIndex = close;
|
|
5842
|
+
}
|
|
5843
|
+
return out;
|
|
4987
5844
|
}
|
|
4988
|
-
function
|
|
4989
|
-
|
|
4990
|
-
const
|
|
4991
|
-
const
|
|
4992
|
-
|
|
5845
|
+
function scanProvenance(bytes) {
|
|
5846
|
+
const head = bytes.subarray(0, PROVENANCE_SCAN_BYTES);
|
|
5847
|
+
const text = latin1(head);
|
|
5848
|
+
const c2pa = C2PA_BOX.test(text) || text.startsWith("\x89PNG") && C2PA_PNG.test(text);
|
|
5849
|
+
const m = MARKER2.exec(text);
|
|
5850
|
+
if (!m) return { digitalSourceType: null, source: null, c2pa, bytes: head.length };
|
|
5851
|
+
const inXmp = xmpPackets(text).some(([a, b]) => m.index >= a && m.index < b);
|
|
5852
|
+
return { digitalSourceType: m[0], source: inXmp ? "xmp" : c2pa ? "c2pa" : "other", c2pa, bytes: head.length };
|
|
4993
5853
|
}
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
if (
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
if (
|
|
5004
|
-
|
|
5005
|
-
return {
|
|
5006
|
-
version: FINGERPRINT_VERSION,
|
|
5007
|
-
accent: accentOf(s),
|
|
5008
|
-
ground: oklch(s.ground) ?? { l: 1, c: 0, h: 0 },
|
|
5009
|
-
...faces(s),
|
|
5010
|
-
roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
|
|
5011
|
-
motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
|
|
5012
|
-
effects: effects.sort(),
|
|
5013
|
-
layout: s.sections.map((x) => x.kind)
|
|
5014
|
-
};
|
|
5854
|
+
|
|
5855
|
+
// src/snapshot/migrate.ts
|
|
5856
|
+
var READABLE_SNAPSHOT_VERSIONS = [1, 2];
|
|
5857
|
+
function readSnapshot(data, source = "snapshot") {
|
|
5858
|
+
const v = data?.version;
|
|
5859
|
+
if (typeof data !== "object" || data === null || !READABLE_SNAPSHOT_VERSIONS.includes(v)) {
|
|
5860
|
+
throw new Error(`${source} is not a snapshot this craft reads (version ${String(v)}; reads ${READABLE_SNAPSHOT_VERSIONS.join(" and ")})`);
|
|
5861
|
+
}
|
|
5862
|
+
const snap = data;
|
|
5863
|
+
if (snap.version === 1) return { ...snap, version: SNAPSHOT_VERSION, migratedFrom: 1 };
|
|
5864
|
+
return snap;
|
|
5015
5865
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5866
|
+
|
|
5867
|
+
// src/snapshot/visual.ts
|
|
5868
|
+
var EDGE_STEP = 32;
|
|
5869
|
+
function visualMeasures(img, maxSamples = 1e6) {
|
|
5870
|
+
const { width: w, height: h, rgba } = img;
|
|
5871
|
+
if (w < 2 || h < 2 || rgba.length < w * h * 4) throw new Error(`visualMeasures: ${w}x${h} image with ${rgba.length} bytes`);
|
|
5872
|
+
const step = Math.max(1, Math.ceil(Math.sqrt(w * h / maxSamples)));
|
|
5873
|
+
const lum = (x, y) => {
|
|
5874
|
+
const i = (y * w + x) * 4;
|
|
5875
|
+
return 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
5876
|
+
};
|
|
5877
|
+
let n = 0;
|
|
5878
|
+
let sumRg = 0;
|
|
5879
|
+
let sumYb = 0;
|
|
5880
|
+
let sumRg2 = 0;
|
|
5881
|
+
let sumYb2 = 0;
|
|
5882
|
+
let edges = 0;
|
|
5883
|
+
let edgeN = 0;
|
|
5884
|
+
let asym = 0;
|
|
5885
|
+
let asymN = 0;
|
|
5886
|
+
for (let y = 0; y < h; y += step) {
|
|
5887
|
+
for (let x = 0; x < w; x += step) {
|
|
5888
|
+
const i = (y * w + x) * 4;
|
|
5889
|
+
const r = rgba[i];
|
|
5890
|
+
const g = rgba[i + 1];
|
|
5891
|
+
const b = rgba[i + 2];
|
|
5892
|
+
const rg = r - g;
|
|
5893
|
+
const yb = 0.5 * (r + g) - b;
|
|
5894
|
+
sumRg += rg;
|
|
5895
|
+
sumYb += yb;
|
|
5896
|
+
sumRg2 += rg * rg;
|
|
5897
|
+
sumYb2 += yb * yb;
|
|
5898
|
+
n += 1;
|
|
5899
|
+
const l = lum(x, y);
|
|
5900
|
+
if (x + step < w && y + step < h) {
|
|
5901
|
+
if (Math.abs(lum(x + step, y) - l) + Math.abs(lum(x, y + step) - l) > EDGE_STEP) edges += 1;
|
|
5902
|
+
edgeN += 1;
|
|
5903
|
+
}
|
|
5904
|
+
if (x < w / 2) {
|
|
5905
|
+
asym += Math.abs(l - lum(w - 1 - x, y));
|
|
5906
|
+
asymN += 1;
|
|
5907
|
+
}
|
|
5026
5908
|
}
|
|
5027
5909
|
}
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
const shared = a.effects.filter((e) => b.effects.includes(e)).length;
|
|
5043
|
-
const effects = union.size === 0 ? 0 : 1 - shared / union.size;
|
|
5044
|
-
const longest = Math.max(a.layout.length, b.layout.length);
|
|
5045
|
-
const layout = longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
|
|
5046
|
-
const parts2 = { accent, ground, type, shape, motion, effects, layout };
|
|
5047
|
-
const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
|
|
5048
|
-
return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
|
|
5049
|
-
}
|
|
5050
|
-
function nearest(target2, set, k = set.length) {
|
|
5051
|
-
return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target2, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
|
|
5910
|
+
const meanRg = sumRg / n;
|
|
5911
|
+
const meanYb = sumYb / n;
|
|
5912
|
+
const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
|
|
5913
|
+
const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
|
|
5914
|
+
const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
|
|
5915
|
+
const round2 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
5916
|
+
return {
|
|
5917
|
+
colourfulness: round2(colourfulness, 1),
|
|
5918
|
+
edgeDensity: round2(edgeN ? edges / edgeN : 0),
|
|
5919
|
+
symmetry: round2(asymN ? 1 - asym / asymN / 255 : 1),
|
|
5920
|
+
width: w,
|
|
5921
|
+
height: h,
|
|
5922
|
+
sampled: n
|
|
5923
|
+
};
|
|
5052
5924
|
}
|
|
5053
5925
|
|
|
5054
5926
|
// src/direction/types.ts
|
|
@@ -5070,13 +5942,124 @@ var SOURCE_KINDS = [
|
|
|
5070
5942
|
];
|
|
5071
5943
|
var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
|
|
5072
5944
|
|
|
5945
|
+
// src/direction/licences.ts
|
|
5946
|
+
var LICENCES = [
|
|
5947
|
+
{
|
|
5948
|
+
name: "Basteleur",
|
|
5949
|
+
subject: "face",
|
|
5950
|
+
licence: "OFL-1.1",
|
|
5951
|
+
multiClient: "yes",
|
|
5952
|
+
checked: "2026-09-24",
|
|
5953
|
+
source: "https://velvetyne.fr/fonts/basteleur/",
|
|
5954
|
+
note: "Velvetyne, designed by Keussel. Keep the OFL notice with the font files."
|
|
5955
|
+
},
|
|
5956
|
+
{
|
|
5957
|
+
name: "Gulax",
|
|
5958
|
+
subject: "face",
|
|
5959
|
+
licence: "OFL-1.1",
|
|
5960
|
+
multiClient: "yes",
|
|
5961
|
+
checked: "2026-09-24",
|
|
5962
|
+
source: "https://velvetyne.fr/fonts/gulax/",
|
|
5963
|
+
note: "Velvetyne. Keep the OFL notice with the font files."
|
|
5964
|
+
},
|
|
5965
|
+
{
|
|
5966
|
+
name: "Le Murmure",
|
|
5967
|
+
subject: "face",
|
|
5968
|
+
licence: "OFL-1.1",
|
|
5969
|
+
multiClient: "yes",
|
|
5970
|
+
checked: "2026-09-24",
|
|
5971
|
+
source: "https://velvetyne.fr/fonts/le-murmure/",
|
|
5972
|
+
note: "Velvetyne. Keep the OFL notice with the font files."
|
|
5973
|
+
},
|
|
5974
|
+
{
|
|
5975
|
+
name: "Blaze Type",
|
|
5976
|
+
subject: "foundry",
|
|
5977
|
+
licence: "commercial",
|
|
5978
|
+
multiClient: "per-site",
|
|
5979
|
+
cap: 10,
|
|
5980
|
+
checked: "2026-09-24",
|
|
5981
|
+
source: "https://blazetype.eu/eula",
|
|
5982
|
+
note: "The EULA sells a professional licence for 1 to 10 websites, and lets an agency buy in a client's name at the listed price. Its /license page says one licence covers every domain; the EULA is the contract, so buy per client."
|
|
5983
|
+
},
|
|
5984
|
+
{
|
|
5985
|
+
name: "Power Type",
|
|
5986
|
+
subject: "foundry",
|
|
5987
|
+
licence: "commercial",
|
|
5988
|
+
multiClient: "unverified",
|
|
5989
|
+
checked: "2026-09-24",
|
|
5990
|
+
source: "https://power-type.com/eula",
|
|
5991
|
+
note: "Research on 2026-09-11 reported a Super License capped at 10 self-hosted domains. The EULA page did not load when checked; read it before relying on the cap."
|
|
5992
|
+
},
|
|
5993
|
+
{
|
|
5994
|
+
name: "Grilli Type",
|
|
5995
|
+
subject: "foundry",
|
|
5996
|
+
licence: "commercial",
|
|
5997
|
+
multiClient: "per-site",
|
|
5998
|
+
checked: "2026-09-24",
|
|
5999
|
+
source: "https://www.grillitype.com/information",
|
|
6000
|
+
note: "Web licences cover any number of domains controlled by the licensee, priced by monthly visitors, so a client site needs the client's own licence. Unlimited tiers start at $10,000 per style."
|
|
6001
|
+
},
|
|
6002
|
+
{
|
|
6003
|
+
name: "Phosphor",
|
|
6004
|
+
subject: "icons",
|
|
6005
|
+
licence: "MIT",
|
|
6006
|
+
multiClient: "yes",
|
|
6007
|
+
checked: "2026-09-24",
|
|
6008
|
+
source: "https://github.com/phosphor-icons/core/blob/main/LICENSE",
|
|
6009
|
+
note: "Six weights. The icon-tile tells treat it exactly as they treat Lucide: the library is not the tell, the tinted tile is."
|
|
6010
|
+
},
|
|
6011
|
+
{
|
|
6012
|
+
name: "Tabler Icons",
|
|
6013
|
+
subject: "icons",
|
|
6014
|
+
licence: "MIT",
|
|
6015
|
+
multiClient: "yes",
|
|
6016
|
+
checked: "2026-09-24",
|
|
6017
|
+
source: "https://github.com/tabler/tabler-icons/blob/main/LICENSE",
|
|
6018
|
+
note: "One stroke grid across the set."
|
|
6019
|
+
},
|
|
6020
|
+
{
|
|
6021
|
+
name: "Iconoir",
|
|
6022
|
+
subject: "icons",
|
|
6023
|
+
licence: "MIT",
|
|
6024
|
+
multiClient: "yes",
|
|
6025
|
+
checked: "2026-09-24",
|
|
6026
|
+
source: "https://github.com/iconoir-icons/iconoir/blob/main/LICENSE",
|
|
6027
|
+
note: "Smaller set, no paid tier."
|
|
6028
|
+
},
|
|
6029
|
+
{
|
|
6030
|
+
name: "Unicons",
|
|
6031
|
+
subject: "icons",
|
|
6032
|
+
licence: "IconScout Simple License",
|
|
6033
|
+
multiClient: "yes",
|
|
6034
|
+
checked: "2026-09-24",
|
|
6035
|
+
source: "https://github.com/Iconscout/unicons/blob/master/LICENSE",
|
|
6036
|
+
note: "IconScout's free set, personal and commercial use, attribution optional."
|
|
6037
|
+
},
|
|
6038
|
+
{
|
|
6039
|
+
name: "IconScout premium",
|
|
6040
|
+
subject: "icons",
|
|
6041
|
+
licence: "commercial",
|
|
6042
|
+
multiClient: "per-site",
|
|
6043
|
+
checked: "2026-09-24",
|
|
6044
|
+
source: "https://iconscout.com/licenses",
|
|
6045
|
+
note: "Each licence covers a single end product. Never in a tool, template or shared kit (create-site, craft, a house component library), even modified. Record the assets each site uses. Assets come from different contributors, so take one site's set from one contributor."
|
|
6046
|
+
}
|
|
6047
|
+
];
|
|
6048
|
+
function faceLicence(family) {
|
|
6049
|
+
const name = family.trim().toLowerCase();
|
|
6050
|
+
return LICENCES.find((l) => l.subject === "face" && l.name.toLowerCase() === name);
|
|
6051
|
+
}
|
|
6052
|
+
function licencesJson() {
|
|
6053
|
+
return JSON.stringify({ licences: LICENCES }, null, 2) + "\n";
|
|
6054
|
+
}
|
|
6055
|
+
|
|
5073
6056
|
// src/direction/validate.ts
|
|
5074
6057
|
var PREFERENCE = /\b(?:client (?:likes|liked|wants|wanted|asked|chose|prefers?)|(?:we|they|i) (?:like|liked|love|loved|prefer)|on[- ]trend|trendy|popular|what (?:everyone|competitors) (?:uses?|does)|brand (?:colou?r|font|guidelines?) (?:is|are|says?))\b/i;
|
|
5075
6058
|
var MOOD = /\b(?:modern|clean|fresh|professional|trustworthy|premium|elegant|sleek|timeless|minimal(?:ist)?|bold|vibrant|friendly|approachable|luxur(?:y|ious)|sophisticated|contemporary|stylish|warm|inviting|high[- ]end)\b/gi;
|
|
5076
6059
|
var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
|
|
5077
|
-
var
|
|
6060
|
+
var STOP2 = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every"]);
|
|
5078
6061
|
function words(text) {
|
|
5079
|
-
return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !
|
|
6062
|
+
return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !STOP2.has(w) && !VALUE_WORDS.has(w)));
|
|
5080
6063
|
}
|
|
5081
6064
|
function tellFor(key, value) {
|
|
5082
6065
|
if (key === "accent") {
|
|
@@ -5193,18 +6176,30 @@ function validateDirection(input, ctx = {}) {
|
|
|
5193
6176
|
ok = false;
|
|
5194
6177
|
}
|
|
5195
6178
|
}
|
|
6179
|
+
if ((key === "display" || key === "body") && typeof c.value === "string" && c.value.trim()) {
|
|
6180
|
+
const family = normaliseFamily(c.value);
|
|
6181
|
+
const entry = faceLicence(family);
|
|
6182
|
+
if (!entry) {
|
|
6183
|
+
warn(`${at}.value`, `${family}: licence not in craft's register. Read the licensor's own terms for use on this client's site, then add it to src/direction/licences.ts.`);
|
|
6184
|
+
} else if (entry.multiClient === "unverified") {
|
|
6185
|
+
warn(`${at}.value`, `${family}: licence unverified (${entry.note})`);
|
|
6186
|
+
} else if (entry.multiClient === "capped" || entry.multiClient === "per-site") {
|
|
6187
|
+
const scope = entry.multiClient === "capped" ? `covers up to ${entry.cap} sites` : "needs a licence for this site, in the client's name";
|
|
6188
|
+
warn(`${at}.value`, `${family}: ${entry.licence} licence ${scope}. Record the purchase before launch.`);
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
5196
6191
|
const fp = ctx.fingerprint;
|
|
5197
6192
|
if (fp && typeof c.value === "string") {
|
|
5198
6193
|
if ((key === "accent" || key === "ground") && parseColour(c.value)) {
|
|
5199
6194
|
const declared = parseColour(c.value).oklch;
|
|
5200
|
-
const
|
|
5201
|
-
if (
|
|
5202
|
-
warn(`${at}.value`, `the page does not show it: its ${key} measures oklch(${
|
|
6195
|
+
const measured2 = key === "accent" ? fp.accent : fp.ground;
|
|
6196
|
+
if (measured2 && deltaEOk(declared, measured2) > 0.08) {
|
|
6197
|
+
warn(`${at}.value`, `the page does not show it: its ${key} measures oklch(${measured2.l.toFixed(2)} ${measured2.c.toFixed(3)} ${measured2.h.toFixed(0)})`);
|
|
5203
6198
|
}
|
|
5204
6199
|
}
|
|
5205
6200
|
if (key === "display" || key === "body") {
|
|
5206
|
-
const
|
|
5207
|
-
if (normaliseFamily(c.value).toLowerCase() !==
|
|
6201
|
+
const measured2 = key === "display" ? fp.display.family : fp.body.family;
|
|
6202
|
+
if (normaliseFamily(c.value).toLowerCase() !== measured2.toLowerCase()) warn(`${at}.value`, `the page sets ${measured2}, not ${c.value}`);
|
|
5208
6203
|
}
|
|
5209
6204
|
}
|
|
5210
6205
|
if (ok) decided += 1;
|
|
@@ -5215,8 +6210,8 @@ function validateDirection(input, ctx = {}) {
|
|
|
5215
6210
|
// src/direction/init.ts
|
|
5216
6211
|
var hex = (o) => {
|
|
5217
6212
|
const { r, g, b } = oklchToRgb(o);
|
|
5218
|
-
const
|
|
5219
|
-
return formatHex({ r:
|
|
6213
|
+
const clamp3 = (n) => Math.min(1, Math.max(0, n));
|
|
6214
|
+
return formatHex({ r: clamp3(r), g: clamp3(g), b: clamp3(b) });
|
|
5220
6215
|
};
|
|
5221
6216
|
function initDirection(input) {
|
|
5222
6217
|
const fp = input.current;
|
|
@@ -5356,240 +6351,6 @@ function propose(input) {
|
|
|
5356
6351
|
};
|
|
5357
6352
|
}
|
|
5358
6353
|
|
|
5359
|
-
// src/null/index.ts
|
|
5360
|
-
var NULL_VERSION = 1;
|
|
5361
|
-
function nullPrompt(brief) {
|
|
5362
|
-
return [
|
|
5363
|
-
`Build the home page for this business: ${brief.trim()}`,
|
|
5364
|
-
"",
|
|
5365
|
-
"Write it as one complete, self-contained HTML file. You may load fonts and libraries from a CDN. Use real copy, not placeholder text. Reply with the HTML only."
|
|
5366
|
-
].join("\n");
|
|
5367
|
-
}
|
|
5368
|
-
function extractHtml(reply) {
|
|
5369
|
-
const start = reply.search(/<!doctype html|<html[\s>]/i);
|
|
5370
|
-
if (start < 0) return null;
|
|
5371
|
-
const end = reply.toLowerCase().lastIndexOf("</html>");
|
|
5372
|
-
const html = end > start ? reply.slice(start, end + "</html>".length) : reply.slice(start).replace(/\s*```\s*$/, "");
|
|
5373
|
-
return html.length > 200 ? html : null;
|
|
5374
|
-
}
|
|
5375
|
-
var MIN_RUNS = 5;
|
|
5376
|
-
var TYPICAL_AT = 0.1;
|
|
5377
|
-
function meanNearest(distances, k) {
|
|
5378
|
-
const sorted = [...distances].sort((a, b) => a - b).slice(0, k);
|
|
5379
|
-
return sorted.reduce((s, d) => s + d, 0) / sorted.length;
|
|
5380
|
-
}
|
|
5381
|
-
function median2(values) {
|
|
5382
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
5383
|
-
const mid = Math.floor(sorted.length / 2);
|
|
5384
|
-
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
5385
|
-
}
|
|
5386
|
-
function typicality(target2, runs, k = 3) {
|
|
5387
|
-
if (runs.length < MIN_RUNS) throw new Error(`a null model needs at least ${MIN_RUNS} runs; this one has ${runs.length}`);
|
|
5388
|
-
const kk = Math.min(k, runs.length - 1);
|
|
5389
|
-
const toTarget = runs.map((r) => ({ id: r.id, distance: fingerprintDistance(target2, r.fingerprint).total }));
|
|
5390
|
-
const distance = meanNearest(
|
|
5391
|
-
toTarget.map((t) => t.distance),
|
|
5392
|
-
kk
|
|
5393
|
-
);
|
|
5394
|
-
const loo = runs.map(
|
|
5395
|
-
(r, i) => meanNearest(
|
|
5396
|
-
runs.filter((_, j) => j !== i).map((o) => fingerprintDistance(r.fingerprint, o.fingerprint).total),
|
|
5397
|
-
kk
|
|
5398
|
-
)
|
|
5399
|
-
);
|
|
5400
|
-
const score = loo.filter((d) => d >= distance).length / loo.length;
|
|
5401
|
-
return {
|
|
5402
|
-
score: Math.round(score * 100) / 100,
|
|
5403
|
-
typical: score >= TYPICAL_AT,
|
|
5404
|
-
distance: Math.round(distance * 1e3) / 1e3,
|
|
5405
|
-
baseline: Math.round(median2(loo) * 1e3) / 1e3,
|
|
5406
|
-
nearest: toTarget.sort((a, b) => a.distance - b.distance).slice(0, kk),
|
|
5407
|
-
shared: sharedChoices(target2, runs)
|
|
5408
|
-
};
|
|
5409
|
-
}
|
|
5410
|
-
var HUES = [
|
|
5411
|
-
["red", 0, 45],
|
|
5412
|
-
["orange", 45, 75],
|
|
5413
|
-
["yellow", 75, 110],
|
|
5414
|
-
["green", 110, 165],
|
|
5415
|
-
["teal", 165, 215],
|
|
5416
|
-
["blue", 215, 265],
|
|
5417
|
-
["violet", 265, 315],
|
|
5418
|
-
["pink", 315, 360]
|
|
5419
|
-
];
|
|
5420
|
-
function hueName(o) {
|
|
5421
|
-
if (!o) return "none";
|
|
5422
|
-
if (o.c < 0.03) return "neutral";
|
|
5423
|
-
return HUES.find(([, lo, hi]) => o.h >= lo && o.h < hi)?.[0] ?? "red";
|
|
5424
|
-
}
|
|
5425
|
-
function groundName(o) {
|
|
5426
|
-
if (o.l < 0.3) return "dark";
|
|
5427
|
-
if (isCream(o)) return "cream";
|
|
5428
|
-
if (o.l >= 0.97 && o.c < 0.012) return "white";
|
|
5429
|
-
return o.c < 0.02 ? "grey" : "tinted";
|
|
5430
|
-
}
|
|
5431
|
-
function shapeName(roundness) {
|
|
5432
|
-
if (roundness === null) return "no buttons";
|
|
5433
|
-
if (roundness >= 0.45) return "pill";
|
|
5434
|
-
return roundness >= 0.15 ? "soft" : "square";
|
|
5435
|
-
}
|
|
5436
|
-
function choicesOf(fp) {
|
|
5437
|
-
return {
|
|
5438
|
-
display: [fp.display.family],
|
|
5439
|
-
body: [fp.body.family],
|
|
5440
|
-
accent: [hueName(fp.accent)],
|
|
5441
|
-
ground: [groundName(fp.ground)],
|
|
5442
|
-
shape: [shapeName(fp.roundness)],
|
|
5443
|
-
motion: [fp.motion >= 0.6 ? "reveals most sections" : fp.motion > 0 ? "reveals some sections" : "no scroll reveals"],
|
|
5444
|
-
effects: fp.effects,
|
|
5445
|
-
opening: [fp.layout.slice(0, 3).join(" > ") || "none"]
|
|
5446
|
-
};
|
|
5447
|
-
}
|
|
5448
|
-
function sharedChoices(target2, runs) {
|
|
5449
|
-
const mine = choicesOf(target2);
|
|
5450
|
-
const all = runs.map((r) => choicesOf(r.fingerprint));
|
|
5451
|
-
const out = [];
|
|
5452
|
-
for (const [part, values] of Object.entries(mine)) {
|
|
5453
|
-
for (const value of values) {
|
|
5454
|
-
const count2 = all.filter((c) => c[part].includes(value)).length;
|
|
5455
|
-
if (count2 / runs.length >= 0.25) out.push({ part, value, runs: count2, of: runs.length });
|
|
5456
|
-
}
|
|
5457
|
-
}
|
|
5458
|
-
return out.sort((a, b) => b.runs - a.runs);
|
|
5459
|
-
}
|
|
5460
|
-
var REFLEX_1 = new Set(REFLEX_FONTS_1.map((f4) => f4.toLowerCase()));
|
|
5461
|
-
var REFLEX_2 = new Set(REFLEX_FONTS_2.map((f4) => f4.toLowerCase()));
|
|
5462
|
-
function knownFor(kind, value, sample) {
|
|
5463
|
-
if (kind === "face") return REFLEX_1.has(value.toLowerCase()) ? "reflex-font" : REFLEX_2.has(value.toLowerCase()) ? "reflex-font-2" : null;
|
|
5464
|
-
if (kind === "accent" && value === "violet") return sample.some((f4) => f4.accent && isAiViolet(f4.accent)) ? "ai-violet" : null;
|
|
5465
|
-
if (kind === "ground" && value === "cream") return "cream-palette";
|
|
5466
|
-
if (kind === "shape" && value === "pill") return "pill-everything";
|
|
5467
|
-
if (kind === "motion" && value === "reveals most sections") return "reveal-everywhere";
|
|
5468
|
-
if (kind === "tell") return value;
|
|
5469
|
-
if (kind === "opening" && /^hero > (?:logos|stats)/.test(value)) return "hero-then-proof";
|
|
5470
|
-
return null;
|
|
5471
|
-
}
|
|
5472
|
-
var STOP2 = new Set(
|
|
5473
|
-
"a an the and or but of to in on for with at by from as is are be we our you your us it its this that these those will can all more get has have not no any".split(" ")
|
|
5474
|
-
);
|
|
5475
|
-
function grams(text) {
|
|
5476
|
-
const out = /* @__PURE__ */ new Set();
|
|
5477
|
-
for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
|
|
5478
|
-
const words2 = clause.match(/[a-z][a-z'-]*/g) ?? [];
|
|
5479
|
-
for (let n = 2; n <= 4; n += 1) {
|
|
5480
|
-
for (let i = 0; i + n <= words2.length; i += 1) {
|
|
5481
|
-
const g = words2.slice(i, i + n);
|
|
5482
|
-
if (STOP2.has(g[0]) || STOP2.has(g[n - 1])) continue;
|
|
5483
|
-
if (g.filter((w) => !STOP2.has(w)).length < 2) continue;
|
|
5484
|
-
out.add(g.join(" "));
|
|
5485
|
-
}
|
|
5486
|
-
}
|
|
5487
|
-
}
|
|
5488
|
-
return out;
|
|
5489
|
-
}
|
|
5490
|
-
var GRAMMAR = /* @__PURE__ */ new Set([
|
|
5491
|
-
"privacy policy",
|
|
5492
|
-
"cookie policy",
|
|
5493
|
-
"rights reserved",
|
|
5494
|
-
"all rights reserved",
|
|
5495
|
-
"terms of service",
|
|
5496
|
-
"terms and conditions",
|
|
5497
|
-
"terms conditions",
|
|
5498
|
-
"how it works",
|
|
5499
|
-
"what we do",
|
|
5500
|
-
"about us",
|
|
5501
|
-
"contact us",
|
|
5502
|
-
"get in touch",
|
|
5503
|
-
"opening hours",
|
|
5504
|
-
"read more",
|
|
5505
|
-
"learn more",
|
|
5506
|
-
"book now",
|
|
5507
|
-
"free quote",
|
|
5508
|
-
"our services",
|
|
5509
|
-
"quick links",
|
|
5510
|
-
"email address",
|
|
5511
|
-
"phone number",
|
|
5512
|
-
"full name",
|
|
5513
|
-
"first name",
|
|
5514
|
-
"last name",
|
|
5515
|
-
"your name",
|
|
5516
|
-
"your email",
|
|
5517
|
-
"send message"
|
|
5518
|
-
]);
|
|
5519
|
-
var LEGAL = /©|©|\bregistered (?:in|office|number)\b|\bcompany (?:no|number|reg)|\bvat (?:no|number|reg)/i;
|
|
5520
|
-
var NOISE = /\S+@\S+|https?:\/\/\S+|\b[\w-]+(?:\.[\w-]+)*\.(?:co\.uk|com|org|net|uk|io)\b|\b\d{1,2}(?:[:.]\d{2})?\s*(?:am|pm)\b|\d+/gi;
|
|
5521
|
-
function textGrams(text) {
|
|
5522
|
-
if (LEGAL.test(text)) return /* @__PURE__ */ new Set();
|
|
5523
|
-
const out = grams(text.replace(NOISE, " | "));
|
|
5524
|
-
for (const g of out) if (GRAMMAR.has(g)) out.delete(g);
|
|
5525
|
-
return out;
|
|
5526
|
-
}
|
|
5527
|
-
function properNouns(brief) {
|
|
5528
|
-
return new Set((brief.match(/\b[A-Z][a-z]+/g) ?? []).map((w) => w.toLowerCase()).filter((w) => !STOP2.has(w)));
|
|
5529
|
-
}
|
|
5530
|
-
function harvest(models, options = {}) {
|
|
5531
|
-
const minShare = options.minShare ?? 0.25;
|
|
5532
|
-
const runs = models.flatMap((m, b) => m.runs.map((r) => ({ ...r, brief: b })));
|
|
5533
|
-
const of = runs.length;
|
|
5534
|
-
if (of === 0) return [];
|
|
5535
|
-
const multiBrief = models.length > 1;
|
|
5536
|
-
const sample = runs.map((r) => r.fingerprint);
|
|
5537
|
-
const tally = /* @__PURE__ */ new Map();
|
|
5538
|
-
const add = (kind, value, run, brief, example) => {
|
|
5539
|
-
const key = `${kind}\0${value}`;
|
|
5540
|
-
const t = tally.get(key) ?? { kind, value, runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example };
|
|
5541
|
-
t.runs.add(run);
|
|
5542
|
-
t.briefs.add(brief);
|
|
5543
|
-
tally.set(key, t);
|
|
5544
|
-
};
|
|
5545
|
-
runs.forEach((r, i) => {
|
|
5546
|
-
const c = choicesOf(r.fingerprint);
|
|
5547
|
-
for (const f4 of /* @__PURE__ */ new Set([...c.display, ...c.body])) add("face", f4, i, r.brief);
|
|
5548
|
-
add("accent", c.accent[0], i, r.brief);
|
|
5549
|
-
add("ground", c.ground[0], i, r.brief);
|
|
5550
|
-
add("shape", c.shape[0], i, r.brief);
|
|
5551
|
-
add("motion", c.motion[0], i, r.brief);
|
|
5552
|
-
add("opening", c.opening[0], i, r.brief);
|
|
5553
|
-
for (const t of r.tells) add("tell", t, i, r.brief);
|
|
5554
|
-
});
|
|
5555
|
-
const out = [];
|
|
5556
|
-
for (const t of tally.values()) {
|
|
5557
|
-
if (t.runs.size / of < minShare) continue;
|
|
5558
|
-
if (t.kind === "accent" && (t.value === "none" || t.value === "neutral")) continue;
|
|
5559
|
-
if (t.kind === "shape" && t.value === "no buttons") continue;
|
|
5560
|
-
if (t.kind === "ground" && (t.value === "white" || t.value === "grey")) continue;
|
|
5561
|
-
if (t.kind === "motion" && t.value === "no scroll reveals") continue;
|
|
5562
|
-
out.push({ kind: t.kind, value: t.value, runs: t.runs.size, of, briefs: t.briefs.size, known: knownFor(t.kind, t.value, sample) });
|
|
5563
|
-
}
|
|
5564
|
-
const phraseMin = Math.max(3, Math.ceil(of * minShare * 0.4));
|
|
5565
|
-
const names = models.map((m) => properNouns(m.brief));
|
|
5566
|
-
const phrases2 = /* @__PURE__ */ new Map();
|
|
5567
|
-
runs.forEach((r, i) => {
|
|
5568
|
-
for (const text of r.copy) {
|
|
5569
|
-
for (const g of textGrams(text)) {
|
|
5570
|
-
if (g.split(" ").some((w) => names[r.brief].has(w))) continue;
|
|
5571
|
-
const p = phrases2.get(g) ?? { runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example: text.trim().slice(0, 140) };
|
|
5572
|
-
p.runs.add(i);
|
|
5573
|
-
p.briefs.add(r.brief);
|
|
5574
|
-
phrases2.set(g, p);
|
|
5575
|
-
}
|
|
5576
|
-
}
|
|
5577
|
-
});
|
|
5578
|
-
const kept = [...phrases2].filter(([, p]) => p.runs.size >= phraseMin && (!multiBrief || p.briefs.size >= 2)).sort((a, b) => b[1].runs.size - a[1].runs.size || b[0].length - a[0].length);
|
|
5579
|
-
const maximal = kept.filter(([g, p]) => !kept.some(([h, q]) => h !== g && h.includes(g) && q.runs.size >= 0.8 * p.runs.size));
|
|
5580
|
-
for (const [g, p] of maximal.slice(0, 40)) {
|
|
5581
|
-
out.push({ kind: "phrase", value: g, runs: p.runs.size, of, briefs: p.briefs.size, known: options.copyTell?.(g) ?? null, example: p.example });
|
|
5582
|
-
}
|
|
5583
|
-
const order = ["face", "accent", "ground", "shape", "motion", "opening", "tell", "phrase"];
|
|
5584
|
-
return out.sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind) || b.runs - a.runs);
|
|
5585
|
-
}
|
|
5586
|
-
function hueSwatch(runs, hue) {
|
|
5587
|
-
const hits = runs.map((r) => r.fingerprint.accent).filter((o) => !!o && hueName(o) === hue);
|
|
5588
|
-
if (hits.length === 0) return null;
|
|
5589
|
-
const centre = hits.reduce((best, o) => hits.reduce((s, p) => s + deltaEOk(o, p), 0) < hits.reduce((s, p) => s + deltaEOk(best, p), 0) ? o : best);
|
|
5590
|
-
return formatHex(oklchToRgb(centre));
|
|
5591
|
-
}
|
|
5592
|
-
|
|
5593
6354
|
// src/estate/index.ts
|
|
5594
6355
|
var ESTATE_VERSION = 1;
|
|
5595
6356
|
var SIBLING_AT = 0.31;
|
|
@@ -5847,6 +6608,7 @@ function retrofitPlan(report2, direction) {
|
|
|
5847
6608
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5848
6609
|
0 && (module.exports = {
|
|
5849
6610
|
AI_PHRASES,
|
|
6611
|
+
AI_SOURCE_TYPES,
|
|
5850
6612
|
AI_VIOLET,
|
|
5851
6613
|
AI_WORDS,
|
|
5852
6614
|
BASELINE_VERSION,
|
|
@@ -5854,6 +6616,7 @@ function retrofitPlan(report2, direction) {
|
|
|
5854
6616
|
CATALOGUE,
|
|
5855
6617
|
CATALOGUE_VERSION,
|
|
5856
6618
|
CHOICE_KEYS,
|
|
6619
|
+
COMPONENT_ROLES,
|
|
5857
6620
|
DEFAULT_IGNORE,
|
|
5858
6621
|
DENSITIES,
|
|
5859
6622
|
DENSITY_NAMES,
|
|
@@ -5863,13 +6626,17 @@ function retrofitPlan(report2, direction) {
|
|
|
5863
6626
|
DURATION_S,
|
|
5864
6627
|
EASE,
|
|
5865
6628
|
EASE_TUPLE,
|
|
6629
|
+
EDGE_STEP,
|
|
5866
6630
|
ESTATE_VERSION,
|
|
5867
6631
|
EXIT_RATIO,
|
|
5868
6632
|
FINGERPRINT_VERSION,
|
|
5869
6633
|
HOUSE,
|
|
5870
6634
|
HOUSE_BUDGET,
|
|
6635
|
+
HOUSE_LOCKED,
|
|
5871
6636
|
HOUSE_TYPE,
|
|
6637
|
+
IMAGERY_TELLS,
|
|
5872
6638
|
JSON_SCHEMA_VERSION,
|
|
6639
|
+
LICENCES,
|
|
5873
6640
|
LIGHTNESS_CURVE,
|
|
5874
6641
|
MIN_DECIDED,
|
|
5875
6642
|
MIN_RUNS,
|
|
@@ -5877,11 +6644,15 @@ function retrofitPlan(report2, direction) {
|
|
|
5877
6644
|
NULL_VERSION,
|
|
5878
6645
|
PILL_LIMIT,
|
|
5879
6646
|
PLAINER_WORDS,
|
|
6647
|
+
PROMO_BADGE,
|
|
5880
6648
|
PROPOSED,
|
|
6649
|
+
PROVENANCE_SCAN_BYTES,
|
|
5881
6650
|
RAMP_STEPS,
|
|
6651
|
+
READABLE_SNAPSHOT_VERSIONS,
|
|
5882
6652
|
REFLEX_FONTS_1,
|
|
5883
6653
|
REFLEX_FONTS_2,
|
|
5884
6654
|
RENDERED_PATHS,
|
|
6655
|
+
RENDERED_TELLS,
|
|
5885
6656
|
REVEAL_LIMIT,
|
|
5886
6657
|
REVIEW_PHRASES,
|
|
5887
6658
|
SCALE,
|
|
@@ -5892,6 +6663,8 @@ function retrofitPlan(report2, direction) {
|
|
|
5892
6663
|
SOURCE_KINDS,
|
|
5893
6664
|
SPACE_STEPS,
|
|
5894
6665
|
SPRING,
|
|
6666
|
+
STOCK_AVATAR_HOSTS,
|
|
6667
|
+
STOCK_PHOTO_HOSTS,
|
|
5895
6668
|
STOCK_PHRASES,
|
|
5896
6669
|
TELL_HEAVY,
|
|
5897
6670
|
TYPE_STEPS,
|
|
@@ -5910,8 +6683,14 @@ function retrofitPlan(report2, direction) {
|
|
|
5910
6683
|
checkPair,
|
|
5911
6684
|
checkRestraint,
|
|
5912
6685
|
choicesOf,
|
|
6686
|
+
closestComponents,
|
|
5913
6687
|
compareFacts,
|
|
5914
6688
|
compareToEstate,
|
|
6689
|
+
componentDistance,
|
|
6690
|
+
componentShared,
|
|
6691
|
+
componentTypicalities,
|
|
6692
|
+
componentTypicality,
|
|
6693
|
+
componentsOf,
|
|
5915
6694
|
craftTokens,
|
|
5916
6695
|
createBaseline,
|
|
5917
6696
|
deltaEOk,
|
|
@@ -5920,9 +6699,11 @@ function retrofitPlan(report2, direction) {
|
|
|
5920
6699
|
densityTokens,
|
|
5921
6700
|
emitCss,
|
|
5922
6701
|
emptyEstate,
|
|
6702
|
+
estateComponentPairs,
|
|
5923
6703
|
estatePairs,
|
|
5924
6704
|
exitDuration,
|
|
5925
6705
|
extractHtml,
|
|
6706
|
+
faceLicence,
|
|
5926
6707
|
findClaims,
|
|
5927
6708
|
findColours,
|
|
5928
6709
|
findingKey,
|
|
@@ -5940,6 +6721,7 @@ function retrofitPlan(report2, direction) {
|
|
|
5940
6721
|
groundName,
|
|
5941
6722
|
harvest,
|
|
5942
6723
|
hexToOklch,
|
|
6724
|
+
holdHouseBlocks,
|
|
5943
6725
|
houseRule,
|
|
5944
6726
|
hueName,
|
|
5945
6727
|
hueSwatch,
|
|
@@ -5949,6 +6731,8 @@ function retrofitPlan(report2, direction) {
|
|
|
5949
6731
|
initDirection,
|
|
5950
6732
|
isAiViolet,
|
|
5951
6733
|
isCream,
|
|
6734
|
+
layoutDistance,
|
|
6735
|
+
licencesJson,
|
|
5952
6736
|
makeSnapshot,
|
|
5953
6737
|
motionTokens,
|
|
5954
6738
|
namedSources,
|
|
@@ -5966,8 +6750,10 @@ function retrofitPlan(report2, direction) {
|
|
|
5966
6750
|
protectedFacts,
|
|
5967
6751
|
ramp,
|
|
5968
6752
|
rampFromAnchors,
|
|
6753
|
+
readSnapshot,
|
|
5969
6754
|
retrofitPlan,
|
|
5970
6755
|
runTell,
|
|
6756
|
+
scanProvenance,
|
|
5971
6757
|
scanSource,
|
|
5972
6758
|
sectionRhythm,
|
|
5973
6759
|
semantic,
|
|
@@ -5976,6 +6762,8 @@ function retrofitPlan(report2, direction) {
|
|
|
5976
6762
|
sharedParts,
|
|
5977
6763
|
shouldAnimate,
|
|
5978
6764
|
siblingLine,
|
|
6765
|
+
stockAvatarSource,
|
|
6766
|
+
stockSource,
|
|
5979
6767
|
tailwindV3Preset,
|
|
5980
6768
|
tailwindV4Theme,
|
|
5981
6769
|
tellById,
|
|
@@ -5986,6 +6774,7 @@ function retrofitPlan(report2, direction) {
|
|
|
5986
6774
|
typicality,
|
|
5987
6775
|
validateDirection,
|
|
5988
6776
|
visibleText,
|
|
6777
|
+
visualMeasures,
|
|
5989
6778
|
wcagContrast,
|
|
5990
6779
|
wordListsJson
|
|
5991
6780
|
});
|