@domandigital/craft 0.15.0 → 0.16.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
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  ESTATE_VERSION: () => ESTATE_VERSION,
50
50
  EXIT_RATIO: () => EXIT_RATIO,
51
51
  FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
52
+ FLAGGED_TARGET: () => FLAGGED_TARGET,
52
53
  FUNCTION_WORDS: () => FUNCTION_WORDS,
53
54
  GROUND_SCALE: () => GROUND_SCALE,
54
55
  GROUND_SHARED: () => GROUND_SHARED,
@@ -59,8 +60,11 @@ __export(index_exports, {
59
60
  HOUSE_BUDGET: () => HOUSE_BUDGET,
60
61
  HOUSE_LOCKED: () => HOUSE_LOCKED,
61
62
  HOUSE_TYPE: () => HOUSE_TYPE,
63
+ HUMAN_TYPICAL_TARGET: () => HUMAN_TYPICAL_TARGET,
62
64
  IMAGERY_TELLS: () => IMAGERY_TELLS,
63
65
  JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
66
+ LABELS: () => LABELS,
67
+ LEXICAL_FEATURES: () => LEXICAL_FEATURES,
64
68
  LICENCES: () => LICENCES,
65
69
  LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
66
70
  MIN_DECIDED: () => MIN_DECIDED,
@@ -101,6 +105,7 @@ __export(index_exports, {
101
105
  STOCK_FIGURES: () => STOCK_FIGURES,
102
106
  STOCK_PHOTO_HOSTS: () => STOCK_PHOTO_HOSTS,
103
107
  STOCK_PHRASES: () => STOCK_PHRASES,
108
+ STUDY_WORDS: () => STUDY_WORDS,
104
109
  TELL_HEAVY: () => TELL_HEAVY,
105
110
  TRADE_NOUNS: () => TRADE_NOUNS,
106
111
  TYPE_STEPS: () => TYPE_STEPS,
@@ -114,6 +119,7 @@ __export(index_exports, {
114
119
  applySeverity: () => applySeverity,
115
120
  auditSnapshot: () => auditSnapshot,
116
121
  briefTerms: () => briefTerms,
122
+ calibrate: () => calibrate,
117
123
  catalogueTable: () => catalogueTable,
118
124
  characterReport: () => characterReport,
119
125
  checkCopy: () => checkCopy,
@@ -143,6 +149,7 @@ __export(index_exports, {
143
149
  deltaEOkHex: () => deltaEOkHex,
144
150
  densityCss: () => densityCss,
145
151
  densityTokens: () => densityTokens,
152
+ designTells: () => designTells,
146
153
  emitCss: () => emitCss,
147
154
  emptyEstate: () => emptyEstate,
148
155
  estateComponentPairs: () => estateComponentPairs,
@@ -159,6 +166,7 @@ __export(index_exports, {
159
166
  fluidSpace: () => fluidSpace,
160
167
  fluidType: () => fluidType,
161
168
  fontClass: () => fontClass,
169
+ formatCalibration: () => formatCalibration,
162
170
  formatClaims: () => formatClaims,
163
171
  formatComparison: () => formatComparison,
164
172
  formatCompetitor: () => formatCompetitor,
@@ -183,8 +191,10 @@ __export(index_exports, {
183
191
  isBlurb: () => isBlurb,
184
192
  isCategoryLabel: () => isCategoryLabel,
185
193
  isCream: () => isCream,
194
+ isFlagged: () => isFlagged,
186
195
  jobWords: () => jobWords,
187
196
  layoutDistance: () => layoutDistance,
197
+ lexicalOf: () => lexicalOf,
188
198
  licencesJson: () => licencesJson,
189
199
  makeSnapshot: () => makeSnapshot,
190
200
  motionTokens: () => motionTokens,
@@ -251,6 +261,8 @@ __export(index_exports, {
251
261
  validateJob: () => validateJob,
252
262
  visibleText: () => visibleText,
253
263
  visualMeasures: () => visualMeasures,
264
+ vocabulary: () => vocabulary,
265
+ vocabularyCounts: () => vocabularyCounts,
254
266
  wcagContrast: () => wcagContrast,
255
267
  wordListsJson: () => wordListsJson
256
268
  });
@@ -1015,10 +1027,10 @@ function stripLayers(css, ignored) {
1015
1027
  const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
1016
1028
  let out = "";
1017
1029
  let i = 0;
1018
- const pattern = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
1030
+ const pattern2 = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
1019
1031
  while (i < css.length) {
1020
- pattern.lastIndex = i;
1021
- const match = pattern.exec(css);
1032
+ pattern2.lastIndex = i;
1033
+ const match = pattern2.exec(css);
1022
1034
  if (match === null) {
1023
1035
  out += css.slice(i);
1024
1036
  break;
@@ -1048,12 +1060,12 @@ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
1048
1060
  ]);
1049
1061
  function atRuleBodies(css, prelude) {
1050
1062
  const out = [];
1051
- const pattern = new RegExp(
1063
+ const pattern2 = new RegExp(
1052
1064
  prelude.source,
1053
1065
  prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
1054
1066
  );
1055
1067
  let match;
1056
- while ((match = pattern.exec(css)) !== null) {
1068
+ while ((match = pattern2.exec(css)) !== null) {
1057
1069
  const start = match.index + match[0].length;
1058
1070
  let depth = 1;
1059
1071
  let i = start;
@@ -1063,7 +1075,7 @@ function atRuleBodies(css, prelude) {
1063
1075
  i += 1;
1064
1076
  }
1065
1077
  out.push(css.slice(start, i - 1));
1066
- pattern.lastIndex = i;
1078
+ pattern2.lastIndex = i;
1067
1079
  }
1068
1080
  return out;
1069
1081
  }
@@ -1096,9 +1108,9 @@ function isVarOnlyList(value) {
1096
1108
  return parts2.length > 1 && parts2.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
1097
1109
  }
1098
1110
  function distinctValues(css, property, props, unresolved) {
1099
- const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
1111
+ const pattern2 = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
1100
1112
  const seen = /* @__PURE__ */ new Set();
1101
- for (const match of css.matchAll(pattern)) {
1113
+ for (const match of css.matchAll(pattern2)) {
1102
1114
  const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
1103
1115
  if (CSS_WIDE_KEYWORDS.has(raw)) continue;
1104
1116
  const value = unwrapShadowColor(raw);
@@ -1517,14 +1529,14 @@ function linkSpans(text) {
1517
1529
  return [...text.matchAll(LINK)].map((m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]);
1518
1530
  }
1519
1531
  var insideLink = (spans, at) => spans.some(([a, b]) => at >= a && at < b);
1520
- function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => false) {
1532
+ function phrases(patterns2, label, source = (ctx) => ctx.blocks, skip = () => false) {
1521
1533
  return (ctx) => {
1522
1534
  const hits = [];
1523
1535
  for (const block of source(ctx)) {
1524
1536
  const spans = [];
1525
1537
  const links = linkSpans(block.text);
1526
- for (const pattern of patterns) {
1527
- const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
1538
+ for (const pattern2 of patterns2) {
1539
+ const global = new RegExp(pattern2.source, pattern2.flags.includes("g") ? pattern2.flags : `${pattern2.flags}g`);
1528
1540
  for (const m of block.text.matchAll(global)) {
1529
1541
  if (skip(block, m[0], m.index ?? 0)) continue;
1530
1542
  if (insideLink(links, m.index ?? 0)) continue;
@@ -2911,11 +2923,11 @@ function baseClass(cls) {
2911
2923
  function bases(unit) {
2912
2924
  return unit.classes.map(baseClass);
2913
2925
  }
2914
- function has(unit, pattern) {
2915
- return bases(unit).some((c) => pattern.test(c));
2926
+ function has(unit, pattern2) {
2927
+ return bases(unit).some((c) => pattern2.test(c));
2916
2928
  }
2917
- function find(unit, pattern) {
2918
- return bases(unit).find((c) => pattern.test(c));
2929
+ function find(unit, pattern2) {
2930
+ return bases(unit).find((c) => pattern2.test(c));
2919
2931
  }
2920
2932
  function arbitrary(cls) {
2921
2933
  const m = cls.match(/\[(.*)\]$/);
@@ -2931,9 +2943,9 @@ function eachUnit(ctx, fn) {
2931
2943
  }
2932
2944
  return hits;
2933
2945
  }
2934
- function firstMatch(text, pattern) {
2935
- const m = pattern.exec(text);
2936
- pattern.lastIndex = 0;
2946
+ function firstMatch(text, pattern2) {
2947
+ const m = pattern2.exec(text);
2948
+ pattern2.lastIndex = 0;
2937
2949
  return m ? m.index : -1;
2938
2950
  }
2939
2951
  var FRAMEWORK_PALETTE = /^--(?:color-)?(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+$|^--tw-/;
@@ -3377,9 +3389,9 @@ function shadcnCardHits(ctx) {
3377
3389
  }
3378
3390
  function marqueeHits(ctx) {
3379
3391
  const hits = [];
3380
- 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;
3392
+ const pattern2 = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
3381
3393
  for (const file of ctx.files) {
3382
- const at = firstMatch(file.text, pattern);
3394
+ const at = firstMatch(file.text, pattern2);
3383
3395
  if (at !== -1) hits.push({ path: file.path, offset: at, message: "scrolling marquee" });
3384
3396
  }
3385
3397
  return hits;
@@ -4221,8 +4233,8 @@ function protectedFacts(text) {
4221
4233
  const mask = (start, length) => {
4222
4234
  rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
4223
4235
  };
4224
- for (const [kind, pattern] of PATTERNS) {
4225
- for (const m of [...rest.matchAll(pattern)]) {
4236
+ for (const [kind, pattern2] of PATTERNS) {
4237
+ for (const m of [...rest.matchAll(pattern2)]) {
4226
4238
  const value = m[0].replace(/[.,;:]+$/, "");
4227
4239
  facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
4228
4240
  mask(m.index ?? 0, m[0].length);
@@ -4548,7 +4560,7 @@ function side(page, other, brief) {
4548
4560
  function compareCompetitor(ours, competitor, brief) {
4549
4561
  return { ours: side(ours, competitor, brief), competitor: side(competitor, ours, brief), brief: Boolean(brief) };
4550
4562
  }
4551
- var pctOf = (share) => share === null ? "no sentences" : `${Math.round(share * 100)}%`;
4563
+ var pctOf = (share2) => share2 === null ? "no sentences" : `${Math.round(share2 * 100)}%`;
4552
4564
  function formatCompetitor(result) {
4553
4565
  const row = (label, s) => ` ${label.padEnd(11)} ${s.interchangeable.length} of ${s.sentences} (${pctOf(s.share)}), ${s.perHundred} specifics per 100 words`;
4554
4566
  const lines = [
@@ -4924,11 +4936,11 @@ function reflexFonts(list) {
4924
4936
  for (const font of s.fonts) {
4925
4937
  const name = wanted.get(normaliseFamily(font.family).toLowerCase());
4926
4938
  if (!name || seen.has(name)) continue;
4927
- const share = font.chars / total;
4939
+ const share2 = font.chars / total;
4928
4940
  const displayShare = font.displayChars / display;
4929
- if (share >= 0.15 || displayShare >= 0.3) {
4941
+ if (share2 >= 0.15 || displayShare >= 0.3) {
4930
4942
  seen.add(name);
4931
- hits.push(hit(s, `${name} sets ${pct(share)} of the text on the page${displayShare >= 0.3 ? ` and ${pct(displayShare)} of the headings` : ""}`));
4943
+ hits.push(hit(s, `${name} sets ${pct(share2)} of the text on the page${displayShare >= 0.3 ? ` and ${pct(displayShare)} of the headings` : ""}`));
4932
4944
  }
4933
4945
  }
4934
4946
  return hits;
@@ -6131,8 +6143,8 @@ var IMPERATIVE = /^(?:Add|Use|Check|Make|Get|Set|Include|Create|Update|List|Keep
6131
6143
  var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
6132
6144
  function namedSources(sentence) {
6133
6145
  const found = /* @__PURE__ */ new Set();
6134
- ATTRIBUTION.forEach((pattern, i) => {
6135
- for (const m of sentence.matchAll(pattern)) {
6146
+ ATTRIBUTION.forEach((pattern2, i) => {
6147
+ for (const m of sentence.matchAll(pattern2)) {
6136
6148
  const raw = m[1].trim();
6137
6149
  const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
6138
6150
  if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
@@ -6376,7 +6388,7 @@ function shapeOf(text) {
6376
6388
  const ss = splitSentences(text);
6377
6389
  const wordCount = words(text).length;
6378
6390
  const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
6379
- const share = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6391
+ const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6380
6392
  const tri = ss.flatMap(tricolons);
6381
6393
  const commas = (text.match(/,/g) ?? []).length;
6382
6394
  return {
@@ -6390,9 +6402,9 @@ function shapeOf(text) {
6390
6402
  hedgeRate: per100(hedgeCount(text)),
6391
6403
  hedgedClose: hedgedClose(ss),
6392
6404
  nominalisationRate: per100(nominalisations(text)),
6393
- pronounOpenerShare: share(PRONOUN_OPENER),
6394
- transitionOpenerShare: share(TRANSITION_OPENER),
6395
- impersonalOpenerShare: share(IMPERSONAL_OPENER),
6405
+ pronounOpenerShare: share2(PRONOUN_OPENER),
6406
+ transitionOpenerShare: share2(TRANSITION_OPENER),
6407
+ impersonalOpenerShare: share2(IMPERSONAL_OPENER),
6396
6408
  commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
6397
6409
  commasPer100: per100(commas),
6398
6410
  participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
@@ -6419,6 +6431,89 @@ function isBlurb(f4) {
6419
6431
  return f4.sentences >= 2 && f4.sentences <= 8 && f4.words >= 20 && f4.words <= 160;
6420
6432
  }
6421
6433
 
6434
+ // src/character/lexical.ts
6435
+ var JUZEK = "Juzek and Ward, arXiv:2412.11385, Appendix A (PubMed abstracts, 2020 to 2024)";
6436
+ var STUDY_WORDS = [
6437
+ "delves",
6438
+ "delved",
6439
+ "delving",
6440
+ "delve",
6441
+ "showcasing",
6442
+ "boasts",
6443
+ "underscores",
6444
+ "underscoring",
6445
+ "comprehending",
6446
+ "intricacies",
6447
+ "surpassing",
6448
+ "intricate"
6449
+ ].map((word) => ({ word, source: JUZEK }));
6450
+ function vocabulary() {
6451
+ const seen = /* @__PURE__ */ new Set();
6452
+ const out = [];
6453
+ const add = (word, source) => {
6454
+ const w = word.toLowerCase();
6455
+ if (seen.has(w)) return;
6456
+ seen.add(w);
6457
+ out.push({ word: w, source });
6458
+ };
6459
+ for (const s of STUDY_WORDS) add(s.word, s.source);
6460
+ for (const w of AI_WORDS) add(w, "AI_WORDS");
6461
+ for (const w of PLAINER_WORDS) add(w, "PLAINER_WORDS");
6462
+ for (const w of AI_PHRASES) add(w, "AI_PHRASES");
6463
+ for (const w of STOCK_PHRASES) add(w, "STOCK_PHRASES");
6464
+ for (const w of BUZZWORDS) add(w, "BUZZWORDS");
6465
+ return out;
6466
+ }
6467
+ var escape4 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6468
+ var pattern = (w) => new RegExp(`(?<![\\w-])${escape4(w).replace(/\s+/g, "\\s+")}(?![\\w-])`, "gi");
6469
+ var compiled = null;
6470
+ var patterns = () => compiled ?? (compiled = vocabulary().map((v) => ({ ...v, re: pattern(v.word) })));
6471
+ var plain = (text) => text.replace(/[’‘]/g, "'").replace(/\s+/g, " ");
6472
+ function vocabularyCounts(text) {
6473
+ const t = plain(text);
6474
+ const out = /* @__PURE__ */ new Map();
6475
+ for (const { word, re } of patterns()) {
6476
+ const n = (t.match(re) ?? []).length;
6477
+ if (n) out.set(word, n);
6478
+ }
6479
+ return out;
6480
+ }
6481
+ var LEXICAL_FEATURES = [
6482
+ "listRate",
6483
+ "studyRate",
6484
+ "hardPer100",
6485
+ "tradePer100",
6486
+ "specificsPer100",
6487
+ "generic",
6488
+ "figures",
6489
+ "names"
6490
+ ];
6491
+ var STUDY = new Set(STUDY_WORDS.map((s) => s.word));
6492
+ var LISTED = new Set([...AI_WORDS, ...PLAINER_WORDS, ...AI_PHRASES, ...STOCK_PHRASES, ...BUZZWORDS].map((w) => w.toLowerCase()));
6493
+ var r3 = (n) => Math.round(n * 1e3) / 1e3;
6494
+ function lexicalOf(text, brief) {
6495
+ const words3 = Math.max(1, countWords2(text));
6496
+ const counts = vocabularyCounts(text);
6497
+ let list = 0;
6498
+ let study = 0;
6499
+ for (const [w, n] of counts) {
6500
+ if (STUDY.has(w)) study += n;
6501
+ if (LISTED.has(w)) list += n;
6502
+ }
6503
+ const s = specificity(text, brief);
6504
+ const kind = (...k) => s.specifics.filter((x) => k.includes(x.kind)).length;
6505
+ return {
6506
+ listRate: r3(list / words3 * 100),
6507
+ studyRate: r3(study / words3 * 100),
6508
+ hardPer100: r3(s.hard / words3 * 100),
6509
+ tradePer100: r3(s.trade / words3 * 100),
6510
+ specificsPer100: r3(s.perHundred),
6511
+ generic: s.generic ? 1 : 0,
6512
+ figures: kind("money", "date", "time", "number"),
6513
+ names: kind("name", "brief")
6514
+ };
6515
+ }
6516
+
6422
6517
  // src/character/baseline.ts
6423
6518
  var BASELINE_VERSION = 1;
6424
6519
  function fnv1a(text) {
@@ -6519,23 +6614,23 @@ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
6519
6614
  return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
6520
6615
  }
6521
6616
  function globToRegExp(glob) {
6522
- const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6617
+ const pattern2 = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6523
6618
  let body = "";
6524
- for (let i = 0; i < pattern.length; i += 1) {
6525
- const c = pattern[i];
6526
- if (c === "*" && pattern[i + 1] === "*") {
6527
- const slash = pattern[i + 2] === "/";
6619
+ for (let i = 0; i < pattern2.length; i += 1) {
6620
+ const c = pattern2[i];
6621
+ if (c === "*" && pattern2[i + 1] === "*") {
6622
+ const slash = pattern2[i + 2] === "/";
6528
6623
  body += slash ? "(?:.*/)?" : ".*";
6529
6624
  i += slash ? 2 : 1;
6530
6625
  } else if (c === "*") body += "[^/]*";
6531
6626
  else if (c === "?") body += "[^/]";
6532
6627
  else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6533
6628
  }
6534
- const anywhere = !pattern.includes("/");
6629
+ const anywhere = !pattern2.includes("/");
6535
6630
  return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
6536
6631
  }
6537
- function ignoreMatcher(patterns) {
6538
- const res = patterns.map(globToRegExp);
6632
+ function ignoreMatcher(patterns2) {
6633
+ const res = patterns2.map(globToRegExp);
6539
6634
  return (path) => {
6540
6635
  const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
6541
6636
  return res.some((re) => re.test(p));
@@ -6702,11 +6797,11 @@ function visualMeasures(img, maxSamples = 1e6) {
6702
6797
  const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
6703
6798
  const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
6704
6799
  const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
6705
- const round2 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
6800
+ const round3 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
6706
6801
  return {
6707
- colourfulness: round2(colourfulness, 1),
6708
- edgeDensity: round2(edgeN ? edges / edgeN : 0),
6709
- symmetry: round2(asymN ? 1 - asym / asymN / 255 : 1),
6802
+ colourfulness: round3(colourfulness, 1),
6803
+ edgeDensity: round3(edgeN ? edges / edgeN : 0),
6804
+ symmetry: round3(asymN ? 1 - asym / asymN / 255 : 1),
6710
6805
  width: w,
6711
6806
  height: h,
6712
6807
  sampled: n
@@ -7570,62 +7665,6 @@ function propose(input) {
7570
7665
  };
7571
7666
  }
7572
7667
 
7573
- // src/estate/index.ts
7574
- var ESTATE_VERSION = 1;
7575
- var SIBLING_AT = 0.31;
7576
- function emptyEstate() {
7577
- return { version: ESTATE_VERSION, sites: [] };
7578
- }
7579
- function addSite(register, site) {
7580
- if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
7581
- const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
7582
- return { version: ESTATE_VERSION, sites };
7583
- }
7584
- function sharedParts(a, b, d = fingerprintDistance(a, b)) {
7585
- const out = [];
7586
- const close = (k) => d.parts[k] <= SHARED_PART;
7587
- if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
7588
- if (close("type")) {
7589
- if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
7590
- if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
7591
- }
7592
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
7593
- if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
7594
- if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
7595
- if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
7596
- if (close("layout")) out.push("running order");
7597
- return out;
7598
- }
7599
- function compareToEstate(target2, register, options = {}) {
7600
- const at = options.siblingAt ?? SIBLING_AT;
7601
- return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
7602
- const d = fingerprintDistance(target2, s.fingerprint);
7603
- return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
7604
- }).sort((a, b) => a.distance - b.distance);
7605
- }
7606
- function estatePairs(register, siblingAt = SIBLING_AT) {
7607
- const out = [];
7608
- const s = register.sites.filter((site) => site.fingerprint);
7609
- for (let i = 0; i < s.length; i += 1) {
7610
- for (let j = i + 1; j < s.length; j += 1) {
7611
- const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
7612
- out.push({ a: s[i].id, b: s[j].id, distance: d.total, sibling: d.total < siblingAt, shared: sharedParts(s[i].fingerprint, s[j].fingerprint, d) });
7613
- }
7614
- }
7615
- return out.sort((x, y) => x.distance - y.distance);
7616
- }
7617
- function siblingLine(briefs) {
7618
- const d = [];
7619
- for (const runs of briefs) {
7620
- for (let i = 0; i < runs.length; i += 1) for (let j = i + 1; j < runs.length; j += 1) d.push(fingerprintDistance(runs[i], runs[j]).total);
7621
- }
7622
- if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
7623
- d.sort((a, b) => a - b);
7624
- const mid = Math.floor(d.length / 2);
7625
- const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
7626
- return Math.round(m * 1e3) / 1e3;
7627
- }
7628
-
7629
7668
  // src/report/index.ts
7630
7669
  var TELL_HEAVY = 3;
7631
7670
  var MIN_DECIDED = 5;
@@ -7781,6 +7820,154 @@ function choiceOfShared(part) {
7781
7820
  return /running order/.test(part) ? void 0 : "motif";
7782
7821
  }
7783
7822
 
7823
+ // src/calibrate/index.ts
7824
+ var LABELS = ["ai", "ai-looking", "human"];
7825
+ var FLAGGED_TARGET = 0.9;
7826
+ var HUMAN_TYPICAL_TARGET = 0.1;
7827
+ var SURFACE = new Map(CATALOGUE.map((t) => [t.id, t.surface]));
7828
+ function designTells(tells) {
7829
+ return tells.filter((t) => SURFACE.has(t) && SURFACE.get(t) !== "copy");
7830
+ }
7831
+ function isFlagged(page) {
7832
+ return Boolean(page.typicality?.typical) || designTells(page.tells).length >= TELL_HEAVY;
7833
+ }
7834
+ var positive = (label) => label !== "human";
7835
+ var round2 = (n) => Math.round(n * 100) / 100;
7836
+ var share = (n, of) => `${n} of ${of} (${Math.round(100 * n / of)}%)`;
7837
+ function summarise(label, pages, typicality2) {
7838
+ const mine = pages.filter((p) => p.label === label);
7839
+ return {
7840
+ label,
7841
+ pages: mine.length,
7842
+ // A set measured partly against the null would count the same page two ways.
7843
+ flagged: mine.filter((p) => typicality2 ? isFlagged(p) : designTells(p.tells).length >= TELL_HEAVY).length,
7844
+ typical: typicality2 ? mine.filter((p) => p.typicality?.typical).length : null,
7845
+ tellHeavy: mine.filter((p) => designTells(p.tells).length >= TELL_HEAVY).length,
7846
+ blocking: mine.reduce((s, p) => s + p.blocking, 0)
7847
+ };
7848
+ }
7849
+ function flaggedTarget(target2, s, missing) {
7850
+ const note = missing ? `; ${missing} not measured, counted neither way` : "";
7851
+ if (s.pages === 0) return { target: target2, met: null, detail: `no page measured${note}` };
7852
+ return { target: target2, met: s.flagged / s.pages >= FLAGGED_TARGET, detail: `${share(s.flagged, s.pages)} flagged, target ${FLAGGED_TARGET * 100}% or more${note}` };
7853
+ }
7854
+ function calibrate(pages, unmeasured = []) {
7855
+ const typicality2 = pages.length > 0 && pages.every((p) => p.typicality !== void 0);
7856
+ const labels = LABELS.map((l) => summarise(l, pages, typicality2));
7857
+ const byLabel = Object.fromEntries(labels.map((s) => [s.label, s]));
7858
+ const missing = (l) => unmeasured.filter((u) => u.label === l).length;
7859
+ const positives = pages.filter((p) => positive(p.label)).length;
7860
+ const fired = new Set(pages.flatMap((p) => p.tells));
7861
+ const tells = [...fired].map((tell) => {
7862
+ const hits = Object.fromEntries(LABELS.map((l) => [l, pages.filter((p) => p.label === l && p.tells.includes(tell)).length]));
7863
+ const on = hits.ai + hits["ai-looking"];
7864
+ return { tell, hits, precision: round2(on / (on + hits.human)), recall: positives ? round2(on / positives) : null };
7865
+ }).sort((a, b) => b.hits.ai + b.hits["ai-looking"] + b.hits.human - (a.hits.ai + a.hits["ai-looking"] + a.hits.human) || a.tell.localeCompare(b.tell));
7866
+ const human = byLabel.human;
7867
+ const humanNote = missing("human") ? `; ${missing("human")} not measured, counted neither way` : "";
7868
+ const targets = [
7869
+ flaggedTarget("1. Generated pages get caught", byLabel.ai, missing("ai")),
7870
+ human.pages === 0 ? { target: "2. Good human sites don't get caught", met: null, detail: `no page measured${humanNote}` } : {
7871
+ target: "2. Good human sites don't get caught",
7872
+ // A block hit fails it outright; without null models the other half is not known.
7873
+ met: human.blocking > 0 ? false : human.typical === null ? null : human.typical / human.pages <= HUMAN_TYPICAL_TARGET,
7874
+ detail: `${human.blocking} block hits, target 0; ${human.typical === null ? "typicality not measured (no null models)" : `${share(human.typical, human.pages)} typical, target ${HUMAN_TYPICAL_TARGET * 100}% or fewer`}${humanNote}`
7875
+ },
7876
+ flaggedTarget("3. Sites a person labelled AI-looking get caught", byLabel["ai-looking"], missing("ai-looking"))
7877
+ ];
7878
+ return {
7879
+ pages: pages.length,
7880
+ typicality: typicality2,
7881
+ labels,
7882
+ tells,
7883
+ silent: CATALOGUE.map((t) => t.id).filter((id) => !fired.has(id)),
7884
+ targets,
7885
+ unmeasured
7886
+ };
7887
+ }
7888
+ function formatCalibration(c, notes = []) {
7889
+ const counts = c.labels.map((s) => `${s.pages} ${s.label}`).join(", ");
7890
+ const lines = [`craft calibrate: ${c.pages} pages measured (${counts})`, ...notes];
7891
+ if (!c.typicality) lines.push("No null models: flagged means tell-heavy only.");
7892
+ if (c.unmeasured.length) {
7893
+ lines.push("", `Not measured: ${c.unmeasured.length}. A page not measured never counts as a pass.`);
7894
+ for (const u of c.unmeasured) lines.push(` ${u.id} (${u.label}): ${u.reason}`);
7895
+ }
7896
+ lines.push("", "Targets");
7897
+ for (const t of c.targets) lines.push(` ${t.target.padEnd(50)} ${t.met === null ? "not known" : t.met ? "met" : "NOT MET"}`, ` ${t.detail}`);
7898
+ lines.push("", ` ${"label".padEnd(11)} ${"pages".padStart(5)} ${"flagged".padStart(8)} ${"typical".padStart(8)} ${"tell-heavy".padStart(11)} ${"blocking".padStart(9)}`);
7899
+ for (const s of c.labels) {
7900
+ lines.push(` ${s.label.padEnd(11)} ${String(s.pages).padStart(5)} ${String(s.flagged).padStart(8)} ${String(s.typical ?? "-").padStart(8)} ${String(s.tellHeavy).padStart(11)} ${String(s.blocking).padStart(9)}`);
7901
+ }
7902
+ lines.push("", "Per tell. A hit on ai or ai-looking counts for the tell; a hit on human counts against it.");
7903
+ lines.push(` ${"tell".padEnd(28)} ${"ai".padStart(7)} ${"ai-looking".padStart(10)} ${"human".padStart(7)} ${"precision".padStart(9)} ${"recall".padStart(7)}`);
7904
+ const of = (l) => c.labels.find((s) => s.label === l)?.pages ?? 0;
7905
+ for (const t of c.tells) {
7906
+ lines.push(
7907
+ ` ${t.tell.padEnd(28)} ${`${t.hits.ai}/${of("ai")}`.padStart(7)} ${`${t.hits["ai-looking"]}/${of("ai-looking")}`.padStart(10)} ${`${t.hits.human}/${of("human")}`.padStart(7)} ${t.precision.toFixed(2).padStart(9)} ${(t.recall === null ? "-" : t.recall.toFixed(2)).padStart(7)}`
7908
+ );
7909
+ }
7910
+ if (!c.tells.length) lines.push(" no tell fired");
7911
+ lines.push("", `Never fired on this set: ${c.silent.length} of ${CATALOGUE.length} catalogue tells.`);
7912
+ return lines.join("\n");
7913
+ }
7914
+
7915
+ // src/estate/index.ts
7916
+ var ESTATE_VERSION = 1;
7917
+ var SIBLING_AT = 0.31;
7918
+ function emptyEstate() {
7919
+ return { version: ESTATE_VERSION, sites: [] };
7920
+ }
7921
+ function addSite(register, site) {
7922
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
7923
+ const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
7924
+ return { version: ESTATE_VERSION, sites };
7925
+ }
7926
+ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
7927
+ const out = [];
7928
+ const close = (k) => d.parts[k] <= SHARED_PART;
7929
+ if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
7930
+ if (close("type")) {
7931
+ if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
7932
+ if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
7933
+ }
7934
+ if (close("ground")) out.push(`${groundName(a.ground)} ground`);
7935
+ if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
7936
+ if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
7937
+ if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
7938
+ if (close("layout")) out.push("running order");
7939
+ return out;
7940
+ }
7941
+ function compareToEstate(target2, register, options = {}) {
7942
+ const at = options.siblingAt ?? SIBLING_AT;
7943
+ return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
7944
+ const d = fingerprintDistance(target2, s.fingerprint);
7945
+ return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
7946
+ }).sort((a, b) => a.distance - b.distance);
7947
+ }
7948
+ function estatePairs(register, siblingAt = SIBLING_AT) {
7949
+ const out = [];
7950
+ const s = register.sites.filter((site) => site.fingerprint);
7951
+ for (let i = 0; i < s.length; i += 1) {
7952
+ for (let j = i + 1; j < s.length; j += 1) {
7953
+ const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
7954
+ out.push({ a: s[i].id, b: s[j].id, distance: d.total, sibling: d.total < siblingAt, shared: sharedParts(s[i].fingerprint, s[j].fingerprint, d) });
7955
+ }
7956
+ }
7957
+ return out.sort((x, y) => x.distance - y.distance);
7958
+ }
7959
+ function siblingLine(briefs) {
7960
+ const d = [];
7961
+ for (const runs of briefs) {
7962
+ for (let i = 0; i < runs.length; i += 1) for (let j = i + 1; j < runs.length; j += 1) d.push(fingerprintDistance(runs[i], runs[j]).total);
7963
+ }
7964
+ if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
7965
+ d.sort((a, b) => a - b);
7966
+ const mid = Math.floor(d.length / 2);
7967
+ const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
7968
+ return Math.round(m * 1e3) / 1e3;
7969
+ }
7970
+
7784
7971
  // src/report/retrofit.ts
7785
7972
  var HEADINGS = {
7786
7973
  direction: "Decide first",
@@ -7863,6 +8050,7 @@ function retrofitPlan(report2, direction) {
7863
8050
  ESTATE_VERSION,
7864
8051
  EXIT_RATIO,
7865
8052
  FINGERPRINT_VERSION,
8053
+ FLAGGED_TARGET,
7866
8054
  FUNCTION_WORDS,
7867
8055
  GROUND_SCALE,
7868
8056
  GROUND_SHARED,
@@ -7873,8 +8061,11 @@ function retrofitPlan(report2, direction) {
7873
8061
  HOUSE_BUDGET,
7874
8062
  HOUSE_LOCKED,
7875
8063
  HOUSE_TYPE,
8064
+ HUMAN_TYPICAL_TARGET,
7876
8065
  IMAGERY_TELLS,
7877
8066
  JSON_SCHEMA_VERSION,
8067
+ LABELS,
8068
+ LEXICAL_FEATURES,
7878
8069
  LICENCES,
7879
8070
  LIGHTNESS_CURVE,
7880
8071
  MIN_DECIDED,
@@ -7915,6 +8106,7 @@ function retrofitPlan(report2, direction) {
7915
8106
  STOCK_FIGURES,
7916
8107
  STOCK_PHOTO_HOSTS,
7917
8108
  STOCK_PHRASES,
8109
+ STUDY_WORDS,
7918
8110
  TELL_HEAVY,
7919
8111
  TRADE_NOUNS,
7920
8112
  TYPE_STEPS,
@@ -7928,6 +8120,7 @@ function retrofitPlan(report2, direction) {
7928
8120
  applySeverity,
7929
8121
  auditSnapshot,
7930
8122
  briefTerms,
8123
+ calibrate,
7931
8124
  catalogueTable,
7932
8125
  characterReport,
7933
8126
  checkCopy,
@@ -7957,6 +8150,7 @@ function retrofitPlan(report2, direction) {
7957
8150
  deltaEOkHex,
7958
8151
  densityCss,
7959
8152
  densityTokens,
8153
+ designTells,
7960
8154
  emitCss,
7961
8155
  emptyEstate,
7962
8156
  estateComponentPairs,
@@ -7973,6 +8167,7 @@ function retrofitPlan(report2, direction) {
7973
8167
  fluidSpace,
7974
8168
  fluidType,
7975
8169
  fontClass,
8170
+ formatCalibration,
7976
8171
  formatClaims,
7977
8172
  formatComparison,
7978
8173
  formatCompetitor,
@@ -7997,8 +8192,10 @@ function retrofitPlan(report2, direction) {
7997
8192
  isBlurb,
7998
8193
  isCategoryLabel,
7999
8194
  isCream,
8195
+ isFlagged,
8000
8196
  jobWords,
8001
8197
  layoutDistance,
8198
+ lexicalOf,
8002
8199
  licencesJson,
8003
8200
  makeSnapshot,
8004
8201
  motionTokens,
@@ -8065,6 +8262,8 @@ function retrofitPlan(report2, direction) {
8065
8262
  validateJob,
8066
8263
  visibleText,
8067
8264
  visualMeasures,
8265
+ vocabulary,
8266
+ vocabularyCounts,
8068
8267
  wcagContrast,
8069
8268
  wordListsJson
8070
8269
  });