@domandigital/craft 0.6.0 → 0.7.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/dist/index.cjs CHANGED
@@ -3,9 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
6
+ var __export = (target2, all) => {
7
7
  for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
8
+ __defProp(target2, name, { get: all[name], enumerable: true });
9
9
  };
10
10
  var __copyProps = (to, from, except, desc) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") {
@@ -35,12 +35,16 @@ __export(index_exports, {
35
35
  DURATION_S: () => DURATION_S,
36
36
  EASE: () => EASE,
37
37
  EASE_TUPLE: () => EASE_TUPLE,
38
+ ESTATE_VERSION: () => ESTATE_VERSION,
38
39
  EXIT_RATIO: () => EXIT_RATIO,
39
40
  FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
40
41
  HOUSE_BUDGET: () => HOUSE_BUDGET,
41
42
  HOUSE_TYPE: () => HOUSE_TYPE,
42
43
  LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
44
+ MIN_DECIDED: () => MIN_DECIDED,
45
+ MIN_RUNS: () => MIN_RUNS,
43
46
  NEGATIVE_REASSURANCE: () => NEGATIVE_REASSURANCE,
47
+ NULL_VERSION: () => NULL_VERSION,
44
48
  PILL_LIMIT: () => PILL_LIMIT,
45
49
  PLAINER_WORDS: () => PLAINER_WORDS,
46
50
  PROPOSED: () => PROPOSED,
@@ -53,27 +57,37 @@ __export(index_exports, {
53
57
  SCALE: () => SCALE,
54
58
  SHADCN_LIMIT: () => SHADCN_LIMIT,
55
59
  SHADCN_PRIMITIVES: () => SHADCN_PRIMITIVES,
60
+ SIBLING_AT: () => SIBLING_AT,
56
61
  SNAPSHOT_VERSION: () => SNAPSHOT_VERSION,
57
62
  SOURCE_KINDS: () => SOURCE_KINDS,
58
63
  SPACE_STEPS: () => SPACE_STEPS,
59
64
  SPRING: () => SPRING,
60
65
  STOCK_PHRASES: () => STOCK_PHRASES,
66
+ TELL_HEAVY: () => TELL_HEAVY,
61
67
  TYPE_STEPS: () => TYPE_STEPS,
68
+ TYPICAL_AT: () => TYPICAL_AT,
62
69
  VAGUE_WORDS: () => VAGUE_WORDS,
63
70
  accentFork: () => accentFork,
71
+ addSite: () => addSite,
64
72
  apcaContrast: () => apcaContrast,
65
73
  auditSnapshot: () => auditSnapshot,
66
74
  catalogueTable: () => catalogueTable,
75
+ characterReport: () => characterReport,
67
76
  checkCopy: () => checkCopy,
68
77
  checkPair: () => checkPair,
69
78
  checkRestraint: () => checkRestraint,
79
+ choicesOf: () => choicesOf,
80
+ compareToEstate: () => compareToEstate,
70
81
  craftTokens: () => craftTokens,
71
82
  deltaEOk: () => deltaEOk,
72
83
  deltaEOkHex: () => deltaEOkHex,
73
84
  densityCss: () => densityCss,
74
85
  densityTokens: () => densityTokens,
75
86
  emitCss: () => emitCss,
87
+ emptyEstate: () => emptyEstate,
88
+ estatePairs: () => estatePairs,
76
89
  exitDuration: () => exitDuration,
90
+ extractHtml: () => extractHtml,
77
91
  findColours: () => findColours,
78
92
  fingerprint: () => fingerprint,
79
93
  fingerprintDistance: () => fingerprintDistance,
@@ -83,7 +97,11 @@ __export(index_exports, {
83
97
  fontClass: () => fontClass,
84
98
  formatHex: () => formatHex,
85
99
  formatReport: () => formatReport,
100
+ groundName: () => groundName,
101
+ harvest: () => harvest,
86
102
  hexToOklch: () => hexToOklch,
103
+ hueName: () => hueName,
104
+ hueSwatch: () => hueSwatch,
87
105
  inP3Gamut: () => inP3Gamut,
88
106
  inSrgbGamut: () => inSrgbGamut,
89
107
  initDirection: () => initDirection,
@@ -93,6 +111,7 @@ __export(index_exports, {
93
111
  motionTokens: () => motionTokens,
94
112
  nearest: () => nearest,
95
113
  normaliseFamily: () => normaliseFamily,
114
+ nullPrompt: () => nullPrompt,
96
115
  oklchToHex: () => oklchToHex,
97
116
  oklchToRgb: () => oklchToRgb,
98
117
  paletteFromPixels: () => paletteFromPixels,
@@ -101,18 +120,24 @@ __export(index_exports, {
101
120
  propose: () => propose,
102
121
  ramp: () => ramp,
103
122
  rampFromAnchors: () => rampFromAnchors,
123
+ retrofitPlan: () => retrofitPlan,
104
124
  runTell: () => runTell,
105
125
  scanSource: () => scanSource,
106
126
  sectionRhythm: () => sectionRhythm,
107
127
  semantic: () => semantic,
128
+ shapeName: () => shapeName,
129
+ sharedParts: () => sharedParts,
108
130
  shouldAnimate: () => shouldAnimate,
131
+ siblingLine: () => siblingLine,
109
132
  tailwindV3Preset: () => tailwindV3Preset,
110
133
  tailwindV4Theme: () => tailwindV4Theme,
111
134
  tellById: () => tellById,
112
135
  toGamut: () => toGamut,
113
136
  typeFeatureTokens: () => typeFeatureTokens,
137
+ typicality: () => typicality,
114
138
  validateDirection: () => validateDirection,
115
- wcagContrast: () => wcagContrast
139
+ wcagContrast: () => wcagContrast,
140
+ wordListsJson: () => wordListsJson
116
141
  });
117
142
  module.exports = __toCommonJS(index_exports);
118
143
 
@@ -945,9 +970,9 @@ function resolveValue(value, props, depth = 0) {
945
970
  if (depth > 8) return null;
946
971
  const match = SINGLE_VAR.exec(value.trim());
947
972
  if (match === null) return value.trim();
948
- const target = props.get(match[1].toLowerCase());
949
- if (target !== void 0 && target !== "initial")
950
- return resolveValue(target, props, depth + 1);
973
+ const target2 = props.get(match[1].toLowerCase());
974
+ if (target2 !== void 0 && target2 !== "initial")
975
+ return resolveValue(target2, props, depth + 1);
951
976
  const fallback = match[2]?.trim();
952
977
  return fallback === void 0 || fallback === "" ? null : resolveValue(fallback, props, depth + 1);
953
978
  }
@@ -2502,7 +2527,7 @@ const inter = Inter({ subsets: ["latin"] });`),
2502
2527
  severity: "warn",
2503
2528
  surface: "source",
2504
2529
  why: "Ban Inter and a model reaches for Inter Tight, DM Sans, Manrope, Space Grotesk or an Instrument/Fraunces serif. A swap inside the reflex list is not a decision.",
2505
- fix: "Same as reflex-font. If this face really is the right one, declare an exception with the reason and the evidence.",
2530
+ fix: "Pick the display face from the client's world (signage, packaging, the trade's lettering) and record why in art-direction.json. If this face really is the right one, declare an exception with the reason and the evidence.",
2506
2531
  detect: fontTell(REFLEX_FONTS_2),
2507
2532
  fixtures: {
2508
2533
  flag: [f2("src/styles.css", `@theme {
@@ -3250,15 +3275,16 @@ function makeSnapshot(patch = {}) {
3250
3275
  }
3251
3276
 
3252
3277
  // src/snapshot/fonts.ts
3278
+ var SYSTEM = /^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|ui-rounded|sans-serif|serif|monospace|cursive)$/i;
3253
3279
  function normaliseFamily(raw) {
3254
3280
  let name = raw.replace(/["']/g, "").trim();
3255
3281
  const next = name.match(/^__(.+?)(?:_Fallback)?_[0-9a-f]{5,8}$/i);
3256
3282
  if (next) name = next[1].replace(/_/g, " ");
3257
3283
  name = name.replace(/\s+Fallback$/i, "").replace(/\s+Variable$/i, "");
3284
+ if (SYSTEM.test(name)) return "system-ui";
3258
3285
  if (!/\s/.test(name) && (/[a-z][A-Z]/.test(name) || /^[a-z]+(?:-[a-z]+)+$/.test(name))) {
3259
3286
  name = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/-/g, " ").replace(/\b([a-z])/g, (c) => c.toUpperCase()).replace(/\bDm\b/, "DM");
3260
3287
  }
3261
- if (/^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|sans-serif|serif|monospace)$/i.test(name)) return "system-ui";
3262
3288
  return name;
3263
3289
  }
3264
3290
  var KNOWN = {
@@ -3635,6 +3661,25 @@ function catalogueTable() {
3635
3661
  ...rows
3636
3662
  ].join("\n");
3637
3663
  }
3664
+ function wordListsJson() {
3665
+ return JSON.stringify(
3666
+ {
3667
+ catalogueVersion: CATALOGUE_VERSION,
3668
+ lists: {
3669
+ aiWords: AI_WORDS,
3670
+ stockPhrases: STOCK_PHRASES,
3671
+ aiPhrases: AI_PHRASES,
3672
+ plainerWords: PLAINER_WORDS,
3673
+ buzzwords: BUZZWORDS,
3674
+ negativeReassurance: NEGATIVE_REASSURANCE,
3675
+ vagueWords: VAGUE_WORDS,
3676
+ reviewPhrases: REVIEW_PHRASES
3677
+ }
3678
+ },
3679
+ null,
3680
+ 2
3681
+ ) + "\n";
3682
+ }
3638
3683
 
3639
3684
  // src/character/format.ts
3640
3685
  function formatReport(report2, title) {
@@ -3674,6 +3719,7 @@ function formatReport(report2, title) {
3674
3719
  // src/fingerprint/index.ts
3675
3720
  var FINGERPRINT_VERSION = 1;
3676
3721
  var CHROMATIC = 0.03;
3722
+ var MIN_ACCENT_AREA = 2e4;
3677
3723
  var oklch = (value) => {
3678
3724
  const parsed = parseColour(value);
3679
3725
  return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
@@ -3686,7 +3732,7 @@ function accentOf(s) {
3686
3732
  }
3687
3733
  const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
3688
3734
  if (topButton) return oklch(topButton[0]);
3689
- const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
3735
+ 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];
3690
3736
  if (bg) return bg.o;
3691
3737
  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];
3692
3738
  return text ? text.o : null;
@@ -3763,8 +3809,8 @@ function fingerprintDistance(a, b) {
3763
3809
  const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
3764
3810
  return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
3765
3811
  }
3766
- function nearest(target, set, k = set.length) {
3767
- return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
3812
+ function nearest(target2, set, k = set.length) {
3813
+ return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target2, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
3768
3814
  }
3769
3815
 
3770
3816
  // src/direction/types.ts
@@ -4071,6 +4117,495 @@ function propose(input) {
4071
4117
  direction: { version: DIRECTION_VERSION, client: input.client, brief: input.brief, sources: input.sources, choices }
4072
4118
  };
4073
4119
  }
4120
+
4121
+ // src/null/index.ts
4122
+ var NULL_VERSION = 1;
4123
+ function nullPrompt(brief) {
4124
+ return [
4125
+ `Build the home page for this business: ${brief.trim()}`,
4126
+ "",
4127
+ "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."
4128
+ ].join("\n");
4129
+ }
4130
+ function extractHtml(reply) {
4131
+ const start = reply.search(/<!doctype html|<html[\s>]/i);
4132
+ if (start < 0) return null;
4133
+ const end = reply.toLowerCase().lastIndexOf("</html>");
4134
+ const html = end > start ? reply.slice(start, end + "</html>".length) : reply.slice(start).replace(/\s*```\s*$/, "");
4135
+ return html.length > 200 ? html : null;
4136
+ }
4137
+ var MIN_RUNS = 5;
4138
+ var TYPICAL_AT = 0.1;
4139
+ function meanNearest(distances, k) {
4140
+ const sorted = [...distances].sort((a, b) => a - b).slice(0, k);
4141
+ return sorted.reduce((s, d) => s + d, 0) / sorted.length;
4142
+ }
4143
+ function median2(values) {
4144
+ const sorted = [...values].sort((a, b) => a - b);
4145
+ const mid = Math.floor(sorted.length / 2);
4146
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
4147
+ }
4148
+ function typicality(target2, runs, k = 3) {
4149
+ if (runs.length < MIN_RUNS) throw new Error(`a null model needs at least ${MIN_RUNS} runs; this one has ${runs.length}`);
4150
+ const kk = Math.min(k, runs.length - 1);
4151
+ const toTarget = runs.map((r) => ({ id: r.id, distance: fingerprintDistance(target2, r.fingerprint).total }));
4152
+ const distance = meanNearest(
4153
+ toTarget.map((t) => t.distance),
4154
+ kk
4155
+ );
4156
+ const loo = runs.map(
4157
+ (r, i) => meanNearest(
4158
+ runs.filter((_, j) => j !== i).map((o) => fingerprintDistance(r.fingerprint, o.fingerprint).total),
4159
+ kk
4160
+ )
4161
+ );
4162
+ const score = loo.filter((d) => d >= distance).length / loo.length;
4163
+ return {
4164
+ score: Math.round(score * 100) / 100,
4165
+ typical: score >= TYPICAL_AT,
4166
+ distance: Math.round(distance * 1e3) / 1e3,
4167
+ baseline: Math.round(median2(loo) * 1e3) / 1e3,
4168
+ nearest: toTarget.sort((a, b) => a.distance - b.distance).slice(0, kk),
4169
+ shared: sharedChoices(target2, runs)
4170
+ };
4171
+ }
4172
+ var HUES = [
4173
+ ["red", 0, 45],
4174
+ ["orange", 45, 75],
4175
+ ["yellow", 75, 110],
4176
+ ["green", 110, 165],
4177
+ ["teal", 165, 215],
4178
+ ["blue", 215, 265],
4179
+ ["violet", 265, 315],
4180
+ ["pink", 315, 360]
4181
+ ];
4182
+ function hueName(o) {
4183
+ if (!o) return "none";
4184
+ if (o.c < 0.03) return "neutral";
4185
+ return HUES.find(([, lo, hi]) => o.h >= lo && o.h < hi)?.[0] ?? "red";
4186
+ }
4187
+ function groundName(o) {
4188
+ if (o.l < 0.3) return "dark";
4189
+ if (isCream(o)) return "cream";
4190
+ if (o.l >= 0.97 && o.c < 0.012) return "white";
4191
+ return o.c < 0.02 ? "grey" : "tinted";
4192
+ }
4193
+ function shapeName(roundness) {
4194
+ if (roundness === null) return "no buttons";
4195
+ if (roundness >= 0.45) return "pill";
4196
+ return roundness >= 0.15 ? "soft" : "square";
4197
+ }
4198
+ function choicesOf(fp) {
4199
+ return {
4200
+ display: [fp.display.family],
4201
+ body: [fp.body.family],
4202
+ accent: [hueName(fp.accent)],
4203
+ ground: [groundName(fp.ground)],
4204
+ shape: [shapeName(fp.roundness)],
4205
+ motion: [fp.motion >= 0.6 ? "reveals most sections" : fp.motion > 0 ? "reveals some sections" : "no scroll reveals"],
4206
+ effects: fp.effects,
4207
+ opening: [fp.layout.slice(0, 3).join(" > ") || "none"]
4208
+ };
4209
+ }
4210
+ function sharedChoices(target2, runs) {
4211
+ const mine = choicesOf(target2);
4212
+ const all = runs.map((r) => choicesOf(r.fingerprint));
4213
+ const out = [];
4214
+ for (const [part, values] of Object.entries(mine)) {
4215
+ for (const value of values) {
4216
+ const count2 = all.filter((c) => c[part].includes(value)).length;
4217
+ if (count2 / runs.length >= 0.25) out.push({ part, value, runs: count2, of: runs.length });
4218
+ }
4219
+ }
4220
+ return out.sort((a, b) => b.runs - a.runs);
4221
+ }
4222
+ var REFLEX_1 = new Set(REFLEX_FONTS_1.map((f3) => f3.toLowerCase()));
4223
+ var REFLEX_2 = new Set(REFLEX_FONTS_2.map((f3) => f3.toLowerCase()));
4224
+ function knownFor(kind, value, sample) {
4225
+ if (kind === "face") return REFLEX_1.has(value.toLowerCase()) ? "reflex-font" : REFLEX_2.has(value.toLowerCase()) ? "reflex-font-2" : null;
4226
+ if (kind === "accent" && value === "violet") return sample.some((f3) => f3.accent && isAiViolet(f3.accent)) ? "ai-violet" : null;
4227
+ if (kind === "ground" && value === "cream") return "cream-palette";
4228
+ if (kind === "shape" && value === "pill") return "pill-everything";
4229
+ if (kind === "motion" && value === "reveals most sections") return "reveal-everywhere";
4230
+ if (kind === "tell") return value;
4231
+ if (kind === "opening" && /^hero > (?:logos|stats)/.test(value)) return "hero-then-proof";
4232
+ return null;
4233
+ }
4234
+ var STOP2 = new Set(
4235
+ "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(" ")
4236
+ );
4237
+ function grams(text) {
4238
+ const out = /* @__PURE__ */ new Set();
4239
+ for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
4240
+ const words2 = clause.match(/[a-z][a-z'-]*/g) ?? [];
4241
+ for (let n = 2; n <= 4; n += 1) {
4242
+ for (let i = 0; i + n <= words2.length; i += 1) {
4243
+ const g = words2.slice(i, i + n);
4244
+ if (STOP2.has(g[0]) || STOP2.has(g[n - 1])) continue;
4245
+ if (g.filter((w) => !STOP2.has(w)).length < 2) continue;
4246
+ out.add(g.join(" "));
4247
+ }
4248
+ }
4249
+ }
4250
+ return out;
4251
+ }
4252
+ var GRAMMAR = /* @__PURE__ */ new Set([
4253
+ "privacy policy",
4254
+ "cookie policy",
4255
+ "rights reserved",
4256
+ "all rights reserved",
4257
+ "terms of service",
4258
+ "terms and conditions",
4259
+ "terms conditions",
4260
+ "how it works",
4261
+ "what we do",
4262
+ "about us",
4263
+ "contact us",
4264
+ "get in touch",
4265
+ "opening hours",
4266
+ "read more",
4267
+ "learn more",
4268
+ "book now",
4269
+ "free quote",
4270
+ "our services",
4271
+ "quick links",
4272
+ "email address",
4273
+ "phone number",
4274
+ "full name",
4275
+ "first name",
4276
+ "last name",
4277
+ "your name",
4278
+ "your email",
4279
+ "send message"
4280
+ ]);
4281
+ var LEGAL = /©|&copy;|\bregistered (?:in|office|number)\b|\bcompany (?:no|number|reg)|\bvat (?:no|number|reg)/i;
4282
+ 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;
4283
+ function textGrams(text) {
4284
+ if (LEGAL.test(text)) return /* @__PURE__ */ new Set();
4285
+ const out = grams(text.replace(NOISE, " | "));
4286
+ for (const g of out) if (GRAMMAR.has(g)) out.delete(g);
4287
+ return out;
4288
+ }
4289
+ function properNouns(brief) {
4290
+ return new Set((brief.match(/\b[A-Z][a-z]+/g) ?? []).map((w) => w.toLowerCase()).filter((w) => !STOP2.has(w)));
4291
+ }
4292
+ function harvest(models, options = {}) {
4293
+ const minShare = options.minShare ?? 0.25;
4294
+ const runs = models.flatMap((m, b) => m.runs.map((r) => ({ ...r, brief: b })));
4295
+ const of = runs.length;
4296
+ if (of === 0) return [];
4297
+ const multiBrief = models.length > 1;
4298
+ const sample = runs.map((r) => r.fingerprint);
4299
+ const tally = /* @__PURE__ */ new Map();
4300
+ const add = (kind, value, run, brief, example) => {
4301
+ const key = `${kind}\0${value}`;
4302
+ const t = tally.get(key) ?? { kind, value, runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example };
4303
+ t.runs.add(run);
4304
+ t.briefs.add(brief);
4305
+ tally.set(key, t);
4306
+ };
4307
+ runs.forEach((r, i) => {
4308
+ const c = choicesOf(r.fingerprint);
4309
+ for (const f3 of /* @__PURE__ */ new Set([...c.display, ...c.body])) add("face", f3, i, r.brief);
4310
+ add("accent", c.accent[0], i, r.brief);
4311
+ add("ground", c.ground[0], i, r.brief);
4312
+ add("shape", c.shape[0], i, r.brief);
4313
+ add("motion", c.motion[0], i, r.brief);
4314
+ add("opening", c.opening[0], i, r.brief);
4315
+ for (const t of r.tells) add("tell", t, i, r.brief);
4316
+ });
4317
+ const out = [];
4318
+ for (const t of tally.values()) {
4319
+ if (t.runs.size / of < minShare) continue;
4320
+ if (t.kind === "accent" && (t.value === "none" || t.value === "neutral")) continue;
4321
+ if (t.kind === "shape" && t.value === "no buttons") continue;
4322
+ if (t.kind === "ground" && (t.value === "white" || t.value === "grey")) continue;
4323
+ if (t.kind === "motion" && t.value === "no scroll reveals") continue;
4324
+ out.push({ kind: t.kind, value: t.value, runs: t.runs.size, of, briefs: t.briefs.size, known: knownFor(t.kind, t.value, sample) });
4325
+ }
4326
+ const phraseMin = Math.max(3, Math.ceil(of * minShare * 0.4));
4327
+ const names = models.map((m) => properNouns(m.brief));
4328
+ const phrases2 = /* @__PURE__ */ new Map();
4329
+ runs.forEach((r, i) => {
4330
+ for (const text of r.copy) {
4331
+ for (const g of textGrams(text)) {
4332
+ if (g.split(" ").some((w) => names[r.brief].has(w))) continue;
4333
+ const p = phrases2.get(g) ?? { runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example: text.trim().slice(0, 140) };
4334
+ p.runs.add(i);
4335
+ p.briefs.add(r.brief);
4336
+ phrases2.set(g, p);
4337
+ }
4338
+ }
4339
+ });
4340
+ 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);
4341
+ const maximal = kept.filter(([g, p]) => !kept.some(([h, q]) => h !== g && h.includes(g) && q.runs.size >= 0.8 * p.runs.size));
4342
+ for (const [g, p] of maximal.slice(0, 40)) {
4343
+ out.push({ kind: "phrase", value: g, runs: p.runs.size, of, briefs: p.briefs.size, known: options.copyTell?.(g) ?? null, example: p.example });
4344
+ }
4345
+ const order = ["face", "accent", "ground", "shape", "motion", "opening", "tell", "phrase"];
4346
+ return out.sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind) || b.runs - a.runs);
4347
+ }
4348
+ function hueSwatch(runs, hue) {
4349
+ const hits = runs.map((r) => r.fingerprint.accent).filter((o) => !!o && hueName(o) === hue);
4350
+ if (hits.length === 0) return null;
4351
+ 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);
4352
+ return formatHex(oklchToRgb(centre));
4353
+ }
4354
+
4355
+ // src/estate/index.ts
4356
+ var ESTATE_VERSION = 1;
4357
+ var SIBLING_AT = 0.31;
4358
+ function emptyEstate() {
4359
+ return { version: ESTATE_VERSION, sites: [] };
4360
+ }
4361
+ function addSite(register, site) {
4362
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
4363
+ const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
4364
+ return { version: ESTATE_VERSION, sites };
4365
+ }
4366
+ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
4367
+ const out = [];
4368
+ const close = (k) => d.parts[k] <= 0.2;
4369
+ if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
4370
+ if (close("type")) {
4371
+ if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
4372
+ if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
4373
+ }
4374
+ if (close("ground")) out.push(`${groundName(a.ground)} ground`);
4375
+ if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
4376
+ if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
4377
+ if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
4378
+ if (close("layout")) out.push("running order");
4379
+ return out;
4380
+ }
4381
+ function compareToEstate(target2, register, options = {}) {
4382
+ const at = options.siblingAt ?? SIBLING_AT;
4383
+ return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
4384
+ const d = fingerprintDistance(target2, s.fingerprint);
4385
+ return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
4386
+ }).sort((a, b) => a.distance - b.distance);
4387
+ }
4388
+ function estatePairs(register, siblingAt = SIBLING_AT) {
4389
+ const out = [];
4390
+ const s = register.sites;
4391
+ for (let i = 0; i < s.length; i += 1) {
4392
+ for (let j = i + 1; j < s.length; j += 1) {
4393
+ const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
4394
+ out.push({ a: s[i].id, b: s[j].id, distance: d.total, sibling: d.total < siblingAt, shared: sharedParts(s[i].fingerprint, s[j].fingerprint, d) });
4395
+ }
4396
+ }
4397
+ return out.sort((x, y) => x.distance - y.distance);
4398
+ }
4399
+ function siblingLine(briefs) {
4400
+ const d = [];
4401
+ for (const runs of briefs) {
4402
+ for (let i = 0; i < runs.length; i += 1) for (let j = i + 1; j < runs.length; j += 1) d.push(fingerprintDistance(runs[i], runs[j]).total);
4403
+ }
4404
+ if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
4405
+ d.sort((a, b) => a - b);
4406
+ const mid = Math.floor(d.length / 2);
4407
+ const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
4408
+ return Math.round(m * 1e3) / 1e3;
4409
+ }
4410
+
4411
+ // src/report/index.ts
4412
+ var TELL_HEAVY = 3;
4413
+ var MIN_DECIDED = 5;
4414
+ var TELL_AREA = {
4415
+ "reflex-font": { area: "type", choice: "display" },
4416
+ "reflex-font-2": { area: "type", choice: "display" },
4417
+ "italic-serif-display": { area: "type", choice: "display" },
4418
+ "ai-violet": { area: "colour", choice: "accent" },
4419
+ "blue-purple-gradient": { area: "colour", choice: "accent" },
4420
+ "gradient-text": { area: "colour", choice: "accent" },
4421
+ "cream-palette": { area: "colour", choice: "ground" },
4422
+ "pill-everything": { area: "shape", choice: "shape" },
4423
+ "shadcn-dump": { area: "shape", choice: "shape" },
4424
+ "glass-panel": { area: "effects", choice: "motif" },
4425
+ "radial-spotlight-glow": { area: "effects", choice: "motif" },
4426
+ "grid-background": { area: "effects", choice: "motif" },
4427
+ "thin-border-wide-shadow": { area: "effects", choice: "shape" },
4428
+ "icon-tile-stack": { area: "effects", choice: "motif" },
4429
+ "reveal-everywhere": { area: "motion", choice: "signature" },
4430
+ "intro-cinematic": { area: "motion", choice: "signature" },
4431
+ marquee: { area: "motion", choice: "signature" },
4432
+ "hero-then-proof": { area: "layout" },
4433
+ "icon-tile-grid": { area: "layout" },
4434
+ "hero-eyebrow-chip": { area: "layout" },
4435
+ "bento-grid": { area: "layout" }
4436
+ };
4437
+ var PART_AREA = {
4438
+ display: { area: "type", choice: "display" },
4439
+ body: { area: "type", choice: "body" },
4440
+ accent: { area: "colour", choice: "accent" },
4441
+ ground: { area: "colour", choice: "ground" },
4442
+ shape: { area: "shape", choice: "shape" },
4443
+ effects: { area: "effects", choice: "motif" },
4444
+ motion: { area: "motion", choice: "signature" }
4445
+ };
4446
+ var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
4447
+ function designFindings(findings, copyTells) {
4448
+ return (findings?.findings ?? []).filter((f3) => !copyTells.has(f3.tell));
4449
+ }
4450
+ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
4451
+ const actions = [];
4452
+ const signals = [];
4453
+ if (input.findings) {
4454
+ const design = designFindings(input.findings, copyTells);
4455
+ const distinct = [...new Set(design.map((f3) => f3.tell))];
4456
+ signals.push({
4457
+ id: "tells",
4458
+ raised: distinct.length >= TELL_HEAVY,
4459
+ detail: `${distinct.length} distinct design tell${distinct.length === 1 ? "" : "s"}${distinct.length ? `: ${distinct.join(", ")}` : ""}`
4460
+ });
4461
+ const seen = /* @__PURE__ */ new Set();
4462
+ for (const f3 of design) {
4463
+ if (seen.has(f3.tell)) continue;
4464
+ seen.add(f3.tell);
4465
+ const map = TELL_AREA[f3.tell] ?? { area: "effects" };
4466
+ const both = design.some((g) => g.tell === f3.tell && g.line === 0) && design.some((g) => g.tell === f3.tell && g.line > 0);
4467
+ actions.push({ area: map.area, what: f3.fix, why: `${f3.name} (${f3.tell}, gen ${f3.generation})${both ? ", in source and on the page" : ""}`, priority: both ? 1 : 2, choice: map.choice });
4468
+ }
4469
+ const copy = (input.findings.findings ?? []).filter((f3) => copyTells.has(f3.tell));
4470
+ const byTell = /* @__PURE__ */ new Map();
4471
+ for (const f3 of copy) byTell.set(f3.tell, [...byTell.get(f3.tell) ?? [], f3]);
4472
+ for (const [tell, list] of byTell) {
4473
+ actions.push({ area: "copy", what: list[0].fix, why: `${list[0].name} (${tell}), ${list.length} place${list.length === 1 ? "" : "s"}, first at ${list[0].path}${list[0].line ? `:${list[0].line}` : ""}`, priority: 3 });
4474
+ }
4475
+ } else {
4476
+ signals.push({ id: "tells", raised: null, detail: "not measured" });
4477
+ }
4478
+ if (input.typicality) {
4479
+ const t = input.typicality;
4480
+ signals.push({ id: "typicality", raised: t.typical, detail: `score ${t.score.toFixed(2)}, ${t.typical ? "typical" : "not typical"} of what Claude builds for this brief` });
4481
+ if (t.typical) {
4482
+ for (const s of t.shared) {
4483
+ const map = PART_AREA[s.part];
4484
+ if (!map || DEFAULTS.test(`${s.part} ${s.value}`)) continue;
4485
+ actions.push({ area: map.area, what: `Move off the model's pick for this brief: ${s.part} ${s.value}.`, why: `${s.runs} of ${s.of} null pages chose it`, priority: 1, choice: map.choice });
4486
+ }
4487
+ }
4488
+ } else {
4489
+ signals.push({ id: "typicality", raised: null, detail: "not measured: no null model for this brief" });
4490
+ }
4491
+ if (input.estate) {
4492
+ const siblings = input.estate.filter((m) => m.sibling);
4493
+ const nearest2 = input.estate[0];
4494
+ signals.push({
4495
+ id: "estate",
4496
+ raised: siblings.length > 0,
4497
+ detail: siblings.length ? `sibling of ${siblings.map((s) => `${s.id} (${s.distance.toFixed(2)})`).join(", ")}` : nearest2 ? `nearest ${nearest2.id} at ${nearest2.distance.toFixed(2)}, no sibling` : "the register is empty"
4498
+ });
4499
+ for (const s of siblings) {
4500
+ for (const part of s.shared) {
4501
+ if (DEFAULTS.test(part)) continue;
4502
+ actions.push({ area: areaOfShared(part), what: `Differ from ${s.id}: both have ${part}.`, why: `sibling at ${s.distance.toFixed(2)}`, priority: 1, choice: choiceOfShared(part) });
4503
+ }
4504
+ }
4505
+ } else {
4506
+ signals.push({ id: "estate", raised: null, detail: "not measured: no estate register" });
4507
+ }
4508
+ if (input.direction) {
4509
+ const d = input.direction;
4510
+ const raised2 = !d.valid || d.decided < MIN_DECIDED;
4511
+ signals.push({ id: "reasons", raised: raised2, detail: `${d.decided} of 7 choices decided with a reason${d.valid ? "" : ", art-direction.json has errors"}` });
4512
+ for (const p of d.problems) {
4513
+ const key = p.at.match(/^choices\.(\w+)/)?.[1];
4514
+ if (p.severity === "error" || p.message === "not decided yet") {
4515
+ actions.push({ area: "direction", what: key && p.message === "not decided yet" ? `Decide ${key}, with a source from the client's world.` : `${p.at || "art-direction.json"}: ${p.message}`, why: "the reason rule", priority: 1, choice: key });
4516
+ }
4517
+ }
4518
+ } else {
4519
+ signals.push({ id: "reasons", raised: null, detail: "not measured: no art-direction.json" });
4520
+ actions.push({ area: "direction", what: "Write art-direction.json: run craft direction init, then give every choice a reason from the client's world.", why: "the reason rule", priority: 1 });
4521
+ }
4522
+ const raised = signals.filter((s) => s.raised === true).length;
4523
+ const unmeasured = signals.filter((s) => s.raised === null).map((s) => s.id);
4524
+ const verdict = raised >= 2 ? "default" : raised === 1 ? "mixed" : unmeasured.length ? "unproven" : "decided";
4525
+ const summary = verdict === "default" ? `${raised} of 4 signals raised: nobody chose how this looks.` : verdict === "mixed" ? `1 of 4 signals raised: ${signals.find((s) => s.raised).id}.` : verdict === "decided" ? "No signal raised, and every choice carries a reason." : `No signal raised, but ${unmeasured.join(", ")} not measured. Unverified is not a pass.`;
4526
+ const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
4527
+ const merged = [];
4528
+ for (const a of actions) {
4529
+ const same = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
4530
+ if (same) {
4531
+ const fromTell = /\(\S+, gen \d\)/.test(a.why);
4532
+ same.why = `${same.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
4533
+ same.priority = Math.min(same.priority, a.priority);
4534
+ } else merged.push({ ...a });
4535
+ }
4536
+ const unique = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
4537
+ unique.sort((a, b) => a.priority - b.priority || order.indexOf(a.area) - order.indexOf(b.area));
4538
+ return { site: input.site, verdict, summary, signals, actions: unique };
4539
+ }
4540
+ function areaOfShared(part) {
4541
+ if (/headline|body$/.test(part)) return "type";
4542
+ if (/accent|ground/.test(part)) return "colour";
4543
+ if (/buttons/.test(part)) return "shape";
4544
+ if (/reveal/.test(part)) return "motion";
4545
+ if (/running order/.test(part)) return "layout";
4546
+ return "effects";
4547
+ }
4548
+ function choiceOfShared(part) {
4549
+ if (/headline/.test(part)) return "display";
4550
+ if (/body$/.test(part)) return "body";
4551
+ if (/accent/.test(part)) return "accent";
4552
+ if (/ground/.test(part)) return "ground";
4553
+ if (/buttons/.test(part)) return "shape";
4554
+ if (/reveal/.test(part)) return "signature";
4555
+ return /running order/.test(part) ? void 0 : "motif";
4556
+ }
4557
+
4558
+ // src/report/retrofit.ts
4559
+ var HEADINGS = {
4560
+ direction: "Decide first",
4561
+ type: "Type",
4562
+ colour: "Colour",
4563
+ shape: "Shape",
4564
+ effects: "Effects",
4565
+ motion: "Motion",
4566
+ layout: "How sections look (not where they sit)",
4567
+ copy: "Copy"
4568
+ };
4569
+ var ORDER2 = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
4570
+ function target(a, direction) {
4571
+ if (!a.choice || a.area === "direction") return "";
4572
+ const c = direction?.choices?.[a.choice];
4573
+ if (c && c.because && !c.because.startsWith("PROPOSED:")) return `
4574
+ - Use: **${c.value}** for ${a.choice}. ${c.because}`;
4575
+ return `
4576
+ - Waits on: deciding **${a.choice}** in art-direction.json.`;
4577
+ }
4578
+ function retrofitPlan(report2, direction) {
4579
+ const lines = [
4580
+ `# Retrofit: ${report2.site}`,
4581
+ "",
4582
+ `**Now:** ${report2.verdict}. ${report2.summary}`,
4583
+ "",
4584
+ "| Signal | Raised | Reading |",
4585
+ "|---|---|---|",
4586
+ ...report2.signals.map((s) => `| ${s.id} | ${s.raised === null ? "not measured" : s.raised ? "yes" : "no"} | ${s.detail} |`),
4587
+ "",
4588
+ "**Done when** `craft report` says *decided*: fewer than three design tells, typicality under 0.10 against this brief's null, no sibling in the estate, and at least five of the seven choices decided with a reason.",
4589
+ ""
4590
+ ];
4591
+ let n = 0;
4592
+ for (const area of ORDER2) {
4593
+ const list = report2.actions.filter((a) => a.area === area);
4594
+ if (list.length === 0) continue;
4595
+ n += 1;
4596
+ lines.push(`## ${n}. ${HEADINGS[area]}`, "");
4597
+ for (const a of list) lines.push(`- [ ] ${a.what} *(${a.why})*${target(a, direction)}`);
4598
+ lines.push("");
4599
+ }
4600
+ if (n === 0) lines.push("Nothing to change.", "");
4601
+ lines.push(
4602
+ "## Leave alone",
4603
+ "",
4604
+ "Navigation, the order of the sections, where the call to action sits, reading order and legibility. Change the look, never the page grammar. A layout tell above is about how a section looks: the icon tiles, the chip, the strip. The section stays where it is.",
4605
+ ""
4606
+ );
4607
+ return lines.join("\n");
4608
+ }
4074
4609
  // Annotate the CommonJS export names for ESM import in node:
4075
4610
  0 && (module.exports = {
4076
4611
  AI_PHRASES,
@@ -4088,12 +4623,16 @@ function propose(input) {
4088
4623
  DURATION_S,
4089
4624
  EASE,
4090
4625
  EASE_TUPLE,
4626
+ ESTATE_VERSION,
4091
4627
  EXIT_RATIO,
4092
4628
  FINGERPRINT_VERSION,
4093
4629
  HOUSE_BUDGET,
4094
4630
  HOUSE_TYPE,
4095
4631
  LIGHTNESS_CURVE,
4632
+ MIN_DECIDED,
4633
+ MIN_RUNS,
4096
4634
  NEGATIVE_REASSURANCE,
4635
+ NULL_VERSION,
4097
4636
  PILL_LIMIT,
4098
4637
  PLAINER_WORDS,
4099
4638
  PROPOSED,
@@ -4106,27 +4645,37 @@ function propose(input) {
4106
4645
  SCALE,
4107
4646
  SHADCN_LIMIT,
4108
4647
  SHADCN_PRIMITIVES,
4648
+ SIBLING_AT,
4109
4649
  SNAPSHOT_VERSION,
4110
4650
  SOURCE_KINDS,
4111
4651
  SPACE_STEPS,
4112
4652
  SPRING,
4113
4653
  STOCK_PHRASES,
4654
+ TELL_HEAVY,
4114
4655
  TYPE_STEPS,
4656
+ TYPICAL_AT,
4115
4657
  VAGUE_WORDS,
4116
4658
  accentFork,
4659
+ addSite,
4117
4660
  apcaContrast,
4118
4661
  auditSnapshot,
4119
4662
  catalogueTable,
4663
+ characterReport,
4120
4664
  checkCopy,
4121
4665
  checkPair,
4122
4666
  checkRestraint,
4667
+ choicesOf,
4668
+ compareToEstate,
4123
4669
  craftTokens,
4124
4670
  deltaEOk,
4125
4671
  deltaEOkHex,
4126
4672
  densityCss,
4127
4673
  densityTokens,
4128
4674
  emitCss,
4675
+ emptyEstate,
4676
+ estatePairs,
4129
4677
  exitDuration,
4678
+ extractHtml,
4130
4679
  findColours,
4131
4680
  fingerprint,
4132
4681
  fingerprintDistance,
@@ -4136,7 +4685,11 @@ function propose(input) {
4136
4685
  fontClass,
4137
4686
  formatHex,
4138
4687
  formatReport,
4688
+ groundName,
4689
+ harvest,
4139
4690
  hexToOklch,
4691
+ hueName,
4692
+ hueSwatch,
4140
4693
  inP3Gamut,
4141
4694
  inSrgbGamut,
4142
4695
  initDirection,
@@ -4146,6 +4699,7 @@ function propose(input) {
4146
4699
  motionTokens,
4147
4700
  nearest,
4148
4701
  normaliseFamily,
4702
+ nullPrompt,
4149
4703
  oklchToHex,
4150
4704
  oklchToRgb,
4151
4705
  paletteFromPixels,
@@ -4154,16 +4708,22 @@ function propose(input) {
4154
4708
  propose,
4155
4709
  ramp,
4156
4710
  rampFromAnchors,
4711
+ retrofitPlan,
4157
4712
  runTell,
4158
4713
  scanSource,
4159
4714
  sectionRhythm,
4160
4715
  semantic,
4716
+ shapeName,
4717
+ sharedParts,
4161
4718
  shouldAnimate,
4719
+ siblingLine,
4162
4720
  tailwindV3Preset,
4163
4721
  tailwindV4Theme,
4164
4722
  tellById,
4165
4723
  toGamut,
4166
4724
  typeFeatureTokens,
4725
+ typicality,
4167
4726
  validateDirection,
4168
- wcagContrast
4727
+ wcagContrast,
4728
+ wordListsJson
4169
4729
  });