@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.js CHANGED
@@ -2742,6 +2742,7 @@ var REFLEX_FONTS_2 = [
2742
2742
  "Plus Jakarta Sans",
2743
2743
  "Outfit",
2744
2744
  "Geist",
2745
+ "Cal Sans",
2745
2746
  "Sora",
2746
2747
  "Instrument Sans",
2747
2748
  "Instrument Serif",
@@ -2963,7 +2964,7 @@ function iconTiles(file) {
2963
2964
  return sized && has(unit, /^rounded-(?:md|lg|xl|2xl|full)$/) && has(unit, /^(?:items-center|place-items-center|place-content-center)$/) && has(unit, /^bg-/);
2964
2965
  });
2965
2966
  }
2966
- var ICON_LIBRARY = /from\s+["'](?:lucide-react|@heroicons\/react[^"']*|react-icons[^"']*|@phosphor-icons\/react|@tabler\/icons-react|@radix-ui\/react-icons)["']/;
2967
+ 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[^"']*)["']/;
2967
2968
  function iconTileStackHits(ctx) {
2968
2969
  const hits = [];
2969
2970
  for (const file of ctx.files) {
@@ -3110,6 +3111,12 @@ function shadcnHits(ctx) {
3110
3111
  const detail = unused.length > 0 ? `; ${unused.length} imported nowhere` : "";
3111
3112
  return [{ path: ui[0].path, offset: 0, message: `${ui.length} stock shadcn primitives${detail}` }];
3112
3113
  }
3114
+ function shadcnCardHits(ctx) {
3115
+ return eachUnit(
3116
+ ctx,
3117
+ (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
3118
+ );
3119
+ }
3113
3120
  function marqueeHits(ctx) {
3114
3121
  const hits = [];
3115
3122
  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;
@@ -3327,6 +3334,23 @@ const inter = Inter({ subsets: ["latin"] });`),
3327
3334
  pass: [f3("src/components/ui/button.tsx", `export function Button() { return null; }`)]
3328
3335
  }
3329
3336
  },
3337
+ {
3338
+ id: "shadcn-card-stock",
3339
+ name: "Stock shadcn Card",
3340
+ generation: 1,
3341
+ severity: "warn",
3342
+ surface: "source",
3343
+ 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.",
3344
+ 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.",
3345
+ detect: shadcnCardHits,
3346
+ fixtures: {
3347
+ flag: [
3348
+ f3("components/ui/card.tsx", `className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}`),
3349
+ 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)}`)
3350
+ ],
3351
+ pass: [f3("components/ui/card.tsx", `className={cn("rounded-sm border-2 border-ink bg-card text-card-foreground", className)}`)]
3352
+ }
3353
+ },
3330
3354
  {
3331
3355
  id: "cream-palette",
3332
3356
  name: "Cream ground",
@@ -3553,6 +3577,17 @@ var HOUSE = {
3553
3577
  function houseRule(id, name = id) {
3554
3578
  return HOUSE[id] ?? { tier: "review", label: name };
3555
3579
  }
3580
+ 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";
3581
+ function holdHouseBlocks(exceptions, severity) {
3582
+ const blocks = (tell) => HOUSE[tell]?.tier === "block";
3583
+ const refusedSeverity = Object.entries(severity).filter(([tell, o]) => blocks(tell) && o.level !== "block").map(([tell]) => tell);
3584
+ return {
3585
+ exceptions: exceptions.filter((e) => !blocks(e.tell)),
3586
+ refused: exceptions.filter((e) => blocks(e.tell)),
3587
+ severity: Object.fromEntries(Object.entries(severity).filter(([tell]) => !refusedSeverity.includes(tell))),
3588
+ refusedSeverity
3589
+ };
3590
+ }
3556
3591
 
3557
3592
  // src/character/parse.ts
3558
3593
  var MARKUP = /\.(tsx|jsx|html?|astro|vue|svelte|mdx)$/i;
@@ -3886,7 +3921,7 @@ function extractStrings(file) {
3886
3921
  }
3887
3922
 
3888
3923
  // src/snapshot/types.ts
3889
- var SNAPSHOT_VERSION = 1;
3924
+ var SNAPSHOT_VERSION = 2;
3890
3925
 
3891
3926
  // src/snapshot/fixture.ts
3892
3927
  function makeSnapshot(patch = {}) {
@@ -4220,7 +4255,7 @@ var RENDERED_PATHS = {
4220
4255
  };
4221
4256
 
4222
4257
  // src/character/check.ts
4223
- var CATALOGUE_VERSION = "2026.09.8";
4258
+ var CATALOGUE_VERSION = "2026.09.9";
4224
4259
  var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
4225
4260
  (t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
4226
4261
  );
@@ -4599,8 +4634,267 @@ function formatClaims(report2, label = "craft copy claims") {
4599
4634
  return lines.join("\n");
4600
4635
  }
4601
4636
 
4637
+ // src/character/baseline.ts
4638
+ var BASELINE_VERSION = 1;
4639
+ function fnv1a(text) {
4640
+ let hash = 2166136261;
4641
+ for (let i = 0; i < text.length; i += 1) {
4642
+ hash ^= text.charCodeAt(i);
4643
+ hash = Math.imul(hash, 16777619);
4644
+ }
4645
+ return (hash >>> 0).toString(16).padStart(8, "0");
4646
+ }
4647
+ var excerptHash = (f4) => fnv1a(f4.excerpt.replace(/\s+/g, " ").trim());
4648
+ var keyOf = (tell, path, excerpt) => `${tell}\0${path}\0${excerpt}`;
4649
+ function findingKey(f4) {
4650
+ return keyOf(f4.tell, f4.path, excerptHash(f4));
4651
+ }
4652
+ function createBaseline(report2) {
4653
+ const counts = /* @__PURE__ */ new Map();
4654
+ for (const f4 of report2.findings) {
4655
+ const excerpt = excerptHash(f4);
4656
+ const key = keyOf(f4.tell, f4.path, excerpt);
4657
+ const entry = counts.get(key);
4658
+ if (entry) entry.count += 1;
4659
+ else counts.set(key, { tell: f4.tell, path: f4.path, excerpt, count: 1 });
4660
+ }
4661
+ const entries = [...counts.values()].sort((a, b) => a.path.localeCompare(b.path) || a.tell.localeCompare(b.tell) || a.excerpt.localeCompare(b.excerpt));
4662
+ return { version: BASELINE_VERSION, catalogueVersion: report2.catalogueVersion, entries };
4663
+ }
4664
+ function parseBaseline(data, source = "baseline") {
4665
+ const b = data;
4666
+ if (!b || typeof b !== "object" || b.version !== BASELINE_VERSION || !Array.isArray(b.entries)) {
4667
+ throw new Error(`${source}: not a craft baseline (version ${BASELINE_VERSION})`);
4668
+ }
4669
+ for (const e of b.entries) {
4670
+ if (typeof e?.tell !== "string" || typeof e.path !== "string" || typeof e.excerpt !== "string" || !Number.isInteger(e.count) || e.count < 1) {
4671
+ throw new Error(`${source}: malformed entry ${JSON.stringify(e)}`);
4672
+ }
4673
+ }
4674
+ return b;
4675
+ }
4676
+ function recount(report2, findings) {
4677
+ const byGeneration = { 1: 0, 2: 0, 3: 0 };
4678
+ const byTell = {};
4679
+ for (const f4 of findings) {
4680
+ byGeneration[f4.generation] += 1;
4681
+ byTell[f4.tell] = (byTell[f4.tell] ?? 0) + 1;
4682
+ }
4683
+ return {
4684
+ ...report2,
4685
+ findings,
4686
+ summary: { ...report2.summary, findings: findings.length, byGeneration, byTell, blocking: findings.filter((f4) => f4.severity === "block").length }
4687
+ };
4688
+ }
4689
+ function applyBaseline(report2, baseline) {
4690
+ const remaining = /* @__PURE__ */ new Map();
4691
+ for (const e of baseline.entries) {
4692
+ const key = keyOf(e.tell, e.path, e.excerpt);
4693
+ remaining.set(key, (remaining.get(key) ?? 0) + e.count);
4694
+ }
4695
+ const kept = [];
4696
+ let baselined = 0;
4697
+ for (const f4 of report2.findings) {
4698
+ const key = findingKey(f4);
4699
+ const left = remaining.get(key) ?? 0;
4700
+ if (left > 0) {
4701
+ remaining.set(key, left - 1);
4702
+ baselined += 1;
4703
+ } else kept.push(f4);
4704
+ }
4705
+ return { ...recount(report2, kept), baselined };
4706
+ }
4707
+
4708
+ // src/character/config.ts
4709
+ var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
4710
+ var KEYS = /* @__PURE__ */ new Set(["$schema", "ignore", "copyPaths", "severity"]);
4711
+ var LEVELS = /* @__PURE__ */ new Set(["warn", "block", "off"]);
4712
+ var MIN_BECAUSE = 12;
4713
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4714
+ var isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
4715
+ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
4716
+ if (!isRecord(data)) throw new Error(`${source}: expected an object`);
4717
+ for (const key of Object.keys(data)) if (!KEYS.has(key)) throw new Error(`${source}: unknown key "${key}"`);
4718
+ const ignore = data.ignore ?? [];
4719
+ if (!isStringArray(ignore)) throw new Error(`${source}: "ignore" must be an array of glob strings`);
4720
+ if (data.copyPaths !== void 0 && !isStringArray(data.copyPaths)) throw new Error(`${source}: "copyPaths" must be an array of paths`);
4721
+ const severity = {};
4722
+ const raw = data.severity ?? {};
4723
+ if (!isRecord(raw)) throw new Error(`${source}: "severity" must map a tell id to { level, because }`);
4724
+ for (const [tell, value] of Object.entries(raw)) {
4725
+ if (!knownTells.has(tell)) throw new Error(`${source}: severity names no tell called "${tell}"`);
4726
+ if (!isRecord(value) || !LEVELS.has(value.level)) {
4727
+ throw new Error(`${source}: severity.${tell} needs a level of "warn", "block" or "off"`);
4728
+ }
4729
+ if (typeof value.because !== "string" || value.because.trim().length < MIN_BECAUSE) {
4730
+ throw new Error(`${source}: severity.${tell} needs a because of at least a sentence`);
4731
+ }
4732
+ severity[tell] = { level: value.level, because: value.because };
4733
+ }
4734
+ return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
4735
+ }
4736
+ function globToRegExp(glob) {
4737
+ const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
4738
+ let body = "";
4739
+ for (let i = 0; i < pattern.length; i += 1) {
4740
+ const c = pattern[i];
4741
+ if (c === "*" && pattern[i + 1] === "*") {
4742
+ const slash = pattern[i + 2] === "/";
4743
+ body += slash ? "(?:.*/)?" : ".*";
4744
+ i += slash ? 2 : 1;
4745
+ } else if (c === "*") body += "[^/]*";
4746
+ else if (c === "?") body += "[^/]";
4747
+ else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4748
+ }
4749
+ const anywhere = !pattern.includes("/");
4750
+ return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
4751
+ }
4752
+ function ignoreMatcher(patterns) {
4753
+ const res = patterns.map(globToRegExp);
4754
+ return (path) => {
4755
+ const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
4756
+ return res.some((re) => re.test(p));
4757
+ };
4758
+ }
4759
+ function severityExceptions(config) {
4760
+ return Object.entries(config.severity).filter(([, o]) => o.level === "off").map(([tell, o]) => ({ tell, because: `craft.config.json: ${o.because}` }));
4761
+ }
4762
+ function applySeverity(report2, config) {
4763
+ const findings = report2.findings.map((f4) => {
4764
+ const o = config.severity[f4.tell];
4765
+ return o && o.level !== "off" ? { ...f4, severity: o.level } : f4;
4766
+ });
4767
+ return { ...report2, findings, summary: { ...report2.summary, blocking: findings.filter((f4) => f4.severity === "block").length } };
4768
+ }
4769
+
4770
+ // src/character/sarif.ts
4771
+ var isUrl = (path) => /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
4772
+ function toSarif(report2, options) {
4773
+ const byId = new Map(options.tells.map((t) => [t.id, t]));
4774
+ const ids = [...new Set(report2.findings.map((f4) => f4.tell))].sort();
4775
+ const index = new Map(ids.map((id, i) => [id, i]));
4776
+ const rules = ids.map((id) => {
4777
+ const t = byId.get(id);
4778
+ return {
4779
+ id,
4780
+ name: t?.name ?? id,
4781
+ shortDescription: { text: t?.name ?? id },
4782
+ ...t ? { fullDescription: { text: t.why }, help: { text: t.fix } } : {}
4783
+ };
4784
+ });
4785
+ const results = report2.findings.map((f4) => ({
4786
+ ruleId: f4.tell,
4787
+ ruleIndex: index.get(f4.tell),
4788
+ level: f4.severity === "block" ? "error" : "warning",
4789
+ message: { text: `${f4.message}. ${f4.fix}` },
4790
+ locations: [
4791
+ {
4792
+ physicalLocation: {
4793
+ artifactLocation: { uri: isUrl(f4.path) ? f4.path : f4.path.replace(/\\/g, "/") },
4794
+ ...f4.line > 0 ? { region: { startLine: f4.line, snippet: { text: f4.excerpt } } } : {}
4795
+ }
4796
+ }
4797
+ ],
4798
+ partialFingerprints: { "craftFinding/v1": fnv1a(findingKey(f4)) }
4799
+ }));
4800
+ return {
4801
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
4802
+ version: "2.1.0",
4803
+ runs: [
4804
+ {
4805
+ tool: {
4806
+ driver: {
4807
+ name: "craft",
4808
+ version: report2.catalogueVersion,
4809
+ informationUri: options.informationUri ?? "https://www.npmjs.com/package/@domandigital/craft",
4810
+ rules
4811
+ }
4812
+ },
4813
+ results
4814
+ }
4815
+ ]
4816
+ };
4817
+ }
4818
+
4819
+ // src/character/json.ts
4820
+ var JSON_SCHEMA_VERSION = 1;
4821
+ function toJson(data) {
4822
+ return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
4823
+ }
4824
+
4825
+ // src/snapshot/migrate.ts
4826
+ var READABLE_SNAPSHOT_VERSIONS = [1, 2];
4827
+ function readSnapshot(data, source = "snapshot") {
4828
+ const v = data?.version;
4829
+ if (typeof data !== "object" || data === null || !READABLE_SNAPSHOT_VERSIONS.includes(v)) {
4830
+ throw new Error(`${source} is not a snapshot this craft reads (version ${String(v)}; reads ${READABLE_SNAPSHOT_VERSIONS.join(" and ")})`);
4831
+ }
4832
+ const snap = data;
4833
+ if (snap.version === 1) return { ...snap, version: SNAPSHOT_VERSION, migratedFrom: 1 };
4834
+ return snap;
4835
+ }
4836
+
4837
+ // src/snapshot/visual.ts
4838
+ var EDGE_STEP = 32;
4839
+ function visualMeasures(img, maxSamples = 1e6) {
4840
+ const { width: w, height: h, rgba } = img;
4841
+ if (w < 2 || h < 2 || rgba.length < w * h * 4) throw new Error(`visualMeasures: ${w}x${h} image with ${rgba.length} bytes`);
4842
+ const step = Math.max(1, Math.ceil(Math.sqrt(w * h / maxSamples)));
4843
+ const lum = (x, y) => {
4844
+ const i = (y * w + x) * 4;
4845
+ return 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
4846
+ };
4847
+ let n = 0;
4848
+ let sumRg = 0;
4849
+ let sumYb = 0;
4850
+ let sumRg2 = 0;
4851
+ let sumYb2 = 0;
4852
+ let edges = 0;
4853
+ let edgeN = 0;
4854
+ let asym = 0;
4855
+ let asymN = 0;
4856
+ for (let y = 0; y < h; y += step) {
4857
+ for (let x = 0; x < w; x += step) {
4858
+ const i = (y * w + x) * 4;
4859
+ const r = rgba[i];
4860
+ const g = rgba[i + 1];
4861
+ const b = rgba[i + 2];
4862
+ const rg = r - g;
4863
+ const yb = 0.5 * (r + g) - b;
4864
+ sumRg += rg;
4865
+ sumYb += yb;
4866
+ sumRg2 += rg * rg;
4867
+ sumYb2 += yb * yb;
4868
+ n += 1;
4869
+ const l = lum(x, y);
4870
+ if (x + step < w && y + step < h) {
4871
+ if (Math.abs(lum(x + step, y) - l) + Math.abs(lum(x, y + step) - l) > EDGE_STEP) edges += 1;
4872
+ edgeN += 1;
4873
+ }
4874
+ if (x < w / 2) {
4875
+ asym += Math.abs(l - lum(w - 1 - x, y));
4876
+ asymN += 1;
4877
+ }
4878
+ }
4879
+ }
4880
+ const meanRg = sumRg / n;
4881
+ const meanYb = sumYb / n;
4882
+ const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
4883
+ const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
4884
+ const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
4885
+ const round2 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
4886
+ return {
4887
+ colourfulness: round2(colourfulness, 1),
4888
+ edgeDensity: round2(edgeN ? edges / edgeN : 0),
4889
+ symmetry: round2(asymN ? 1 - asym / asymN / 255 : 1),
4890
+ width: w,
4891
+ height: h,
4892
+ sampled: n
4893
+ };
4894
+ }
4895
+
4602
4896
  // src/fingerprint/index.ts
4603
- var FINGERPRINT_VERSION = 1;
4897
+ var FINGERPRINT_VERSION = 2;
4604
4898
  var CHROMATIC = 0.03;
4605
4899
  var MIN_ACCENT_AREA = 2e4;
4606
4900
  var oklch = (value) => {
@@ -4655,23 +4949,36 @@ function fingerprint(s) {
4655
4949
  roundness: median(s.controls.map((c) => Math.min(0.5, c.radiusPx / Math.max(1, c.heightPx)))),
4656
4950
  motion: below ? Math.min(1, s.motion.hiddenSections / below) : 0,
4657
4951
  effects: effects.sort(),
4658
- layout: s.sections.map((x) => x.kind)
4952
+ layout: s.sections.map((x) => x.kind),
4953
+ ...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 })) } : {}
4659
4954
  };
4660
4955
  }
4661
4956
  var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
4662
- function levenshtein(a, b) {
4957
+ function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
4663
4958
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
4664
4959
  for (let i = 1; i <= a.length; i += 1) {
4665
4960
  let prev = row[0];
4666
4961
  row[0] = i;
4667
4962
  for (let j = 1; j <= b.length; j += 1) {
4668
4963
  const cur = row[j];
4669
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
4964
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost(a[i - 1], b[j - 1]));
4670
4965
  prev = cur;
4671
4966
  }
4672
4967
  }
4673
4968
  return row[b.length];
4674
4969
  }
4970
+ function sectionCost(x, y) {
4971
+ if (x.role !== y.role) return 1;
4972
+ return Math.min(0.5, (Math.abs(x.centred - y.centred) + Math.abs(x.width - y.width)) / 2);
4973
+ }
4974
+ function layoutDistance(a, b) {
4975
+ if (a.sections && b.sections) {
4976
+ const longest2 = Math.max(a.sections.length, b.sections.length);
4977
+ return longest2 === 0 ? 0 : levenshtein(a.sections, b.sections, sectionCost) / longest2;
4978
+ }
4979
+ const longest = Math.max(a.layout.length, b.layout.length);
4980
+ return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
4981
+ }
4675
4982
  var clamp = (n) => Math.max(0, Math.min(1, n));
4676
4983
  function faceDistance(a, b) {
4677
4984
  if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
@@ -4686,8 +4993,7 @@ function fingerprintDistance(a, b) {
4686
4993
  const union = /* @__PURE__ */ new Set([...a.effects, ...b.effects]);
4687
4994
  const shared = a.effects.filter((e) => b.effects.includes(e)).length;
4688
4995
  const effects = union.size === 0 ? 0 : 1 - shared / union.size;
4689
- const longest = Math.max(a.layout.length, b.layout.length);
4690
- const layout = longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
4996
+ const layout = layoutDistance(a, b);
4691
4997
  const parts2 = { accent, ground, type, shape, motion, effects, layout };
4692
4998
  const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
4693
4999
  return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
@@ -4715,6 +5021,117 @@ var SOURCE_KINDS = [
4715
5021
  ];
4716
5022
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
4717
5023
 
5024
+ // src/direction/licences.ts
5025
+ var LICENCES = [
5026
+ {
5027
+ name: "Basteleur",
5028
+ subject: "face",
5029
+ licence: "OFL-1.1",
5030
+ multiClient: "yes",
5031
+ checked: "2026-09-24",
5032
+ source: "https://velvetyne.fr/fonts/basteleur/",
5033
+ note: "Velvetyne, designed by Keussel. Keep the OFL notice with the font files."
5034
+ },
5035
+ {
5036
+ name: "Gulax",
5037
+ subject: "face",
5038
+ licence: "OFL-1.1",
5039
+ multiClient: "yes",
5040
+ checked: "2026-09-24",
5041
+ source: "https://velvetyne.fr/fonts/gulax/",
5042
+ note: "Velvetyne. Keep the OFL notice with the font files."
5043
+ },
5044
+ {
5045
+ name: "Le Murmure",
5046
+ subject: "face",
5047
+ licence: "OFL-1.1",
5048
+ multiClient: "yes",
5049
+ checked: "2026-09-24",
5050
+ source: "https://velvetyne.fr/fonts/le-murmure/",
5051
+ note: "Velvetyne. Keep the OFL notice with the font files."
5052
+ },
5053
+ {
5054
+ name: "Blaze Type",
5055
+ subject: "foundry",
5056
+ licence: "commercial",
5057
+ multiClient: "per-site",
5058
+ cap: 10,
5059
+ checked: "2026-09-24",
5060
+ source: "https://blazetype.eu/eula",
5061
+ 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."
5062
+ },
5063
+ {
5064
+ name: "Power Type",
5065
+ subject: "foundry",
5066
+ licence: "commercial",
5067
+ multiClient: "unverified",
5068
+ checked: "2026-09-24",
5069
+ source: "https://power-type.com/eula",
5070
+ 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."
5071
+ },
5072
+ {
5073
+ name: "Grilli Type",
5074
+ subject: "foundry",
5075
+ licence: "commercial",
5076
+ multiClient: "per-site",
5077
+ checked: "2026-09-24",
5078
+ source: "https://www.grillitype.com/information",
5079
+ 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."
5080
+ },
5081
+ {
5082
+ name: "Phosphor",
5083
+ subject: "icons",
5084
+ licence: "MIT",
5085
+ multiClient: "yes",
5086
+ checked: "2026-09-24",
5087
+ source: "https://github.com/phosphor-icons/core/blob/main/LICENSE",
5088
+ note: "Six weights. The icon-tile tells treat it exactly as they treat Lucide: the library is not the tell, the tinted tile is."
5089
+ },
5090
+ {
5091
+ name: "Tabler Icons",
5092
+ subject: "icons",
5093
+ licence: "MIT",
5094
+ multiClient: "yes",
5095
+ checked: "2026-09-24",
5096
+ source: "https://github.com/tabler/tabler-icons/blob/main/LICENSE",
5097
+ note: "One stroke grid across the set."
5098
+ },
5099
+ {
5100
+ name: "Iconoir",
5101
+ subject: "icons",
5102
+ licence: "MIT",
5103
+ multiClient: "yes",
5104
+ checked: "2026-09-24",
5105
+ source: "https://github.com/iconoir-icons/iconoir/blob/main/LICENSE",
5106
+ note: "Smaller set, no paid tier."
5107
+ },
5108
+ {
5109
+ name: "Unicons",
5110
+ subject: "icons",
5111
+ licence: "IconScout Simple License",
5112
+ multiClient: "yes",
5113
+ checked: "2026-09-24",
5114
+ source: "https://github.com/Iconscout/unicons/blob/master/LICENSE",
5115
+ note: "IconScout's free set, personal and commercial use, attribution optional."
5116
+ },
5117
+ {
5118
+ name: "IconScout premium",
5119
+ subject: "icons",
5120
+ licence: "commercial",
5121
+ multiClient: "per-site",
5122
+ checked: "2026-09-24",
5123
+ source: "https://iconscout.com/licenses",
5124
+ 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."
5125
+ }
5126
+ ];
5127
+ function faceLicence(family) {
5128
+ const name = family.trim().toLowerCase();
5129
+ return LICENCES.find((l) => l.subject === "face" && l.name.toLowerCase() === name);
5130
+ }
5131
+ function licencesJson() {
5132
+ return JSON.stringify({ licences: LICENCES }, null, 2) + "\n";
5133
+ }
5134
+
4718
5135
  // src/direction/validate.ts
4719
5136
  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;
4720
5137
  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;
@@ -4838,6 +5255,18 @@ function validateDirection(input, ctx = {}) {
4838
5255
  ok = false;
4839
5256
  }
4840
5257
  }
5258
+ if ((key === "display" || key === "body") && typeof c.value === "string" && c.value.trim()) {
5259
+ const family = normaliseFamily(c.value);
5260
+ const entry = faceLicence(family);
5261
+ if (!entry) {
5262
+ 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.`);
5263
+ } else if (entry.multiClient === "unverified") {
5264
+ warn(`${at}.value`, `${family}: licence unverified (${entry.note})`);
5265
+ } else if (entry.multiClient === "capped" || entry.multiClient === "per-site") {
5266
+ const scope = entry.multiClient === "capped" ? `covers up to ${entry.cap} sites` : "needs a licence for this site, in the client's name";
5267
+ warn(`${at}.value`, `${family}: ${entry.licence} licence ${scope}. Record the purchase before launch.`);
5268
+ }
5269
+ }
4841
5270
  const fp = ctx.fingerprint;
4842
5271
  if (fp && typeof c.value === "string") {
4843
5272
  if ((key === "accent" || key === "ground") && parseColour(c.value)) {
@@ -5493,10 +5922,12 @@ export {
5493
5922
  AI_PHRASES,
5494
5923
  AI_VIOLET,
5495
5924
  AI_WORDS,
5925
+ BASELINE_VERSION,
5496
5926
  BUZZWORDS,
5497
5927
  CATALOGUE,
5498
5928
  CATALOGUE_VERSION,
5499
5929
  CHOICE_KEYS,
5930
+ DEFAULT_IGNORE,
5500
5931
  DENSITIES,
5501
5932
  DENSITY_NAMES,
5502
5933
  DIRECTION_VERSION,
@@ -5505,12 +5936,16 @@ export {
5505
5936
  DURATION_S,
5506
5937
  EASE,
5507
5938
  EASE_TUPLE,
5939
+ EDGE_STEP,
5508
5940
  ESTATE_VERSION,
5509
5941
  EXIT_RATIO,
5510
5942
  FINGERPRINT_VERSION,
5511
5943
  HOUSE,
5512
5944
  HOUSE_BUDGET,
5945
+ HOUSE_LOCKED,
5513
5946
  HOUSE_TYPE,
5947
+ JSON_SCHEMA_VERSION,
5948
+ LICENCES,
5514
5949
  LIGHTNESS_CURVE,
5515
5950
  MIN_DECIDED,
5516
5951
  MIN_RUNS,
@@ -5520,6 +5955,7 @@ export {
5520
5955
  PLAINER_WORDS,
5521
5956
  PROPOSED,
5522
5957
  RAMP_STEPS,
5958
+ READABLE_SNAPSHOT_VERSIONS,
5523
5959
  REFLEX_FONTS_1,
5524
5960
  REFLEX_FONTS_2,
5525
5961
  RENDERED_PATHS,
@@ -5541,7 +5977,9 @@ export {
5541
5977
  accentFork,
5542
5978
  addSite,
5543
5979
  apcaContrast,
5980
+ applyBaseline,
5544
5981
  applyHouseGate,
5982
+ applySeverity,
5545
5983
  auditSnapshot,
5546
5984
  catalogueTable,
5547
5985
  characterReport,
@@ -5552,6 +5990,7 @@ export {
5552
5990
  compareFacts,
5553
5991
  compareToEstate,
5554
5992
  craftTokens,
5993
+ createBaseline,
5555
5994
  deltaEOk,
5556
5995
  deltaEOkHex,
5557
5996
  densityCss,
@@ -5561,8 +6000,10 @@ export {
5561
6000
  estatePairs,
5562
6001
  exitDuration,
5563
6002
  extractHtml,
6003
+ faceLicence,
5564
6004
  findClaims,
5565
6005
  findColours,
6006
+ findingKey,
5566
6007
  fingerprint,
5567
6008
  fingerprintDistance,
5568
6009
  fluidClamp,
@@ -5573,17 +6014,22 @@ export {
5573
6014
  formatComparison,
5574
6015
  formatHex,
5575
6016
  formatReport,
6017
+ globToRegExp,
5576
6018
  groundName,
5577
6019
  harvest,
5578
6020
  hexToOklch,
6021
+ holdHouseBlocks,
5579
6022
  houseRule,
5580
6023
  hueName,
5581
6024
  hueSwatch,
6025
+ ignoreMatcher,
5582
6026
  inP3Gamut,
5583
6027
  inSrgbGamut,
5584
6028
  initDirection,
5585
6029
  isAiViolet,
5586
6030
  isCream,
6031
+ layoutDistance,
6032
+ licencesJson,
5587
6033
  makeSnapshot,
5588
6034
  motionTokens,
5589
6035
  namedSources,
@@ -5593,17 +6039,21 @@ export {
5593
6039
  oklchToHex,
5594
6040
  oklchToRgb,
5595
6041
  paletteFromPixels,
6042
+ parseBaseline,
5596
6043
  parseColour,
6044
+ parseCraftConfig,
5597
6045
  parseHex,
5598
6046
  propose,
5599
6047
  protectedFacts,
5600
6048
  ramp,
5601
6049
  rampFromAnchors,
6050
+ readSnapshot,
5602
6051
  retrofitPlan,
5603
6052
  runTell,
5604
6053
  scanSource,
5605
6054
  sectionRhythm,
5606
6055
  semantic,
6056
+ severityExceptions,
5607
6057
  shapeName,
5608
6058
  sharedParts,
5609
6059
  shouldAnimate,
@@ -5612,10 +6062,13 @@ export {
5612
6062
  tailwindV4Theme,
5613
6063
  tellById,
5614
6064
  toGamut,
6065
+ toJson,
6066
+ toSarif,
5615
6067
  typeFeatureTokens,
5616
6068
  typicality,
5617
6069
  validateDirection,
5618
6070
  visibleText,
6071
+ visualMeasures,
5619
6072
  wcagContrast,
5620
6073
  wordListsJson
5621
6074
  };