@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.js CHANGED
@@ -827,9 +827,9 @@ function resolveValue(value, props, depth = 0) {
827
827
  if (depth > 8) return null;
828
828
  const match = SINGLE_VAR.exec(value.trim());
829
829
  if (match === null) return value.trim();
830
- const target = props.get(match[1].toLowerCase());
831
- if (target !== void 0 && target !== "initial")
832
- return resolveValue(target, props, depth + 1);
830
+ const target2 = props.get(match[1].toLowerCase());
831
+ if (target2 !== void 0 && target2 !== "initial")
832
+ return resolveValue(target2, props, depth + 1);
833
833
  const fallback = match[2]?.trim();
834
834
  return fallback === void 0 || fallback === "" ? null : resolveValue(fallback, props, depth + 1);
835
835
  }
@@ -2384,7 +2384,7 @@ const inter = Inter({ subsets: ["latin"] });`),
2384
2384
  severity: "warn",
2385
2385
  surface: "source",
2386
2386
  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.",
2387
- fix: "Same as reflex-font. If this face really is the right one, declare an exception with the reason and the evidence.",
2387
+ 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.",
2388
2388
  detect: fontTell(REFLEX_FONTS_2),
2389
2389
  fixtures: {
2390
2390
  flag: [f2("src/styles.css", `@theme {
@@ -3132,15 +3132,16 @@ function makeSnapshot(patch = {}) {
3132
3132
  }
3133
3133
 
3134
3134
  // src/snapshot/fonts.ts
3135
+ var SYSTEM = /^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|ui-rounded|sans-serif|serif|monospace|cursive)$/i;
3135
3136
  function normaliseFamily(raw) {
3136
3137
  let name = raw.replace(/["']/g, "").trim();
3137
3138
  const next = name.match(/^__(.+?)(?:_Fallback)?_[0-9a-f]{5,8}$/i);
3138
3139
  if (next) name = next[1].replace(/_/g, " ");
3139
3140
  name = name.replace(/\s+Fallback$/i, "").replace(/\s+Variable$/i, "");
3141
+ if (SYSTEM.test(name)) return "system-ui";
3140
3142
  if (!/\s/.test(name) && (/[a-z][A-Z]/.test(name) || /^[a-z]+(?:-[a-z]+)+$/.test(name))) {
3141
3143
  name = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/-/g, " ").replace(/\b([a-z])/g, (c) => c.toUpperCase()).replace(/\bDm\b/, "DM");
3142
3144
  }
3143
- if (/^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|sans-serif|serif|monospace)$/i.test(name)) return "system-ui";
3144
3145
  return name;
3145
3146
  }
3146
3147
  var KNOWN = {
@@ -3517,6 +3518,25 @@ function catalogueTable() {
3517
3518
  ...rows
3518
3519
  ].join("\n");
3519
3520
  }
3521
+ function wordListsJson() {
3522
+ return JSON.stringify(
3523
+ {
3524
+ catalogueVersion: CATALOGUE_VERSION,
3525
+ lists: {
3526
+ aiWords: AI_WORDS,
3527
+ stockPhrases: STOCK_PHRASES,
3528
+ aiPhrases: AI_PHRASES,
3529
+ plainerWords: PLAINER_WORDS,
3530
+ buzzwords: BUZZWORDS,
3531
+ negativeReassurance: NEGATIVE_REASSURANCE,
3532
+ vagueWords: VAGUE_WORDS,
3533
+ reviewPhrases: REVIEW_PHRASES
3534
+ }
3535
+ },
3536
+ null,
3537
+ 2
3538
+ ) + "\n";
3539
+ }
3520
3540
 
3521
3541
  // src/character/format.ts
3522
3542
  function formatReport(report2, title) {
@@ -3556,6 +3576,7 @@ function formatReport(report2, title) {
3556
3576
  // src/fingerprint/index.ts
3557
3577
  var FINGERPRINT_VERSION = 1;
3558
3578
  var CHROMATIC = 0.03;
3579
+ var MIN_ACCENT_AREA = 2e4;
3559
3580
  var oklch = (value) => {
3560
3581
  const parsed = parseColour(value);
3561
3582
  return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
@@ -3568,7 +3589,7 @@ function accentOf(s) {
3568
3589
  }
3569
3590
  const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
3570
3591
  if (topButton) return oklch(topButton[0]);
3571
- 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];
3592
+ 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];
3572
3593
  if (bg) return bg.o;
3573
3594
  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];
3574
3595
  return text ? text.o : null;
@@ -3645,8 +3666,8 @@ function fingerprintDistance(a, b) {
3645
3666
  const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
3646
3667
  return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
3647
3668
  }
3648
- function nearest(target, set, k = set.length) {
3649
- return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
3669
+ function nearest(target2, set, k = set.length) {
3670
+ return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target2, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
3650
3671
  }
3651
3672
 
3652
3673
  // src/direction/types.ts
@@ -3953,6 +3974,495 @@ function propose(input) {
3953
3974
  direction: { version: DIRECTION_VERSION, client: input.client, brief: input.brief, sources: input.sources, choices }
3954
3975
  };
3955
3976
  }
3977
+
3978
+ // src/null/index.ts
3979
+ var NULL_VERSION = 1;
3980
+ function nullPrompt(brief) {
3981
+ return [
3982
+ `Build the home page for this business: ${brief.trim()}`,
3983
+ "",
3984
+ "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."
3985
+ ].join("\n");
3986
+ }
3987
+ function extractHtml(reply) {
3988
+ const start = reply.search(/<!doctype html|<html[\s>]/i);
3989
+ if (start < 0) return null;
3990
+ const end = reply.toLowerCase().lastIndexOf("</html>");
3991
+ const html = end > start ? reply.slice(start, end + "</html>".length) : reply.slice(start).replace(/\s*```\s*$/, "");
3992
+ return html.length > 200 ? html : null;
3993
+ }
3994
+ var MIN_RUNS = 5;
3995
+ var TYPICAL_AT = 0.1;
3996
+ function meanNearest(distances, k) {
3997
+ const sorted = [...distances].sort((a, b) => a - b).slice(0, k);
3998
+ return sorted.reduce((s, d) => s + d, 0) / sorted.length;
3999
+ }
4000
+ function median2(values) {
4001
+ const sorted = [...values].sort((a, b) => a - b);
4002
+ const mid = Math.floor(sorted.length / 2);
4003
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
4004
+ }
4005
+ function typicality(target2, runs, k = 3) {
4006
+ if (runs.length < MIN_RUNS) throw new Error(`a null model needs at least ${MIN_RUNS} runs; this one has ${runs.length}`);
4007
+ const kk = Math.min(k, runs.length - 1);
4008
+ const toTarget = runs.map((r) => ({ id: r.id, distance: fingerprintDistance(target2, r.fingerprint).total }));
4009
+ const distance = meanNearest(
4010
+ toTarget.map((t) => t.distance),
4011
+ kk
4012
+ );
4013
+ const loo = runs.map(
4014
+ (r, i) => meanNearest(
4015
+ runs.filter((_, j) => j !== i).map((o) => fingerprintDistance(r.fingerprint, o.fingerprint).total),
4016
+ kk
4017
+ )
4018
+ );
4019
+ const score = loo.filter((d) => d >= distance).length / loo.length;
4020
+ return {
4021
+ score: Math.round(score * 100) / 100,
4022
+ typical: score >= TYPICAL_AT,
4023
+ distance: Math.round(distance * 1e3) / 1e3,
4024
+ baseline: Math.round(median2(loo) * 1e3) / 1e3,
4025
+ nearest: toTarget.sort((a, b) => a.distance - b.distance).slice(0, kk),
4026
+ shared: sharedChoices(target2, runs)
4027
+ };
4028
+ }
4029
+ var HUES = [
4030
+ ["red", 0, 45],
4031
+ ["orange", 45, 75],
4032
+ ["yellow", 75, 110],
4033
+ ["green", 110, 165],
4034
+ ["teal", 165, 215],
4035
+ ["blue", 215, 265],
4036
+ ["violet", 265, 315],
4037
+ ["pink", 315, 360]
4038
+ ];
4039
+ function hueName(o) {
4040
+ if (!o) return "none";
4041
+ if (o.c < 0.03) return "neutral";
4042
+ return HUES.find(([, lo, hi]) => o.h >= lo && o.h < hi)?.[0] ?? "red";
4043
+ }
4044
+ function groundName(o) {
4045
+ if (o.l < 0.3) return "dark";
4046
+ if (isCream(o)) return "cream";
4047
+ if (o.l >= 0.97 && o.c < 0.012) return "white";
4048
+ return o.c < 0.02 ? "grey" : "tinted";
4049
+ }
4050
+ function shapeName(roundness) {
4051
+ if (roundness === null) return "no buttons";
4052
+ if (roundness >= 0.45) return "pill";
4053
+ return roundness >= 0.15 ? "soft" : "square";
4054
+ }
4055
+ function choicesOf(fp) {
4056
+ return {
4057
+ display: [fp.display.family],
4058
+ body: [fp.body.family],
4059
+ accent: [hueName(fp.accent)],
4060
+ ground: [groundName(fp.ground)],
4061
+ shape: [shapeName(fp.roundness)],
4062
+ motion: [fp.motion >= 0.6 ? "reveals most sections" : fp.motion > 0 ? "reveals some sections" : "no scroll reveals"],
4063
+ effects: fp.effects,
4064
+ opening: [fp.layout.slice(0, 3).join(" > ") || "none"]
4065
+ };
4066
+ }
4067
+ function sharedChoices(target2, runs) {
4068
+ const mine = choicesOf(target2);
4069
+ const all = runs.map((r) => choicesOf(r.fingerprint));
4070
+ const out = [];
4071
+ for (const [part, values] of Object.entries(mine)) {
4072
+ for (const value of values) {
4073
+ const count2 = all.filter((c) => c[part].includes(value)).length;
4074
+ if (count2 / runs.length >= 0.25) out.push({ part, value, runs: count2, of: runs.length });
4075
+ }
4076
+ }
4077
+ return out.sort((a, b) => b.runs - a.runs);
4078
+ }
4079
+ var REFLEX_1 = new Set(REFLEX_FONTS_1.map((f3) => f3.toLowerCase()));
4080
+ var REFLEX_2 = new Set(REFLEX_FONTS_2.map((f3) => f3.toLowerCase()));
4081
+ function knownFor(kind, value, sample) {
4082
+ if (kind === "face") return REFLEX_1.has(value.toLowerCase()) ? "reflex-font" : REFLEX_2.has(value.toLowerCase()) ? "reflex-font-2" : null;
4083
+ if (kind === "accent" && value === "violet") return sample.some((f3) => f3.accent && isAiViolet(f3.accent)) ? "ai-violet" : null;
4084
+ if (kind === "ground" && value === "cream") return "cream-palette";
4085
+ if (kind === "shape" && value === "pill") return "pill-everything";
4086
+ if (kind === "motion" && value === "reveals most sections") return "reveal-everywhere";
4087
+ if (kind === "tell") return value;
4088
+ if (kind === "opening" && /^hero > (?:logos|stats)/.test(value)) return "hero-then-proof";
4089
+ return null;
4090
+ }
4091
+ var STOP2 = new Set(
4092
+ "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(" ")
4093
+ );
4094
+ function grams(text) {
4095
+ const out = /* @__PURE__ */ new Set();
4096
+ for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
4097
+ const words2 = clause.match(/[a-z][a-z'-]*/g) ?? [];
4098
+ for (let n = 2; n <= 4; n += 1) {
4099
+ for (let i = 0; i + n <= words2.length; i += 1) {
4100
+ const g = words2.slice(i, i + n);
4101
+ if (STOP2.has(g[0]) || STOP2.has(g[n - 1])) continue;
4102
+ if (g.filter((w) => !STOP2.has(w)).length < 2) continue;
4103
+ out.add(g.join(" "));
4104
+ }
4105
+ }
4106
+ }
4107
+ return out;
4108
+ }
4109
+ var GRAMMAR = /* @__PURE__ */ new Set([
4110
+ "privacy policy",
4111
+ "cookie policy",
4112
+ "rights reserved",
4113
+ "all rights reserved",
4114
+ "terms of service",
4115
+ "terms and conditions",
4116
+ "terms conditions",
4117
+ "how it works",
4118
+ "what we do",
4119
+ "about us",
4120
+ "contact us",
4121
+ "get in touch",
4122
+ "opening hours",
4123
+ "read more",
4124
+ "learn more",
4125
+ "book now",
4126
+ "free quote",
4127
+ "our services",
4128
+ "quick links",
4129
+ "email address",
4130
+ "phone number",
4131
+ "full name",
4132
+ "first name",
4133
+ "last name",
4134
+ "your name",
4135
+ "your email",
4136
+ "send message"
4137
+ ]);
4138
+ var LEGAL = /©|&copy;|\bregistered (?:in|office|number)\b|\bcompany (?:no|number|reg)|\bvat (?:no|number|reg)/i;
4139
+ 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;
4140
+ function textGrams(text) {
4141
+ if (LEGAL.test(text)) return /* @__PURE__ */ new Set();
4142
+ const out = grams(text.replace(NOISE, " | "));
4143
+ for (const g of out) if (GRAMMAR.has(g)) out.delete(g);
4144
+ return out;
4145
+ }
4146
+ function properNouns(brief) {
4147
+ return new Set((brief.match(/\b[A-Z][a-z]+/g) ?? []).map((w) => w.toLowerCase()).filter((w) => !STOP2.has(w)));
4148
+ }
4149
+ function harvest(models, options = {}) {
4150
+ const minShare = options.minShare ?? 0.25;
4151
+ const runs = models.flatMap((m, b) => m.runs.map((r) => ({ ...r, brief: b })));
4152
+ const of = runs.length;
4153
+ if (of === 0) return [];
4154
+ const multiBrief = models.length > 1;
4155
+ const sample = runs.map((r) => r.fingerprint);
4156
+ const tally = /* @__PURE__ */ new Map();
4157
+ const add = (kind, value, run, brief, example) => {
4158
+ const key = `${kind}\0${value}`;
4159
+ const t = tally.get(key) ?? { kind, value, runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example };
4160
+ t.runs.add(run);
4161
+ t.briefs.add(brief);
4162
+ tally.set(key, t);
4163
+ };
4164
+ runs.forEach((r, i) => {
4165
+ const c = choicesOf(r.fingerprint);
4166
+ for (const f3 of /* @__PURE__ */ new Set([...c.display, ...c.body])) add("face", f3, i, r.brief);
4167
+ add("accent", c.accent[0], i, r.brief);
4168
+ add("ground", c.ground[0], i, r.brief);
4169
+ add("shape", c.shape[0], i, r.brief);
4170
+ add("motion", c.motion[0], i, r.brief);
4171
+ add("opening", c.opening[0], i, r.brief);
4172
+ for (const t of r.tells) add("tell", t, i, r.brief);
4173
+ });
4174
+ const out = [];
4175
+ for (const t of tally.values()) {
4176
+ if (t.runs.size / of < minShare) continue;
4177
+ if (t.kind === "accent" && (t.value === "none" || t.value === "neutral")) continue;
4178
+ if (t.kind === "shape" && t.value === "no buttons") continue;
4179
+ if (t.kind === "ground" && (t.value === "white" || t.value === "grey")) continue;
4180
+ if (t.kind === "motion" && t.value === "no scroll reveals") continue;
4181
+ out.push({ kind: t.kind, value: t.value, runs: t.runs.size, of, briefs: t.briefs.size, known: knownFor(t.kind, t.value, sample) });
4182
+ }
4183
+ const phraseMin = Math.max(3, Math.ceil(of * minShare * 0.4));
4184
+ const names = models.map((m) => properNouns(m.brief));
4185
+ const phrases2 = /* @__PURE__ */ new Map();
4186
+ runs.forEach((r, i) => {
4187
+ for (const text of r.copy) {
4188
+ for (const g of textGrams(text)) {
4189
+ if (g.split(" ").some((w) => names[r.brief].has(w))) continue;
4190
+ const p = phrases2.get(g) ?? { runs: /* @__PURE__ */ new Set(), briefs: /* @__PURE__ */ new Set(), example: text.trim().slice(0, 140) };
4191
+ p.runs.add(i);
4192
+ p.briefs.add(r.brief);
4193
+ phrases2.set(g, p);
4194
+ }
4195
+ }
4196
+ });
4197
+ 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);
4198
+ const maximal = kept.filter(([g, p]) => !kept.some(([h, q]) => h !== g && h.includes(g) && q.runs.size >= 0.8 * p.runs.size));
4199
+ for (const [g, p] of maximal.slice(0, 40)) {
4200
+ out.push({ kind: "phrase", value: g, runs: p.runs.size, of, briefs: p.briefs.size, known: options.copyTell?.(g) ?? null, example: p.example });
4201
+ }
4202
+ const order = ["face", "accent", "ground", "shape", "motion", "opening", "tell", "phrase"];
4203
+ return out.sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind) || b.runs - a.runs);
4204
+ }
4205
+ function hueSwatch(runs, hue) {
4206
+ const hits = runs.map((r) => r.fingerprint.accent).filter((o) => !!o && hueName(o) === hue);
4207
+ if (hits.length === 0) return null;
4208
+ 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);
4209
+ return formatHex(oklchToRgb(centre));
4210
+ }
4211
+
4212
+ // src/estate/index.ts
4213
+ var ESTATE_VERSION = 1;
4214
+ var SIBLING_AT = 0.31;
4215
+ function emptyEstate() {
4216
+ return { version: ESTATE_VERSION, sites: [] };
4217
+ }
4218
+ function addSite(register, site) {
4219
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
4220
+ const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
4221
+ return { version: ESTATE_VERSION, sites };
4222
+ }
4223
+ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
4224
+ const out = [];
4225
+ const close = (k) => d.parts[k] <= 0.2;
4226
+ if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
4227
+ if (close("type")) {
4228
+ if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
4229
+ if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
4230
+ }
4231
+ if (close("ground")) out.push(`${groundName(a.ground)} ground`);
4232
+ if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
4233
+ if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
4234
+ if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
4235
+ if (close("layout")) out.push("running order");
4236
+ return out;
4237
+ }
4238
+ function compareToEstate(target2, register, options = {}) {
4239
+ const at = options.siblingAt ?? SIBLING_AT;
4240
+ return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
4241
+ const d = fingerprintDistance(target2, s.fingerprint);
4242
+ return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
4243
+ }).sort((a, b) => a.distance - b.distance);
4244
+ }
4245
+ function estatePairs(register, siblingAt = SIBLING_AT) {
4246
+ const out = [];
4247
+ const s = register.sites;
4248
+ for (let i = 0; i < s.length; i += 1) {
4249
+ for (let j = i + 1; j < s.length; j += 1) {
4250
+ const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
4251
+ 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) });
4252
+ }
4253
+ }
4254
+ return out.sort((x, y) => x.distance - y.distance);
4255
+ }
4256
+ function siblingLine(briefs) {
4257
+ const d = [];
4258
+ for (const runs of briefs) {
4259
+ 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);
4260
+ }
4261
+ if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
4262
+ d.sort((a, b) => a - b);
4263
+ const mid = Math.floor(d.length / 2);
4264
+ const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
4265
+ return Math.round(m * 1e3) / 1e3;
4266
+ }
4267
+
4268
+ // src/report/index.ts
4269
+ var TELL_HEAVY = 3;
4270
+ var MIN_DECIDED = 5;
4271
+ var TELL_AREA = {
4272
+ "reflex-font": { area: "type", choice: "display" },
4273
+ "reflex-font-2": { area: "type", choice: "display" },
4274
+ "italic-serif-display": { area: "type", choice: "display" },
4275
+ "ai-violet": { area: "colour", choice: "accent" },
4276
+ "blue-purple-gradient": { area: "colour", choice: "accent" },
4277
+ "gradient-text": { area: "colour", choice: "accent" },
4278
+ "cream-palette": { area: "colour", choice: "ground" },
4279
+ "pill-everything": { area: "shape", choice: "shape" },
4280
+ "shadcn-dump": { area: "shape", choice: "shape" },
4281
+ "glass-panel": { area: "effects", choice: "motif" },
4282
+ "radial-spotlight-glow": { area: "effects", choice: "motif" },
4283
+ "grid-background": { area: "effects", choice: "motif" },
4284
+ "thin-border-wide-shadow": { area: "effects", choice: "shape" },
4285
+ "icon-tile-stack": { area: "effects", choice: "motif" },
4286
+ "reveal-everywhere": { area: "motion", choice: "signature" },
4287
+ "intro-cinematic": { area: "motion", choice: "signature" },
4288
+ marquee: { area: "motion", choice: "signature" },
4289
+ "hero-then-proof": { area: "layout" },
4290
+ "icon-tile-grid": { area: "layout" },
4291
+ "hero-eyebrow-chip": { area: "layout" },
4292
+ "bento-grid": { area: "layout" }
4293
+ };
4294
+ var PART_AREA = {
4295
+ display: { area: "type", choice: "display" },
4296
+ body: { area: "type", choice: "body" },
4297
+ accent: { area: "colour", choice: "accent" },
4298
+ ground: { area: "colour", choice: "ground" },
4299
+ shape: { area: "shape", choice: "shape" },
4300
+ effects: { area: "effects", choice: "motif" },
4301
+ motion: { area: "motion", choice: "signature" }
4302
+ };
4303
+ var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
4304
+ function designFindings(findings, copyTells) {
4305
+ return (findings?.findings ?? []).filter((f3) => !copyTells.has(f3.tell));
4306
+ }
4307
+ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
4308
+ const actions = [];
4309
+ const signals = [];
4310
+ if (input.findings) {
4311
+ const design = designFindings(input.findings, copyTells);
4312
+ const distinct = [...new Set(design.map((f3) => f3.tell))];
4313
+ signals.push({
4314
+ id: "tells",
4315
+ raised: distinct.length >= TELL_HEAVY,
4316
+ detail: `${distinct.length} distinct design tell${distinct.length === 1 ? "" : "s"}${distinct.length ? `: ${distinct.join(", ")}` : ""}`
4317
+ });
4318
+ const seen = /* @__PURE__ */ new Set();
4319
+ for (const f3 of design) {
4320
+ if (seen.has(f3.tell)) continue;
4321
+ seen.add(f3.tell);
4322
+ const map = TELL_AREA[f3.tell] ?? { area: "effects" };
4323
+ const both = design.some((g) => g.tell === f3.tell && g.line === 0) && design.some((g) => g.tell === f3.tell && g.line > 0);
4324
+ 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 });
4325
+ }
4326
+ const copy = (input.findings.findings ?? []).filter((f3) => copyTells.has(f3.tell));
4327
+ const byTell = /* @__PURE__ */ new Map();
4328
+ for (const f3 of copy) byTell.set(f3.tell, [...byTell.get(f3.tell) ?? [], f3]);
4329
+ for (const [tell, list] of byTell) {
4330
+ 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 });
4331
+ }
4332
+ } else {
4333
+ signals.push({ id: "tells", raised: null, detail: "not measured" });
4334
+ }
4335
+ if (input.typicality) {
4336
+ const t = input.typicality;
4337
+ 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` });
4338
+ if (t.typical) {
4339
+ for (const s of t.shared) {
4340
+ const map = PART_AREA[s.part];
4341
+ if (!map || DEFAULTS.test(`${s.part} ${s.value}`)) continue;
4342
+ 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 });
4343
+ }
4344
+ }
4345
+ } else {
4346
+ signals.push({ id: "typicality", raised: null, detail: "not measured: no null model for this brief" });
4347
+ }
4348
+ if (input.estate) {
4349
+ const siblings = input.estate.filter((m) => m.sibling);
4350
+ const nearest2 = input.estate[0];
4351
+ signals.push({
4352
+ id: "estate",
4353
+ raised: siblings.length > 0,
4354
+ 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"
4355
+ });
4356
+ for (const s of siblings) {
4357
+ for (const part of s.shared) {
4358
+ if (DEFAULTS.test(part)) continue;
4359
+ 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) });
4360
+ }
4361
+ }
4362
+ } else {
4363
+ signals.push({ id: "estate", raised: null, detail: "not measured: no estate register" });
4364
+ }
4365
+ if (input.direction) {
4366
+ const d = input.direction;
4367
+ const raised2 = !d.valid || d.decided < MIN_DECIDED;
4368
+ signals.push({ id: "reasons", raised: raised2, detail: `${d.decided} of 7 choices decided with a reason${d.valid ? "" : ", art-direction.json has errors"}` });
4369
+ for (const p of d.problems) {
4370
+ const key = p.at.match(/^choices\.(\w+)/)?.[1];
4371
+ if (p.severity === "error" || p.message === "not decided yet") {
4372
+ 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 });
4373
+ }
4374
+ }
4375
+ } else {
4376
+ signals.push({ id: "reasons", raised: null, detail: "not measured: no art-direction.json" });
4377
+ 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 });
4378
+ }
4379
+ const raised = signals.filter((s) => s.raised === true).length;
4380
+ const unmeasured = signals.filter((s) => s.raised === null).map((s) => s.id);
4381
+ const verdict = raised >= 2 ? "default" : raised === 1 ? "mixed" : unmeasured.length ? "unproven" : "decided";
4382
+ 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.`;
4383
+ const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
4384
+ const merged = [];
4385
+ for (const a of actions) {
4386
+ const same = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
4387
+ if (same) {
4388
+ const fromTell = /\(\S+, gen \d\)/.test(a.why);
4389
+ same.why = `${same.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
4390
+ same.priority = Math.min(same.priority, a.priority);
4391
+ } else merged.push({ ...a });
4392
+ }
4393
+ const unique = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
4394
+ unique.sort((a, b) => a.priority - b.priority || order.indexOf(a.area) - order.indexOf(b.area));
4395
+ return { site: input.site, verdict, summary, signals, actions: unique };
4396
+ }
4397
+ function areaOfShared(part) {
4398
+ if (/headline|body$/.test(part)) return "type";
4399
+ if (/accent|ground/.test(part)) return "colour";
4400
+ if (/buttons/.test(part)) return "shape";
4401
+ if (/reveal/.test(part)) return "motion";
4402
+ if (/running order/.test(part)) return "layout";
4403
+ return "effects";
4404
+ }
4405
+ function choiceOfShared(part) {
4406
+ if (/headline/.test(part)) return "display";
4407
+ if (/body$/.test(part)) return "body";
4408
+ if (/accent/.test(part)) return "accent";
4409
+ if (/ground/.test(part)) return "ground";
4410
+ if (/buttons/.test(part)) return "shape";
4411
+ if (/reveal/.test(part)) return "signature";
4412
+ return /running order/.test(part) ? void 0 : "motif";
4413
+ }
4414
+
4415
+ // src/report/retrofit.ts
4416
+ var HEADINGS = {
4417
+ direction: "Decide first",
4418
+ type: "Type",
4419
+ colour: "Colour",
4420
+ shape: "Shape",
4421
+ effects: "Effects",
4422
+ motion: "Motion",
4423
+ layout: "How sections look (not where they sit)",
4424
+ copy: "Copy"
4425
+ };
4426
+ var ORDER2 = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
4427
+ function target(a, direction) {
4428
+ if (!a.choice || a.area === "direction") return "";
4429
+ const c = direction?.choices?.[a.choice];
4430
+ if (c && c.because && !c.because.startsWith("PROPOSED:")) return `
4431
+ - Use: **${c.value}** for ${a.choice}. ${c.because}`;
4432
+ return `
4433
+ - Waits on: deciding **${a.choice}** in art-direction.json.`;
4434
+ }
4435
+ function retrofitPlan(report2, direction) {
4436
+ const lines = [
4437
+ `# Retrofit: ${report2.site}`,
4438
+ "",
4439
+ `**Now:** ${report2.verdict}. ${report2.summary}`,
4440
+ "",
4441
+ "| Signal | Raised | Reading |",
4442
+ "|---|---|---|",
4443
+ ...report2.signals.map((s) => `| ${s.id} | ${s.raised === null ? "not measured" : s.raised ? "yes" : "no"} | ${s.detail} |`),
4444
+ "",
4445
+ "**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.",
4446
+ ""
4447
+ ];
4448
+ let n = 0;
4449
+ for (const area of ORDER2) {
4450
+ const list = report2.actions.filter((a) => a.area === area);
4451
+ if (list.length === 0) continue;
4452
+ n += 1;
4453
+ lines.push(`## ${n}. ${HEADINGS[area]}`, "");
4454
+ for (const a of list) lines.push(`- [ ] ${a.what} *(${a.why})*${target(a, direction)}`);
4455
+ lines.push("");
4456
+ }
4457
+ if (n === 0) lines.push("Nothing to change.", "");
4458
+ lines.push(
4459
+ "## Leave alone",
4460
+ "",
4461
+ "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.",
4462
+ ""
4463
+ );
4464
+ return lines.join("\n");
4465
+ }
3956
4466
  export {
3957
4467
  AI_PHRASES,
3958
4468
  AI_VIOLET,
@@ -3969,12 +4479,16 @@ export {
3969
4479
  DURATION_S,
3970
4480
  EASE,
3971
4481
  EASE_TUPLE,
4482
+ ESTATE_VERSION,
3972
4483
  EXIT_RATIO,
3973
4484
  FINGERPRINT_VERSION,
3974
4485
  HOUSE_BUDGET,
3975
4486
  HOUSE_TYPE,
3976
4487
  LIGHTNESS_CURVE,
4488
+ MIN_DECIDED,
4489
+ MIN_RUNS,
3977
4490
  NEGATIVE_REASSURANCE,
4491
+ NULL_VERSION,
3978
4492
  PILL_LIMIT,
3979
4493
  PLAINER_WORDS,
3980
4494
  PROPOSED,
@@ -3987,27 +4501,37 @@ export {
3987
4501
  SCALE,
3988
4502
  SHADCN_LIMIT,
3989
4503
  SHADCN_PRIMITIVES,
4504
+ SIBLING_AT,
3990
4505
  SNAPSHOT_VERSION,
3991
4506
  SOURCE_KINDS,
3992
4507
  SPACE_STEPS,
3993
4508
  SPRING,
3994
4509
  STOCK_PHRASES,
4510
+ TELL_HEAVY,
3995
4511
  TYPE_STEPS,
4512
+ TYPICAL_AT,
3996
4513
  VAGUE_WORDS,
3997
4514
  accentFork,
4515
+ addSite,
3998
4516
  apcaContrast,
3999
4517
  auditSnapshot,
4000
4518
  catalogueTable,
4519
+ characterReport,
4001
4520
  checkCopy,
4002
4521
  checkPair,
4003
4522
  checkRestraint,
4523
+ choicesOf,
4524
+ compareToEstate,
4004
4525
  craftTokens,
4005
4526
  deltaEOk,
4006
4527
  deltaEOkHex,
4007
4528
  densityCss,
4008
4529
  densityTokens,
4009
4530
  emitCss,
4531
+ emptyEstate,
4532
+ estatePairs,
4010
4533
  exitDuration,
4534
+ extractHtml,
4011
4535
  findColours,
4012
4536
  fingerprint,
4013
4537
  fingerprintDistance,
@@ -4017,7 +4541,11 @@ export {
4017
4541
  fontClass,
4018
4542
  formatHex,
4019
4543
  formatReport,
4544
+ groundName,
4545
+ harvest,
4020
4546
  hexToOklch,
4547
+ hueName,
4548
+ hueSwatch,
4021
4549
  inP3Gamut,
4022
4550
  inSrgbGamut,
4023
4551
  initDirection,
@@ -4027,6 +4555,7 @@ export {
4027
4555
  motionTokens,
4028
4556
  nearest,
4029
4557
  normaliseFamily,
4558
+ nullPrompt,
4030
4559
  oklchToHex,
4031
4560
  oklchToRgb,
4032
4561
  paletteFromPixels,
@@ -4035,16 +4564,22 @@ export {
4035
4564
  propose,
4036
4565
  ramp,
4037
4566
  rampFromAnchors,
4567
+ retrofitPlan,
4038
4568
  runTell,
4039
4569
  scanSource,
4040
4570
  sectionRhythm,
4041
4571
  semantic,
4572
+ shapeName,
4573
+ sharedParts,
4042
4574
  shouldAnimate,
4575
+ siblingLine,
4043
4576
  tailwindV3Preset,
4044
4577
  tailwindV4Theme,
4045
4578
  tellById,
4046
4579
  toGamut,
4047
4580
  typeFeatureTokens,
4581
+ typicality,
4048
4582
  validateDirection,
4049
- wcagContrast
4583
+ wcagContrast,
4584
+ wordListsJson
4050
4585
  };