@domandigital/craft 0.14.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
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACCENT_SCALE: () => ACCENT_SCALE,
24
+ ACCENT_SHARED: () => ACCENT_SHARED,
23
25
  AI_PHRASES: () => AI_PHRASES,
24
26
  AI_SOURCE_TYPES: () => AI_SOURCE_TYPES,
25
27
  AI_VIOLET: () => AI_VIOLET,
@@ -30,10 +32,14 @@ __export(index_exports, {
30
32
  CATALOGUE_VERSION: () => CATALOGUE_VERSION,
31
33
  CHOICE_KEYS: () => CHOICE_KEYS,
32
34
  COMPONENT_ROLES: () => COMPONENT_ROLES,
35
+ CONSIDERATION: () => CONSIDERATION,
36
+ CONVENTION: () => CONVENTION,
37
+ CUSTOMER_KINDS: () => CUSTOMER_KINDS,
33
38
  DEFAULT_IGNORE: () => DEFAULT_IGNORE,
34
39
  DENSITIES: () => DENSITIES,
35
40
  DENSITY_NAMES: () => DENSITY_NAMES,
36
41
  DIRECTION_VERSION: () => DIRECTION_VERSION,
42
+ DIRECTION_VERSIONS: () => DIRECTION_VERSIONS,
37
43
  DISTANCE_WEIGHTS: () => DISTANCE_WEIGHTS,
38
44
  DURATION_MS: () => DURATION_MS,
39
45
  DURATION_S: () => DURATION_S,
@@ -43,22 +49,33 @@ __export(index_exports, {
43
49
  ESTATE_VERSION: () => ESTATE_VERSION,
44
50
  EXIT_RATIO: () => EXIT_RATIO,
45
51
  FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
52
+ FLAGGED_TARGET: () => FLAGGED_TARGET,
53
+ FUNCTION_WORDS: () => FUNCTION_WORDS,
54
+ GROUND_SCALE: () => GROUND_SCALE,
55
+ GROUND_SHARED: () => GROUND_SHARED,
56
+ HIERARCHY_FIELDS: () => HIERARCHY_FIELDS,
46
57
  HOME_FILE: () => HOME_FILE,
47
58
  HOME_URL: () => HOME_URL,
48
59
  HOUSE: () => HOUSE,
49
60
  HOUSE_BUDGET: () => HOUSE_BUDGET,
50
61
  HOUSE_LOCKED: () => HOUSE_LOCKED,
51
62
  HOUSE_TYPE: () => HOUSE_TYPE,
63
+ HUMAN_TYPICAL_TARGET: () => HUMAN_TYPICAL_TARGET,
52
64
  IMAGERY_TELLS: () => IMAGERY_TELLS,
53
65
  JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
66
+ LABELS: () => LABELS,
67
+ LEXICAL_FEATURES: () => LEXICAL_FEATURES,
54
68
  LICENCES: () => LICENCES,
55
69
  LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
56
70
  MIN_DECIDED: () => MIN_DECIDED,
57
71
  MIN_RUNS: () => MIN_RUNS,
72
+ MOOD: () => MOOD,
58
73
  NEGATIVE_REASSURANCE: () => NEGATIVE_REASSURANCE,
59
74
  NULL_VERSION: () => NULL_VERSION,
75
+ PAGE_TYPES: () => PAGE_TYPES,
60
76
  PILL_LIMIT: () => PILL_LIMIT,
61
77
  PLAINER_WORDS: () => PLAINER_WORDS,
78
+ PREFERENCE: () => PREFERENCE,
62
79
  PROMO_BADGE: () => PROMO_BADGE,
63
80
  PROOF_NAMES: () => PROOF_NAMES,
64
81
  PROPOSED: () => PROPOSED,
@@ -72,8 +89,10 @@ __export(index_exports, {
72
89
  REVEAL_LIMIT: () => REVEAL_LIMIT,
73
90
  REVIEW_PHRASES: () => REVIEW_PHRASES,
74
91
  SCALE: () => SCALE,
92
+ SECTION_ROLES: () => SECTION_ROLES,
75
93
  SHADCN_LIMIT: () => SHADCN_LIMIT,
76
94
  SHADCN_PRIMITIVES: () => SHADCN_PRIMITIVES,
95
+ SHARED_PART: () => SHARED_PART,
77
96
  SIBLING_AT: () => SIBLING_AT,
78
97
  SNAPSHOT_VERSION: () => SNAPSHOT_VERSION,
79
98
  SOURCE_KINDS: () => SOURCE_KINDS,
@@ -86,6 +105,7 @@ __export(index_exports, {
86
105
  STOCK_FIGURES: () => STOCK_FIGURES,
87
106
  STOCK_PHOTO_HOSTS: () => STOCK_PHOTO_HOSTS,
88
107
  STOCK_PHRASES: () => STOCK_PHRASES,
108
+ STUDY_WORDS: () => STUDY_WORDS,
89
109
  TELL_HEAVY: () => TELL_HEAVY,
90
110
  TRADE_NOUNS: () => TRADE_NOUNS,
91
111
  TYPE_STEPS: () => TYPE_STEPS,
@@ -99,28 +119,37 @@ __export(index_exports, {
99
119
  applySeverity: () => applySeverity,
100
120
  auditSnapshot: () => auditSnapshot,
101
121
  briefTerms: () => briefTerms,
122
+ calibrate: () => calibrate,
102
123
  catalogueTable: () => catalogueTable,
103
124
  characterReport: () => characterReport,
104
125
  checkCopy: () => checkCopy,
126
+ checkOrder: () => checkOrder,
105
127
  checkPair: () => checkPair,
128
+ checkReason: () => checkReason,
106
129
  checkRestraint: () => checkRestraint,
107
130
  choicesOf: () => choicesOf,
131
+ clauseDepth: () => clauseDepth,
108
132
  closestComponents: () => closestComponents,
109
133
  compareCompetitor: () => compareCompetitor,
134
+ compareDirections: () => compareDirections,
110
135
  compareFacts: () => compareFacts,
136
+ compareToDeclared: () => compareToDeclared,
111
137
  compareToEstate: () => compareToEstate,
112
138
  componentDistance: () => componentDistance,
113
139
  componentShared: () => componentShared,
114
140
  componentTypicalities: () => componentTypicalities,
115
141
  componentTypicality: () => componentTypicality,
116
142
  componentsOf: () => componentsOf,
143
+ contentWords: () => contentWords,
117
144
  countWords: () => countWords2,
118
145
  craftTokens: () => craftTokens,
119
146
  createBaseline: () => createBaseline,
147
+ cv: () => cv,
120
148
  deltaEOk: () => deltaEOk,
121
149
  deltaEOkHex: () => deltaEOkHex,
122
150
  densityCss: () => densityCss,
123
151
  densityTokens: () => densityTokens,
152
+ designTells: () => designTells,
124
153
  emitCss: () => emitCss,
125
154
  emptyEstate: () => emptyEstate,
126
155
  estateComponentPairs: () => estateComponentPairs,
@@ -137,14 +166,18 @@ __export(index_exports, {
137
166
  fluidSpace: () => fluidSpace,
138
167
  fluidType: () => fluidType,
139
168
  fontClass: () => fontClass,
169
+ formatCalibration: () => formatCalibration,
140
170
  formatClaims: () => formatClaims,
141
171
  formatComparison: () => formatComparison,
142
172
  formatCompetitor: () => formatCompetitor,
173
+ formatDirectionMatches: () => formatDirectionMatches,
143
174
  formatHex: () => formatHex,
144
175
  formatReport: () => formatReport,
145
176
  globToRegExp: () => globToRegExp,
146
177
  groundName: () => groundName,
147
178
  harvest: () => harvest,
179
+ hedgeCount: () => hedgeCount,
180
+ hedgedClose: () => hedgedClose,
148
181
  hexToOklch: () => hexToOklch,
149
182
  holdHouseBlocks: () => holdHouseBlocks,
150
183
  houseRule: () => houseRule,
@@ -155,28 +188,39 @@ __export(index_exports, {
155
188
  inSrgbGamut: () => inSrgbGamut,
156
189
  initDirection: () => initDirection,
157
190
  isAiViolet: () => isAiViolet,
191
+ isBlurb: () => isBlurb,
192
+ isCategoryLabel: () => isCategoryLabel,
158
193
  isCream: () => isCream,
194
+ isFlagged: () => isFlagged,
195
+ jobWords: () => jobWords,
159
196
  layoutDistance: () => layoutDistance,
197
+ lexicalOf: () => lexicalOf,
160
198
  licencesJson: () => licencesJson,
161
199
  makeSnapshot: () => makeSnapshot,
162
200
  motionTokens: () => motionTokens,
163
201
  namedSources: () => namedSources,
164
202
  nearest: () => nearest,
203
+ nominalisations: () => nominalisations,
165
204
  normaliseFamily: () => normaliseFamily,
166
205
  nullPrompt: () => nullPrompt,
167
206
  oklchToHex: () => oklchToHex,
168
207
  oklchToRgb: () => oklchToRgb,
169
208
  pageCopy: () => pageCopy,
170
209
  paletteFromPixels: () => paletteFromPixels,
210
+ paragraphUniformity: () => paragraphUniformity,
171
211
  parseBaseline: () => parseBaseline,
172
212
  parseColour: () => parseColour,
173
213
  parseCraftConfig: () => parseCraftConfig,
174
214
  parseHex: () => parseHex,
215
+ participialClauses: () => participialClauses,
216
+ phrasalCoordinations: () => phrasalCoordinations,
175
217
  propose: () => propose,
176
218
  protectedFacts: () => protectedFacts,
177
219
  ramp: () => ramp,
178
220
  rampFromAnchors: () => rampFromAnchors,
179
221
  readSnapshot: () => readSnapshot,
222
+ renderedOrder: () => renderedOrder,
223
+ researchPrompt: () => researchPrompt,
180
224
  retrofitPlan: () => retrofitPlan,
181
225
  runTell: () => runTell,
182
226
  scanProvenance: () => scanProvenance,
@@ -185,26 +229,40 @@ __export(index_exports, {
185
229
  semantic: () => semantic,
186
230
  sentencesOf: () => sentencesOf,
187
231
  severityExceptions: () => severityExceptions,
232
+ shapeMoves: () => shapeMoves,
188
233
  shapeName: () => shapeName,
234
+ shapeOf: () => shapeOf,
235
+ shapeWords: () => words,
189
236
  sharedParts: () => sharedParts,
190
237
  shouldAnimate: () => shouldAnimate,
191
238
  siblingLine: () => siblingLine,
239
+ skeleton: () => skeleton,
240
+ skeletonRepeat: () => skeletonRepeat,
192
241
  specificity: () => specificity,
193
242
  specifics: () => specifics,
243
+ splitSentences: () => splitSentences,
244
+ stackedConditional: () => stackedConditional,
194
245
  stockAvatarSource: () => stockAvatarSource,
195
246
  stockSource: () => stockSource,
247
+ summariseDirection: () => summariseDirection,
196
248
  tailwindV3Preset: () => tailwindV3Preset,
197
249
  tailwindV4Theme: () => tailwindV4Theme,
198
250
  tellById: () => tellById,
199
251
  toGamut: () => toGamut,
200
252
  toJson: () => toJson,
201
253
  toSarif: () => toSarif,
254
+ tradeOf: () => tradeOf,
255
+ tricolons: () => tricolons,
202
256
  typeFeatureTokens: () => typeFeatureTokens,
203
257
  typicality: () => typicality,
204
258
  unprovenClaims: () => unprovenClaims,
205
259
  validateDirection: () => validateDirection,
260
+ validateHierarchy: () => validateHierarchy,
261
+ validateJob: () => validateJob,
206
262
  visibleText: () => visibleText,
207
263
  visualMeasures: () => visualMeasures,
264
+ vocabulary: () => vocabulary,
265
+ vocabularyCounts: () => vocabularyCounts,
208
266
  wcagContrast: () => wcagContrast,
209
267
  wordListsJson: () => wordListsJson
210
268
  });
@@ -702,9 +760,9 @@ function fluidClamp(minPx, maxPx, minViewport, maxViewport, rootPx = 16) {
702
760
  const interceptRem = round((minPx - slope * minViewport) / rootPx);
703
761
  const slopeVw = round(slope * 100);
704
762
  const preferred = interceptRem === 0 ? `${slopeVw}vw` : `${interceptRem}rem + ${slopeVw}vw`;
705
- const lower = Math.min(minRem, maxRem);
763
+ const lower2 = Math.min(minRem, maxRem);
706
764
  const upper = Math.max(minRem, maxRem);
707
- return `clamp(${lower}rem, ${preferred}, ${upper}rem)`;
765
+ return `clamp(${lower2}rem, ${preferred}, ${upper}rem)`;
708
766
  }
709
767
  function fluidType(options = {}) {
710
768
  const {
@@ -969,10 +1027,10 @@ function stripLayers(css, ignored) {
969
1027
  const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
970
1028
  let out = "";
971
1029
  let i = 0;
972
- const pattern = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
1030
+ const pattern2 = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
973
1031
  while (i < css.length) {
974
- pattern.lastIndex = i;
975
- const match = pattern.exec(css);
1032
+ pattern2.lastIndex = i;
1033
+ const match = pattern2.exec(css);
976
1034
  if (match === null) {
977
1035
  out += css.slice(i);
978
1036
  break;
@@ -1002,12 +1060,12 @@ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
1002
1060
  ]);
1003
1061
  function atRuleBodies(css, prelude) {
1004
1062
  const out = [];
1005
- const pattern = new RegExp(
1063
+ const pattern2 = new RegExp(
1006
1064
  prelude.source,
1007
1065
  prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
1008
1066
  );
1009
1067
  let match;
1010
- while ((match = pattern.exec(css)) !== null) {
1068
+ while ((match = pattern2.exec(css)) !== null) {
1011
1069
  const start = match.index + match[0].length;
1012
1070
  let depth = 1;
1013
1071
  let i = start;
@@ -1017,7 +1075,7 @@ function atRuleBodies(css, prelude) {
1017
1075
  i += 1;
1018
1076
  }
1019
1077
  out.push(css.slice(start, i - 1));
1020
- pattern.lastIndex = i;
1078
+ pattern2.lastIndex = i;
1021
1079
  }
1022
1080
  return out;
1023
1081
  }
@@ -1050,9 +1108,9 @@ function isVarOnlyList(value) {
1050
1108
  return parts2.length > 1 && parts2.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
1051
1109
  }
1052
1110
  function distinctValues(css, property, props, unresolved) {
1053
- const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
1111
+ const pattern2 = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
1054
1112
  const seen = /* @__PURE__ */ new Set();
1055
- for (const match of css.matchAll(pattern)) {
1113
+ for (const match of css.matchAll(pattern2)) {
1056
1114
  const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
1057
1115
  if (CSS_WIDE_KEYWORDS.has(raw)) continue;
1058
1116
  const value = unwrapShadowColor(raw);
@@ -1471,14 +1529,14 @@ function linkSpans(text) {
1471
1529
  return [...text.matchAll(LINK)].map((m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]);
1472
1530
  }
1473
1531
  var insideLink = (spans, at) => spans.some(([a, b]) => at >= a && at < b);
1474
- function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => false) {
1532
+ function phrases(patterns2, label, source = (ctx) => ctx.blocks, skip = () => false) {
1475
1533
  return (ctx) => {
1476
1534
  const hits = [];
1477
1535
  for (const block of source(ctx)) {
1478
1536
  const spans = [];
1479
1537
  const links = linkSpans(block.text);
1480
- for (const pattern of patterns) {
1481
- 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`);
1482
1540
  for (const m of block.text.matchAll(global)) {
1483
1541
  if (skip(block, m[0], m.index ?? 0)) continue;
1484
1542
  if (insideLink(links, m.index ?? 0)) continue;
@@ -1498,7 +1556,7 @@ function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => fal
1498
1556
  };
1499
1557
  }
1500
1558
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/'/g, "['\u2019]?").replace(/ /g, "\\s+");
1501
- var wordList = (words2) => new RegExp(`\\b(?:${[...words2].sort((a, b) => b.length - a.length).map(escape).join("|")})\\b`, "i");
1559
+ var wordList = (words3) => new RegExp(`\\b(?:${[...words3].sort((a, b) => b.length - a.length).map(escape).join("|")})\\b`, "i");
1502
1560
  var quoted = (m) => `"${m}"`;
1503
1561
  var AI_WORDS = [
1504
1562
  "tapestry",
@@ -2336,8 +2394,8 @@ function documents(ctx) {
2336
2394
  const docs = [];
2337
2395
  for (const [path, blocks] of byPath) {
2338
2396
  const text = blocks.map((b) => withoutHeadings(b.text)).join("\n");
2339
- const words2 = countWords(text);
2340
- if (words2 >= MIN_WORDS) docs.push({ path, blocks, words: words2 });
2397
+ const words3 = countWords(text);
2398
+ if (words3 >= MIN_WORDS) docs.push({ path, blocks, words: words3 });
2341
2399
  }
2342
2400
  return docs;
2343
2401
  }
@@ -2481,14 +2539,14 @@ function rhythmHits(ctx) {
2481
2539
  const list = sentences(doc2);
2482
2540
  if (list.length < 40) continue;
2483
2541
  const lengths = list.map((s) => countWords(s.text));
2484
- const mean = lengths.reduce((a, b) => a + b, 0) / lengths.length;
2485
- const sd = Math.sqrt(lengths.reduce((a, b) => a + (b - mean) ** 2, 0) / lengths.length);
2486
- const cv = sd / mean;
2487
- if (cv < 0.42) {
2542
+ const mean2 = lengths.reduce((a, b) => a + b, 0) / lengths.length;
2543
+ const sd2 = Math.sqrt(lengths.reduce((a, b) => a + (b - mean2) ** 2, 0) / lengths.length);
2544
+ const cv2 = sd2 / mean2;
2545
+ if (cv2 < 0.42) {
2488
2546
  hits.push({
2489
2547
  path: doc2.path,
2490
2548
  offset: list[0].offset,
2491
- message: `${list.length} sentences averaging ${mean.toFixed(1)} words with little variation (spread ${cv.toFixed(2)}, reads metronomic below 0.42)`
2549
+ message: `${list.length} sentences averaging ${mean2.toFixed(1)} words with little variation (spread ${cv2.toFixed(2)}, reads metronomic below 0.42)`
2492
2550
  });
2493
2551
  }
2494
2552
  }
@@ -2502,19 +2560,19 @@ function repeatHits(ctx) {
2502
2560
  const whole = /* @__PURE__ */ new Map();
2503
2561
  const runs = /* @__PURE__ */ new Map();
2504
2562
  for (const s of sentences(doc2)) {
2505
- const words2 = normal(s.text);
2506
- if (words2.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
2563
+ const words3 = normal(s.text);
2564
+ if (words3.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
2507
2565
  if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
2508
- const key = words2.join(" ");
2566
+ const key = words3.join(" ");
2509
2567
  const earlier = whole.get(key);
2510
2568
  if (earlier) {
2511
- hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${words2.length} words)`, excerpt: s.text.slice(0, 120) });
2569
+ hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${words3.length} words)`, excerpt: s.text.slice(0, 120) });
2512
2570
  continue;
2513
2571
  }
2514
2572
  whole.set(key, s);
2515
2573
  let reported = false;
2516
- for (let i = 0; i + RUN <= words2.length && !reported; i += 1) {
2517
- const run = words2.slice(i, i + RUN).join(" ");
2574
+ for (let i = 0; i + RUN <= words3.length && !reported; i += 1) {
2575
+ const run = words3.slice(i, i + RUN).join(" ");
2518
2576
  const seen = runs.get(run);
2519
2577
  if (seen && seen.key !== key) {
2520
2578
  hits.push({ path: doc2.path, offset: s.offset, message: `shares a ${RUN}-word run with an earlier sentence: "${run}"`, excerpt: s.text.slice(0, 120) });
@@ -2571,11 +2629,11 @@ function headingEchoHits(ctx) {
2571
2629
  const hits = [];
2572
2630
  for (const { file } of longFiles(ctx)) {
2573
2631
  for (const heading of headings(file)) {
2574
- const words2 = normal(heading.text);
2575
- if (words2.length < 4) continue;
2632
+ const words3 = normal(heading.text);
2633
+ if (words3.length < 4) continue;
2576
2634
  const after = file.text.slice(heading.end, heading.end + 600).trimStart().replace(/^\s*(?:<\/?(?:p|div|section)\b[^>]*>\s*)*/i, "");
2577
- const opening = normal(after.split(/\n\s*\n/)[0] ?? "").slice(0, words2.length);
2578
- if (opening.join(" ") === words2.join(" ")) {
2635
+ const opening = normal(after.split(/\n\s*\n/)[0] ?? "").slice(0, words3.length);
2636
+ if (opening.join(" ") === words3.join(" ")) {
2579
2637
  hits.push({ path: file.path, offset: heading.offset, message: `the heading "${heading.text}" is repeated as the first words under it` });
2580
2638
  }
2581
2639
  }
@@ -2583,7 +2641,7 @@ function headingEchoHits(ctx) {
2583
2641
  return hits;
2584
2642
  }
2585
2643
  var FILLER_WORDS = "boiler van kitchen garden studio market street window roof tile paint brush ladder hedge lawn fence gate shop counter oven bread flour kettle chair table shelf lamp cable socket switch meter valve pipe tap sink drain gutter brick mortar timber plank screw bolt hinge lock key door frame glass mirror towel basin morning evening winter summer spring autumn monday tuesday friday weekend hour minute week month year fitted checked cleaned painted mended booked quoted delivered collected measured sanded sealed wired local quiet small large early late busy bright warm dry".split(" ");
2586
- function filler(words2, seed = 1, options = {}) {
2644
+ function filler(words3, seed = 1, options = {}) {
2587
2645
  const { contractions = true, uniform } = options;
2588
2646
  const shape = [5, 17, 9, 24, 6, 13, 31, 8, 11, 4, 20, 15];
2589
2647
  let state = seed;
@@ -2594,7 +2652,7 @@ function filler(words2, seed = 1, options = {}) {
2594
2652
  const out = [];
2595
2653
  let count4 = 0;
2596
2654
  let i = 0;
2597
- while (count4 < words2) {
2655
+ while (count4 < words3) {
2598
2656
  const length = uniform ?? shape[i % shape.length];
2599
2657
  const s = [];
2600
2658
  for (let w = 0; w < length; w += 1) s.push(FILLER_WORDS[next(FILLER_WORDS.length)]);
@@ -2865,11 +2923,11 @@ function baseClass(cls) {
2865
2923
  function bases(unit) {
2866
2924
  return unit.classes.map(baseClass);
2867
2925
  }
2868
- function has(unit, pattern) {
2869
- return bases(unit).some((c) => pattern.test(c));
2926
+ function has(unit, pattern2) {
2927
+ return bases(unit).some((c) => pattern2.test(c));
2870
2928
  }
2871
- function find(unit, pattern) {
2872
- return bases(unit).find((c) => pattern.test(c));
2929
+ function find(unit, pattern2) {
2930
+ return bases(unit).find((c) => pattern2.test(c));
2873
2931
  }
2874
2932
  function arbitrary(cls) {
2875
2933
  const m = cls.match(/\[(.*)\]$/);
@@ -2885,9 +2943,9 @@ function eachUnit(ctx, fn) {
2885
2943
  }
2886
2944
  return hits;
2887
2945
  }
2888
- function firstMatch(text, pattern) {
2889
- const m = pattern.exec(text);
2890
- pattern.lastIndex = 0;
2946
+ function firstMatch(text, pattern2) {
2947
+ const m = pattern2.exec(text);
2948
+ pattern2.lastIndex = 0;
2891
2949
  return m ? m.index : -1;
2892
2950
  }
2893
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-/;
@@ -3331,9 +3389,9 @@ function shadcnCardHits(ctx) {
3331
3389
  }
3332
3390
  function marqueeHits(ctx) {
3333
3391
  const hits = [];
3334
- 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;
3335
3393
  for (const file of ctx.files) {
3336
- const at = firstMatch(file.text, pattern);
3394
+ const at = firstMatch(file.text, pattern2);
3337
3395
  if (at !== -1) hits.push({ path: file.path, offset: at, message: "scrolling marquee" });
3338
3396
  }
3339
3397
  return hits;
@@ -3983,15 +4041,15 @@ var blank = (s) => s.replace(/[^\n]/g, " ");
3983
4041
  function readsAsProse(value) {
3984
4042
  if (value.length < 12 || !/\s/.test(value)) return false;
3985
4043
  if (looksLikeClasses(value) || looksLikeAddress(value)) return false;
3986
- const words2 = withoutMarkup(value);
3987
- if (looksLikeCode(words2) || /[<>]/.test(words2)) return false;
3988
- return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(words2);
4044
+ const words3 = withoutMarkup(value);
4045
+ if (looksLikeCode(words3) || /[<>]/.test(words3)) return false;
4046
+ return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(words3);
3989
4047
  }
3990
4048
  function withoutMarkup(value) {
3991
4049
  return value.replace(/<\/?[a-zA-Z][^<>]*>/g, " ").replace(/\$\{[^}]*\}/g, " ").replace(/\{\{[^}]*\}\}/g, " ").replace(/&(?:#\d+|#x[0-9a-f]+|[a-z]+);/gi, "'");
3992
4050
  }
3993
- function looksLikeCode(words2) {
3994
- return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(words2);
4051
+ function looksLikeCode(words3) {
4052
+ return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(words3);
3995
4053
  }
3996
4054
  var UTILITY = /^(?:flex|grid|block|inline|hidden|contents|relative|absolute|fixed|sticky|static|rounded|border|shadow|underline|italic|uppercase|lowercase|capitalize|truncate|container|group|peer|transition|antialiased|prose|sr-only|outline|ring|grow|shrink|isolate|invisible|visible|collapse)$/;
3997
4055
  function looksLikeClasses(value) {
@@ -4175,8 +4233,8 @@ function protectedFacts(text) {
4175
4233
  const mask = (start, length) => {
4176
4234
  rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
4177
4235
  };
4178
- for (const [kind, pattern] of PATTERNS) {
4179
- for (const m of [...rest.matchAll(pattern)]) {
4236
+ for (const [kind, pattern2] of PATTERNS) {
4237
+ for (const m of [...rest.matchAll(pattern2)]) {
4180
4238
  const value = m[0].replace(/[.,;:]+$/, "");
4181
4239
  facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
4182
4240
  mask(m.index ?? 0, m[0].length);
@@ -4394,8 +4452,8 @@ var termPattern = (term) => new RegExp(`(?<![\\w\xA3$\u20AC])${term.split(/\s+/)
4394
4452
  var TRADE_PATTERNS = TRADE_NOUNS.map((t) => [t, termPattern(t)]);
4395
4453
  var SMALL_WORDS = /* @__PURE__ */ new Set(["a", "an", "the", "and", "or", "of", "in", "on", "at", "to", "for", "by", "with", "from"]);
4396
4454
  function caseless(line) {
4397
- const words2 = line.match(/[A-Za-z][A-Za-z’'-]*/g) ?? [];
4398
- const letters = words2.filter((w) => w.length >= 2);
4455
+ const words3 = line.match(/[A-Za-z][A-Za-z’'-]*/g) ?? [];
4456
+ const letters = words3.filter((w) => w.length >= 2);
4399
4457
  if (letters.length >= 3 && letters.filter((w) => w === w.toUpperCase()).length / letters.length >= 0.6) return "shout";
4400
4458
  const content = letters.filter((w) => !SMALL_WORDS.has(w.toLowerCase()));
4401
4459
  if (content.length >= 3 && content.filter((w) => /^[A-Z]/.test(w)).length / content.length >= 0.8) return "title";
@@ -4405,8 +4463,8 @@ var maskStock = (text) => STOCK_FIGURES.reduce((t, re) => t.replace(re, (m) => "
4405
4463
  function briefTerms(brief) {
4406
4464
  const terms = /* @__PURE__ */ new Set();
4407
4465
  for (const w of brief.match(/[A-Za-z][A-Za-z’'&-]*/g) ?? []) {
4408
- const lower = w.toLowerCase();
4409
- if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(lower) && !NOT_NAMES.has(lower)) terms.add(lower);
4466
+ const lower2 = w.toLowerCase();
4467
+ if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(lower2) && !NOT_NAMES.has(lower2)) terms.add(lower2);
4410
4468
  }
4411
4469
  for (const [term, re] of TRADE_PATTERNS) if (re.test(brief)) terms.add(term);
4412
4470
  for (const f4 of protectedFacts(maskStock(brief))) if (f4.kind !== "name") terms.add(f4.value.toLowerCase());
@@ -4429,12 +4487,12 @@ function specifics(text, brief) {
4429
4487
  for (const acronym of f4.value.match(/\b[A-Z]{2,}\b/g) ?? []) add("name", acronym);
4430
4488
  continue;
4431
4489
  }
4432
- const words2 = f4.value.split(/\s+/);
4490
+ const words3 = f4.value.split(/\s+/);
4433
4491
  const named = (w) => !NOT_NAMES.has(w.toLowerCase()) && w !== "&";
4434
- const first = words2.findIndex(named);
4492
+ const first = words3.findIndex(named);
4435
4493
  if (first === -1) continue;
4436
- const last = words2.length - 1 - [...words2].reverse().findIndex(named);
4437
- add("name", words2.slice(first, last + 1).join(" "));
4494
+ const last = words3.length - 1 - [...words3].reverse().findIndex(named);
4495
+ add("name", words3.slice(first, last + 1).join(" "));
4438
4496
  continue;
4439
4497
  }
4440
4498
  add(f4.kind, f4.value);
@@ -4454,15 +4512,15 @@ function specifics(text, brief) {
4454
4512
  }
4455
4513
  function specificity(text, brief) {
4456
4514
  const list = specifics(text, brief);
4457
- const words2 = countWords2(text);
4515
+ const words3 = countWords2(text);
4458
4516
  const trade = list.filter((s) => s.kind === "trade").length;
4459
4517
  const hard = list.length - trade;
4460
4518
  return {
4461
- words: words2,
4519
+ words: words3,
4462
4520
  specifics: list,
4463
4521
  hard,
4464
4522
  trade,
4465
- perHundred: words2 ? Math.round(list.length / words2 * 1e3) / 10 : 0,
4523
+ perHundred: words3 ? Math.round(list.length / words3 * 1e3) / 10 : 0,
4466
4524
  generic: hard === 0 && trade < 2
4467
4525
  };
4468
4526
  }
@@ -4502,7 +4560,7 @@ function side(page, other, brief) {
4502
4560
  function compareCompetitor(ours, competitor, brief) {
4503
4561
  return { ours: side(ours, competitor, brief), competitor: side(competitor, ours, brief), brief: Boolean(brief) };
4504
4562
  }
4505
- var pctOf = (share) => share === null ? "no sentences" : `${Math.round(share * 100)}%`;
4563
+ var pctOf = (share2) => share2 === null ? "no sentences" : `${Math.round(share2 * 100)}%`;
4506
4564
  function formatCompetitor(result) {
4507
4565
  const row = (label, s) => ` ${label.padEnd(11)} ${s.interchangeable.length} of ${s.sentences} (${pctOf(s.share)}), ${s.perHundred} specifics per 100 words`;
4508
4566
  const lines = [
@@ -4878,11 +4936,11 @@ function reflexFonts(list) {
4878
4936
  for (const font of s.fonts) {
4879
4937
  const name = wanted.get(normaliseFamily(font.family).toLowerCase());
4880
4938
  if (!name || seen.has(name)) continue;
4881
- const share = font.chars / total;
4939
+ const share2 = font.chars / total;
4882
4940
  const displayShare = font.displayChars / display;
4883
- if (share >= 0.15 || displayShare >= 0.3) {
4941
+ if (share2 >= 0.15 || displayShare >= 0.3) {
4884
4942
  seen.add(name);
4885
- 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` : ""}`));
4886
4944
  }
4887
4945
  }
4888
4946
  return hits;
@@ -5171,9 +5229,14 @@ function faceDistance(a, b) {
5171
5229
  if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
5172
5230
  return a.class === b.class ? 0.5 : 1;
5173
5231
  }
5232
+ var ACCENT_SCALE = 0.3;
5233
+ var GROUND_SCALE = 0.15;
5234
+ var SHARED_PART = 0.2;
5235
+ var ACCENT_SHARED = ACCENT_SCALE * SHARED_PART;
5236
+ var GROUND_SHARED = GROUND_SCALE * SHARED_PART;
5174
5237
  function fingerprintDistance(a, b) {
5175
- const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / 0.3) : a.accent || b.accent ? 1 : 0;
5176
- const ground = clamp(deltaEOk(a.ground, b.ground) / 0.15);
5238
+ const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / ACCENT_SCALE) : a.accent || b.accent ? 1 : 0;
5239
+ const ground = clamp(deltaEOk(a.ground, b.ground) / GROUND_SCALE);
5177
5240
  const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
5178
5241
  const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
5179
5242
  const motion = clamp(Math.abs(a.motion - b.motion));
@@ -5308,10 +5371,10 @@ var STOP = new Set(
5308
5371
  function grams(text) {
5309
5372
  const out = /* @__PURE__ */ new Set();
5310
5373
  for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
5311
- const words2 = clause.match(/[a-z][a-z'-]*/g) ?? [];
5374
+ const words3 = clause.match(/[a-z][a-z'-]*/g) ?? [];
5312
5375
  for (let n = 2; n <= 4; n += 1) {
5313
- for (let i = 0; i + n <= words2.length; i += 1) {
5314
- const g = words2.slice(i, i + n);
5376
+ for (let i = 0; i + n <= words3.length; i += 1) {
5377
+ const g = words3.slice(i, i + n);
5315
5378
  if (STOP.has(g[0]) || STOP.has(g[n - 1])) continue;
5316
5379
  if (g.filter((w) => !STOP.has(w)).length < 2) continue;
5317
5380
  out.add(g.join(" "));
@@ -5478,7 +5541,7 @@ function closestComponents(a, b, role) {
5478
5541
  }
5479
5542
  function estateComponentPairs(register, role) {
5480
5543
  const out = [];
5481
- const s = register.sites;
5544
+ const s = register.sites.filter((site) => site.fingerprint);
5482
5545
  for (let i = 0; i < s.length; i += 1) {
5483
5546
  for (let j = i + 1; j < s.length; j += 1) {
5484
5547
  const c = closestComponents(s[i].fingerprint, s[j].fingerprint, role);
@@ -6072,16 +6135,16 @@ var ATTRIBUTION = [
6072
6135
  ];
6073
6136
  var LINK2 = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|\bhttps?:\/\/[^\s<>"')\]]+/g;
6074
6137
  function cleanName(raw) {
6075
- const words2 = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
6076
- while (words2.length && NOT_A_SOURCE.test(words2[0])) words2.shift();
6077
- return words2.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
6138
+ const words3 = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
6139
+ while (words3.length && NOT_A_SOURCE.test(words3[0])) words3.shift();
6140
+ return words3.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
6078
6141
  }
6079
6142
  var IMPERATIVE = /^(?:Add|Use|Check|Make|Get|Set|Include|Create|Update|List|Keep|Ask|Write|Track|Test|Claim|Build|Link|Submit|Verify|Review|Fix|Remove|Upload|Respond|Reply|Choose|Pick|Start|Run|Open|Install|Connect|Enable|Turn|Put|Give|Send|Book|Call|Read|See|Try|Compare|Aim|Target|Avoid|Post|Share|Mark|Tag)$/;
6080
6143
  var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
6081
6144
  function namedSources(sentence) {
6082
6145
  const found = /* @__PURE__ */ new Set();
6083
- ATTRIBUTION.forEach((pattern, i) => {
6084
- for (const m of sentence.matchAll(pattern)) {
6146
+ ATTRIBUTION.forEach((pattern2, i) => {
6147
+ for (const m of sentence.matchAll(pattern2)) {
6085
6148
  const raw = m[1].trim();
6086
6149
  const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
6087
6150
  if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
@@ -6167,6 +6230,290 @@ function formatClaims(report2, label = "craft copy claims") {
6167
6230
  return lines.join("\n");
6168
6231
  }
6169
6232
 
6233
+ // src/character/shape.ts
6234
+ var ABBREVIATIONS = /\b(?:e\.g|i\.e|etc|vs|Mr|Mrs|Ms|Dr|St|No|approx|incl|Ltd|Co|Inc|Jr|Sr)\./g;
6235
+ var DOT = "\0";
6236
+ function splitSentences(text) {
6237
+ const guarded = text.replace(/\s+/g, " ").replace(ABBREVIATIONS, (m) => m.replace(/\./g, DOT)).replace(/(\d)\.(\d)/g, `$1${DOT}$2`);
6238
+ return guarded.split(/(?<=[.!?]["'’”)]*)\s+(?=["'‘“(]?[A-Z0-9£$€])/).map((s) => s.split(DOT).join(".").trim()).filter((s) => words(s).length > 0);
6239
+ }
6240
+ var WORD3 = /[A-Za-z0-9£$€][A-Za-z0-9£$€%'’-]*/g;
6241
+ function words(text) {
6242
+ return text.match(WORD3) ?? [];
6243
+ }
6244
+ var lower = (w) => w.toLowerCase().replace(/’/g, "'");
6245
+ var FUNCTION_WORDS = new Set(
6246
+ "a an the this that these those my your our their his her its we you they i he she it me us them and or but nor so yet for if when while where whereas because although though unless once since until as than of to in on at by with from into onto over under about after before between through during without within across is are was were be been being am do does did done have has had will would can could may might must shall should not no every each all any some more most many much few such own same other only just also very too there here what which who whom whose how why then which's it's you'll we'll you're we're they're don't won't can't isn't aren't".split(" ")
6247
+ );
6248
+ var ITEM = "[A-Za-z0-9\xA3$\u20AC'\u2019-]+(?: [A-Za-z0-9\xA3$\u20AC'\u2019-]+){0,5}";
6249
+ var TRICOLON = new RegExp(`(${ITEM}), (${ITEM}),? (and|or) (${ITEM})(?=[,.;:!?)]|$)`, "g");
6250
+ function tricolons(sentence) {
6251
+ const s = sentence.replace(/\s+/g, " ");
6252
+ const out = [];
6253
+ for (const m of s.matchAll(TRICOLON)) {
6254
+ const [, aSpan, b, , c] = m;
6255
+ const bWords = words(b);
6256
+ const cWords = words(c);
6257
+ if (bWords.length === 0 || cWords.length === 0 || cWords.length > 6) continue;
6258
+ const aWords = words(aSpan);
6259
+ const a = aWords.slice(-Math.max(1, bWords.length));
6260
+ const lead = lower(bWords[0]);
6261
+ const parallel = bWords.length >= 2 && FUNCTION_WORDS.has(lead) && (lower(cWords[0]) === lead || lower(a[0]) === lead);
6262
+ out.push({ text: m[0], parallel });
6263
+ }
6264
+ return out;
6265
+ }
6266
+ var STACKED = /,\s+(?:and|but|so|or)\s+(?:if|when|where|once|unless|provided)\b[^,.;!?]{1,60},/i;
6267
+ function stackedConditional(sentence) {
6268
+ return STACKED.test(sentence);
6269
+ }
6270
+ var SUBORDINATOR = /\b(?:if|when|which|because|so|while|where)\b/gi;
6271
+ function clauseDepth(sentence) {
6272
+ return (sentence.match(/,/g) ?? []).length + (sentence.match(SUBORDINATOR) ?? []).length;
6273
+ }
6274
+ var HEDGES = [
6275
+ "if it's a fit",
6276
+ "if it is a fit",
6277
+ "where needed",
6278
+ "if needed",
6279
+ "as needed",
6280
+ "where appropriate",
6281
+ "if appropriate",
6282
+ "as appropriate",
6283
+ "as required",
6284
+ "where required",
6285
+ "if required",
6286
+ "where possible",
6287
+ "if possible",
6288
+ "where relevant",
6289
+ "if relevant",
6290
+ "depending on",
6291
+ "can help",
6292
+ "tend to",
6293
+ "tends to",
6294
+ "may",
6295
+ "might",
6296
+ "could",
6297
+ "typically",
6298
+ "usually",
6299
+ "generally",
6300
+ "often",
6301
+ "potentially",
6302
+ "likely"
6303
+ ];
6304
+ var HEDGE = new RegExp(`\\b(?:${HEDGES.map((h) => h.replace(/'/g, "['\u2019]").replace(/ /g, "\\s+")).join("|")})\\b`, "gi");
6305
+ var CONDITIONAL_CLOSE = /\b(?:if|unless|where|provided|depending)\b/i;
6306
+ function hedgeCount(text) {
6307
+ return (text.match(HEDGE) ?? []).length;
6308
+ }
6309
+ function hedgedClose(sentences3) {
6310
+ const last = sentences3[sentences3.length - 1];
6311
+ if (!last) return false;
6312
+ HEDGE.lastIndex = 0;
6313
+ return HEDGE.test(last) || CONDITIONAL_CLOSE.test(last);
6314
+ }
6315
+ var NOMINAL = /^[a-z]{3,}(?:tion|sion|ment|ness|ity|ance|ence)s?$/;
6316
+ var NOT_NOMINAL = new Set(
6317
+ [
6318
+ "nation station ration lotion potion portion question moment cement comment element garment segment fragment apartment document ointment",
6319
+ "city pity entity quality sentence science audience silence balance finance distance instance entrance substance romance province"
6320
+ ].join(" ").split(" ").flatMap((w) => [w, `${w}s`])
6321
+ );
6322
+ function nominalisations(text) {
6323
+ return words(text).filter((w) => {
6324
+ const l = lower(w);
6325
+ return NOMINAL.test(l) && !NOT_NOMINAL.has(l);
6326
+ }).length;
6327
+ }
6328
+ var PRONOUN_OPENER = /^["'‘“(]?(?:you|your|we|our)\b/i;
6329
+ var TRANSITION_OPENER = /^["'‘“(]?(?:however|moreover|additionally|furthermore|thus|therefore|consequently|in addition|as a result|ultimately|overall|importantly|notably)\b/i;
6330
+ var IMPERSONAL_OPENER = /^["'‘“(]?(?:it is|it's|it’s|there is|there are|there's|there’s)\b/i;
6331
+ var NOT_PARTICIPLE = new Set(
6332
+ "thing nothing something anything everything bring string spring sting swing king ring sing wing during morning evening ceiling building meeting wedding pudding clothing lightning including".split(" ")
6333
+ );
6334
+ function participialClauses(sentence) {
6335
+ let n = 0;
6336
+ for (const m of sentence.matchAll(/,\s+([A-Za-z]{2,}ing)\b/g)) if (!NOT_PARTICIPLE.has(lower(m[1]))) n += 1;
6337
+ const open = sentence.match(/^["'‘“(]?([A-Z][a-z]{2,}ing)\b[^,]{0,60},/);
6338
+ if (open && !NOT_PARTICIPLE.has(lower(open[1]))) n += 1;
6339
+ return n;
6340
+ }
6341
+ function phrasalCoordinations(sentence) {
6342
+ let n = 0;
6343
+ for (const m of sentence.matchAll(/(^|[^,]\s)([A-Za-z][a-z'’-]{2,}) (?:and|or) ([A-Za-z][a-z'’-]{2,})\b/g)) {
6344
+ const a = lower(m[2]);
6345
+ const b = lower(m[3]);
6346
+ if (FUNCTION_WORDS.has(a) || FUNCTION_WORDS.has(b) || a.endsWith("ly") || b.endsWith("ly")) continue;
6347
+ n += 1;
6348
+ }
6349
+ return n;
6350
+ }
6351
+ function skeleton(sentence) {
6352
+ const out = [];
6353
+ for (const w of words(sentence)) {
6354
+ const l = lower(w);
6355
+ const token = FUNCTION_WORDS.has(l) ? l : "_";
6356
+ if (token === "_" && out[out.length - 1] === "_") continue;
6357
+ out.push(token);
6358
+ }
6359
+ return out;
6360
+ }
6361
+ function skeletonRepeat(sentences3) {
6362
+ const grams2 = /* @__PURE__ */ new Map();
6363
+ let total = 0;
6364
+ for (const s of sentences3) {
6365
+ const sk = skeleton(s);
6366
+ for (let i = 0; i + 4 <= sk.length; i += 1) {
6367
+ const g = sk.slice(i, i + 4).join(" ");
6368
+ grams2.set(g, (grams2.get(g) ?? 0) + 1);
6369
+ total += 1;
6370
+ }
6371
+ }
6372
+ if (total === 0) return 0;
6373
+ let repeated = 0;
6374
+ for (const count4 of grams2.values()) if (count4 > 1) repeated += count4;
6375
+ return repeated / total;
6376
+ }
6377
+ var mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
6378
+ var sd = (xs) => {
6379
+ const m = mean(xs);
6380
+ return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / xs.length);
6381
+ };
6382
+ function cv(xs, min) {
6383
+ if (xs.length < min) return null;
6384
+ const m = mean(xs);
6385
+ return m === 0 ? null : sd(xs) / m;
6386
+ }
6387
+ function shapeOf(text) {
6388
+ const ss = splitSentences(text);
6389
+ const wordCount = words(text).length;
6390
+ const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
6391
+ const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6392
+ const tri = ss.flatMap(tricolons);
6393
+ const commas = (text.match(/,/g) ?? []).length;
6394
+ return {
6395
+ sentences: ss.length,
6396
+ words: wordCount,
6397
+ tricolons: tri.length,
6398
+ parallelTriads: tri.filter((t) => t.parallel).length,
6399
+ stackedConditionals: ss.filter(stackedConditional).length,
6400
+ clauseDepth: ss.length === 0 ? 0 : mean(ss.map(clauseDepth)),
6401
+ sentenceCv: cv(ss.map((s) => words(s).length), 3),
6402
+ hedgeRate: per100(hedgeCount(text)),
6403
+ hedgedClose: hedgedClose(ss),
6404
+ nominalisationRate: per100(nominalisations(text)),
6405
+ pronounOpenerShare: share2(PRONOUN_OPENER),
6406
+ transitionOpenerShare: share2(TRANSITION_OPENER),
6407
+ impersonalOpenerShare: share2(IMPERSONAL_OPENER),
6408
+ commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
6409
+ commasPer100: per100(commas),
6410
+ participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
6411
+ coordinationRate: per100(ss.reduce((s, x) => s + phrasalCoordinations(x), 0)),
6412
+ skeletonRepeat: skeletonRepeat(ss)
6413
+ };
6414
+ }
6415
+ function shapeMoves(f4) {
6416
+ const out = [];
6417
+ if (f4.parallelTriads > 0) out.push("parallel triad");
6418
+ else if (f4.tricolons > 0) out.push("tricolon");
6419
+ if (f4.stackedConditionals > 0) out.push("stacked conditional");
6420
+ if (f4.hedgedClose) out.push("hedged close");
6421
+ if (f4.participialRate > 0) out.push("participial clause");
6422
+ return out;
6423
+ }
6424
+ function paragraphUniformity(paragraphs) {
6425
+ return cv(
6426
+ paragraphs.map((p) => words(p).length).filter((n) => n > 0),
6427
+ 4
6428
+ );
6429
+ }
6430
+ function isBlurb(f4) {
6431
+ return f4.sentences >= 2 && f4.sentences <= 8 && f4.words >= 20 && f4.words <= 160;
6432
+ }
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
+
6170
6517
  // src/character/baseline.ts
6171
6518
  var BASELINE_VERSION = 1;
6172
6519
  function fnv1a(text) {
@@ -6267,23 +6614,23 @@ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
6267
6614
  return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
6268
6615
  }
6269
6616
  function globToRegExp(glob) {
6270
- const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6617
+ const pattern2 = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
6271
6618
  let body = "";
6272
- for (let i = 0; i < pattern.length; i += 1) {
6273
- const c = pattern[i];
6274
- if (c === "*" && pattern[i + 1] === "*") {
6275
- 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] === "/";
6276
6623
  body += slash ? "(?:.*/)?" : ".*";
6277
6624
  i += slash ? 2 : 1;
6278
6625
  } else if (c === "*") body += "[^/]*";
6279
6626
  else if (c === "?") body += "[^/]";
6280
6627
  else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6281
6628
  }
6282
- const anywhere = !pattern.includes("/");
6629
+ const anywhere = !pattern2.includes("/");
6283
6630
  return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
6284
6631
  }
6285
- function ignoreMatcher(patterns) {
6286
- const res = patterns.map(globToRegExp);
6632
+ function ignoreMatcher(patterns2) {
6633
+ const res = patterns2.map(globToRegExp);
6287
6634
  return (path) => {
6288
6635
  const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
6289
6636
  return res.some((re) => re.test(p));
@@ -6450,11 +6797,11 @@ function visualMeasures(img, maxSamples = 1e6) {
6450
6797
  const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
6451
6798
  const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
6452
6799
  const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
6453
- 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;
6454
6801
  return {
6455
- colourfulness: round2(colourfulness, 1),
6456
- edgeDensity: round2(edgeN ? edges / edgeN : 0),
6457
- 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),
6458
6805
  width: w,
6459
6806
  height: h,
6460
6807
  sampled: n
@@ -6462,7 +6809,9 @@ function visualMeasures(img, maxSamples = 1e6) {
6462
6809
  }
6463
6810
 
6464
6811
  // src/direction/types.ts
6465
- var DIRECTION_VERSION = 1;
6812
+ var DIRECTION_VERSION = 2;
6813
+ var DIRECTION_VERSIONS = [1, 2];
6814
+ var CUSTOMER_KINDS = ["review", "enquiry", "conversation", "search"];
6466
6815
  var SOURCE_KINDS = [
6467
6816
  "livery",
6468
6817
  "shopfront",
@@ -6476,9 +6825,413 @@ var SOURCE_KINDS = [
6476
6825
  "trade",
6477
6826
  "print",
6478
6827
  "photo",
6479
- "founder"
6828
+ "founder",
6829
+ ...CUSTOMER_KINDS,
6830
+ "reference"
6480
6831
  ];
6481
6832
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
6833
+ var CONSIDERATION = ["low", "considered", "high"];
6834
+ var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
6835
+ var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
6836
+
6837
+ // src/direction/reason.ts
6838
+ var PROPOSED = "PROPOSED:";
6839
+ var PREFERENCE = /\b(?:client (?:likes|liked|wants|wanted|asked|chose|prefers?)|(?:we|they|i) (?:like|liked|love|loved|prefer)|on[- ]trend|trendy|popular|brand (?:colou?r|font|guidelines?) (?:is|are|says?))\b/i;
6840
+ 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;
6841
+ var CONVENTION = new RegExp(
6842
+ [
6843
+ // authority
6844
+ String.raw`industry[- ]standard`,
6845
+ String.raw`best[- ]practices?`,
6846
+ String.raw`the norm\b`,
6847
+ String.raw`standard (?:layout|structure|practice|pattern|format)`,
6848
+ String.raw`tried[- ]and[- ]tested`,
6849
+ String.raw`proven (?:layout|structure|pattern|format)`,
6850
+ // the crowd
6851
+ String.raw`(?:most|other|typical|many|all|similar)\s+(?:\w+\s+){0,2}(?:web)?sites\b`,
6852
+ String.raw`competitors?(?:'|’)?s?\b`,
6853
+ String.raw`(?:like|as) (?:everyone|everybody|all the others|the rest)\b`,
6854
+ String.raw`commonly used`,
6855
+ String.raw`(?:what|how) (?:everyone|others|the industry) (?:does|do|uses?)`,
6856
+ // familiarity
6857
+ String.raw`familiar (?:layout|structure|pattern|format|order|shape|design)`,
6858
+ String.raw`recogni[sz]able (?:layout|structure|pattern|format|order)`,
6859
+ String.raw`conventional(?:ly)?\b`,
6860
+ String.raw`(?:visitors|users|people|customers|anyone) (?:are|is) used to`,
6861
+ String.raw`feels? (?:at home|comfortable|familiar)(?: with)?(?: the)? (?:layout|site|page|structure|design)?`,
6862
+ String.raw`intuitive(?:ly)?\b`,
6863
+ String.raw`as usual\b`,
6864
+ String.raw`what (?:visitors|users|people) expect\b`
6865
+ ].join("|"),
6866
+ "i"
6867
+ );
6868
+ var GENERIC = new Set(
6869
+ "customer customers visitor visitors client clients people person someone business businesses service services website site page pages want wants need needs help first before after know feel time work good right sure make find look looking trust reason reasons thing things just really very able also".split(" ")
6870
+ );
6871
+ var STOP2 = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every", "your", "them", "then", "than", "will", "would", "could", "should", "because", "so", "the", "and"]);
6872
+ var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
6873
+ function contentWords(text) {
6874
+ return new Set(
6875
+ (text.toLowerCase().match(/[a-z][a-z'’-]{3,}/g) ?? []).map((w) => w.replace(/['’]s$/, "").replace(/['’]/g, "")).filter((w) => !STOP2.has(w) && !VALUE_WORDS.has(w) && !GENERIC.has(w))
6876
+ );
6877
+ }
6878
+ var stem = (w) => w.replace(/(?:ing|ers|er|ed|es|s)$/, "");
6879
+ function shares(a, b) {
6880
+ const bs = new Set([...b].map(stem));
6881
+ return [...a].filter((w) => bs.has(stem(w)));
6882
+ }
6883
+ function jobWords(job) {
6884
+ return contentWords(
6885
+ [
6886
+ job.statement?.verb,
6887
+ job.statement?.object,
6888
+ job.statement?.context,
6889
+ job.functional?.value,
6890
+ job.emotional?.value,
6891
+ job.social?.value,
6892
+ ...Array.isArray(job.objections) ? job.objections : [],
6893
+ ...Array.isArray(job.language) ? job.language : []
6894
+ ].filter((x) => typeof x === "string").join(" ")
6895
+ );
6896
+ }
6897
+ var isCustomerVoice = (s) => Boolean(s && CUSTOMER_KINDS.includes(s.kind));
6898
+ function checkReason(input) {
6899
+ const problems = [];
6900
+ const err = (field, message) => problems.push({ severity: "error", at: `${input.at}.${field}`, message });
6901
+ const because = typeof input.because === "string" ? input.because.trim() : "";
6902
+ const evidence = Array.isArray(input.evidence) ? input.evidence.filter((e) => typeof e === "string") : [];
6903
+ if (input.evidence !== void 0 && (!Array.isArray(input.evidence) || input.evidence.some((e) => typeof e !== "string"))) err("evidence", "evidence must be a list of source ids");
6904
+ const ids = new Map(input.sources.filter((s) => s && typeof s.id === "string").map((s) => [s.id, s]));
6905
+ if (because.split(/\s+/).filter(Boolean).length < 8) {
6906
+ err("because", "a reason is at least a sentence. Without one this is a default, however good it looks.");
6907
+ return problems;
6908
+ }
6909
+ if (because.startsWith(PROPOSED)) {
6910
+ err("because", "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
6911
+ return problems;
6912
+ }
6913
+ if (PREFERENCE.test(because)) err("because", "that is a preference, not a reason. Say what in the client's world this comes from.");
6914
+ const moods = because.match(MOOD) ?? [];
6915
+ if (moods.length >= 2) err("because", `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
6916
+ const convention = because.match(CONVENTION);
6917
+ if (convention) {
6918
+ err("because", `"${convention[0]}" is the category's default speaking, not a reason. Say what in this client's job or world it answers.`);
6919
+ }
6920
+ const needsEvidence = input.tie !== "job";
6921
+ if (needsEvidence && evidence.length === 0) err("evidence", "cite at least one source");
6922
+ for (const id of evidence) if (!ids.has(id)) err("evidence", `no source called "${id}"`);
6923
+ if (input.tie === "customer") {
6924
+ for (const id of evidence) {
6925
+ if (ids.has(id) && !isCustomerVoice(ids.get(id))) err("evidence", `"${id}" is a ${ids.get(id).kind}. The job is evidenced by the customer's own words: ${CUSTOMER_KINDS.join(", ")}.`);
6926
+ }
6927
+ }
6928
+ if (input.tie === "source") {
6929
+ for (const id of evidence) {
6930
+ if (isCustomerVoice(ids.get(id))) err("evidence", `"${id}" is customer voice. Token decisions need a physical source or an outside-category reference.`);
6931
+ }
6932
+ }
6933
+ if (problems.length) return problems;
6934
+ const words3 = contentWords(because);
6935
+ if (input.tie === "job") {
6936
+ if (!input.job) {
6937
+ err("because", "a structural reason is checked against the job, and the file has no job map yet");
6938
+ } else if (shares(words3, jobWords(input.job)).length === 0) {
6939
+ err("because", "the reason does not use this client's job (its statement, objections or the customer's words). A reason that would fit any client's site is a default.");
6940
+ }
6941
+ } else {
6942
+ const cited = evidence.map((id) => ids.get(id));
6943
+ const tied = cited.some((s) => words3.has(s.id) || shares(words3, contentWords(`${s.note} ${s.lettering ?? ""}`)).length > 0);
6944
+ if (!tied) err("because", `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
6945
+ }
6946
+ return problems;
6947
+ }
6948
+
6949
+ // src/direction/job.ts
6950
+ var JOB_VERBS = new Set(
6951
+ "find choose pick book get fix stop avoid know feel understand compare decide keep protect prepare plan sort arrange replace repair learn prove show recover reduce save spend hire check see buy sell move start finish handle manage meet reach return treat look end make give take bring hand leave trust rely feel prove explain secure settle solve restore improve change become stay pass sign agree ask call reassure justify confirm".split(" ")
6952
+ );
6953
+ var LABEL_WORDS = /* @__PURE__ */ new Set(["website", "websites", "site", "sites", "page", "pages", "homepage", "web", "online", "presence", "company", "business", "services", "service", "local", "best", "good", "an", "a", "the", "for", "uk"]);
6954
+ var NOUNISH = /(?:tion|sion|ment|ness|ity|site|page)$/;
6955
+ var words2 = (s) => s.toLowerCase().match(/[a-z][a-z'’-]*/g) ?? [];
6956
+ function isCategoryLabel(object, brief = "") {
6957
+ const trade = /* @__PURE__ */ new Set([...TRADE_NOUNS.flatMap((t) => t.split(" ")), ...words2(brief).filter((w) => w.length > 3)]);
6958
+ const ws = words2(object);
6959
+ return ws.length > 0 && ws.every((w) => LABEL_WORDS.has(w) || trade.has(w) || trade.has(w.replace(/s$/, "")));
6960
+ }
6961
+ var normalise2 = (s) => s.toLowerCase().replace(/[“”"‘’'.,!?;:()-]/g, " ").replace(/\s+/g, " ").trim();
6962
+ function validateJob(job, sources, ctx = {}) {
6963
+ const problems = [];
6964
+ const err = (at, message) => problems.push({ severity: "error", at: `job${at}`, message });
6965
+ const warn = (at, message) => problems.push({ severity: "warn", at: `job${at}`, message });
6966
+ if (!job || typeof job !== "object" || Array.isArray(job)) {
6967
+ err("", "no job map: say what the visitor is trying to get done, in their words, before deciding structure or look");
6968
+ return { problems, decided: false };
6969
+ }
6970
+ const j = job;
6971
+ const ids = new Map(sources.map((s) => [s.id, s]));
6972
+ const st = j.statement;
6973
+ if (!st || typeof st !== "object") err(".statement", "a job statement is verb, object and context");
6974
+ else {
6975
+ const verb = typeof st.verb === "string" ? st.verb.trim().toLowerCase() : "";
6976
+ if (!verb) err(".statement.verb", "what the visitor is trying to do, as a verb: find, choose, book, fix");
6977
+ else if (verb.split(/\s+/).length > 3 || NOUNISH.test(verb) || TRADE_NOUNS.includes(verb)) err(".statement.verb", `"${st.verb}" is not a verb. A job starts with what the visitor is trying to do.`);
6978
+ else if (!JOB_VERBS.has(verb.split(/\s+/)[0])) warn(".statement.verb", `"${st.verb}" is not a verb craft knows. Check it says what the visitor does, not what the site is.`);
6979
+ const object = typeof st.object === "string" ? st.object.trim() : "";
6980
+ if (words2(object).length < 3) err(".statement.object", "the object needs a few words: what they are trying to get, and what makes it hard");
6981
+ else if (isCategoryLabel(object, ctx.brief)) err(".statement.object", `"${object}" names the category, not the job. Say what the visitor needs from it: "an electrician they can trust with a dangerous fault".`);
6982
+ if (typeof st.context !== "string" || words2(st.context).length < 3) err(".statement.context", "the context: when, under what pressure, or what they are trying to avoid");
6983
+ }
6984
+ for (const part of ["functional", "emotional", "social"]) {
6985
+ const p = j[part];
6986
+ if (!p || typeof p !== "object") {
6987
+ err(`.${part}`, `the ${part} side of the job, with the customer-voice source that shows it`);
6988
+ continue;
6989
+ }
6990
+ if (typeof p.value !== "string" || words2(p.value).length < 6) err(`.${part}.value`, "a sentence: what they need, in the terms they would use");
6991
+ else problems.push(...checkReason({ at: `job.${part}`, because: p.value, evidence: p.evidence, sources, tie: "customer" }).map((x) => ({ ...x, at: x.at.replace(/\.because$/, ".value") })));
6992
+ }
6993
+ const c = j.consideration;
6994
+ if (!c || typeof c !== "object") err(".consideration", `how long and how carefully they decide: ${CONSIDERATION.join(", ")}`);
6995
+ else {
6996
+ if (!CONSIDERATION.includes(c.value)) err(".consideration.value", `one of ${CONSIDERATION.join(", ")}`);
6997
+ problems.push(...checkReason({ at: "job.consideration", because: c.because, evidence: c.evidence, sources, tie: "customer" }));
6998
+ }
6999
+ const objections = Array.isArray(j.objections) ? j.objections : [];
7000
+ if (objections.length === 0) err(".objections", "what stops them: the worries they bring to the page");
7001
+ objections.forEach((o, i) => {
7002
+ if (typeof o !== "string" || words2(o).length < 3) err(`.objections[${i}]`, "an objection is a few words: what they worry about");
7003
+ });
7004
+ const evidence = Array.isArray(j.evidence) ? j.evidence : [];
7005
+ const voice = evidence.map((id) => ids.get(id)).filter(isCustomerVoice);
7006
+ if (evidence.length === 0 || voice.length === 0) err(".evidence", "cite the reviews, enquiries or conversations the job comes from");
7007
+ for (const id of evidence) if (!ids.has(id)) err(".evidence", `no source called "${id}"`);
7008
+ const language = Array.isArray(j.language) ? j.language : [];
7009
+ if (language.length === 0) err(".language", "quote the customer: the words they use for the problem");
7010
+ const texts = voice.map((s) => normalise2(`${s.note} ${ctx.readSource?.(s) ?? ""}`));
7011
+ language.forEach((q, i) => {
7012
+ if (typeof q !== "string" || !q.trim()) return err(`.language[${i}]`, "an empty quote");
7013
+ const n = normalise2(q);
7014
+ if (!texts.some((t) => t.includes(n))) warn(`.language[${i}]`, `"${q}" is not in any cited source's note or file. Quote it from the source, or it may be invented.`);
7015
+ });
7016
+ return { problems, decided: problems.every((p) => p.severity !== "error") };
7017
+ }
7018
+
7019
+ // src/direction/hierarchy.ts
7020
+ var SECTION_ROLES = [
7021
+ "hero",
7022
+ "logos",
7023
+ "stats",
7024
+ "cards",
7025
+ "marquee",
7026
+ "text",
7027
+ "other",
7028
+ "cta-band",
7029
+ "footer-cta",
7030
+ "pricing",
7031
+ "testimonials",
7032
+ "faq",
7033
+ "process",
7034
+ "features",
7035
+ "team",
7036
+ "contact"
7037
+ ];
7038
+ var POSITIONS = /* @__PURE__ */ new Set([...SECTION_ROLES, "header", "sticky"]);
7039
+ function considerationTied(because, job) {
7040
+ const text = because.toLowerCase();
7041
+ const level = job.consideration?.value;
7042
+ if (level && new RegExp(`\\b${level}\\b`).test(text)) return true;
7043
+ const said = contentWords(because);
7044
+ return (Array.isArray(job.objections) ? job.objections : []).some((o) => typeof o === "string" && [...contentWords(o)].some((w) => said.has(w)));
7045
+ }
7046
+ function validateHierarchy(hierarchy, job, sources) {
7047
+ const problems = [];
7048
+ const err = (at, message) => problems.push({ severity: "error", at: `hierarchy${at}`, message });
7049
+ const pages = [];
7050
+ if (!hierarchy || typeof hierarchy !== "object" || Array.isArray(hierarchy)) {
7051
+ err("", "no hierarchy: decide what the home page leads with, where it asks, and the order, each from the job");
7052
+ return { problems, pages };
7053
+ }
7054
+ const h = hierarchy;
7055
+ for (const key of Object.keys(h)) if (!PAGE_TYPES.includes(key)) err(`.${key}`, `not a page type craft knows. Page types are: ${PAGE_TYPES.join(", ")}`);
7056
+ if (!h.home) err(".home", "no home page hierarchy: the home page is where most visitors start");
7057
+ for (const page of PAGE_TYPES) {
7058
+ const p = h[page];
7059
+ if (!p) continue;
7060
+ let decided = 0;
7061
+ const at = (f4) => `hierarchy.${page}.${f4}`;
7062
+ const reason = (where, because, evidence) => checkReason({ at: where, because, evidence, sources, tie: "job", job });
7063
+ const count4 = (found) => {
7064
+ problems.push(...found);
7065
+ if (!found.some((x) => x.severity === "error")) decided += 1;
7066
+ };
7067
+ if (!p.primaryAction) err(`.${page}.primaryAction`, "not decided yet");
7068
+ else {
7069
+ const a = p.primaryAction;
7070
+ const found = [];
7071
+ if (typeof a.value !== "string" || !a.value.trim()) found.push({ severity: "error", at: `${at("primaryAction")}.value`, message: "what the visitor is asked to do" });
7072
+ const positions = Array.isArray(a.positions) ? a.positions : [];
7073
+ if (positions.length === 0) found.push({ severity: "error", at: `${at("primaryAction")}.positions`, message: 'where the ask sits: one place, or several ("hero", "footer-cta")' });
7074
+ for (const pos of positions) if (!POSITIONS.has(pos)) found.push({ severity: "error", at: `${at("primaryAction")}.positions`, message: `"${pos}" is not a section role, "header" or "sticky"` });
7075
+ const r = reason(at("primaryAction"), a.because, a.evidence);
7076
+ found.push(...r);
7077
+ if (r.length === 0 && job && typeof a.because === "string" && !considerationTied(a.because, job)) {
7078
+ found.push({
7079
+ severity: "error",
7080
+ at: `${at("primaryAction")}.because`,
7081
+ message: `say how the job's consideration (${job.consideration?.value ?? "not set"}) or one of its objections leads to asking here. Placement evidence is mixed, so the reason is what decides it.`
7082
+ });
7083
+ }
7084
+ count4(found);
7085
+ }
7086
+ const order = Array.isArray(p.order) ? p.order : [];
7087
+ if (order.length < 2) err(`.${page}.order`, "the running order, two sections or more, each with why it sits there");
7088
+ else {
7089
+ const found = [];
7090
+ order.forEach((o, i) => {
7091
+ if (!o || !SECTION_ROLES.includes(o.role)) found.push({ severity: "error", at: `${at("order")}[${i}].role`, message: `a section role: ${SECTION_ROLES.join(", ")}` });
7092
+ found.push(...reason(`${at("order")}[${i}]`, o?.because));
7093
+ });
7094
+ count4(found);
7095
+ }
7096
+ if (!p.lead) err(`.${page}.lead`, "not decided yet");
7097
+ else {
7098
+ const found = [];
7099
+ if (typeof p.lead.value !== "string" || !p.lead.value.trim()) found.push({ severity: "error", at: `${at("lead")}.value`, message: "what the page leads with" });
7100
+ found.push(...reason(at("lead"), p.lead.because));
7101
+ count4(found);
7102
+ }
7103
+ const journey = Array.isArray(p.journey) ? p.journey : [];
7104
+ if (journey.length < 2) err(`.${page}.journey`, "the stages the visitor goes through, two or more, each with why");
7105
+ else {
7106
+ const found = [];
7107
+ journey.forEach((s, i) => {
7108
+ if (!s || typeof s.stage !== "string" || !s.stage.trim()) found.push({ severity: "error", at: `${at("journey")}[${i}].stage`, message: "name the stage" });
7109
+ found.push(...reason(`${at("journey")}[${i}]`, s?.because));
7110
+ });
7111
+ count4(found);
7112
+ }
7113
+ pages.push({ page, decided, total: HIERARCHY_FIELDS.length });
7114
+ }
7115
+ return { problems, pages };
7116
+ }
7117
+ function renderedOrder(snapshot) {
7118
+ const sections = snapshot.sections ?? [];
7119
+ if (sections.length === 0 || !sections.every((s) => s.role)) return null;
7120
+ return sections.map((s) => s.role).filter((r) => r !== "other");
7121
+ }
7122
+ function checkOrder(declared, page, snapshot, strict = false) {
7123
+ if (!declared || !Array.isArray(declared.order)) return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}`, message: "no declared hierarchy for this page: its running order cannot be checked" }];
7124
+ const rendered = renderedOrder(snapshot);
7125
+ if (!rendered) return [{ severity: "warn", at: `hierarchy.${page}.order`, message: "the snapshot has no section roles (version 1): take a fresh one to check the order" }];
7126
+ const said = declared.order.map((o) => o.role).filter((r) => r !== "other");
7127
+ if (said.join(",") === rendered.join(",")) return [];
7128
+ return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
7129
+ }
7130
+
7131
+ // src/direction/research.ts
7132
+ function tradeOf(brief) {
7133
+ const text = brief.toLowerCase();
7134
+ return TRADE_NOUNS.filter((t) => new RegExp(`\\b${t.replace(/ /g, "\\s+")}s?\\b`).test(text));
7135
+ }
7136
+ function categoryMatchesBrief(category, brief) {
7137
+ const known = tradeOf(category);
7138
+ if (tradeOf(brief).some((t) => known.includes(t))) return true;
7139
+ const label = category.trim().replace(/^(?:(?:a|an|the|local|independent)\s+)+/i, "");
7140
+ if (!label) return false;
7141
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
7142
+ return new RegExp(`\\b${escaped}s?\\b`, "i").test(brief);
7143
+ }
7144
+ function researchPrompt(input) {
7145
+ const trade = tradeOf(input.brief);
7146
+ const category = trade.length ? trade.join(", ") : "this kind of business";
7147
+ const who = input.client ? `${input.client}: ${input.brief}` : input.brief;
7148
+ const lines = [
7149
+ "# Research brief: the customer's job",
7150
+ "",
7151
+ `The business: ${who}`,
7152
+ "",
7153
+ "Find out about the people who hire a business like this, not about the businesses. Answer each question with sources: reviews, forum threads, Q&A sites, complaints, search suggestions. Quote people's own words and link to where they said them.",
7154
+ "",
7155
+ "1. **The situation.** What has just happened, or is about to, when someone starts looking? What pressure are they under: time, money, safety, someone else waiting on them?",
7156
+ '2. **The job.** What are they trying to get done, in one sentence that starts with a verb? What would "done" look like to them?',
7157
+ "3. **Objections.** What do they worry about before getting in touch? What has gone wrong for them, or people they know, before?",
7158
+ "4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
7159
+ "5. **Who else is involved.** Who do they have to explain or justify the choice to (a partner, a landlord, a tenant, a manager), and what would make them look good or bad to that person?",
7160
+ "6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
7161
+ "7. **Where they look.** What do they search for, and where do they ask?",
7162
+ "",
7163
+ "## Do not",
7164
+ "",
7165
+ `- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
7166
+ '- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
7167
+ "- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
7168
+ "- Do not invent quotes. If you cannot find a source for something, say so."
7169
+ ];
7170
+ if (input.visual) {
7171
+ lines.push(
7172
+ "",
7173
+ "# Visual references, from outside the category",
7174
+ "",
7175
+ `Separately, and only from outside ${category}: find things this business already resembles or shares a world with. Print, signage, packaging, tools, vehicles, uniforms, places, materials, other trades and crafts. For each, say what it is, where it is from, and what about it could carry over.`,
7176
+ "",
7177
+ `Nothing from a ${category} website, and nothing from a web design gallery.`
7178
+ );
7179
+ }
7180
+ return `${lines.join("\n")}
7181
+ `;
7182
+ }
7183
+
7184
+ // src/estate/direction.ts
7185
+ function summariseDirection(d) {
7186
+ const c = d.choices ?? {};
7187
+ const home = d.hierarchy?.home;
7188
+ const s = {};
7189
+ if (typeof c.accent?.value === "string" && c.accent.value) s.accent = c.accent.value;
7190
+ if (typeof c.ground?.value === "string" && c.ground.value) s.ground = c.ground.value;
7191
+ if (typeof c.display?.value === "string" && c.display.value) s.display = normaliseFamily(c.display.value);
7192
+ if (typeof c.body?.value === "string" && c.body.value) s.body = normaliseFamily(c.body.value);
7193
+ if (typeof c.shape?.value === "string" && c.shape.value) s.shape = c.shape.value;
7194
+ if (Array.isArray(home?.order) && home.order.length) s.order = home.order.filter((o) => o && typeof o.role === "string").map((o) => o.role);
7195
+ if (typeof home?.primaryAction?.value === "string" && home.primaryAction.value) {
7196
+ s.primaryAction = home.primaryAction.value;
7197
+ s.positions = Array.isArray(home.primaryAction.positions) ? [...home.primaryAction.positions] : [];
7198
+ }
7199
+ return s;
7200
+ }
7201
+ var delta = (a, b) => {
7202
+ const x = a ? parseColour(a) : null;
7203
+ const y = b ? parseColour(b) : null;
7204
+ return x && y ? deltaEOk(x.oklch, y.oklch) : null;
7205
+ };
7206
+ var same = (a, b) => Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase());
7207
+ function compareDirections(a, b) {
7208
+ const shared = [];
7209
+ const accentDelta = delta(a.accent, b.accent);
7210
+ const groundDelta = delta(a.ground, b.ground);
7211
+ if (accentDelta !== null && accentDelta <= ACCENT_SHARED) shared.push(`accent (\u0394E ${accentDelta.toFixed(3)})`);
7212
+ if (groundDelta !== null && groundDelta <= GROUND_SHARED) shared.push(`ground (\u0394E ${groundDelta.toFixed(3)})`);
7213
+ if (a.display && b.display && same(normaliseFamily(a.display), normaliseFamily(b.display))) shared.push(`${normaliseFamily(a.display)} headline`);
7214
+ if (a.body && b.body && same(normaliseFamily(a.body), normaliseFamily(b.body))) shared.push(`${normaliseFamily(a.body)} body`);
7215
+ if (same(a.shape, b.shape)) shared.push(`shape "${a.shape}"`);
7216
+ let orderDistance = null;
7217
+ if (a.order?.length && b.order?.length) {
7218
+ orderDistance = levenshtein(a.order, b.order) / Math.max(a.order.length, b.order.length);
7219
+ if (orderDistance <= SHARED_PART) shared.push(orderDistance === 0 ? "the same section order" : "nearly the same section order");
7220
+ }
7221
+ if (same(a.primaryAction, b.primaryAction) && [...a.positions ?? []].sort().join(",") === [...b.positions ?? []].sort().join(",")) shared.push(`"${a.primaryAction}" asked in the same places`);
7222
+ const surface = shared.filter((s) => /^(?:accent|ground|shape)|headline$/.test(s)).length;
7223
+ const flagged = surface >= 2 || orderDistance !== null && orderDistance <= SHARED_PART;
7224
+ return { shared, accentDelta, groundDelta, orderDistance, flagged };
7225
+ }
7226
+ function compareToDeclared(target2, sites, exclude) {
7227
+ return sites.filter((s) => s.direction && s.id !== exclude).map((s) => ({ id: s.id, ...compareDirections(target2, s.direction) })).sort((x, y) => y.shared.length - x.shared.length || (x.accentDelta ?? 1) - (y.accentDelta ?? 1));
7228
+ }
7229
+ function formatDirectionMatches(label, matches) {
7230
+ const lines = [`${label}: declared directions in the estate (ranked, not judged: no sibling line yet)`];
7231
+ for (const m of matches) lines.push(` ${m.id.padEnd(20)} ${m.flagged ? "LOOK " : " "}${m.shared.length ? `shares ${m.shared.join(", ")}` : "nothing shared"}`);
7232
+ if (matches.length === 0) lines.push(" no site in the register has a declared direction yet: craft estate add --direction <file>");
7233
+ return lines.join("\n");
7234
+ }
6482
7235
 
6483
7236
  // src/direction/licences.ts
6484
7237
  var LICENCES = [
@@ -6592,13 +7345,6 @@ function licencesJson() {
6592
7345
  }
6593
7346
 
6594
7347
  // src/direction/validate.ts
6595
- 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;
6596
- 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;
6597
- var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
6598
- var STOP2 = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every"]);
6599
- function words(text) {
6600
- return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !STOP2.has(w) && !VALUE_WORDS.has(w)));
6601
- }
6602
7348
  function tellFor(key, value) {
6603
7349
  if (key === "accent") {
6604
7350
  const c = parseColour(value);
@@ -6615,44 +7361,58 @@ function tellFor(key, value) {
6615
7361
  }
6616
7362
  return null;
6617
7363
  }
7364
+ var MIN_TOKENS = 5;
7365
+ var emptyLayers = () => ({ job: false, hierarchy: [], tokens: { decided: 0, total: CHOICE_KEYS.length }, complete: false });
6618
7366
  function validateDirection(input, ctx = {}) {
6619
7367
  const problems = [];
6620
7368
  const err = (at, message) => problems.push({ severity: "error", at, message });
6621
7369
  const warn = (at, message) => problems.push({ severity: "warn", at, message });
6622
7370
  if (!input || typeof input !== "object" || Array.isArray(input)) {
6623
7371
  err("", "art-direction.json must be a JSON object");
6624
- return { valid: false, problems, decided: 0 };
7372
+ return { valid: false, problems, decided: 0, layers: emptyLayers() };
6625
7373
  }
6626
7374
  const d = input;
6627
7375
  const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
6628
7376
  if (legacy) {
6629
7377
  warn("", "exceptions only: run craft direction init to record the choices and their reasons");
6630
- return { valid: true, problems, decided: 0 };
7378
+ return { valid: true, problems, decided: 0, layers: emptyLayers() };
6631
7379
  }
6632
- if (d.version !== DIRECTION_VERSION) err("version", `version must be ${DIRECTION_VERSION}`);
7380
+ if (!DIRECTION_VERSIONS.includes(d.version)) err("version", `version must be ${DIRECTION_VERSIONS.join(" or ")}`);
7381
+ else if (d.version === 1) warn("version", "version 1: the job map and the hierarchy are not recorded, so the site cannot be decided. Move to version 2 (craft direction init --out) and add them.");
6633
7382
  if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
6634
7383
  if (typeof d.brief !== "string" || d.brief.trim().split(/\s+/).length < 8) err("brief", "the brief needs a sentence: who, where, and what they do");
6635
- const sources = Array.isArray(d.sources) ? d.sources : [];
7384
+ const rawSources = Array.isArray(d.sources) ? d.sources : [];
7385
+ const sources = rawSources.filter((s) => Boolean(s && typeof s === "object" && !Array.isArray(s)));
6636
7386
  if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
6637
7387
  if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
6638
7388
  const ids = /* @__PURE__ */ new Set();
6639
- sources.forEach((s, i) => {
7389
+ rawSources.forEach((s, i) => {
6640
7390
  const at = `sources[${i}]`;
6641
- if (!s || typeof s !== "object") return err(at, "a source must be an object");
7391
+ if (!s || typeof s !== "object" || Array.isArray(s)) return err(at, "a source must be an object");
6642
7392
  if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
6643
7393
  else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
6644
7394
  else ids.add(s.id);
6645
7395
  if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
6646
7396
  if (typeof s.note !== "string" || s.note.trim().split(/\s+/).length < 3) err(`${at}.note`, "say what it is and where");
6647
- if (s.path && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
6648
- for (const [j, c] of (s.colours ?? []).entries()) if (!parseColour(c)) err(`${at}.colours[${j}]`, `"${c}" is not a colour`);
7397
+ if (s.path !== void 0 && typeof s.path !== "string") err(`${at}.path`, "a path must be text");
7398
+ if (typeof s.path === "string" && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
7399
+ if (s.colours !== void 0 && !Array.isArray(s.colours)) err(`${at}.colours`, "colours must be a list");
7400
+ for (const [j, c] of (Array.isArray(s.colours) ? s.colours : []).entries()) if (typeof c !== "string" || !parseColour(c)) err(`${at}.colours[${j}]`, `"${c}" is not a colour`);
7401
+ if (s.kind === "reference") {
7402
+ if (typeof s.category !== "string" || !s.category.trim()) err(`${at}.category`, "say where the reference comes from: print, a place, another trade");
7403
+ else if (/\b(?:web ?sites?|web design|competitors?|dribbble|behance|awwwards|themeforest)\b/i.test(s.category) || categoryMatchesBrief(s.category, typeof d.brief === "string" ? d.brief : "")) {
7404
+ err(`${at}.category`, `"${s.category}" is inside the client's category. A reference must come from outside it, or it hands back the category average.`);
7405
+ }
7406
+ if (typeof s.path === "string" && /^https?:/.test(s.path)) warn(`${at}.path`, "a web page as a reference: check it is not a site in the client's category");
7407
+ }
7408
+ if (/\bcompetitor(?:s|['’]s)?\b/i.test(`${s.note} ${s.category ?? ""}`)) err(at, "a source cannot be a competitor's website; use customer voice or a reference from outside the category");
6649
7409
  });
6650
7410
  const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
6651
7411
  if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
6652
7412
  const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
6653
7413
  let decided = 0;
6654
7414
  for (const key of Object.keys(choices)) {
6655
- if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}. Layout and navigation stay conventional.`);
7415
+ if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}. Record section order and the primary action in hierarchy.`);
6656
7416
  }
6657
7417
  for (const key of CHOICE_KEYS) {
6658
7418
  const c = choices[key];
@@ -6669,44 +7429,9 @@ function validateDirection(input, ctx = {}) {
6669
7429
  err(`${at}.value`, `"${c.value}" is not a colour`);
6670
7430
  ok = false;
6671
7431
  }
6672
- const because = typeof c.because === "string" ? c.because.trim() : "";
6673
- const evidence = Array.isArray(c.evidence) ? c.evidence : [];
6674
- if (because.split(/\s+/).filter(Boolean).length < 8) {
6675
- err(`${at}.because`, "a reason is at least a sentence. Without one this is a default, however good it looks.");
6676
- ok = false;
6677
- } else if (because.startsWith("PROPOSED:")) {
6678
- err(`${at}.because`, "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
6679
- ok = false;
6680
- } else {
6681
- if (PREFERENCE.test(because)) {
6682
- err(`${at}.because`, "that is a preference, not a reason. Say what in the client's world this comes from.");
6683
- ok = false;
6684
- }
6685
- const moods = because.match(MOOD) ?? [];
6686
- if (moods.length >= 2) {
6687
- err(`${at}.because`, `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
6688
- ok = false;
6689
- }
6690
- }
6691
- if (evidence.length === 0) {
6692
- err(`${at}.evidence`, "cite at least one source");
6693
- ok = false;
6694
- }
6695
- for (const id of evidence) {
6696
- if (!ids.has(id)) {
6697
- err(`${at}.evidence`, `no source called "${id}"`);
6698
- ok = false;
6699
- }
6700
- }
6701
- if (ok) {
6702
- const cited = sources.filter((s) => evidence.includes(s.id));
6703
- const reasonWords = words(because);
6704
- const tied = cited.some((s) => reasonWords.has(s.id) || [...words(`${s.note} ${s.lettering ?? ""}`)].some((w) => reasonWords.has(w)));
6705
- if (!tied) {
6706
- err(`${at}.because`, `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
6707
- ok = false;
6708
- }
6709
- }
7432
+ const reasons = checkReason({ at, because: c.because, evidence: c.evidence, sources, tie: "source" });
7433
+ problems.push(...reasons);
7434
+ if (reasons.length) ok = false;
6710
7435
  if (typeof c.value === "string") {
6711
7436
  const tell = tellFor(key, c.value);
6712
7437
  if (tell && !exceptions.some((e) => e.tell === tell)) {
@@ -6742,7 +7467,33 @@ function validateDirection(input, ctx = {}) {
6742
7467
  }
6743
7468
  if (ok) decided += 1;
6744
7469
  }
6745
- return { valid: problems.every((p) => p.severity !== "error"), problems, decided };
7470
+ let job = false;
7471
+ let hierarchy = [];
7472
+ if (d.version === 2) {
7473
+ const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
7474
+ problems.push(...j.problems);
7475
+ job = j.decided && d.job !== void 0;
7476
+ const h = validateHierarchy(d.hierarchy, d.job, sources);
7477
+ problems.push(...h.problems);
7478
+ hierarchy = h.pages;
7479
+ if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
7480
+ }
7481
+ if (ctx.estate?.length) {
7482
+ for (const m of compareToDeclared(summariseDirection(d), ctx.estate, ctx.estateId)) {
7483
+ if (m.flagged) warn("", `close to ${m.id}'s declared direction: shares ${m.shared.join(", ")}. Decide which of these should differ before either site is built.`);
7484
+ }
7485
+ }
7486
+ if (ctx.strict) {
7487
+ for (const p of problems) if (p.severity === "warn" && /the page (?:does not show|sets|runs)/.test(p.message)) p.severity = "error";
7488
+ }
7489
+ const home = hierarchy.find((p) => p.page === "home");
7490
+ const complete = job && Boolean(home) && hierarchy.every((p) => p.decided === p.total) && decided >= MIN_TOKENS;
7491
+ return {
7492
+ valid: problems.every((p) => p.severity !== "error"),
7493
+ problems,
7494
+ decided,
7495
+ layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
7496
+ };
6746
7497
  }
6747
7498
 
6748
7499
  // src/direction/init.ts
@@ -6761,11 +7512,31 @@ function initDirection(input) {
6761
7512
  body: empty(fp.body.family),
6762
7513
  ...fp.roundness !== null ? { shape: empty(fp.roundness >= 0.45 ? "pill buttons" : fp.roundness <= 0.05 ? "square corners" : `corners at ${Math.round(fp.roundness * 100)}% of button height`) } : {}
6763
7514
  } : {};
7515
+ const part = () => ({ value: "", evidence: [] });
6764
7516
  return {
6765
7517
  $schema: "https://unpkg.com/@domandigital/craft/art-direction.schema.json",
6766
7518
  version: DIRECTION_VERSION,
6767
7519
  client: input.client ?? "",
6768
7520
  brief: input.brief ?? "",
7521
+ // Empty on purpose, like the reasons: the first validate lists the job and the structure as undecided.
7522
+ job: {
7523
+ statement: { verb: "", object: "", context: "" },
7524
+ functional: part(),
7525
+ emotional: part(),
7526
+ social: part(),
7527
+ consideration: { value: "low", because: "", evidence: [] },
7528
+ objections: [],
7529
+ language: [],
7530
+ evidence: []
7531
+ },
7532
+ hierarchy: {
7533
+ home: {
7534
+ primaryAction: { value: "", positions: [], because: "" },
7535
+ order: fp?.sections?.length ? fp.sections.map((s) => ({ role: s.role, because: "" })) : [],
7536
+ lead: { value: "", because: "" },
7537
+ journey: []
7538
+ }
7539
+ },
6769
7540
  sources: [],
6770
7541
  choices,
6771
7542
  ...input.exceptions?.length ? { exceptions: input.exceptions } : {}
@@ -6773,7 +7544,6 @@ function initDirection(input) {
6773
7544
  }
6774
7545
 
6775
7546
  // src/direction/propose.ts
6776
- var PROPOSED = "PROPOSED:";
6777
7547
  function srgbToLinear2(c) {
6778
7548
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
6779
7549
  }
@@ -6835,7 +7605,13 @@ function accentCandidates(input) {
6835
7605
  const parsed = parseColour(c);
6836
7606
  if (!parsed || !chromatic(parsed.oklch)) continue;
6837
7607
  const o = parsed.oklch;
6838
- const siblings = (input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 }));
7608
+ const siblings = [
7609
+ ...(input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 })),
7610
+ ...(input.declared ?? []).flatMap((e) => {
7611
+ const p = parseColour(e.accent);
7612
+ return p ? [{ id: `${e.id} (declared)`, d: deltaEOk(o, p.oklch) }] : [];
7613
+ })
7614
+ ];
6839
7615
  const nearest2 = siblings.sort((a, b) => a.d - b.d)[0];
6840
7616
  const penalty = isAiViolet(o) ? 1 : 0;
6841
7617
  out.push({
@@ -6889,62 +7665,6 @@ function propose(input) {
6889
7665
  };
6890
7666
  }
6891
7667
 
6892
- // src/estate/index.ts
6893
- var ESTATE_VERSION = 1;
6894
- var SIBLING_AT = 0.31;
6895
- function emptyEstate() {
6896
- return { version: ESTATE_VERSION, sites: [] };
6897
- }
6898
- function addSite(register, site) {
6899
- if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
6900
- const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
6901
- return { version: ESTATE_VERSION, sites };
6902
- }
6903
- function sharedParts(a, b, d = fingerprintDistance(a, b)) {
6904
- const out = [];
6905
- const close = (k) => d.parts[k] <= 0.2;
6906
- if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
6907
- if (close("type")) {
6908
- if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
6909
- if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
6910
- }
6911
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
6912
- if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
6913
- if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
6914
- if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
6915
- if (close("layout")) out.push("running order");
6916
- return out;
6917
- }
6918
- function compareToEstate(target2, register, options = {}) {
6919
- const at = options.siblingAt ?? SIBLING_AT;
6920
- return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
6921
- const d = fingerprintDistance(target2, s.fingerprint);
6922
- return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
6923
- }).sort((a, b) => a.distance - b.distance);
6924
- }
6925
- function estatePairs(register, siblingAt = SIBLING_AT) {
6926
- const out = [];
6927
- const s = register.sites;
6928
- for (let i = 0; i < s.length; i += 1) {
6929
- for (let j = i + 1; j < s.length; j += 1) {
6930
- const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
6931
- 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) });
6932
- }
6933
- }
6934
- return out.sort((x, y) => x.distance - y.distance);
6935
- }
6936
- function siblingLine(briefs) {
6937
- const d = [];
6938
- for (const runs of briefs) {
6939
- 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);
6940
- }
6941
- if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
6942
- d.sort((a, b) => a - b);
6943
- const mid = Math.floor(d.length / 2);
6944
- const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
6945
- return Math.round(m * 1e3) / 1e3;
6946
- }
6947
-
6948
7668
  // src/report/index.ts
6949
7669
  var TELL_HEAVY = 3;
6950
7670
  var MIN_DECIDED = 5;
@@ -7044,8 +7764,16 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
7044
7764
  }
7045
7765
  if (input.direction) {
7046
7766
  const d = input.direction;
7047
- const raised2 = !d.valid || d.decided < MIN_DECIDED;
7048
- signals.push({ id: "reasons", raised: raised2, detail: `${d.decided} of 7 choices decided with a reason${d.valid ? "" : ", art-direction.json has errors"}` });
7767
+ const layers = d.layers;
7768
+ const raised2 = !d.valid || d.decided < MIN_DECIDED || !layers.complete;
7769
+ const pages = layers.hierarchy.map((p) => `${p.page} ${p.decided} of ${p.total}`).join(", ");
7770
+ const detail = [
7771
+ `job ${layers.job ? "decided" : "not decided"}`,
7772
+ `hierarchy ${pages || "not decided"}`,
7773
+ `${d.decided} of 7 choices decided with a reason`
7774
+ ].join("; ");
7775
+ signals.push({ id: "reasons", raised: raised2, detail: `${detail}${d.valid ? "" : ", art-direction.json has errors"}` });
7776
+ if (!layers.job) actions.push({ area: "direction", what: "Write the job map: what the visitor is trying to get done, from reviews, enquiries and conversations. Run craft direction research for the questions.", why: "structure and look are decided from the job", priority: 1 });
7049
7777
  for (const p of d.problems) {
7050
7778
  const key = p.at.match(/^choices\.(\w+)/)?.[1];
7051
7779
  if (p.severity === "error" || p.message === "not decided yet") {
@@ -7063,11 +7791,11 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
7063
7791
  const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
7064
7792
  const merged = [];
7065
7793
  for (const a of actions) {
7066
- const same = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
7067
- if (same) {
7794
+ const same2 = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
7795
+ if (same2) {
7068
7796
  const fromTell = /\(\S+, gen \d\)/.test(a.why);
7069
- same.why = `${same.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
7070
- same.priority = Math.min(same.priority, a.priority);
7797
+ same2.why = `${same2.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
7798
+ same2.priority = Math.min(same2.priority, a.priority);
7071
7799
  } else merged.push({ ...a });
7072
7800
  }
7073
7801
  const unique2 = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
@@ -7092,6 +7820,154 @@ function choiceOfShared(part) {
7092
7820
  return /running order/.test(part) ? void 0 : "motif";
7093
7821
  }
7094
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
+
7095
7971
  // src/report/retrofit.ts
7096
7972
  var HEADINGS = {
7097
7973
  direction: "Decide first",
@@ -7145,6 +8021,8 @@ function retrofitPlan(report2, direction) {
7145
8021
  }
7146
8022
  // Annotate the CommonJS export names for ESM import in node:
7147
8023
  0 && (module.exports = {
8024
+ ACCENT_SCALE,
8025
+ ACCENT_SHARED,
7148
8026
  AI_PHRASES,
7149
8027
  AI_SOURCE_TYPES,
7150
8028
  AI_VIOLET,
@@ -7155,10 +8033,14 @@ function retrofitPlan(report2, direction) {
7155
8033
  CATALOGUE_VERSION,
7156
8034
  CHOICE_KEYS,
7157
8035
  COMPONENT_ROLES,
8036
+ CONSIDERATION,
8037
+ CONVENTION,
8038
+ CUSTOMER_KINDS,
7158
8039
  DEFAULT_IGNORE,
7159
8040
  DENSITIES,
7160
8041
  DENSITY_NAMES,
7161
8042
  DIRECTION_VERSION,
8043
+ DIRECTION_VERSIONS,
7162
8044
  DISTANCE_WEIGHTS,
7163
8045
  DURATION_MS,
7164
8046
  DURATION_S,
@@ -7168,22 +8050,33 @@ function retrofitPlan(report2, direction) {
7168
8050
  ESTATE_VERSION,
7169
8051
  EXIT_RATIO,
7170
8052
  FINGERPRINT_VERSION,
8053
+ FLAGGED_TARGET,
8054
+ FUNCTION_WORDS,
8055
+ GROUND_SCALE,
8056
+ GROUND_SHARED,
8057
+ HIERARCHY_FIELDS,
7171
8058
  HOME_FILE,
7172
8059
  HOME_URL,
7173
8060
  HOUSE,
7174
8061
  HOUSE_BUDGET,
7175
8062
  HOUSE_LOCKED,
7176
8063
  HOUSE_TYPE,
8064
+ HUMAN_TYPICAL_TARGET,
7177
8065
  IMAGERY_TELLS,
7178
8066
  JSON_SCHEMA_VERSION,
8067
+ LABELS,
8068
+ LEXICAL_FEATURES,
7179
8069
  LICENCES,
7180
8070
  LIGHTNESS_CURVE,
7181
8071
  MIN_DECIDED,
7182
8072
  MIN_RUNS,
8073
+ MOOD,
7183
8074
  NEGATIVE_REASSURANCE,
7184
8075
  NULL_VERSION,
8076
+ PAGE_TYPES,
7185
8077
  PILL_LIMIT,
7186
8078
  PLAINER_WORDS,
8079
+ PREFERENCE,
7187
8080
  PROMO_BADGE,
7188
8081
  PROOF_NAMES,
7189
8082
  PROPOSED,
@@ -7197,8 +8090,10 @@ function retrofitPlan(report2, direction) {
7197
8090
  REVEAL_LIMIT,
7198
8091
  REVIEW_PHRASES,
7199
8092
  SCALE,
8093
+ SECTION_ROLES,
7200
8094
  SHADCN_LIMIT,
7201
8095
  SHADCN_PRIMITIVES,
8096
+ SHARED_PART,
7202
8097
  SIBLING_AT,
7203
8098
  SNAPSHOT_VERSION,
7204
8099
  SOURCE_KINDS,
@@ -7211,6 +8106,7 @@ function retrofitPlan(report2, direction) {
7211
8106
  STOCK_FIGURES,
7212
8107
  STOCK_PHOTO_HOSTS,
7213
8108
  STOCK_PHRASES,
8109
+ STUDY_WORDS,
7214
8110
  TELL_HEAVY,
7215
8111
  TRADE_NOUNS,
7216
8112
  TYPE_STEPS,
@@ -7224,28 +8120,37 @@ function retrofitPlan(report2, direction) {
7224
8120
  applySeverity,
7225
8121
  auditSnapshot,
7226
8122
  briefTerms,
8123
+ calibrate,
7227
8124
  catalogueTable,
7228
8125
  characterReport,
7229
8126
  checkCopy,
8127
+ checkOrder,
7230
8128
  checkPair,
8129
+ checkReason,
7231
8130
  checkRestraint,
7232
8131
  choicesOf,
8132
+ clauseDepth,
7233
8133
  closestComponents,
7234
8134
  compareCompetitor,
8135
+ compareDirections,
7235
8136
  compareFacts,
8137
+ compareToDeclared,
7236
8138
  compareToEstate,
7237
8139
  componentDistance,
7238
8140
  componentShared,
7239
8141
  componentTypicalities,
7240
8142
  componentTypicality,
7241
8143
  componentsOf,
8144
+ contentWords,
7242
8145
  countWords,
7243
8146
  craftTokens,
7244
8147
  createBaseline,
8148
+ cv,
7245
8149
  deltaEOk,
7246
8150
  deltaEOkHex,
7247
8151
  densityCss,
7248
8152
  densityTokens,
8153
+ designTells,
7249
8154
  emitCss,
7250
8155
  emptyEstate,
7251
8156
  estateComponentPairs,
@@ -7262,14 +8167,18 @@ function retrofitPlan(report2, direction) {
7262
8167
  fluidSpace,
7263
8168
  fluidType,
7264
8169
  fontClass,
8170
+ formatCalibration,
7265
8171
  formatClaims,
7266
8172
  formatComparison,
7267
8173
  formatCompetitor,
8174
+ formatDirectionMatches,
7268
8175
  formatHex,
7269
8176
  formatReport,
7270
8177
  globToRegExp,
7271
8178
  groundName,
7272
8179
  harvest,
8180
+ hedgeCount,
8181
+ hedgedClose,
7273
8182
  hexToOklch,
7274
8183
  holdHouseBlocks,
7275
8184
  houseRule,
@@ -7280,28 +8189,39 @@ function retrofitPlan(report2, direction) {
7280
8189
  inSrgbGamut,
7281
8190
  initDirection,
7282
8191
  isAiViolet,
8192
+ isBlurb,
8193
+ isCategoryLabel,
7283
8194
  isCream,
8195
+ isFlagged,
8196
+ jobWords,
7284
8197
  layoutDistance,
8198
+ lexicalOf,
7285
8199
  licencesJson,
7286
8200
  makeSnapshot,
7287
8201
  motionTokens,
7288
8202
  namedSources,
7289
8203
  nearest,
8204
+ nominalisations,
7290
8205
  normaliseFamily,
7291
8206
  nullPrompt,
7292
8207
  oklchToHex,
7293
8208
  oklchToRgb,
7294
8209
  pageCopy,
7295
8210
  paletteFromPixels,
8211
+ paragraphUniformity,
7296
8212
  parseBaseline,
7297
8213
  parseColour,
7298
8214
  parseCraftConfig,
7299
8215
  parseHex,
8216
+ participialClauses,
8217
+ phrasalCoordinations,
7300
8218
  propose,
7301
8219
  protectedFacts,
7302
8220
  ramp,
7303
8221
  rampFromAnchors,
7304
8222
  readSnapshot,
8223
+ renderedOrder,
8224
+ researchPrompt,
7305
8225
  retrofitPlan,
7306
8226
  runTell,
7307
8227
  scanProvenance,
@@ -7310,26 +8230,40 @@ function retrofitPlan(report2, direction) {
7310
8230
  semantic,
7311
8231
  sentencesOf,
7312
8232
  severityExceptions,
8233
+ shapeMoves,
7313
8234
  shapeName,
8235
+ shapeOf,
8236
+ shapeWords,
7314
8237
  sharedParts,
7315
8238
  shouldAnimate,
7316
8239
  siblingLine,
8240
+ skeleton,
8241
+ skeletonRepeat,
7317
8242
  specificity,
7318
8243
  specifics,
8244
+ splitSentences,
8245
+ stackedConditional,
7319
8246
  stockAvatarSource,
7320
8247
  stockSource,
8248
+ summariseDirection,
7321
8249
  tailwindV3Preset,
7322
8250
  tailwindV4Theme,
7323
8251
  tellById,
7324
8252
  toGamut,
7325
8253
  toJson,
7326
8254
  toSarif,
8255
+ tradeOf,
8256
+ tricolons,
7327
8257
  typeFeatureTokens,
7328
8258
  typicality,
7329
8259
  unprovenClaims,
7330
8260
  validateDirection,
8261
+ validateHierarchy,
8262
+ validateJob,
7331
8263
  visibleText,
7332
8264
  visualMeasures,
8265
+ vocabulary,
8266
+ vocabularyCounts,
7333
8267
  wcagContrast,
7334
8268
  wordListsJson
7335
8269
  });