@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/CHANGELOG.md +35 -0
- package/CHARACTER.md +9 -1
- package/COPY.md +6 -2
- package/README.md +40 -0
- package/dist/{audit-EGDNWSQB.js → audit-5QWFLZ6L.js} +6 -3
- package/dist/audit.cjs +279 -16
- package/dist/audit.d.cts +81 -4
- package/dist/audit.d.ts +81 -4
- package/dist/audit.js +278 -16
- package/dist/{chunk-3NGWXZZE.js → chunk-AL6MRG6V.js} +40 -3
- package/dist/{chunk-2NQFIDLE.js → chunk-CDAJUS5U.js} +13 -11
- package/dist/{chunk-JKIDWHG3.js → chunk-J3QUROJ5.js} +299 -32
- package/dist/chunk-JYNXGCYZ.js +75 -0
- package/dist/{chunk-APO4BT2F.js → chunk-LHGY43LL.js} +11 -7
- package/dist/chunk-PU5G5DUC.js +206 -0
- package/dist/{chunk-QMYERCBB.js → chunk-PZ4MF2VV.js} +19 -7
- package/dist/{chunk-JWHC67ZS.js → chunk-S3NZAVU6.js} +121 -1
- package/dist/{chunk-TSKRBIOC.js → chunk-ULPI335S.js} +181 -13
- package/dist/chunk-ZKVJ7K6X.js +19 -0
- package/dist/cli-2DY3V3SF.js +17 -0
- package/dist/{cli-PRWTNFVP.js → cli-C3JLFXMG.js} +5 -4
- package/dist/cli-QM2AQQMF.js +19 -0
- package/dist/{cli-ADQQURNR.js → cli-ZB2VJ7CY.js} +25 -17
- package/dist/{cli-2WBGTBKD.js → cli-ZPJIWVEP.js} +15 -80
- package/dist/cli.js +2 -2
- package/dist/index.cjs +486 -9
- package/dist/index.d.cts +364 -6
- package/dist/index.d.ts +364 -6
- package/dist/index.js +462 -9
- package/licences.json +104 -0
- package/package.json +15 -7
- package/word-lists.json +1 -1
- package/dist/chunk-U2JTFQKO.js +0 -112
- package/dist/cli-EK3GSAOW.js +0 -15
- package/dist/cli-VQUNNIZM.js +0 -16
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 =
|
|
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.
|
|
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 =
|
|
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]
|
|
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
|
|
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
|
};
|