@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.js CHANGED
@@ -757,10 +757,10 @@ function stripLayers(css, ignored) {
757
757
  const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
758
758
  let out = "";
759
759
  let i = 0;
760
- const pattern = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
760
+ const pattern2 = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
761
761
  while (i < css.length) {
762
- pattern.lastIndex = i;
763
- const match = pattern.exec(css);
762
+ pattern2.lastIndex = i;
763
+ const match = pattern2.exec(css);
764
764
  if (match === null) {
765
765
  out += css.slice(i);
766
766
  break;
@@ -790,12 +790,12 @@ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
790
790
  ]);
791
791
  function atRuleBodies(css, prelude) {
792
792
  const out = [];
793
- const pattern = new RegExp(
793
+ const pattern2 = new RegExp(
794
794
  prelude.source,
795
795
  prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
796
796
  );
797
797
  let match;
798
- while ((match = pattern.exec(css)) !== null) {
798
+ while ((match = pattern2.exec(css)) !== null) {
799
799
  const start = match.index + match[0].length;
800
800
  let depth = 1;
801
801
  let i = start;
@@ -805,7 +805,7 @@ function atRuleBodies(css, prelude) {
805
805
  i += 1;
806
806
  }
807
807
  out.push(css.slice(start, i - 1));
808
- pattern.lastIndex = i;
808
+ pattern2.lastIndex = i;
809
809
  }
810
810
  return out;
811
811
  }
@@ -838,9 +838,9 @@ function isVarOnlyList(value) {
838
838
  return parts2.length > 1 && parts2.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
839
839
  }
840
840
  function distinctValues(css, property, props, unresolved) {
841
- const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
841
+ const pattern2 = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
842
842
  const seen = /* @__PURE__ */ new Set();
843
- for (const match of css.matchAll(pattern)) {
843
+ for (const match of css.matchAll(pattern2)) {
844
844
  const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
845
845
  if (CSS_WIDE_KEYWORDS.has(raw)) continue;
846
846
  const value = unwrapShadowColor(raw);
@@ -1259,14 +1259,14 @@ function linkSpans(text) {
1259
1259
  return [...text.matchAll(LINK)].map((m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]);
1260
1260
  }
1261
1261
  var insideLink = (spans, at) => spans.some(([a, b]) => at >= a && at < b);
1262
- function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => false) {
1262
+ function phrases(patterns2, label, source = (ctx) => ctx.blocks, skip = () => false) {
1263
1263
  return (ctx) => {
1264
1264
  const hits = [];
1265
1265
  for (const block of source(ctx)) {
1266
1266
  const spans = [];
1267
1267
  const links = linkSpans(block.text);
1268
- for (const pattern of patterns) {
1269
- const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
1268
+ for (const pattern2 of patterns2) {
1269
+ const global = new RegExp(pattern2.source, pattern2.flags.includes("g") ? pattern2.flags : `${pattern2.flags}g`);
1270
1270
  for (const m of block.text.matchAll(global)) {
1271
1271
  if (skip(block, m[0], m.index ?? 0)) continue;
1272
1272
  if (insideLink(links, m.index ?? 0)) continue;
@@ -2653,11 +2653,11 @@ function baseClass(cls) {
2653
2653
  function bases(unit) {
2654
2654
  return unit.classes.map(baseClass);
2655
2655
  }
2656
- function has(unit, pattern) {
2657
- return bases(unit).some((c) => pattern.test(c));
2656
+ function has(unit, pattern2) {
2657
+ return bases(unit).some((c) => pattern2.test(c));
2658
2658
  }
2659
- function find(unit, pattern) {
2660
- return bases(unit).find((c) => pattern.test(c));
2659
+ function find(unit, pattern2) {
2660
+ return bases(unit).find((c) => pattern2.test(c));
2661
2661
  }
2662
2662
  function arbitrary(cls) {
2663
2663
  const m = cls.match(/\[(.*)\]$/);
@@ -2673,9 +2673,9 @@ function eachUnit(ctx, fn) {
2673
2673
  }
2674
2674
  return hits;
2675
2675
  }
2676
- function firstMatch(text, pattern) {
2677
- const m = pattern.exec(text);
2678
- pattern.lastIndex = 0;
2676
+ function firstMatch(text, pattern2) {
2677
+ const m = pattern2.exec(text);
2678
+ pattern2.lastIndex = 0;
2679
2679
  return m ? m.index : -1;
2680
2680
  }
2681
2681
  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-/;
@@ -3119,9 +3119,9 @@ function shadcnCardHits(ctx) {
3119
3119
  }
3120
3120
  function marqueeHits(ctx) {
3121
3121
  const hits = [];
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;
3122
+ 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;
3123
3123
  for (const file of ctx.files) {
3124
- const at = firstMatch(file.text, pattern);
3124
+ const at = firstMatch(file.text, pattern2);
3125
3125
  if (at !== -1) hits.push({ path: file.path, offset: at, message: "scrolling marquee" });
3126
3126
  }
3127
3127
  return hits;
@@ -3963,8 +3963,8 @@ function protectedFacts(text) {
3963
3963
  const mask = (start, length) => {
3964
3964
  rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
3965
3965
  };
3966
- for (const [kind, pattern] of PATTERNS) {
3967
- for (const m of [...rest.matchAll(pattern)]) {
3966
+ for (const [kind, pattern2] of PATTERNS) {
3967
+ for (const m of [...rest.matchAll(pattern2)]) {
3968
3968
  const value = m[0].replace(/[.,;:]+$/, "");
3969
3969
  facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
3970
3970
  mask(m.index ?? 0, m[0].length);
@@ -4290,7 +4290,7 @@ function side(page, other, brief) {
4290
4290
  function compareCompetitor(ours, competitor, brief) {
4291
4291
  return { ours: side(ours, competitor, brief), competitor: side(competitor, ours, brief), brief: Boolean(brief) };
4292
4292
  }
4293
- var pctOf = (share) => share === null ? "no sentences" : `${Math.round(share * 100)}%`;
4293
+ var pctOf = (share2) => share2 === null ? "no sentences" : `${Math.round(share2 * 100)}%`;
4294
4294
  function formatCompetitor(result) {
4295
4295
  const row = (label, s) => ` ${label.padEnd(11)} ${s.interchangeable.length} of ${s.sentences} (${pctOf(s.share)}), ${s.perHundred} specifics per 100 words`;
4296
4296
  const lines = [
@@ -4666,11 +4666,11 @@ function reflexFonts(list) {
4666
4666
  for (const font of s.fonts) {
4667
4667
  const name = wanted.get(normaliseFamily(font.family).toLowerCase());
4668
4668
  if (!name || seen.has(name)) continue;
4669
- const share = font.chars / total;
4669
+ const share2 = font.chars / total;
4670
4670
  const displayShare = font.displayChars / display;
4671
- if (share >= 0.15 || displayShare >= 0.3) {
4671
+ if (share2 >= 0.15 || displayShare >= 0.3) {
4672
4672
  seen.add(name);
4673
- hits.push(hit(s, `${name} sets ${pct(share)} of the text on the page${displayShare >= 0.3 ? ` and ${pct(displayShare)} of the headings` : ""}`));
4673
+ hits.push(hit(s, `${name} sets ${pct(share2)} of the text on the page${displayShare >= 0.3 ? ` and ${pct(displayShare)} of the headings` : ""}`));
4674
4674
  }
4675
4675
  }
4676
4676
  return hits;
@@ -5873,8 +5873,8 @@ var IMPERATIVE = /^(?:Add|Use|Check|Make|Get|Set|Include|Create|Update|List|Keep
5873
5873
  var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
5874
5874
  function namedSources(sentence) {
5875
5875
  const found = /* @__PURE__ */ new Set();
5876
- ATTRIBUTION.forEach((pattern, i) => {
5877
- for (const m of sentence.matchAll(pattern)) {
5876
+ ATTRIBUTION.forEach((pattern2, i) => {
5877
+ for (const m of sentence.matchAll(pattern2)) {
5878
5878
  const raw = m[1].trim();
5879
5879
  const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
5880
5880
  if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
@@ -6118,7 +6118,7 @@ function shapeOf(text) {
6118
6118
  const ss = splitSentences(text);
6119
6119
  const wordCount = words(text).length;
6120
6120
  const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
6121
- const share = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6121
+ const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6122
6122
  const tri = ss.flatMap(tricolons);
6123
6123
  const commas = (text.match(/,/g) ?? []).length;
6124
6124
  return {
@@ -6132,9 +6132,9 @@ function shapeOf(text) {
6132
6132
  hedgeRate: per100(hedgeCount(text)),
6133
6133
  hedgedClose: hedgedClose(ss),
6134
6134
  nominalisationRate: per100(nominalisations(text)),
6135
- pronounOpenerShare: share(PRONOUN_OPENER),
6136
- transitionOpenerShare: share(TRANSITION_OPENER),
6137
- impersonalOpenerShare: share(IMPERSONAL_OPENER),
6135
+ pronounOpenerShare: share2(PRONOUN_OPENER),
6136
+ transitionOpenerShare: share2(TRANSITION_OPENER),
6137
+ impersonalOpenerShare: share2(IMPERSONAL_OPENER),
6138
6138
  commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
6139
6139
  commasPer100: per100(commas),
6140
6140
  participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
@@ -6161,6 +6161,89 @@ function isBlurb(f4) {
6161
6161
  return f4.sentences >= 2 && f4.sentences <= 8 && f4.words >= 20 && f4.words <= 160;
6162
6162
  }
6163
6163
 
6164
+ // src/character/lexical.ts
6165
+ var JUZEK = "Juzek and Ward, arXiv:2412.11385, Appendix A (PubMed abstracts, 2020 to 2024)";
6166
+ var STUDY_WORDS = [
6167
+ "delves",
6168
+ "delved",
6169
+ "delving",
6170
+ "delve",
6171
+ "showcasing",
6172
+ "boasts",
6173
+ "underscores",
6174
+ "underscoring",
6175
+ "comprehending",
6176
+ "intricacies",
6177
+ "surpassing",
6178
+ "intricate"
6179
+ ].map((word) => ({ word, source: JUZEK }));
6180
+ function vocabulary() {
6181
+ const seen = /* @__PURE__ */ new Set();
6182
+ const out = [];
6183
+ const add = (word, source) => {
6184
+ const w = word.toLowerCase();
6185
+ if (seen.has(w)) return;
6186
+ seen.add(w);
6187
+ out.push({ word: w, source });
6188
+ };
6189
+ for (const s of STUDY_WORDS) add(s.word, s.source);
6190
+ for (const w of AI_WORDS) add(w, "AI_WORDS");
6191
+ for (const w of PLAINER_WORDS) add(w, "PLAINER_WORDS");
6192
+ for (const w of AI_PHRASES) add(w, "AI_PHRASES");
6193
+ for (const w of STOCK_PHRASES) add(w, "STOCK_PHRASES");
6194
+ for (const w of BUZZWORDS) add(w, "BUZZWORDS");
6195
+ return out;
6196
+ }
6197
+ var escape4 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6198
+ var pattern = (w) => new RegExp(`(?<![\\w-])${escape4(w).replace(/\s+/g, "\\s+")}(?![\\w-])`, "gi");
6199
+ var compiled = null;
6200
+ var patterns = () => compiled ?? (compiled = vocabulary().map((v) => ({ ...v, re: pattern(v.word) })));
6201
+ var plain = (text) => text.replace(/[’‘]/g, "'").replace(/\s+/g, " ");
6202
+ function vocabularyCounts(text) {
6203
+ const t = plain(text);
6204
+ const out = /* @__PURE__ */ new Map();
6205
+ for (const { word, re } of patterns()) {
6206
+ const n = (t.match(re) ?? []).length;
6207
+ if (n) out.set(word, n);
6208
+ }
6209
+ return out;
6210
+ }
6211
+ var LEXICAL_FEATURES = [
6212
+ "listRate",
6213
+ "studyRate",
6214
+ "hardPer100",
6215
+ "tradePer100",
6216
+ "specificsPer100",
6217
+ "generic",
6218
+ "figures",
6219
+ "names"
6220
+ ];
6221
+ var STUDY = new Set(STUDY_WORDS.map((s) => s.word));
6222
+ var LISTED = new Set([...AI_WORDS, ...PLAINER_WORDS, ...AI_PHRASES, ...STOCK_PHRASES, ...BUZZWORDS].map((w) => w.toLowerCase()));
6223
+ var r3 = (n) => Math.round(n * 1e3) / 1e3;
6224
+ function lexicalOf(text, brief) {
6225
+ const words3 = Math.max(1, countWords2(text));
6226
+ const counts = vocabularyCounts(text);
6227
+ let list = 0;
6228
+ let study = 0;
6229
+ for (const [w, n] of counts) {
6230
+ if (STUDY.has(w)) study += n;
6231
+ if (LISTED.has(w)) list += n;
6232
+ }
6233
+ const s = specificity(text, brief);
6234
+ const kind = (...k) => s.specifics.filter((x) => k.includes(x.kind)).length;
6235
+ return {
6236
+ listRate: r3(list / words3 * 100),
6237
+ studyRate: r3(study / words3 * 100),
6238
+ hardPer100: r3(s.hard / words3 * 100),
6239
+ tradePer100: r3(s.trade / words3 * 100),
6240
+ specificsPer100: r3(s.perHundred),
6241
+ generic: s.generic ? 1 : 0,
6242
+ figures: kind("money", "date", "time", "number"),
6243
+ names: kind("name", "brief")
6244
+ };
6245
+ }
6246
+
6164
6247
  // src/character/baseline.ts
6165
6248
  var BASELINE_VERSION = 1;
6166
6249
  function fnv1a(text) {
@@ -6261,23 +6344,23 @@ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
6261
6344
  return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
6262
6345
  }
6263
6346
  function globToRegExp(glob) {
6264
- const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6347
+ const pattern2 = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6265
6348
  let body = "";
6266
- for (let i = 0; i < pattern.length; i += 1) {
6267
- const c = pattern[i];
6268
- if (c === "*" && pattern[i + 1] === "*") {
6269
- const slash = pattern[i + 2] === "/";
6349
+ for (let i = 0; i < pattern2.length; i += 1) {
6350
+ const c = pattern2[i];
6351
+ if (c === "*" && pattern2[i + 1] === "*") {
6352
+ const slash = pattern2[i + 2] === "/";
6270
6353
  body += slash ? "(?:.*/)?" : ".*";
6271
6354
  i += slash ? 2 : 1;
6272
6355
  } else if (c === "*") body += "[^/]*";
6273
6356
  else if (c === "?") body += "[^/]";
6274
6357
  else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6275
6358
  }
6276
- const anywhere = !pattern.includes("/");
6359
+ const anywhere = !pattern2.includes("/");
6277
6360
  return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
6278
6361
  }
6279
- function ignoreMatcher(patterns) {
6280
- const res = patterns.map(globToRegExp);
6362
+ function ignoreMatcher(patterns2) {
6363
+ const res = patterns2.map(globToRegExp);
6281
6364
  return (path) => {
6282
6365
  const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
6283
6366
  return res.some((re) => re.test(p));
@@ -6444,11 +6527,11 @@ function visualMeasures(img, maxSamples = 1e6) {
6444
6527
  const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
6445
6528
  const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
6446
6529
  const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
6447
- const round2 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
6530
+ const round3 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
6448
6531
  return {
6449
- colourfulness: round2(colourfulness, 1),
6450
- edgeDensity: round2(edgeN ? edges / edgeN : 0),
6451
- symmetry: round2(asymN ? 1 - asym / asymN / 255 : 1),
6532
+ colourfulness: round3(colourfulness, 1),
6533
+ edgeDensity: round3(edgeN ? edges / edgeN : 0),
6534
+ symmetry: round3(asymN ? 1 - asym / asymN / 255 : 1),
6452
6535
  width: w,
6453
6536
  height: h,
6454
6537
  sampled: n
@@ -7312,62 +7395,6 @@ function propose(input) {
7312
7395
  };
7313
7396
  }
7314
7397
 
7315
- // src/estate/index.ts
7316
- var ESTATE_VERSION = 1;
7317
- var SIBLING_AT = 0.31;
7318
- function emptyEstate() {
7319
- return { version: ESTATE_VERSION, sites: [] };
7320
- }
7321
- function addSite(register, site) {
7322
- if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
7323
- const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
7324
- return { version: ESTATE_VERSION, sites };
7325
- }
7326
- function sharedParts(a, b, d = fingerprintDistance(a, b)) {
7327
- const out = [];
7328
- const close = (k) => d.parts[k] <= SHARED_PART;
7329
- if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
7330
- if (close("type")) {
7331
- if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
7332
- if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
7333
- }
7334
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
7335
- if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
7336
- if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
7337
- if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
7338
- if (close("layout")) out.push("running order");
7339
- return out;
7340
- }
7341
- function compareToEstate(target2, register, options = {}) {
7342
- const at = options.siblingAt ?? SIBLING_AT;
7343
- return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
7344
- const d = fingerprintDistance(target2, s.fingerprint);
7345
- return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
7346
- }).sort((a, b) => a.distance - b.distance);
7347
- }
7348
- function estatePairs(register, siblingAt = SIBLING_AT) {
7349
- const out = [];
7350
- const s = register.sites.filter((site) => site.fingerprint);
7351
- for (let i = 0; i < s.length; i += 1) {
7352
- for (let j = i + 1; j < s.length; j += 1) {
7353
- const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
7354
- 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) });
7355
- }
7356
- }
7357
- return out.sort((x, y) => x.distance - y.distance);
7358
- }
7359
- function siblingLine(briefs) {
7360
- const d = [];
7361
- for (const runs of briefs) {
7362
- 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);
7363
- }
7364
- if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
7365
- d.sort((a, b) => a - b);
7366
- const mid = Math.floor(d.length / 2);
7367
- const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
7368
- return Math.round(m * 1e3) / 1e3;
7369
- }
7370
-
7371
7398
  // src/report/index.ts
7372
7399
  var TELL_HEAVY = 3;
7373
7400
  var MIN_DECIDED = 5;
@@ -7523,6 +7550,154 @@ function choiceOfShared(part) {
7523
7550
  return /running order/.test(part) ? void 0 : "motif";
7524
7551
  }
7525
7552
 
7553
+ // src/calibrate/index.ts
7554
+ var LABELS = ["ai", "ai-looking", "human"];
7555
+ var FLAGGED_TARGET = 0.9;
7556
+ var HUMAN_TYPICAL_TARGET = 0.1;
7557
+ var SURFACE = new Map(CATALOGUE.map((t) => [t.id, t.surface]));
7558
+ function designTells(tells) {
7559
+ return tells.filter((t) => SURFACE.has(t) && SURFACE.get(t) !== "copy");
7560
+ }
7561
+ function isFlagged(page) {
7562
+ return Boolean(page.typicality?.typical) || designTells(page.tells).length >= TELL_HEAVY;
7563
+ }
7564
+ var positive = (label) => label !== "human";
7565
+ var round2 = (n) => Math.round(n * 100) / 100;
7566
+ var share = (n, of) => `${n} of ${of} (${Math.round(100 * n / of)}%)`;
7567
+ function summarise(label, pages, typicality2) {
7568
+ const mine = pages.filter((p) => p.label === label);
7569
+ return {
7570
+ label,
7571
+ pages: mine.length,
7572
+ // A set measured partly against the null would count the same page two ways.
7573
+ flagged: mine.filter((p) => typicality2 ? isFlagged(p) : designTells(p.tells).length >= TELL_HEAVY).length,
7574
+ typical: typicality2 ? mine.filter((p) => p.typicality?.typical).length : null,
7575
+ tellHeavy: mine.filter((p) => designTells(p.tells).length >= TELL_HEAVY).length,
7576
+ blocking: mine.reduce((s, p) => s + p.blocking, 0)
7577
+ };
7578
+ }
7579
+ function flaggedTarget(target2, s, missing) {
7580
+ const note = missing ? `; ${missing} not measured, counted neither way` : "";
7581
+ if (s.pages === 0) return { target: target2, met: null, detail: `no page measured${note}` };
7582
+ return { target: target2, met: s.flagged / s.pages >= FLAGGED_TARGET, detail: `${share(s.flagged, s.pages)} flagged, target ${FLAGGED_TARGET * 100}% or more${note}` };
7583
+ }
7584
+ function calibrate(pages, unmeasured = []) {
7585
+ const typicality2 = pages.length > 0 && pages.every((p) => p.typicality !== void 0);
7586
+ const labels = LABELS.map((l) => summarise(l, pages, typicality2));
7587
+ const byLabel = Object.fromEntries(labels.map((s) => [s.label, s]));
7588
+ const missing = (l) => unmeasured.filter((u) => u.label === l).length;
7589
+ const positives = pages.filter((p) => positive(p.label)).length;
7590
+ const fired = new Set(pages.flatMap((p) => p.tells));
7591
+ const tells = [...fired].map((tell) => {
7592
+ const hits = Object.fromEntries(LABELS.map((l) => [l, pages.filter((p) => p.label === l && p.tells.includes(tell)).length]));
7593
+ const on = hits.ai + hits["ai-looking"];
7594
+ return { tell, hits, precision: round2(on / (on + hits.human)), recall: positives ? round2(on / positives) : null };
7595
+ }).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));
7596
+ const human = byLabel.human;
7597
+ const humanNote = missing("human") ? `; ${missing("human")} not measured, counted neither way` : "";
7598
+ const targets = [
7599
+ flaggedTarget("1. Generated pages get caught", byLabel.ai, missing("ai")),
7600
+ human.pages === 0 ? { target: "2. Good human sites don't get caught", met: null, detail: `no page measured${humanNote}` } : {
7601
+ target: "2. Good human sites don't get caught",
7602
+ // A block hit fails it outright; without null models the other half is not known.
7603
+ met: human.blocking > 0 ? false : human.typical === null ? null : human.typical / human.pages <= HUMAN_TYPICAL_TARGET,
7604
+ 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}`
7605
+ },
7606
+ flaggedTarget("3. Sites a person labelled AI-looking get caught", byLabel["ai-looking"], missing("ai-looking"))
7607
+ ];
7608
+ return {
7609
+ pages: pages.length,
7610
+ typicality: typicality2,
7611
+ labels,
7612
+ tells,
7613
+ silent: CATALOGUE.map((t) => t.id).filter((id) => !fired.has(id)),
7614
+ targets,
7615
+ unmeasured
7616
+ };
7617
+ }
7618
+ function formatCalibration(c, notes = []) {
7619
+ const counts = c.labels.map((s) => `${s.pages} ${s.label}`).join(", ");
7620
+ const lines = [`craft calibrate: ${c.pages} pages measured (${counts})`, ...notes];
7621
+ if (!c.typicality) lines.push("No null models: flagged means tell-heavy only.");
7622
+ if (c.unmeasured.length) {
7623
+ lines.push("", `Not measured: ${c.unmeasured.length}. A page not measured never counts as a pass.`);
7624
+ for (const u of c.unmeasured) lines.push(` ${u.id} (${u.label}): ${u.reason}`);
7625
+ }
7626
+ lines.push("", "Targets");
7627
+ for (const t of c.targets) lines.push(` ${t.target.padEnd(50)} ${t.met === null ? "not known" : t.met ? "met" : "NOT MET"}`, ` ${t.detail}`);
7628
+ lines.push("", ` ${"label".padEnd(11)} ${"pages".padStart(5)} ${"flagged".padStart(8)} ${"typical".padStart(8)} ${"tell-heavy".padStart(11)} ${"blocking".padStart(9)}`);
7629
+ for (const s of c.labels) {
7630
+ 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)}`);
7631
+ }
7632
+ lines.push("", "Per tell. A hit on ai or ai-looking counts for the tell; a hit on human counts against it.");
7633
+ lines.push(` ${"tell".padEnd(28)} ${"ai".padStart(7)} ${"ai-looking".padStart(10)} ${"human".padStart(7)} ${"precision".padStart(9)} ${"recall".padStart(7)}`);
7634
+ const of = (l) => c.labels.find((s) => s.label === l)?.pages ?? 0;
7635
+ for (const t of c.tells) {
7636
+ lines.push(
7637
+ ` ${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)}`
7638
+ );
7639
+ }
7640
+ if (!c.tells.length) lines.push(" no tell fired");
7641
+ lines.push("", `Never fired on this set: ${c.silent.length} of ${CATALOGUE.length} catalogue tells.`);
7642
+ return lines.join("\n");
7643
+ }
7644
+
7645
+ // src/estate/index.ts
7646
+ var ESTATE_VERSION = 1;
7647
+ var SIBLING_AT = 0.31;
7648
+ function emptyEstate() {
7649
+ return { version: ESTATE_VERSION, sites: [] };
7650
+ }
7651
+ function addSite(register, site) {
7652
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
7653
+ const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
7654
+ return { version: ESTATE_VERSION, sites };
7655
+ }
7656
+ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
7657
+ const out = [];
7658
+ const close = (k) => d.parts[k] <= SHARED_PART;
7659
+ if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
7660
+ if (close("type")) {
7661
+ if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
7662
+ if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
7663
+ }
7664
+ if (close("ground")) out.push(`${groundName(a.ground)} ground`);
7665
+ if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
7666
+ if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
7667
+ if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
7668
+ if (close("layout")) out.push("running order");
7669
+ return out;
7670
+ }
7671
+ function compareToEstate(target2, register, options = {}) {
7672
+ const at = options.siblingAt ?? SIBLING_AT;
7673
+ return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
7674
+ const d = fingerprintDistance(target2, s.fingerprint);
7675
+ return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
7676
+ }).sort((a, b) => a.distance - b.distance);
7677
+ }
7678
+ function estatePairs(register, siblingAt = SIBLING_AT) {
7679
+ const out = [];
7680
+ const s = register.sites.filter((site) => site.fingerprint);
7681
+ for (let i = 0; i < s.length; i += 1) {
7682
+ for (let j = i + 1; j < s.length; j += 1) {
7683
+ const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
7684
+ 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) });
7685
+ }
7686
+ }
7687
+ return out.sort((x, y) => x.distance - y.distance);
7688
+ }
7689
+ function siblingLine(briefs) {
7690
+ const d = [];
7691
+ for (const runs of briefs) {
7692
+ 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);
7693
+ }
7694
+ if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
7695
+ d.sort((a, b) => a - b);
7696
+ const mid = Math.floor(d.length / 2);
7697
+ const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
7698
+ return Math.round(m * 1e3) / 1e3;
7699
+ }
7700
+
7526
7701
  // src/report/retrofit.ts
7527
7702
  var HEADINGS = {
7528
7703
  direction: "Decide first",
@@ -7604,6 +7779,7 @@ export {
7604
7779
  ESTATE_VERSION,
7605
7780
  EXIT_RATIO,
7606
7781
  FINGERPRINT_VERSION,
7782
+ FLAGGED_TARGET,
7607
7783
  FUNCTION_WORDS,
7608
7784
  GROUND_SCALE,
7609
7785
  GROUND_SHARED,
@@ -7614,8 +7790,11 @@ export {
7614
7790
  HOUSE_BUDGET,
7615
7791
  HOUSE_LOCKED,
7616
7792
  HOUSE_TYPE,
7793
+ HUMAN_TYPICAL_TARGET,
7617
7794
  IMAGERY_TELLS,
7618
7795
  JSON_SCHEMA_VERSION,
7796
+ LABELS,
7797
+ LEXICAL_FEATURES,
7619
7798
  LICENCES,
7620
7799
  LIGHTNESS_CURVE,
7621
7800
  MIN_DECIDED,
@@ -7656,6 +7835,7 @@ export {
7656
7835
  STOCK_FIGURES,
7657
7836
  STOCK_PHOTO_HOSTS,
7658
7837
  STOCK_PHRASES,
7838
+ STUDY_WORDS,
7659
7839
  TELL_HEAVY,
7660
7840
  TRADE_NOUNS,
7661
7841
  TYPE_STEPS,
@@ -7669,6 +7849,7 @@ export {
7669
7849
  applySeverity,
7670
7850
  auditSnapshot,
7671
7851
  briefTerms,
7852
+ calibrate,
7672
7853
  catalogueTable,
7673
7854
  characterReport,
7674
7855
  checkCopy,
@@ -7698,6 +7879,7 @@ export {
7698
7879
  deltaEOkHex,
7699
7880
  densityCss,
7700
7881
  densityTokens,
7882
+ designTells,
7701
7883
  emitCss,
7702
7884
  emptyEstate,
7703
7885
  estateComponentPairs,
@@ -7714,6 +7896,7 @@ export {
7714
7896
  fluidSpace,
7715
7897
  fluidType,
7716
7898
  fontClass,
7899
+ formatCalibration,
7717
7900
  formatClaims,
7718
7901
  formatComparison,
7719
7902
  formatCompetitor,
@@ -7738,8 +7921,10 @@ export {
7738
7921
  isBlurb,
7739
7922
  isCategoryLabel,
7740
7923
  isCream,
7924
+ isFlagged,
7741
7925
  jobWords,
7742
7926
  layoutDistance,
7927
+ lexicalOf,
7743
7928
  licencesJson,
7744
7929
  makeSnapshot,
7745
7930
  motionTokens,
@@ -7806,6 +7991,8 @@ export {
7806
7991
  validateJob,
7807
7992
  visibleText,
7808
7993
  visualMeasures,
7994
+ vocabulary,
7995
+ vocabularyCounts,
7809
7996
  wcagContrast,
7810
7997
  wordListsJson
7811
7998
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",