@domandigital/craft 0.10.0 → 0.12.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
@@ -23,10 +23,12 @@ __export(index_exports, {
23
23
  AI_PHRASES: () => AI_PHRASES,
24
24
  AI_VIOLET: () => AI_VIOLET,
25
25
  AI_WORDS: () => AI_WORDS,
26
+ BASELINE_VERSION: () => BASELINE_VERSION,
26
27
  BUZZWORDS: () => BUZZWORDS,
27
28
  CATALOGUE: () => CATALOGUE,
28
29
  CATALOGUE_VERSION: () => CATALOGUE_VERSION,
29
30
  CHOICE_KEYS: () => CHOICE_KEYS,
31
+ DEFAULT_IGNORE: () => DEFAULT_IGNORE,
30
32
  DENSITIES: () => DENSITIES,
31
33
  DENSITY_NAMES: () => DENSITY_NAMES,
32
34
  DIRECTION_VERSION: () => DIRECTION_VERSION,
@@ -35,12 +37,16 @@ __export(index_exports, {
35
37
  DURATION_S: () => DURATION_S,
36
38
  EASE: () => EASE,
37
39
  EASE_TUPLE: () => EASE_TUPLE,
40
+ EDGE_STEP: () => EDGE_STEP,
38
41
  ESTATE_VERSION: () => ESTATE_VERSION,
39
42
  EXIT_RATIO: () => EXIT_RATIO,
40
43
  FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
41
44
  HOUSE: () => HOUSE,
42
45
  HOUSE_BUDGET: () => HOUSE_BUDGET,
46
+ HOUSE_LOCKED: () => HOUSE_LOCKED,
43
47
  HOUSE_TYPE: () => HOUSE_TYPE,
48
+ JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
49
+ LICENCES: () => LICENCES,
44
50
  LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
45
51
  MIN_DECIDED: () => MIN_DECIDED,
46
52
  MIN_RUNS: () => MIN_RUNS,
@@ -50,6 +56,7 @@ __export(index_exports, {
50
56
  PLAINER_WORDS: () => PLAINER_WORDS,
51
57
  PROPOSED: () => PROPOSED,
52
58
  RAMP_STEPS: () => RAMP_STEPS,
59
+ READABLE_SNAPSHOT_VERSIONS: () => READABLE_SNAPSHOT_VERSIONS,
53
60
  REFLEX_FONTS_1: () => REFLEX_FONTS_1,
54
61
  REFLEX_FONTS_2: () => REFLEX_FONTS_2,
55
62
  RENDERED_PATHS: () => RENDERED_PATHS,
@@ -71,7 +78,9 @@ __export(index_exports, {
71
78
  accentFork: () => accentFork,
72
79
  addSite: () => addSite,
73
80
  apcaContrast: () => apcaContrast,
81
+ applyBaseline: () => applyBaseline,
74
82
  applyHouseGate: () => applyHouseGate,
83
+ applySeverity: () => applySeverity,
75
84
  auditSnapshot: () => auditSnapshot,
76
85
  catalogueTable: () => catalogueTable,
77
86
  characterReport: () => characterReport,
@@ -82,6 +91,7 @@ __export(index_exports, {
82
91
  compareFacts: () => compareFacts,
83
92
  compareToEstate: () => compareToEstate,
84
93
  craftTokens: () => craftTokens,
94
+ createBaseline: () => createBaseline,
85
95
  deltaEOk: () => deltaEOk,
86
96
  deltaEOkHex: () => deltaEOkHex,
87
97
  densityCss: () => densityCss,
@@ -91,8 +101,10 @@ __export(index_exports, {
91
101
  estatePairs: () => estatePairs,
92
102
  exitDuration: () => exitDuration,
93
103
  extractHtml: () => extractHtml,
104
+ faceLicence: () => faceLicence,
94
105
  findClaims: () => findClaims,
95
106
  findColours: () => findColours,
107
+ findingKey: () => findingKey,
96
108
  fingerprint: () => fingerprint,
97
109
  fingerprintDistance: () => fingerprintDistance,
98
110
  fluidClamp: () => fluidClamp,
@@ -103,17 +115,22 @@ __export(index_exports, {
103
115
  formatComparison: () => formatComparison,
104
116
  formatHex: () => formatHex,
105
117
  formatReport: () => formatReport,
118
+ globToRegExp: () => globToRegExp,
106
119
  groundName: () => groundName,
107
120
  harvest: () => harvest,
108
121
  hexToOklch: () => hexToOklch,
122
+ holdHouseBlocks: () => holdHouseBlocks,
109
123
  houseRule: () => houseRule,
110
124
  hueName: () => hueName,
111
125
  hueSwatch: () => hueSwatch,
126
+ ignoreMatcher: () => ignoreMatcher,
112
127
  inP3Gamut: () => inP3Gamut,
113
128
  inSrgbGamut: () => inSrgbGamut,
114
129
  initDirection: () => initDirection,
115
130
  isAiViolet: () => isAiViolet,
116
131
  isCream: () => isCream,
132
+ layoutDistance: () => layoutDistance,
133
+ licencesJson: () => licencesJson,
117
134
  makeSnapshot: () => makeSnapshot,
118
135
  motionTokens: () => motionTokens,
119
136
  namedSources: () => namedSources,
@@ -123,17 +140,21 @@ __export(index_exports, {
123
140
  oklchToHex: () => oklchToHex,
124
141
  oklchToRgb: () => oklchToRgb,
125
142
  paletteFromPixels: () => paletteFromPixels,
143
+ parseBaseline: () => parseBaseline,
126
144
  parseColour: () => parseColour,
145
+ parseCraftConfig: () => parseCraftConfig,
127
146
  parseHex: () => parseHex,
128
147
  propose: () => propose,
129
148
  protectedFacts: () => protectedFacts,
130
149
  ramp: () => ramp,
131
150
  rampFromAnchors: () => rampFromAnchors,
151
+ readSnapshot: () => readSnapshot,
132
152
  retrofitPlan: () => retrofitPlan,
133
153
  runTell: () => runTell,
134
154
  scanSource: () => scanSource,
135
155
  sectionRhythm: () => sectionRhythm,
136
156
  semantic: () => semantic,
157
+ severityExceptions: () => severityExceptions,
137
158
  shapeName: () => shapeName,
138
159
  sharedParts: () => sharedParts,
139
160
  shouldAnimate: () => shouldAnimate,
@@ -142,10 +163,13 @@ __export(index_exports, {
142
163
  tailwindV4Theme: () => tailwindV4Theme,
143
164
  tellById: () => tellById,
144
165
  toGamut: () => toGamut,
166
+ toJson: () => toJson,
167
+ toSarif: () => toSarif,
145
168
  typeFeatureTokens: () => typeFeatureTokens,
146
169
  typicality: () => typicality,
147
170
  validateDirection: () => validateDirection,
148
171
  visibleText: () => visibleText,
172
+ visualMeasures: () => visualMeasures,
149
173
  wcagContrast: () => wcagContrast,
150
174
  wordListsJson: () => wordListsJson
151
175
  });
@@ -2895,6 +2919,7 @@ var REFLEX_FONTS_2 = [
2895
2919
  "Plus Jakarta Sans",
2896
2920
  "Outfit",
2897
2921
  "Geist",
2922
+ "Cal Sans",
2898
2923
  "Sora",
2899
2924
  "Instrument Sans",
2900
2925
  "Instrument Serif",
@@ -3116,7 +3141,7 @@ function iconTiles(file) {
3116
3141
  return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
3117
3142
  });
3118
3143
  }
3119
- var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
3144
+ 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[^"']*)["']/;
3120
3145
  function iconTileStackHits(ctx) {
3121
3146
  const hits = [];
3122
3147
  for (const file of ctx.files) {
@@ -3263,6 +3288,12 @@ function shadcnHits(ctx) {
3263
3288
  const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
3264
3289
  return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
3265
3290
  }
3291
+ function shadcnCardHits(ctx) {
3292
+ return eachUnit(
3293
+ ctx,
3294
+ (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
3295
+ );
3296
+ }
3266
3297
  function marqueeHits(ctx) {
3267
3298
  const hits = [];
3268
3299
  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;
@@ -3480,6 +3511,23 @@ const inter = Inter({ subsets: ["latin"] });`),
3480
3511
  pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
3481
3512
  }
3482
3513
  },
3514
+ {
3515
+ id: "shadcn-card-stock",
3516
+ name: "Stock shadcn Card",
3517
+ generation: 1,
3518
+ severity: "warn",
3519
+ surface: "source",
3520
+ 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.",
3521
+ 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.",
3522
+ detect: shadcnCardHits,
3523
+ fixtures: {
3524
+ flag: [
3525
+ f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
3526
+ 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)}`)
3527
+ ],
3528
+ pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
3529
+ }
3530
+ },
3483
3531
  {
3484
3532
  id: "cream-palette",
3485
3533
  name: "Cream ground",
@@ -3706,6 +3754,17 @@ var HOUSE = {
3706
3754
  function houseRule(id, name = id) {
3707
3755
  return HOUSE[id] ?? { tier: "review", label: name };
3708
3756
  }
3757
+ 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";
3758
+ function holdHouseBlocks(exceptions, severity) {
3759
+ const blocks = (tell) => HOUSE[tell]?.tier === "block";
3760
+ const refusedSeverity = Object.entries(severity).filter(([tell, o]) => blocks(tell) && o.level !== "block").map(([tell]) => tell);
3761
+ return {
3762
+ exceptions: exceptions.filter((e) => !blocks(e.tell)),
3763
+ refused: exceptions.filter((e) => blocks(e.tell)),
3764
+ severity: Object.fromEntries(Object.entries(severity).filter(([tell]) => !refusedSeverity.includes(tell))),
3765
+ refusedSeverity
3766
+ };
3767
+ }
3709
3768
 
3710
3769
  // src/character/parse.ts
3711
3770
  var MARKUP = /\.(tsx|jsx|html?|astro|vue|svelte|mdx)$/i;
@@ -4039,7 +4098,7 @@ function extractStrings(file) {
4039
4098
  }
4040
4099
 
4041
4100
  // src/snapshot/types.ts
4042
- var SNAPSHOT_VERSION = 1;
4101
+ var SNAPSHOT_VERSION = 2;
4043
4102
 
4044
4103
  // src/snapshot/fixture.ts
4045
4104
  function makeSnapshot(patch = {}) {
@@ -4373,7 +4432,7 @@ var RENDERED_PATHS = {
4373
4432
  };
4374
4433
 
4375
4434
  // src/character/check.ts
4376
- var CATALOGUE_VERSION = "2026.09.8";
4435
+ var CATALOGUE_VERSION = "2026.09.9";
4377
4436
  var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
4378
4437
  (t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
4379
4438
  );
@@ -4752,8 +4811,267 @@ function formatClaims(report2, label = "craft copy claims") {
4752
4811
  return lines.join("\n");
4753
4812
  }
4754
4813
 
4814
+ // src/character/baseline.ts
4815
+ var BASELINE_VERSION = 1;
4816
+ function fnv1a(text) {
4817
+ let hash = 2166136261;
4818
+ for (let i = 0; i < text.length; i += 1) {
4819
+ hash ^= text.charCodeAt(i);
4820
+ hash = Math.imul(hash, 16777619);
4821
+ }
4822
+ return (hash >>> 0).toString(16).padStart(8, "0");
4823
+ }
4824
+ var excerptHash = (f4) => fnv1a(f4.excerpt.replace(/\s+/g, " ").trim());
4825
+ var keyOf = (tell, path, excerpt) => `${tell}\0${path}\0${excerpt}`;
4826
+ function findingKey(f4) {
4827
+ return keyOf(f4.tell, f4.path, excerptHash(f4));
4828
+ }
4829
+ function createBaseline(report2) {
4830
+ const counts = /* @__PURE__ */ new Map();
4831
+ for (const f4 of report2.findings) {
4832
+ const excerpt = excerptHash(f4);
4833
+ const key = keyOf(f4.tell, f4.path, excerpt);
4834
+ const entry = counts.get(key);
4835
+ if (entry) entry.count += 1;
4836
+ else counts.set(key, { tell: f4.tell, path: f4.path, excerpt, count: 1 });
4837
+ }
4838
+ const entries = [...counts.values()].sort((a, b) => a.path.localeCompare(b.path) || a.tell.localeCompare(b.tell) || a.excerpt.localeCompare(b.excerpt));
4839
+ return { version: BASELINE_VERSION, catalogueVersion: report2.catalogueVersion, entries };
4840
+ }
4841
+ function parseBaseline(data, source = "baseline") {
4842
+ const b = data;
4843
+ if (!b || typeof b !== "object" || b.version !== BASELINE_VERSION || !Array.isArray(b.entries)) {
4844
+ throw new Error(`${source}: not a craft baseline (version ${BASELINE_VERSION})`);
4845
+ }
4846
+ for (const e of b.entries) {
4847
+ if (typeof e?.tell !== "string" || typeof e.path !== "string" || typeof e.excerpt !== "string" || !Number.isInteger(e.count) || e.count < 1) {
4848
+ throw new Error(`${source}: malformed entry ${JSON.stringify(e)}`);
4849
+ }
4850
+ }
4851
+ return b;
4852
+ }
4853
+ function recount(report2, findings) {
4854
+ const byGeneration = { 1: 0, 2: 0, 3: 0 };
4855
+ const byTell = {};
4856
+ for (const f4 of findings) {
4857
+ byGeneration[f4.generation] += 1;
4858
+ byTell[f4.tell] = (byTell[f4.tell] ?? 0) + 1;
4859
+ }
4860
+ return {
4861
+ ...report2,
4862
+ findings,
4863
+ summary: { ...report2.summary, findings: findings.length, byGeneration, byTell, blocking: findings.filter((f4) => f4.severity === "block").length }
4864
+ };
4865
+ }
4866
+ function applyBaseline(report2, baseline) {
4867
+ const remaining = /* @__PURE__ */ new Map();
4868
+ for (const e of baseline.entries) {
4869
+ const key = keyOf(e.tell, e.path, e.excerpt);
4870
+ remaining.set(key, (remaining.get(key) ?? 0) + e.count);
4871
+ }
4872
+ const kept = [];
4873
+ let baselined = 0;
4874
+ for (const f4 of report2.findings) {
4875
+ const key = findingKey(f4);
4876
+ const left = remaining.get(key) ?? 0;
4877
+ if (left > 0) {
4878
+ remaining.set(key, left - 1);
4879
+ baselined += 1;
4880
+ } else kept.push(f4);
4881
+ }
4882
+ return { ...recount(report2, kept), baselined };
4883
+ }
4884
+
4885
+ // src/character/config.ts
4886
+ var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
4887
+ var KEYS = /* @__PURE__ */ new Set(["$schema", "ignore", "copyPaths", "severity"]);
4888
+ var LEVELS = /* @__PURE__ */ new Set(["warn", "block", "off"]);
4889
+ var MIN_BECAUSE = 12;
4890
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4891
+ var isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
4892
+ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
4893
+ if (!isRecord(data)) throw new Error(`${source}: expected an object`);
4894
+ for (const key of Object.keys(data)) if (!KEYS.has(key)) throw new Error(`${source}: unknown key "${key}"`);
4895
+ const ignore = data.ignore ?? [];
4896
+ if (!isStringArray(ignore)) throw new Error(`${source}: "ignore" must be an array of glob strings`);
4897
+ if (data.copyPaths !== void 0 && !isStringArray(data.copyPaths)) throw new Error(`${source}: "copyPaths" must be an array of paths`);
4898
+ const severity = {};
4899
+ const raw = data.severity ?? {};
4900
+ if (!isRecord(raw)) throw new Error(`${source}: "severity" must map a tell id to { level, because }`);
4901
+ for (const [tell, value] of Object.entries(raw)) {
4902
+ if (!knownTells.has(tell)) throw new Error(`${source}: severity names no tell called "${tell}"`);
4903
+ if (!isRecord(value) || !LEVELS.has(value.level)) {
4904
+ throw new Error(`${source}: severity.${tell} needs a level of "warn", "block" or "off"`);
4905
+ }
4906
+ if (typeof value.because !== "string" || value.because.trim().length < MIN_BECAUSE) {
4907
+ throw new Error(`${source}: severity.${tell} needs a because of at least a sentence`);
4908
+ }
4909
+ severity[tell] = { level: value.level, because: value.because };
4910
+ }
4911
+ return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
4912
+ }
4913
+ function globToRegExp(glob) {
4914
+ const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
4915
+ let body = "";
4916
+ for (let i = 0; i < pattern.length; i += 1) {
4917
+ const c = pattern[i];
4918
+ if (c === "*" && pattern[i + 1] === "*") {
4919
+ const slash = pattern[i + 2] === "/";
4920
+ body += slash ? "(?:.*/)?" : ".*";
4921
+ i += slash ? 2 : 1;
4922
+ } else if (c === "*") body += "[^/]*";
4923
+ else if (c === "?") body += "[^/]";
4924
+ else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4925
+ }
4926
+ const anywhere = !pattern.includes("/");
4927
+ return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
4928
+ }
4929
+ function ignoreMatcher(patterns) {
4930
+ const res = patterns.map(globToRegExp);
4931
+ return (path) => {
4932
+ const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
4933
+ return res.some((re) => re.test(p));
4934
+ };
4935
+ }
4936
+ function severityExceptions(config) {
4937
+ return Object.entries(config.severity).filter(([, o]) => o.level === "off").map(([tell, o]) => ({ tell, because: `craft.config.json: ${o.because}` }));
4938
+ }
4939
+ function applySeverity(report2, config) {
4940
+ const findings = report2.findings.map((f4) => {
4941
+ const o = config.severity[f4.tell];
4942
+ return o && o.level !== "off" ? { ...f4, severity: o.level } : f4;
4943
+ });
4944
+ return { ...report2, findings, summary: { ...report2.summary, blocking: findings.filter((f4) => f4.severity === "block").length } };
4945
+ }
4946
+
4947
+ // src/character/sarif.ts
4948
+ var isUrl = (path) => /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
4949
+ function toSarif(report2, options) {
4950
+ const byId = new Map(options.tells.map((t) => [t.id, t]));
4951
+ const ids = [...new Set(report2.findings.map((f4) => f4.tell))].sort();
4952
+ const index = new Map(ids.map((id, i) => [id, i]));
4953
+ const rules = ids.map((id) => {
4954
+ const t = byId.get(id);
4955
+ return {
4956
+ id,
4957
+ name: t?.name ?? id,
4958
+ shortDescription: { text: t?.name ?? id },
4959
+ ...t ? { fullDescription: { text: t.why }, help: { text: t.fix } } : {}
4960
+ };
4961
+ });
4962
+ const results = report2.findings.map((f4) => ({
4963
+ ruleId: f4.tell,
4964
+ ruleIndex: index.get(f4.tell),
4965
+ level: f4.severity === "block" ? "error" : "warning",
4966
+ message: { text: `${f4.message}. ${f4.fix}` },
4967
+ locations: [
4968
+ {
4969
+ physicalLocation: {
4970
+ artifactLocation: { uri: isUrl(f4.path) ? f4.path : f4.path.replace(/\\/g, "/") },
4971
+ ...f4.line > 0 ? { region: { startLine: f4.line, snippet: { text: f4.excerpt } } } : {}
4972
+ }
4973
+ }
4974
+ ],
4975
+ partialFingerprints: { "craftFinding/v1": fnv1a(findingKey(f4)) }
4976
+ }));
4977
+ return {
4978
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
4979
+ version: "2.1.0",
4980
+ runs: [
4981
+ {
4982
+ tool: {
4983
+ driver: {
4984
+ name: "craft",
4985
+ version: report2.catalogueVersion,
4986
+ informationUri: options.informationUri ?? "https://www.npmjs.com/package/@domandigital/craft",
4987
+ rules
4988
+ }
4989
+ },
4990
+ results
4991
+ }
4992
+ ]
4993
+ };
4994
+ }
4995
+
4996
+ // src/character/json.ts
4997
+ var JSON_SCHEMA_VERSION = 1;
4998
+ function toJson(data) {
4999
+ return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
5000
+ }
5001
+
5002
+ // src/snapshot/migrate.ts
5003
+ var READABLE_SNAPSHOT_VERSIONS = [1, 2];
5004
+ function readSnapshot(data, source = "snapshot") {
5005
+ const v = data?.version;
5006
+ if (typeof data !== "object" || data === null || !READABLE_SNAPSHOT_VERSIONS.includes(v)) {
5007
+ throw new Error(`${source} is not a snapshot this craft reads (version ${String(v)}; reads ${READABLE_SNAPSHOT_VERSIONS.join(" and ")})`);
5008
+ }
5009
+ const snap = data;
5010
+ if (snap.version === 1) return { ...snap, version: SNAPSHOT_VERSION, migratedFrom: 1 };
5011
+ return snap;
5012
+ }
5013
+
5014
+ // src/snapshot/visual.ts
5015
+ var EDGE_STEP = 32;
5016
+ function visualMeasures(img, maxSamples = 1e6) {
5017
+ const { width: w, height: h, rgba } = img;
5018
+ if (w < 2 || h < 2 || rgba.length < w * h * 4) throw new Error(`visualMeasures: ${w}x${h} image with ${rgba.length} bytes`);
5019
+ const step = Math.max(1, Math.ceil(Math.sqrt(w * h / maxSamples)));
5020
+ const lum = (x, y) => {
5021
+ const i = (y * w + x) * 4;
5022
+ return 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
5023
+ };
5024
+ let n = 0;
5025
+ let sumRg = 0;
5026
+ let sumYb = 0;
5027
+ let sumRg2 = 0;
5028
+ let sumYb2 = 0;
5029
+ let edges = 0;
5030
+ let edgeN = 0;
5031
+ let asym = 0;
5032
+ let asymN = 0;
5033
+ for (let y = 0; y < h; y += step) {
5034
+ for (let x = 0; x < w; x += step) {
5035
+ const i = (y * w + x) * 4;
5036
+ const r = rgba[i];
5037
+ const g = rgba[i + 1];
5038
+ const b = rgba[i + 2];
5039
+ const rg = r - g;
5040
+ const yb = 0.5 * (r + g) - b;
5041
+ sumRg += rg;
5042
+ sumYb += yb;
5043
+ sumRg2 += rg * rg;
5044
+ sumYb2 += yb * yb;
5045
+ n += 1;
5046
+ const l = lum(x, y);
5047
+ if (x + step < w && y + step < h) {
5048
+ if (Math.abs(lum(x + step, y) - l) + Math.abs(lum(x, y + step) - l) > EDGE_STEP) edges += 1;
5049
+ edgeN += 1;
5050
+ }
5051
+ if (x < w / 2) {
5052
+ asym += Math.abs(l - lum(w - 1 - x, y));
5053
+ asymN += 1;
5054
+ }
5055
+ }
5056
+ }
5057
+ const meanRg = sumRg / n;
5058
+ const meanYb = sumYb / n;
5059
+ const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
5060
+ const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
5061
+ const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
5062
+ const round2 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
5063
+ return {
5064
+ colourfulness: round2(colourfulness, 1),
5065
+ edgeDensity: round2(edgeN ? edges / edgeN : 0),
5066
+ symmetry: round2(asymN ? 1 - asym / asymN / 255 : 1),
5067
+ width: w,
5068
+ height: h,
5069
+ sampled: n
5070
+ };
5071
+ }
5072
+
4755
5073
  // src/fingerprint/index.ts
4756
- var FINGERPRINT_VERSION = 1;
5074
+ var FINGERPRINT_VERSION = 2;
4757
5075
  var CHROMATIC = 0.03;
4758
5076
  var MIN_ACCENT_AREA = 2e4;
4759
5077
  var oklch = (value) => {
@@ -4808,23 +5126,36 @@ function fingerprint(s) {
4808
5126
  roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
4809
5127
  motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
4810
5128
  effects: effects.sort(),
4811
- layout: s.sections.map((x) => x.kind)
5129
+ layout: s.sections.map((x) => x.kind),
5130
+ ...s.sections.length > 0 && s.sections.every((x) => x.role && x.geometry) ? { sections: s.sections.map((x) => ({ role: x.role, centred: x.geometry.centredShare, width: x.geometry.contentWidthRatio })) } : {}
4812
5131
  };
4813
5132
  }
4814
5133
  var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
4815
- function levenshtein(a, b) {
5134
+ function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
4816
5135
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
4817
5136
  for (let i = 1; i <= a.length; i += 1) {
4818
5137
  let prev = row[0];
4819
5138
  row[0] = i;
4820
5139
  for (let j = 1; j <= b.length; j += 1) {
4821
5140
  const cur = row[j];
4822
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
5141
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost(a[i - 1], b[j - 1]));
4823
5142
  prev = cur;
4824
5143
  }
4825
5144
  }
4826
5145
  return row[b.length];
4827
5146
  }
5147
+ function sectionCost(x, y) {
5148
+ if (x.role !== y.role) return 1;
5149
+ return Math.min(0.5, (Math.abs(x.centred - y.centred) + Math.abs(x.width - y.width)) / 2);
5150
+ }
5151
+ function layoutDistance(a, b) {
5152
+ if (a.sections && b.sections) {
5153
+ const longest2 = Math.max(a.sections.length, b.sections.length);
5154
+ return longest2 === 0 ? 0 : levenshtein(a.sections, b.sections, sectionCost) / longest2;
5155
+ }
5156
+ const longest = Math.max(a.layout.length, b.layout.length);
5157
+ return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
5158
+ }
4828
5159
  var clamp = (n) => Math.max(0, Math.min(1, n));
4829
5160
  function faceDistance(a, b) {
4830
5161
  if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
@@ -4839,8 +5170,7 @@ function fingerprintDistance(a, b) {
4839
5170
  const union = /* @__PURE__ */ new Set([...a.effects, ...b.effects]);
4840
5171
  const shared = a.effects.filter((e) => b.effects.includes(e)).length;
4841
5172
  const effects = union.size === 0 ? 0 : 1 - shared / union.size;
4842
- const longest = Math.max(a.layout.length, b.layout.length);
4843
- const layout = longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
5173
+ const layout = layoutDistance(a, b);
4844
5174
  const parts2 = { accent, ground, type, shape, motion, effects, layout };
4845
5175
  const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
4846
5176
  return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
@@ -4868,6 +5198,117 @@ var SOURCE_KINDS = [
4868
5198
  ];
4869
5199
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
4870
5200
 
5201
+ // src/direction/licences.ts
5202
+ var LICENCES = [
5203
+ {
5204
+ name: "Basteleur",
5205
+ subject: "face",
5206
+ licence: "OFL-1.1",
5207
+ multiClient: "yes",
5208
+ checked: "2026-09-24",
5209
+ source: "https://velvetyne.fr/fonts/basteleur/",
5210
+ note: "Velvetyne, designed by Keussel. Keep the OFL notice with the font files."
5211
+ },
5212
+ {
5213
+ name: "Gulax",
5214
+ subject: "face",
5215
+ licence: "OFL-1.1",
5216
+ multiClient: "yes",
5217
+ checked: "2026-09-24",
5218
+ source: "https://velvetyne.fr/fonts/gulax/",
5219
+ note: "Velvetyne. Keep the OFL notice with the font files."
5220
+ },
5221
+ {
5222
+ name: "Le Murmure",
5223
+ subject: "face",
5224
+ licence: "OFL-1.1",
5225
+ multiClient: "yes",
5226
+ checked: "2026-09-24",
5227
+ source: "https://velvetyne.fr/fonts/le-murmure/",
5228
+ note: "Velvetyne. Keep the OFL notice with the font files."
5229
+ },
5230
+ {
5231
+ name: "Blaze Type",
5232
+ subject: "foundry",
5233
+ licence: "commercial",
5234
+ multiClient: "per-site",
5235
+ cap: 10,
5236
+ checked: "2026-09-24",
5237
+ source: "https://blazetype.eu/eula",
5238
+ 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."
5239
+ },
5240
+ {
5241
+ name: "Power Type",
5242
+ subject: "foundry",
5243
+ licence: "commercial",
5244
+ multiClient: "unverified",
5245
+ checked: "2026-09-24",
5246
+ source: "https://power-type.com/eula",
5247
+ 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."
5248
+ },
5249
+ {
5250
+ name: "Grilli Type",
5251
+ subject: "foundry",
5252
+ licence: "commercial",
5253
+ multiClient: "per-site",
5254
+ checked: "2026-09-24",
5255
+ source: "https://www.grillitype.com/information",
5256
+ 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."
5257
+ },
5258
+ {
5259
+ name: "Phosphor",
5260
+ subject: "icons",
5261
+ licence: "MIT",
5262
+ multiClient: "yes",
5263
+ checked: "2026-09-24",
5264
+ source: "https://github.com/phosphor-icons/core/blob/main/LICENSE",
5265
+ note: "Six weights. The icon-tile tells treat it exactly as they treat Lucide: the library is not the tell, the tinted tile is."
5266
+ },
5267
+ {
5268
+ name: "Tabler Icons",
5269
+ subject: "icons",
5270
+ licence: "MIT",
5271
+ multiClient: "yes",
5272
+ checked: "2026-09-24",
5273
+ source: "https://github.com/tabler/tabler-icons/blob/main/LICENSE",
5274
+ note: "One stroke grid across the set."
5275
+ },
5276
+ {
5277
+ name: "Iconoir",
5278
+ subject: "icons",
5279
+ licence: "MIT",
5280
+ multiClient: "yes",
5281
+ checked: "2026-09-24",
5282
+ source: "https://github.com/iconoir-icons/iconoir/blob/main/LICENSE",
5283
+ note: "Smaller set, no paid tier."
5284
+ },
5285
+ {
5286
+ name: "Unicons",
5287
+ subject: "icons",
5288
+ licence: "IconScout Simple License",
5289
+ multiClient: "yes",
5290
+ checked: "2026-09-24",
5291
+ source: "https://github.com/Iconscout/unicons/blob/master/LICENSE",
5292
+ note: "IconScout's free set, personal and commercial use, attribution optional."
5293
+ },
5294
+ {
5295
+ name: "IconScout premium",
5296
+ subject: "icons",
5297
+ licence: "commercial",
5298
+ multiClient: "per-site",
5299
+ checked: "2026-09-24",
5300
+ source: "https://iconscout.com/licenses",
5301
+ 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."
5302
+ }
5303
+ ];
5304
+ function faceLicence(family) {
5305
+ const name = family.trim().toLowerCase();
5306
+ return LICENCES.find((l) => l.subject === "face" && l.name.toLowerCase() === name);
5307
+ }
5308
+ function licencesJson() {
5309
+ return JSON.stringify({ licences: LICENCES }, null, 2) + "\n";
5310
+ }
5311
+
4871
5312
  // src/direction/validate.ts
4872
5313
  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;
4873
5314
  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;
@@ -4991,6 +5432,18 @@ function validateDirection(input, ctx = {}) {
4991
5432
  ok = false;
4992
5433
  }
4993
5434
  }
5435
+ if ((key === "display" || key === "body") && typeof c.value === "string" && c.value.trim()) {
5436
+ const family = normaliseFamily(c.value);
5437
+ const entry = faceLicence(family);
5438
+ if (!entry) {
5439
+ 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.`);
5440
+ } else if (entry.multiClient === "unverified") {
5441
+ warn(`${at}.value`, `${family}: licence unverified (${entry.note})`);
5442
+ } else if (entry.multiClient === "capped" || entry.multiClient === "per-site") {
5443
+ const scope = entry.multiClient === "capped" ? `covers up to ${entry.cap} sites` : "needs a licence for this site, in the client's name";
5444
+ warn(`${at}.value`, `${family}: ${entry.licence} licence ${scope}. Record the purchase before launch.`);
5445
+ }
5446
+ }
4994
5447
  const fp = ctx.fingerprint;
4995
5448
  if (fp && typeof c.value === "string") {
4996
5449
  if ((key === "accent" || key === "ground") && parseColour(c.value)) {
@@ -5647,10 +6100,12 @@ function retrofitPlan(report2, direction) {
5647
6100
  AI_PHRASES,
5648
6101
  AI_VIOLET,
5649
6102
  AI_WORDS,
6103
+ BASELINE_VERSION,
5650
6104
  BUZZWORDS,
5651
6105
  CATALOGUE,
5652
6106
  CATALOGUE_VERSION,
5653
6107
  CHOICE_KEYS,
6108
+ DEFAULT_IGNORE,
5654
6109
  DENSITIES,
5655
6110
  DENSITY_NAMES,
5656
6111
  DIRECTION_VERSION,
@@ -5659,12 +6114,16 @@ function retrofitPlan(report2, direction) {
5659
6114
  DURATION_S,
5660
6115
  EASE,
5661
6116
  EASE_TUPLE,
6117
+ EDGE_STEP,
5662
6118
  ESTATE_VERSION,
5663
6119
  EXIT_RATIO,
5664
6120
  FINGERPRINT_VERSION,
5665
6121
  HOUSE,
5666
6122
  HOUSE_BUDGET,
6123
+ HOUSE_LOCKED,
5667
6124
  HOUSE_TYPE,
6125
+ JSON_SCHEMA_VERSION,
6126
+ LICENCES,
5668
6127
  LIGHTNESS_CURVE,
5669
6128
  MIN_DECIDED,
5670
6129
  MIN_RUNS,
@@ -5674,6 +6133,7 @@ function retrofitPlan(report2, direction) {
5674
6133
  PLAINER_WORDS,
5675
6134
  PROPOSED,
5676
6135
  RAMP_STEPS,
6136
+ READABLE_SNAPSHOT_VERSIONS,
5677
6137
  REFLEX_FONTS_1,
5678
6138
  REFLEX_FONTS_2,
5679
6139
  RENDERED_PATHS,
@@ -5695,7 +6155,9 @@ function retrofitPlan(report2, direction) {
5695
6155
  accentFork,
5696
6156
  addSite,
5697
6157
  apcaContrast,
6158
+ applyBaseline,
5698
6159
  applyHouseGate,
6160
+ applySeverity,
5699
6161
  auditSnapshot,
5700
6162
  catalogueTable,
5701
6163
  characterReport,
@@ -5706,6 +6168,7 @@ function retrofitPlan(report2, direction) {
5706
6168
  compareFacts,
5707
6169
  compareToEstate,
5708
6170
  craftTokens,
6171
+ createBaseline,
5709
6172
  deltaEOk,
5710
6173
  deltaEOkHex,
5711
6174
  densityCss,
@@ -5715,8 +6178,10 @@ function retrofitPlan(report2, direction) {
5715
6178
  estatePairs,
5716
6179
  exitDuration,
5717
6180
  extractHtml,
6181
+ faceLicence,
5718
6182
  findClaims,
5719
6183
  findColours,
6184
+ findingKey,
5720
6185
  fingerprint,
5721
6186
  fingerprintDistance,
5722
6187
  fluidClamp,
@@ -5727,17 +6192,22 @@ function retrofitPlan(report2, direction) {
5727
6192
  formatComparison,
5728
6193
  formatHex,
5729
6194
  formatReport,
6195
+ globToRegExp,
5730
6196
  groundName,
5731
6197
  harvest,
5732
6198
  hexToOklch,
6199
+ holdHouseBlocks,
5733
6200
  houseRule,
5734
6201
  hueName,
5735
6202
  hueSwatch,
6203
+ ignoreMatcher,
5736
6204
  inP3Gamut,
5737
6205
  inSrgbGamut,
5738
6206
  initDirection,
5739
6207
  isAiViolet,
5740
6208
  isCream,
6209
+ layoutDistance,
6210
+ licencesJson,
5741
6211
  makeSnapshot,
5742
6212
  motionTokens,
5743
6213
  namedSources,
@@ -5747,17 +6217,21 @@ function retrofitPlan(report2, direction) {
5747
6217
  oklchToHex,
5748
6218
  oklchToRgb,
5749
6219
  paletteFromPixels,
6220
+ parseBaseline,
5750
6221
  parseColour,
6222
+ parseCraftConfig,
5751
6223
  parseHex,
5752
6224
  propose,
5753
6225
  protectedFacts,
5754
6226
  ramp,
5755
6227
  rampFromAnchors,
6228
+ readSnapshot,
5756
6229
  retrofitPlan,
5757
6230
  runTell,
5758
6231
  scanSource,
5759
6232
  sectionRhythm,
5760
6233
  semantic,
6234
+ severityExceptions,
5761
6235
  shapeName,
5762
6236
  sharedParts,
5763
6237
  shouldAnimate,
@@ -5766,10 +6240,13 @@ function retrofitPlan(report2, direction) {
5766
6240
  tailwindV4Theme,
5767
6241
  tellById,
5768
6242
  toGamut,
6243
+ toJson,
6244
+ toSarif,
5769
6245
  typeFeatureTokens,
5770
6246
  typicality,
5771
6247
  validateDirection,
5772
6248
  visibleText,
6249
+ visualMeasures,
5773
6250
  wcagContrast,
5774
6251
  wordListsJson
5775
6252
  });