@domandigital/craft 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/CHARACTER.md +213 -28
- package/README.md +19 -1
- package/art-direction.schema.json +428 -25
- package/dist/{audit-6WHR7ZTV.js → audit-MTUC24XU.js} +2 -2
- package/dist/audit.cjs +5 -0
- package/dist/audit.js +5 -0
- package/dist/{chunk-7T5WR74C.js → chunk-2IQ5OXFR.js} +4 -4
- package/dist/{chunk-GQVEYTBY.js → chunk-LFNSB6W7.js} +13 -3
- package/dist/{chunk-3OTKMKJQ.js → chunk-MUVFECFC.js} +19 -15
- package/dist/chunk-OQXTQQAU.js +27 -0
- package/dist/{chunk-Q7ILL2HV.js → chunk-OUQ25LY2.js} +1 -1
- package/dist/{chunk-PBK6SYH5.js → chunk-QU6Q2QZZ.js} +46 -11
- package/dist/{chunk-XVBR2UPH.js → chunk-RXXRXV3V.js} +7 -24
- package/dist/chunk-YDURAOYR.js +711 -0
- package/dist/{cli-3CGUKXY4.js → cli-22TSWAX6.js} +6 -4
- package/dist/{cli-WSHPSWVU.js → cli-5ACKICBO.js} +6 -5
- package/dist/{cli-O3CDSONO.js → cli-7OZCYOQY.js} +86 -20
- package/dist/{cli-WG3UJNUF.js → cli-FCKHYAVZ.js} +36 -13
- package/dist/{cli-Z2IQG3LC.js → cli-FLG3ZCAF.js} +4 -3
- package/dist/cli.js +2 -2
- package/dist/generate.js +6 -0
- package/dist/index.cjs +850 -115
- package/dist/index.d.cts +455 -56
- package/dist/index.d.ts +455 -56
- package/dist/index.js +804 -115
- package/package.json +1 -1
- package/dist/chunk-OOZOEWGD.js +0 -295
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,6 +49,10 @@ __export(index_exports, {
|
|
|
43
49
|
ESTATE_VERSION: () => ESTATE_VERSION,
|
|
44
50
|
EXIT_RATIO: () => EXIT_RATIO,
|
|
45
51
|
FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
|
|
52
|
+
FUNCTION_WORDS: () => FUNCTION_WORDS,
|
|
53
|
+
GROUND_SCALE: () => GROUND_SCALE,
|
|
54
|
+
GROUND_SHARED: () => GROUND_SHARED,
|
|
55
|
+
HIERARCHY_FIELDS: () => HIERARCHY_FIELDS,
|
|
46
56
|
HOME_FILE: () => HOME_FILE,
|
|
47
57
|
HOME_URL: () => HOME_URL,
|
|
48
58
|
HOUSE: () => HOUSE,
|
|
@@ -55,10 +65,13 @@ __export(index_exports, {
|
|
|
55
65
|
LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
|
|
56
66
|
MIN_DECIDED: () => MIN_DECIDED,
|
|
57
67
|
MIN_RUNS: () => MIN_RUNS,
|
|
68
|
+
MOOD: () => MOOD,
|
|
58
69
|
NEGATIVE_REASSURANCE: () => NEGATIVE_REASSURANCE,
|
|
59
70
|
NULL_VERSION: () => NULL_VERSION,
|
|
71
|
+
PAGE_TYPES: () => PAGE_TYPES,
|
|
60
72
|
PILL_LIMIT: () => PILL_LIMIT,
|
|
61
73
|
PLAINER_WORDS: () => PLAINER_WORDS,
|
|
74
|
+
PREFERENCE: () => PREFERENCE,
|
|
62
75
|
PROMO_BADGE: () => PROMO_BADGE,
|
|
63
76
|
PROOF_NAMES: () => PROOF_NAMES,
|
|
64
77
|
PROPOSED: () => PROPOSED,
|
|
@@ -72,8 +85,10 @@ __export(index_exports, {
|
|
|
72
85
|
REVEAL_LIMIT: () => REVEAL_LIMIT,
|
|
73
86
|
REVIEW_PHRASES: () => REVIEW_PHRASES,
|
|
74
87
|
SCALE: () => SCALE,
|
|
88
|
+
SECTION_ROLES: () => SECTION_ROLES,
|
|
75
89
|
SHADCN_LIMIT: () => SHADCN_LIMIT,
|
|
76
90
|
SHADCN_PRIMITIVES: () => SHADCN_PRIMITIVES,
|
|
91
|
+
SHARED_PART: () => SHARED_PART,
|
|
77
92
|
SIBLING_AT: () => SIBLING_AT,
|
|
78
93
|
SNAPSHOT_VERSION: () => SNAPSHOT_VERSION,
|
|
79
94
|
SOURCE_KINDS: () => SOURCE_KINDS,
|
|
@@ -102,21 +117,28 @@ __export(index_exports, {
|
|
|
102
117
|
catalogueTable: () => catalogueTable,
|
|
103
118
|
characterReport: () => characterReport,
|
|
104
119
|
checkCopy: () => checkCopy,
|
|
120
|
+
checkOrder: () => checkOrder,
|
|
105
121
|
checkPair: () => checkPair,
|
|
122
|
+
checkReason: () => checkReason,
|
|
106
123
|
checkRestraint: () => checkRestraint,
|
|
107
124
|
choicesOf: () => choicesOf,
|
|
125
|
+
clauseDepth: () => clauseDepth,
|
|
108
126
|
closestComponents: () => closestComponents,
|
|
109
127
|
compareCompetitor: () => compareCompetitor,
|
|
128
|
+
compareDirections: () => compareDirections,
|
|
110
129
|
compareFacts: () => compareFacts,
|
|
130
|
+
compareToDeclared: () => compareToDeclared,
|
|
111
131
|
compareToEstate: () => compareToEstate,
|
|
112
132
|
componentDistance: () => componentDistance,
|
|
113
133
|
componentShared: () => componentShared,
|
|
114
134
|
componentTypicalities: () => componentTypicalities,
|
|
115
135
|
componentTypicality: () => componentTypicality,
|
|
116
136
|
componentsOf: () => componentsOf,
|
|
137
|
+
contentWords: () => contentWords,
|
|
117
138
|
countWords: () => countWords2,
|
|
118
139
|
craftTokens: () => craftTokens,
|
|
119
140
|
createBaseline: () => createBaseline,
|
|
141
|
+
cv: () => cv,
|
|
120
142
|
deltaEOk: () => deltaEOk,
|
|
121
143
|
deltaEOkHex: () => deltaEOkHex,
|
|
122
144
|
densityCss: () => densityCss,
|
|
@@ -140,11 +162,14 @@ __export(index_exports, {
|
|
|
140
162
|
formatClaims: () => formatClaims,
|
|
141
163
|
formatComparison: () => formatComparison,
|
|
142
164
|
formatCompetitor: () => formatCompetitor,
|
|
165
|
+
formatDirectionMatches: () => formatDirectionMatches,
|
|
143
166
|
formatHex: () => formatHex,
|
|
144
167
|
formatReport: () => formatReport,
|
|
145
168
|
globToRegExp: () => globToRegExp,
|
|
146
169
|
groundName: () => groundName,
|
|
147
170
|
harvest: () => harvest,
|
|
171
|
+
hedgeCount: () => hedgeCount,
|
|
172
|
+
hedgedClose: () => hedgedClose,
|
|
148
173
|
hexToOklch: () => hexToOklch,
|
|
149
174
|
holdHouseBlocks: () => holdHouseBlocks,
|
|
150
175
|
houseRule: () => houseRule,
|
|
@@ -155,28 +180,37 @@ __export(index_exports, {
|
|
|
155
180
|
inSrgbGamut: () => inSrgbGamut,
|
|
156
181
|
initDirection: () => initDirection,
|
|
157
182
|
isAiViolet: () => isAiViolet,
|
|
183
|
+
isBlurb: () => isBlurb,
|
|
184
|
+
isCategoryLabel: () => isCategoryLabel,
|
|
158
185
|
isCream: () => isCream,
|
|
186
|
+
jobWords: () => jobWords,
|
|
159
187
|
layoutDistance: () => layoutDistance,
|
|
160
188
|
licencesJson: () => licencesJson,
|
|
161
189
|
makeSnapshot: () => makeSnapshot,
|
|
162
190
|
motionTokens: () => motionTokens,
|
|
163
191
|
namedSources: () => namedSources,
|
|
164
192
|
nearest: () => nearest,
|
|
193
|
+
nominalisations: () => nominalisations,
|
|
165
194
|
normaliseFamily: () => normaliseFamily,
|
|
166
195
|
nullPrompt: () => nullPrompt,
|
|
167
196
|
oklchToHex: () => oklchToHex,
|
|
168
197
|
oklchToRgb: () => oklchToRgb,
|
|
169
198
|
pageCopy: () => pageCopy,
|
|
170
199
|
paletteFromPixels: () => paletteFromPixels,
|
|
200
|
+
paragraphUniformity: () => paragraphUniformity,
|
|
171
201
|
parseBaseline: () => parseBaseline,
|
|
172
202
|
parseColour: () => parseColour,
|
|
173
203
|
parseCraftConfig: () => parseCraftConfig,
|
|
174
204
|
parseHex: () => parseHex,
|
|
205
|
+
participialClauses: () => participialClauses,
|
|
206
|
+
phrasalCoordinations: () => phrasalCoordinations,
|
|
175
207
|
propose: () => propose,
|
|
176
208
|
protectedFacts: () => protectedFacts,
|
|
177
209
|
ramp: () => ramp,
|
|
178
210
|
rampFromAnchors: () => rampFromAnchors,
|
|
179
211
|
readSnapshot: () => readSnapshot,
|
|
212
|
+
renderedOrder: () => renderedOrder,
|
|
213
|
+
researchPrompt: () => researchPrompt,
|
|
180
214
|
retrofitPlan: () => retrofitPlan,
|
|
181
215
|
runTell: () => runTell,
|
|
182
216
|
scanProvenance: () => scanProvenance,
|
|
@@ -185,24 +219,36 @@ __export(index_exports, {
|
|
|
185
219
|
semantic: () => semantic,
|
|
186
220
|
sentencesOf: () => sentencesOf,
|
|
187
221
|
severityExceptions: () => severityExceptions,
|
|
222
|
+
shapeMoves: () => shapeMoves,
|
|
188
223
|
shapeName: () => shapeName,
|
|
224
|
+
shapeOf: () => shapeOf,
|
|
225
|
+
shapeWords: () => words,
|
|
189
226
|
sharedParts: () => sharedParts,
|
|
190
227
|
shouldAnimate: () => shouldAnimate,
|
|
191
228
|
siblingLine: () => siblingLine,
|
|
229
|
+
skeleton: () => skeleton,
|
|
230
|
+
skeletonRepeat: () => skeletonRepeat,
|
|
192
231
|
specificity: () => specificity,
|
|
193
232
|
specifics: () => specifics,
|
|
233
|
+
splitSentences: () => splitSentences,
|
|
234
|
+
stackedConditional: () => stackedConditional,
|
|
194
235
|
stockAvatarSource: () => stockAvatarSource,
|
|
195
236
|
stockSource: () => stockSource,
|
|
237
|
+
summariseDirection: () => summariseDirection,
|
|
196
238
|
tailwindV3Preset: () => tailwindV3Preset,
|
|
197
239
|
tailwindV4Theme: () => tailwindV4Theme,
|
|
198
240
|
tellById: () => tellById,
|
|
199
241
|
toGamut: () => toGamut,
|
|
200
242
|
toJson: () => toJson,
|
|
201
243
|
toSarif: () => toSarif,
|
|
244
|
+
tradeOf: () => tradeOf,
|
|
245
|
+
tricolons: () => tricolons,
|
|
202
246
|
typeFeatureTokens: () => typeFeatureTokens,
|
|
203
247
|
typicality: () => typicality,
|
|
204
248
|
unprovenClaims: () => unprovenClaims,
|
|
205
249
|
validateDirection: () => validateDirection,
|
|
250
|
+
validateHierarchy: () => validateHierarchy,
|
|
251
|
+
validateJob: () => validateJob,
|
|
206
252
|
visibleText: () => visibleText,
|
|
207
253
|
visualMeasures: () => visualMeasures,
|
|
208
254
|
wcagContrast: () => wcagContrast,
|
|
@@ -702,9 +748,9 @@ function fluidClamp(minPx, maxPx, minViewport, maxViewport, rootPx = 16) {
|
|
|
702
748
|
const interceptRem = round((minPx - slope * minViewport) / rootPx);
|
|
703
749
|
const slopeVw = round(slope * 100);
|
|
704
750
|
const preferred = interceptRem === 0 ? `${slopeVw}vw` : `${interceptRem}rem + ${slopeVw}vw`;
|
|
705
|
-
const
|
|
751
|
+
const lower2 = Math.min(minRem, maxRem);
|
|
706
752
|
const upper = Math.max(minRem, maxRem);
|
|
707
|
-
return `clamp(${
|
|
753
|
+
return `clamp(${lower2}rem, ${preferred}, ${upper}rem)`;
|
|
708
754
|
}
|
|
709
755
|
function fluidType(options = {}) {
|
|
710
756
|
const {
|
|
@@ -1498,7 +1544,7 @@ function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => fal
|
|
|
1498
1544
|
};
|
|
1499
1545
|
}
|
|
1500
1546
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/'/g, "['\u2019]?").replace(/ /g, "\\s+");
|
|
1501
|
-
var wordList = (
|
|
1547
|
+
var wordList = (words3) => new RegExp(`\\b(?:${[...words3].sort((a, b) => b.length - a.length).map(escape).join("|")})\\b`, "i");
|
|
1502
1548
|
var quoted = (m) => `"${m}"`;
|
|
1503
1549
|
var AI_WORDS = [
|
|
1504
1550
|
"tapestry",
|
|
@@ -2336,8 +2382,8 @@ function documents(ctx) {
|
|
|
2336
2382
|
const docs = [];
|
|
2337
2383
|
for (const [path, blocks] of byPath) {
|
|
2338
2384
|
const text = blocks.map((b) => withoutHeadings(b.text)).join("\n");
|
|
2339
|
-
const
|
|
2340
|
-
if (
|
|
2385
|
+
const words3 = countWords(text);
|
|
2386
|
+
if (words3 >= MIN_WORDS) docs.push({ path, blocks, words: words3 });
|
|
2341
2387
|
}
|
|
2342
2388
|
return docs;
|
|
2343
2389
|
}
|
|
@@ -2481,14 +2527,14 @@ function rhythmHits(ctx) {
|
|
|
2481
2527
|
const list = sentences(doc2);
|
|
2482
2528
|
if (list.length < 40) continue;
|
|
2483
2529
|
const lengths = list.map((s) => countWords(s.text));
|
|
2484
|
-
const
|
|
2485
|
-
const
|
|
2486
|
-
const
|
|
2487
|
-
if (
|
|
2530
|
+
const mean2 = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
2531
|
+
const sd2 = Math.sqrt(lengths.reduce((a, b) => a + (b - mean2) ** 2, 0) / lengths.length);
|
|
2532
|
+
const cv2 = sd2 / mean2;
|
|
2533
|
+
if (cv2 < 0.42) {
|
|
2488
2534
|
hits.push({
|
|
2489
2535
|
path: doc2.path,
|
|
2490
2536
|
offset: list[0].offset,
|
|
2491
|
-
message: `${list.length} sentences averaging ${
|
|
2537
|
+
message: `${list.length} sentences averaging ${mean2.toFixed(1)} words with little variation (spread ${cv2.toFixed(2)}, reads metronomic below 0.42)`
|
|
2492
2538
|
});
|
|
2493
2539
|
}
|
|
2494
2540
|
}
|
|
@@ -2502,19 +2548,19 @@ function repeatHits(ctx) {
|
|
|
2502
2548
|
const whole = /* @__PURE__ */ new Map();
|
|
2503
2549
|
const runs = /* @__PURE__ */ new Map();
|
|
2504
2550
|
for (const s of sentences(doc2)) {
|
|
2505
|
-
const
|
|
2506
|
-
if (
|
|
2551
|
+
const words3 = normal(s.text);
|
|
2552
|
+
if (words3.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
2507
2553
|
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
2508
|
-
const key =
|
|
2554
|
+
const key = words3.join(" ");
|
|
2509
2555
|
const earlier = whole.get(key);
|
|
2510
2556
|
if (earlier) {
|
|
2511
|
-
hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${
|
|
2557
|
+
hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${words3.length} words)`, excerpt: s.text.slice(0, 120) });
|
|
2512
2558
|
continue;
|
|
2513
2559
|
}
|
|
2514
2560
|
whole.set(key, s);
|
|
2515
2561
|
let reported = false;
|
|
2516
|
-
for (let i = 0; i + RUN <=
|
|
2517
|
-
const run =
|
|
2562
|
+
for (let i = 0; i + RUN <= words3.length && !reported; i += 1) {
|
|
2563
|
+
const run = words3.slice(i, i + RUN).join(" ");
|
|
2518
2564
|
const seen = runs.get(run);
|
|
2519
2565
|
if (seen && seen.key !== key) {
|
|
2520
2566
|
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 +2617,11 @@ function headingEchoHits(ctx) {
|
|
|
2571
2617
|
const hits = [];
|
|
2572
2618
|
for (const { file } of longFiles(ctx)) {
|
|
2573
2619
|
for (const heading of headings(file)) {
|
|
2574
|
-
const
|
|
2575
|
-
if (
|
|
2620
|
+
const words3 = normal(heading.text);
|
|
2621
|
+
if (words3.length < 4) continue;
|
|
2576
2622
|
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,
|
|
2578
|
-
if (opening.join(" ") ===
|
|
2623
|
+
const opening = normal(after.split(/\n\s*\n/)[0] ?? "").slice(0, words3.length);
|
|
2624
|
+
if (opening.join(" ") === words3.join(" ")) {
|
|
2579
2625
|
hits.push({ path: file.path, offset: heading.offset, message: `the heading "${heading.text}" is repeated as the first words under it` });
|
|
2580
2626
|
}
|
|
2581
2627
|
}
|
|
@@ -2583,7 +2629,7 @@ function headingEchoHits(ctx) {
|
|
|
2583
2629
|
return hits;
|
|
2584
2630
|
}
|
|
2585
2631
|
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(
|
|
2632
|
+
function filler(words3, seed = 1, options = {}) {
|
|
2587
2633
|
const { contractions = true, uniform } = options;
|
|
2588
2634
|
const shape = [5, 17, 9, 24, 6, 13, 31, 8, 11, 4, 20, 15];
|
|
2589
2635
|
let state = seed;
|
|
@@ -2594,7 +2640,7 @@ function filler(words2, seed = 1, options = {}) {
|
|
|
2594
2640
|
const out = [];
|
|
2595
2641
|
let count4 = 0;
|
|
2596
2642
|
let i = 0;
|
|
2597
|
-
while (count4 <
|
|
2643
|
+
while (count4 < words3) {
|
|
2598
2644
|
const length = uniform ?? shape[i % shape.length];
|
|
2599
2645
|
const s = [];
|
|
2600
2646
|
for (let w = 0; w < length; w += 1) s.push(FILLER_WORDS[next(FILLER_WORDS.length)]);
|
|
@@ -3983,15 +4029,15 @@ var blank = (s) => s.replace(/[^\n]/g, " ");
|
|
|
3983
4029
|
function readsAsProse(value) {
|
|
3984
4030
|
if (value.length < 12 || !/\s/.test(value)) return false;
|
|
3985
4031
|
if (looksLikeClasses(value) || looksLikeAddress(value)) return false;
|
|
3986
|
-
const
|
|
3987
|
-
if (looksLikeCode(
|
|
3988
|
-
return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(
|
|
4032
|
+
const words3 = withoutMarkup(value);
|
|
4033
|
+
if (looksLikeCode(words3) || /[<>]/.test(words3)) return false;
|
|
4034
|
+
return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(words3);
|
|
3989
4035
|
}
|
|
3990
4036
|
function withoutMarkup(value) {
|
|
3991
4037
|
return value.replace(/<\/?[a-zA-Z][^<>]*>/g, " ").replace(/\$\{[^}]*\}/g, " ").replace(/\{\{[^}]*\}\}/g, " ").replace(/&(?:#\d+|#x[0-9a-f]+|[a-z]+);/gi, "'");
|
|
3992
4038
|
}
|
|
3993
|
-
function looksLikeCode(
|
|
3994
|
-
return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(
|
|
4039
|
+
function looksLikeCode(words3) {
|
|
4040
|
+
return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(words3);
|
|
3995
4041
|
}
|
|
3996
4042
|
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
4043
|
function looksLikeClasses(value) {
|
|
@@ -4394,8 +4440,8 @@ var termPattern = (term) => new RegExp(`(?<![\\w\xA3$\u20AC])${term.split(/\s+/)
|
|
|
4394
4440
|
var TRADE_PATTERNS = TRADE_NOUNS.map((t) => [t, termPattern(t)]);
|
|
4395
4441
|
var SMALL_WORDS = /* @__PURE__ */ new Set(["a", "an", "the", "and", "or", "of", "in", "on", "at", "to", "for", "by", "with", "from"]);
|
|
4396
4442
|
function caseless(line) {
|
|
4397
|
-
const
|
|
4398
|
-
const letters =
|
|
4443
|
+
const words3 = line.match(/[A-Za-z][A-Za-z’'-]*/g) ?? [];
|
|
4444
|
+
const letters = words3.filter((w) => w.length >= 2);
|
|
4399
4445
|
if (letters.length >= 3 && letters.filter((w) => w === w.toUpperCase()).length / letters.length >= 0.6) return "shout";
|
|
4400
4446
|
const content = letters.filter((w) => !SMALL_WORDS.has(w.toLowerCase()));
|
|
4401
4447
|
if (content.length >= 3 && content.filter((w) => /^[A-Z]/.test(w)).length / content.length >= 0.8) return "title";
|
|
@@ -4405,8 +4451,8 @@ var maskStock = (text) => STOCK_FIGURES.reduce((t, re) => t.replace(re, (m) => "
|
|
|
4405
4451
|
function briefTerms(brief) {
|
|
4406
4452
|
const terms = /* @__PURE__ */ new Set();
|
|
4407
4453
|
for (const w of brief.match(/[A-Za-z][A-Za-z’'&-]*/g) ?? []) {
|
|
4408
|
-
const
|
|
4409
|
-
if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(
|
|
4454
|
+
const lower2 = w.toLowerCase();
|
|
4455
|
+
if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(lower2) && !NOT_NAMES.has(lower2)) terms.add(lower2);
|
|
4410
4456
|
}
|
|
4411
4457
|
for (const [term, re] of TRADE_PATTERNS) if (re.test(brief)) terms.add(term);
|
|
4412
4458
|
for (const f4 of protectedFacts(maskStock(brief))) if (f4.kind !== "name") terms.add(f4.value.toLowerCase());
|
|
@@ -4429,12 +4475,12 @@ function specifics(text, brief) {
|
|
|
4429
4475
|
for (const acronym of f4.value.match(/\b[A-Z]{2,}\b/g) ?? []) add("name", acronym);
|
|
4430
4476
|
continue;
|
|
4431
4477
|
}
|
|
4432
|
-
const
|
|
4478
|
+
const words3 = f4.value.split(/\s+/);
|
|
4433
4479
|
const named = (w) => !NOT_NAMES.has(w.toLowerCase()) && w !== "&";
|
|
4434
|
-
const first =
|
|
4480
|
+
const first = words3.findIndex(named);
|
|
4435
4481
|
if (first === -1) continue;
|
|
4436
|
-
const last =
|
|
4437
|
-
add("name",
|
|
4482
|
+
const last = words3.length - 1 - [...words3].reverse().findIndex(named);
|
|
4483
|
+
add("name", words3.slice(first, last + 1).join(" "));
|
|
4438
4484
|
continue;
|
|
4439
4485
|
}
|
|
4440
4486
|
add(f4.kind, f4.value);
|
|
@@ -4454,15 +4500,15 @@ function specifics(text, brief) {
|
|
|
4454
4500
|
}
|
|
4455
4501
|
function specificity(text, brief) {
|
|
4456
4502
|
const list = specifics(text, brief);
|
|
4457
|
-
const
|
|
4503
|
+
const words3 = countWords2(text);
|
|
4458
4504
|
const trade = list.filter((s) => s.kind === "trade").length;
|
|
4459
4505
|
const hard = list.length - trade;
|
|
4460
4506
|
return {
|
|
4461
|
-
words:
|
|
4507
|
+
words: words3,
|
|
4462
4508
|
specifics: list,
|
|
4463
4509
|
hard,
|
|
4464
4510
|
trade,
|
|
4465
|
-
perHundred:
|
|
4511
|
+
perHundred: words3 ? Math.round(list.length / words3 * 1e3) / 10 : 0,
|
|
4466
4512
|
generic: hard === 0 && trade < 2
|
|
4467
4513
|
};
|
|
4468
4514
|
}
|
|
@@ -5171,9 +5217,14 @@ function faceDistance(a, b) {
|
|
|
5171
5217
|
if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
|
|
5172
5218
|
return a.class === b.class ? 0.5 : 1;
|
|
5173
5219
|
}
|
|
5220
|
+
var ACCENT_SCALE = 0.3;
|
|
5221
|
+
var GROUND_SCALE = 0.15;
|
|
5222
|
+
var SHARED_PART = 0.2;
|
|
5223
|
+
var ACCENT_SHARED = ACCENT_SCALE * SHARED_PART;
|
|
5224
|
+
var GROUND_SHARED = GROUND_SCALE * SHARED_PART;
|
|
5174
5225
|
function fingerprintDistance(a, b) {
|
|
5175
|
-
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) /
|
|
5176
|
-
const ground = clamp(deltaEOk(a.ground, b.ground) /
|
|
5226
|
+
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / ACCENT_SCALE) : a.accent || b.accent ? 1 : 0;
|
|
5227
|
+
const ground = clamp(deltaEOk(a.ground, b.ground) / GROUND_SCALE);
|
|
5177
5228
|
const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
|
|
5178
5229
|
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
5230
|
const motion = clamp(Math.abs(a.motion - b.motion));
|
|
@@ -5308,10 +5359,10 @@ var STOP = new Set(
|
|
|
5308
5359
|
function grams(text) {
|
|
5309
5360
|
const out = /* @__PURE__ */ new Set();
|
|
5310
5361
|
for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
|
|
5311
|
-
const
|
|
5362
|
+
const words3 = clause.match(/[a-z][a-z'-]*/g) ?? [];
|
|
5312
5363
|
for (let n = 2; n <= 4; n += 1) {
|
|
5313
|
-
for (let i = 0; i + n <=
|
|
5314
|
-
const g =
|
|
5364
|
+
for (let i = 0; i + n <= words3.length; i += 1) {
|
|
5365
|
+
const g = words3.slice(i, i + n);
|
|
5315
5366
|
if (STOP.has(g[0]) || STOP.has(g[n - 1])) continue;
|
|
5316
5367
|
if (g.filter((w) => !STOP.has(w)).length < 2) continue;
|
|
5317
5368
|
out.add(g.join(" "));
|
|
@@ -5478,7 +5529,7 @@ function closestComponents(a, b, role) {
|
|
|
5478
5529
|
}
|
|
5479
5530
|
function estateComponentPairs(register, role) {
|
|
5480
5531
|
const out = [];
|
|
5481
|
-
const s = register.sites;
|
|
5532
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
5482
5533
|
for (let i = 0; i < s.length; i += 1) {
|
|
5483
5534
|
for (let j = i + 1; j < s.length; j += 1) {
|
|
5484
5535
|
const c = closestComponents(s[i].fingerprint, s[j].fingerprint, role);
|
|
@@ -6072,9 +6123,9 @@ var ATTRIBUTION = [
|
|
|
6072
6123
|
];
|
|
6073
6124
|
var LINK2 = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|\bhttps?:\/\/[^\s<>"')\]]+/g;
|
|
6074
6125
|
function cleanName(raw) {
|
|
6075
|
-
const
|
|
6076
|
-
while (
|
|
6077
|
-
return
|
|
6126
|
+
const words3 = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
|
|
6127
|
+
while (words3.length && NOT_A_SOURCE.test(words3[0])) words3.shift();
|
|
6128
|
+
return words3.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
|
|
6078
6129
|
}
|
|
6079
6130
|
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
6131
|
var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
|
|
@@ -6167,6 +6218,207 @@ function formatClaims(report2, label = "craft copy claims") {
|
|
|
6167
6218
|
return lines.join("\n");
|
|
6168
6219
|
}
|
|
6169
6220
|
|
|
6221
|
+
// src/character/shape.ts
|
|
6222
|
+
var ABBREVIATIONS = /\b(?:e\.g|i\.e|etc|vs|Mr|Mrs|Ms|Dr|St|No|approx|incl|Ltd|Co|Inc|Jr|Sr)\./g;
|
|
6223
|
+
var DOT = "\0";
|
|
6224
|
+
function splitSentences(text) {
|
|
6225
|
+
const guarded = text.replace(/\s+/g, " ").replace(ABBREVIATIONS, (m) => m.replace(/\./g, DOT)).replace(/(\d)\.(\d)/g, `$1${DOT}$2`);
|
|
6226
|
+
return guarded.split(/(?<=[.!?]["'’”)]*)\s+(?=["'‘“(]?[A-Z0-9£$€])/).map((s) => s.split(DOT).join(".").trim()).filter((s) => words(s).length > 0);
|
|
6227
|
+
}
|
|
6228
|
+
var WORD3 = /[A-Za-z0-9£$€][A-Za-z0-9£$€%'’-]*/g;
|
|
6229
|
+
function words(text) {
|
|
6230
|
+
return text.match(WORD3) ?? [];
|
|
6231
|
+
}
|
|
6232
|
+
var lower = (w) => w.toLowerCase().replace(/’/g, "'");
|
|
6233
|
+
var FUNCTION_WORDS = new Set(
|
|
6234
|
+
"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(" ")
|
|
6235
|
+
);
|
|
6236
|
+
var ITEM = "[A-Za-z0-9\xA3$\u20AC'\u2019-]+(?: [A-Za-z0-9\xA3$\u20AC'\u2019-]+){0,5}";
|
|
6237
|
+
var TRICOLON = new RegExp(`(${ITEM}), (${ITEM}),? (and|or) (${ITEM})(?=[,.;:!?)]|$)`, "g");
|
|
6238
|
+
function tricolons(sentence) {
|
|
6239
|
+
const s = sentence.replace(/\s+/g, " ");
|
|
6240
|
+
const out = [];
|
|
6241
|
+
for (const m of s.matchAll(TRICOLON)) {
|
|
6242
|
+
const [, aSpan, b, , c] = m;
|
|
6243
|
+
const bWords = words(b);
|
|
6244
|
+
const cWords = words(c);
|
|
6245
|
+
if (bWords.length === 0 || cWords.length === 0 || cWords.length > 6) continue;
|
|
6246
|
+
const aWords = words(aSpan);
|
|
6247
|
+
const a = aWords.slice(-Math.max(1, bWords.length));
|
|
6248
|
+
const lead = lower(bWords[0]);
|
|
6249
|
+
const parallel = bWords.length >= 2 && FUNCTION_WORDS.has(lead) && (lower(cWords[0]) === lead || lower(a[0]) === lead);
|
|
6250
|
+
out.push({ text: m[0], parallel });
|
|
6251
|
+
}
|
|
6252
|
+
return out;
|
|
6253
|
+
}
|
|
6254
|
+
var STACKED = /,\s+(?:and|but|so|or)\s+(?:if|when|where|once|unless|provided)\b[^,.;!?]{1,60},/i;
|
|
6255
|
+
function stackedConditional(sentence) {
|
|
6256
|
+
return STACKED.test(sentence);
|
|
6257
|
+
}
|
|
6258
|
+
var SUBORDINATOR = /\b(?:if|when|which|because|so|while|where)\b/gi;
|
|
6259
|
+
function clauseDepth(sentence) {
|
|
6260
|
+
return (sentence.match(/,/g) ?? []).length + (sentence.match(SUBORDINATOR) ?? []).length;
|
|
6261
|
+
}
|
|
6262
|
+
var HEDGES = [
|
|
6263
|
+
"if it's a fit",
|
|
6264
|
+
"if it is a fit",
|
|
6265
|
+
"where needed",
|
|
6266
|
+
"if needed",
|
|
6267
|
+
"as needed",
|
|
6268
|
+
"where appropriate",
|
|
6269
|
+
"if appropriate",
|
|
6270
|
+
"as appropriate",
|
|
6271
|
+
"as required",
|
|
6272
|
+
"where required",
|
|
6273
|
+
"if required",
|
|
6274
|
+
"where possible",
|
|
6275
|
+
"if possible",
|
|
6276
|
+
"where relevant",
|
|
6277
|
+
"if relevant",
|
|
6278
|
+
"depending on",
|
|
6279
|
+
"can help",
|
|
6280
|
+
"tend to",
|
|
6281
|
+
"tends to",
|
|
6282
|
+
"may",
|
|
6283
|
+
"might",
|
|
6284
|
+
"could",
|
|
6285
|
+
"typically",
|
|
6286
|
+
"usually",
|
|
6287
|
+
"generally",
|
|
6288
|
+
"often",
|
|
6289
|
+
"potentially",
|
|
6290
|
+
"likely"
|
|
6291
|
+
];
|
|
6292
|
+
var HEDGE = new RegExp(`\\b(?:${HEDGES.map((h) => h.replace(/'/g, "['\u2019]").replace(/ /g, "\\s+")).join("|")})\\b`, "gi");
|
|
6293
|
+
var CONDITIONAL_CLOSE = /\b(?:if|unless|where|provided|depending)\b/i;
|
|
6294
|
+
function hedgeCount(text) {
|
|
6295
|
+
return (text.match(HEDGE) ?? []).length;
|
|
6296
|
+
}
|
|
6297
|
+
function hedgedClose(sentences3) {
|
|
6298
|
+
const last = sentences3[sentences3.length - 1];
|
|
6299
|
+
if (!last) return false;
|
|
6300
|
+
HEDGE.lastIndex = 0;
|
|
6301
|
+
return HEDGE.test(last) || CONDITIONAL_CLOSE.test(last);
|
|
6302
|
+
}
|
|
6303
|
+
var NOMINAL = /^[a-z]{3,}(?:tion|sion|ment|ness|ity|ance|ence)s?$/;
|
|
6304
|
+
var NOT_NOMINAL = new Set(
|
|
6305
|
+
[
|
|
6306
|
+
"nation station ration lotion potion portion question moment cement comment element garment segment fragment apartment document ointment",
|
|
6307
|
+
"city pity entity quality sentence science audience silence balance finance distance instance entrance substance romance province"
|
|
6308
|
+
].join(" ").split(" ").flatMap((w) => [w, `${w}s`])
|
|
6309
|
+
);
|
|
6310
|
+
function nominalisations(text) {
|
|
6311
|
+
return words(text).filter((w) => {
|
|
6312
|
+
const l = lower(w);
|
|
6313
|
+
return NOMINAL.test(l) && !NOT_NOMINAL.has(l);
|
|
6314
|
+
}).length;
|
|
6315
|
+
}
|
|
6316
|
+
var PRONOUN_OPENER = /^["'‘“(]?(?:you|your|we|our)\b/i;
|
|
6317
|
+
var TRANSITION_OPENER = /^["'‘“(]?(?:however|moreover|additionally|furthermore|thus|therefore|consequently|in addition|as a result|ultimately|overall|importantly|notably)\b/i;
|
|
6318
|
+
var IMPERSONAL_OPENER = /^["'‘“(]?(?:it is|it's|it’s|there is|there are|there's|there’s)\b/i;
|
|
6319
|
+
var NOT_PARTICIPLE = new Set(
|
|
6320
|
+
"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(" ")
|
|
6321
|
+
);
|
|
6322
|
+
function participialClauses(sentence) {
|
|
6323
|
+
let n = 0;
|
|
6324
|
+
for (const m of sentence.matchAll(/,\s+([A-Za-z]{2,}ing)\b/g)) if (!NOT_PARTICIPLE.has(lower(m[1]))) n += 1;
|
|
6325
|
+
const open = sentence.match(/^["'‘“(]?([A-Z][a-z]{2,}ing)\b[^,]{0,60},/);
|
|
6326
|
+
if (open && !NOT_PARTICIPLE.has(lower(open[1]))) n += 1;
|
|
6327
|
+
return n;
|
|
6328
|
+
}
|
|
6329
|
+
function phrasalCoordinations(sentence) {
|
|
6330
|
+
let n = 0;
|
|
6331
|
+
for (const m of sentence.matchAll(/(^|[^,]\s)([A-Za-z][a-z'’-]{2,}) (?:and|or) ([A-Za-z][a-z'’-]{2,})\b/g)) {
|
|
6332
|
+
const a = lower(m[2]);
|
|
6333
|
+
const b = lower(m[3]);
|
|
6334
|
+
if (FUNCTION_WORDS.has(a) || FUNCTION_WORDS.has(b) || a.endsWith("ly") || b.endsWith("ly")) continue;
|
|
6335
|
+
n += 1;
|
|
6336
|
+
}
|
|
6337
|
+
return n;
|
|
6338
|
+
}
|
|
6339
|
+
function skeleton(sentence) {
|
|
6340
|
+
const out = [];
|
|
6341
|
+
for (const w of words(sentence)) {
|
|
6342
|
+
const l = lower(w);
|
|
6343
|
+
const token = FUNCTION_WORDS.has(l) ? l : "_";
|
|
6344
|
+
if (token === "_" && out[out.length - 1] === "_") continue;
|
|
6345
|
+
out.push(token);
|
|
6346
|
+
}
|
|
6347
|
+
return out;
|
|
6348
|
+
}
|
|
6349
|
+
function skeletonRepeat(sentences3) {
|
|
6350
|
+
const grams2 = /* @__PURE__ */ new Map();
|
|
6351
|
+
let total = 0;
|
|
6352
|
+
for (const s of sentences3) {
|
|
6353
|
+
const sk = skeleton(s);
|
|
6354
|
+
for (let i = 0; i + 4 <= sk.length; i += 1) {
|
|
6355
|
+
const g = sk.slice(i, i + 4).join(" ");
|
|
6356
|
+
grams2.set(g, (grams2.get(g) ?? 0) + 1);
|
|
6357
|
+
total += 1;
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
if (total === 0) return 0;
|
|
6361
|
+
let repeated = 0;
|
|
6362
|
+
for (const count4 of grams2.values()) if (count4 > 1) repeated += count4;
|
|
6363
|
+
return repeated / total;
|
|
6364
|
+
}
|
|
6365
|
+
var mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
6366
|
+
var sd = (xs) => {
|
|
6367
|
+
const m = mean(xs);
|
|
6368
|
+
return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / xs.length);
|
|
6369
|
+
};
|
|
6370
|
+
function cv(xs, min) {
|
|
6371
|
+
if (xs.length < min) return null;
|
|
6372
|
+
const m = mean(xs);
|
|
6373
|
+
return m === 0 ? null : sd(xs) / m;
|
|
6374
|
+
}
|
|
6375
|
+
function shapeOf(text) {
|
|
6376
|
+
const ss = splitSentences(text);
|
|
6377
|
+
const wordCount = words(text).length;
|
|
6378
|
+
const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
|
|
6379
|
+
const share = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
|
|
6380
|
+
const tri = ss.flatMap(tricolons);
|
|
6381
|
+
const commas = (text.match(/,/g) ?? []).length;
|
|
6382
|
+
return {
|
|
6383
|
+
sentences: ss.length,
|
|
6384
|
+
words: wordCount,
|
|
6385
|
+
tricolons: tri.length,
|
|
6386
|
+
parallelTriads: tri.filter((t) => t.parallel).length,
|
|
6387
|
+
stackedConditionals: ss.filter(stackedConditional).length,
|
|
6388
|
+
clauseDepth: ss.length === 0 ? 0 : mean(ss.map(clauseDepth)),
|
|
6389
|
+
sentenceCv: cv(ss.map((s) => words(s).length), 3),
|
|
6390
|
+
hedgeRate: per100(hedgeCount(text)),
|
|
6391
|
+
hedgedClose: hedgedClose(ss),
|
|
6392
|
+
nominalisationRate: per100(nominalisations(text)),
|
|
6393
|
+
pronounOpenerShare: share(PRONOUN_OPENER),
|
|
6394
|
+
transitionOpenerShare: share(TRANSITION_OPENER),
|
|
6395
|
+
impersonalOpenerShare: share(IMPERSONAL_OPENER),
|
|
6396
|
+
commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
|
|
6397
|
+
commasPer100: per100(commas),
|
|
6398
|
+
participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
|
|
6399
|
+
coordinationRate: per100(ss.reduce((s, x) => s + phrasalCoordinations(x), 0)),
|
|
6400
|
+
skeletonRepeat: skeletonRepeat(ss)
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6403
|
+
function shapeMoves(f4) {
|
|
6404
|
+
const out = [];
|
|
6405
|
+
if (f4.parallelTriads > 0) out.push("parallel triad");
|
|
6406
|
+
else if (f4.tricolons > 0) out.push("tricolon");
|
|
6407
|
+
if (f4.stackedConditionals > 0) out.push("stacked conditional");
|
|
6408
|
+
if (f4.hedgedClose) out.push("hedged close");
|
|
6409
|
+
if (f4.participialRate > 0) out.push("participial clause");
|
|
6410
|
+
return out;
|
|
6411
|
+
}
|
|
6412
|
+
function paragraphUniformity(paragraphs) {
|
|
6413
|
+
return cv(
|
|
6414
|
+
paragraphs.map((p) => words(p).length).filter((n) => n > 0),
|
|
6415
|
+
4
|
|
6416
|
+
);
|
|
6417
|
+
}
|
|
6418
|
+
function isBlurb(f4) {
|
|
6419
|
+
return f4.sentences >= 2 && f4.sentences <= 8 && f4.words >= 20 && f4.words <= 160;
|
|
6420
|
+
}
|
|
6421
|
+
|
|
6170
6422
|
// src/character/baseline.ts
|
|
6171
6423
|
var BASELINE_VERSION = 1;
|
|
6172
6424
|
function fnv1a(text) {
|
|
@@ -6462,7 +6714,9 @@ function visualMeasures(img, maxSamples = 1e6) {
|
|
|
6462
6714
|
}
|
|
6463
6715
|
|
|
6464
6716
|
// src/direction/types.ts
|
|
6465
|
-
var DIRECTION_VERSION =
|
|
6717
|
+
var DIRECTION_VERSION = 2;
|
|
6718
|
+
var DIRECTION_VERSIONS = [1, 2];
|
|
6719
|
+
var CUSTOMER_KINDS = ["review", "enquiry", "conversation", "search"];
|
|
6466
6720
|
var SOURCE_KINDS = [
|
|
6467
6721
|
"livery",
|
|
6468
6722
|
"shopfront",
|
|
@@ -6476,9 +6730,413 @@ var SOURCE_KINDS = [
|
|
|
6476
6730
|
"trade",
|
|
6477
6731
|
"print",
|
|
6478
6732
|
"photo",
|
|
6479
|
-
"founder"
|
|
6733
|
+
"founder",
|
|
6734
|
+
...CUSTOMER_KINDS,
|
|
6735
|
+
"reference"
|
|
6480
6736
|
];
|
|
6481
6737
|
var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
|
|
6738
|
+
var CONSIDERATION = ["low", "considered", "high"];
|
|
6739
|
+
var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
|
|
6740
|
+
var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
|
|
6741
|
+
|
|
6742
|
+
// src/direction/reason.ts
|
|
6743
|
+
var PROPOSED = "PROPOSED:";
|
|
6744
|
+
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;
|
|
6745
|
+
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;
|
|
6746
|
+
var CONVENTION = new RegExp(
|
|
6747
|
+
[
|
|
6748
|
+
// authority
|
|
6749
|
+
String.raw`industry[- ]standard`,
|
|
6750
|
+
String.raw`best[- ]practices?`,
|
|
6751
|
+
String.raw`the norm\b`,
|
|
6752
|
+
String.raw`standard (?:layout|structure|practice|pattern|format)`,
|
|
6753
|
+
String.raw`tried[- ]and[- ]tested`,
|
|
6754
|
+
String.raw`proven (?:layout|structure|pattern|format)`,
|
|
6755
|
+
// the crowd
|
|
6756
|
+
String.raw`(?:most|other|typical|many|all|similar)\s+(?:\w+\s+){0,2}(?:web)?sites\b`,
|
|
6757
|
+
String.raw`competitors?(?:'|’)?s?\b`,
|
|
6758
|
+
String.raw`(?:like|as) (?:everyone|everybody|all the others|the rest)\b`,
|
|
6759
|
+
String.raw`commonly used`,
|
|
6760
|
+
String.raw`(?:what|how) (?:everyone|others|the industry) (?:does|do|uses?)`,
|
|
6761
|
+
// familiarity
|
|
6762
|
+
String.raw`familiar (?:layout|structure|pattern|format|order|shape|design)`,
|
|
6763
|
+
String.raw`recogni[sz]able (?:layout|structure|pattern|format|order)`,
|
|
6764
|
+
String.raw`conventional(?:ly)?\b`,
|
|
6765
|
+
String.raw`(?:visitors|users|people|customers|anyone) (?:are|is) used to`,
|
|
6766
|
+
String.raw`feels? (?:at home|comfortable|familiar)(?: with)?(?: the)? (?:layout|site|page|structure|design)?`,
|
|
6767
|
+
String.raw`intuitive(?:ly)?\b`,
|
|
6768
|
+
String.raw`as usual\b`,
|
|
6769
|
+
String.raw`what (?:visitors|users|people) expect\b`
|
|
6770
|
+
].join("|"),
|
|
6771
|
+
"i"
|
|
6772
|
+
);
|
|
6773
|
+
var GENERIC = new Set(
|
|
6774
|
+
"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(" ")
|
|
6775
|
+
);
|
|
6776
|
+
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"]);
|
|
6777
|
+
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"]);
|
|
6778
|
+
function contentWords(text) {
|
|
6779
|
+
return new Set(
|
|
6780
|
+
(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))
|
|
6781
|
+
);
|
|
6782
|
+
}
|
|
6783
|
+
var stem = (w) => w.replace(/(?:ing|ers|er|ed|es|s)$/, "");
|
|
6784
|
+
function shares(a, b) {
|
|
6785
|
+
const bs = new Set([...b].map(stem));
|
|
6786
|
+
return [...a].filter((w) => bs.has(stem(w)));
|
|
6787
|
+
}
|
|
6788
|
+
function jobWords(job) {
|
|
6789
|
+
return contentWords(
|
|
6790
|
+
[
|
|
6791
|
+
job.statement?.verb,
|
|
6792
|
+
job.statement?.object,
|
|
6793
|
+
job.statement?.context,
|
|
6794
|
+
job.functional?.value,
|
|
6795
|
+
job.emotional?.value,
|
|
6796
|
+
job.social?.value,
|
|
6797
|
+
...Array.isArray(job.objections) ? job.objections : [],
|
|
6798
|
+
...Array.isArray(job.language) ? job.language : []
|
|
6799
|
+
].filter((x) => typeof x === "string").join(" ")
|
|
6800
|
+
);
|
|
6801
|
+
}
|
|
6802
|
+
var isCustomerVoice = (s) => Boolean(s && CUSTOMER_KINDS.includes(s.kind));
|
|
6803
|
+
function checkReason(input) {
|
|
6804
|
+
const problems = [];
|
|
6805
|
+
const err = (field, message) => problems.push({ severity: "error", at: `${input.at}.${field}`, message });
|
|
6806
|
+
const because = typeof input.because === "string" ? input.because.trim() : "";
|
|
6807
|
+
const evidence = Array.isArray(input.evidence) ? input.evidence.filter((e) => typeof e === "string") : [];
|
|
6808
|
+
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");
|
|
6809
|
+
const ids = new Map(input.sources.filter((s) => s && typeof s.id === "string").map((s) => [s.id, s]));
|
|
6810
|
+
if (because.split(/\s+/).filter(Boolean).length < 8) {
|
|
6811
|
+
err("because", "a reason is at least a sentence. Without one this is a default, however good it looks.");
|
|
6812
|
+
return problems;
|
|
6813
|
+
}
|
|
6814
|
+
if (because.startsWith(PROPOSED)) {
|
|
6815
|
+
err("because", "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
|
|
6816
|
+
return problems;
|
|
6817
|
+
}
|
|
6818
|
+
if (PREFERENCE.test(because)) err("because", "that is a preference, not a reason. Say what in the client's world this comes from.");
|
|
6819
|
+
const moods = because.match(MOOD) ?? [];
|
|
6820
|
+
if (moods.length >= 2) err("because", `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
|
|
6821
|
+
const convention = because.match(CONVENTION);
|
|
6822
|
+
if (convention) {
|
|
6823
|
+
err("because", `"${convention[0]}" is the category's default speaking, not a reason. Say what in this client's job or world it answers.`);
|
|
6824
|
+
}
|
|
6825
|
+
const needsEvidence = input.tie !== "job";
|
|
6826
|
+
if (needsEvidence && evidence.length === 0) err("evidence", "cite at least one source");
|
|
6827
|
+
for (const id of evidence) if (!ids.has(id)) err("evidence", `no source called "${id}"`);
|
|
6828
|
+
if (input.tie === "customer") {
|
|
6829
|
+
for (const id of evidence) {
|
|
6830
|
+
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(", ")}.`);
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
if (input.tie === "source") {
|
|
6834
|
+
for (const id of evidence) {
|
|
6835
|
+
if (isCustomerVoice(ids.get(id))) err("evidence", `"${id}" is customer voice. Token decisions need a physical source or an outside-category reference.`);
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
if (problems.length) return problems;
|
|
6839
|
+
const words3 = contentWords(because);
|
|
6840
|
+
if (input.tie === "job") {
|
|
6841
|
+
if (!input.job) {
|
|
6842
|
+
err("because", "a structural reason is checked against the job, and the file has no job map yet");
|
|
6843
|
+
} else if (shares(words3, jobWords(input.job)).length === 0) {
|
|
6844
|
+
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.");
|
|
6845
|
+
}
|
|
6846
|
+
} else {
|
|
6847
|
+
const cited = evidence.map((id) => ids.get(id));
|
|
6848
|
+
const tied = cited.some((s) => words3.has(s.id) || shares(words3, contentWords(`${s.note} ${s.lettering ?? ""}`)).length > 0);
|
|
6849
|
+
if (!tied) err("because", `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
|
|
6850
|
+
}
|
|
6851
|
+
return problems;
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
// src/direction/job.ts
|
|
6855
|
+
var JOB_VERBS = new Set(
|
|
6856
|
+
"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(" ")
|
|
6857
|
+
);
|
|
6858
|
+
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"]);
|
|
6859
|
+
var NOUNISH = /(?:tion|sion|ment|ness|ity|site|page)$/;
|
|
6860
|
+
var words2 = (s) => s.toLowerCase().match(/[a-z][a-z'’-]*/g) ?? [];
|
|
6861
|
+
function isCategoryLabel(object, brief = "") {
|
|
6862
|
+
const trade = /* @__PURE__ */ new Set([...TRADE_NOUNS.flatMap((t) => t.split(" ")), ...words2(brief).filter((w) => w.length > 3)]);
|
|
6863
|
+
const ws = words2(object);
|
|
6864
|
+
return ws.length > 0 && ws.every((w) => LABEL_WORDS.has(w) || trade.has(w) || trade.has(w.replace(/s$/, "")));
|
|
6865
|
+
}
|
|
6866
|
+
var normalise2 = (s) => s.toLowerCase().replace(/[“”"‘’'.,!?;:()-]/g, " ").replace(/\s+/g, " ").trim();
|
|
6867
|
+
function validateJob(job, sources, ctx = {}) {
|
|
6868
|
+
const problems = [];
|
|
6869
|
+
const err = (at, message) => problems.push({ severity: "error", at: `job${at}`, message });
|
|
6870
|
+
const warn = (at, message) => problems.push({ severity: "warn", at: `job${at}`, message });
|
|
6871
|
+
if (!job || typeof job !== "object" || Array.isArray(job)) {
|
|
6872
|
+
err("", "no job map: say what the visitor is trying to get done, in their words, before deciding structure or look");
|
|
6873
|
+
return { problems, decided: false };
|
|
6874
|
+
}
|
|
6875
|
+
const j = job;
|
|
6876
|
+
const ids = new Map(sources.map((s) => [s.id, s]));
|
|
6877
|
+
const st = j.statement;
|
|
6878
|
+
if (!st || typeof st !== "object") err(".statement", "a job statement is verb, object and context");
|
|
6879
|
+
else {
|
|
6880
|
+
const verb = typeof st.verb === "string" ? st.verb.trim().toLowerCase() : "";
|
|
6881
|
+
if (!verb) err(".statement.verb", "what the visitor is trying to do, as a verb: find, choose, book, fix");
|
|
6882
|
+
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.`);
|
|
6883
|
+
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.`);
|
|
6884
|
+
const object = typeof st.object === "string" ? st.object.trim() : "";
|
|
6885
|
+
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");
|
|
6886
|
+
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".`);
|
|
6887
|
+
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");
|
|
6888
|
+
}
|
|
6889
|
+
for (const part of ["functional", "emotional", "social"]) {
|
|
6890
|
+
const p = j[part];
|
|
6891
|
+
if (!p || typeof p !== "object") {
|
|
6892
|
+
err(`.${part}`, `the ${part} side of the job, with the customer-voice source that shows it`);
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
if (typeof p.value !== "string" || words2(p.value).length < 6) err(`.${part}.value`, "a sentence: what they need, in the terms they would use");
|
|
6896
|
+
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") })));
|
|
6897
|
+
}
|
|
6898
|
+
const c = j.consideration;
|
|
6899
|
+
if (!c || typeof c !== "object") err(".consideration", `how long and how carefully they decide: ${CONSIDERATION.join(", ")}`);
|
|
6900
|
+
else {
|
|
6901
|
+
if (!CONSIDERATION.includes(c.value)) err(".consideration.value", `one of ${CONSIDERATION.join(", ")}`);
|
|
6902
|
+
problems.push(...checkReason({ at: "job.consideration", because: c.because, evidence: c.evidence, sources, tie: "customer" }));
|
|
6903
|
+
}
|
|
6904
|
+
const objections = Array.isArray(j.objections) ? j.objections : [];
|
|
6905
|
+
if (objections.length === 0) err(".objections", "what stops them: the worries they bring to the page");
|
|
6906
|
+
objections.forEach((o, i) => {
|
|
6907
|
+
if (typeof o !== "string" || words2(o).length < 3) err(`.objections[${i}]`, "an objection is a few words: what they worry about");
|
|
6908
|
+
});
|
|
6909
|
+
const evidence = Array.isArray(j.evidence) ? j.evidence : [];
|
|
6910
|
+
const voice = evidence.map((id) => ids.get(id)).filter(isCustomerVoice);
|
|
6911
|
+
if (evidence.length === 0 || voice.length === 0) err(".evidence", "cite the reviews, enquiries or conversations the job comes from");
|
|
6912
|
+
for (const id of evidence) if (!ids.has(id)) err(".evidence", `no source called "${id}"`);
|
|
6913
|
+
const language = Array.isArray(j.language) ? j.language : [];
|
|
6914
|
+
if (language.length === 0) err(".language", "quote the customer: the words they use for the problem");
|
|
6915
|
+
const texts = voice.map((s) => normalise2(`${s.note} ${ctx.readSource?.(s) ?? ""}`));
|
|
6916
|
+
language.forEach((q, i) => {
|
|
6917
|
+
if (typeof q !== "string" || !q.trim()) return err(`.language[${i}]`, "an empty quote");
|
|
6918
|
+
const n = normalise2(q);
|
|
6919
|
+
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.`);
|
|
6920
|
+
});
|
|
6921
|
+
return { problems, decided: problems.every((p) => p.severity !== "error") };
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
// src/direction/hierarchy.ts
|
|
6925
|
+
var SECTION_ROLES = [
|
|
6926
|
+
"hero",
|
|
6927
|
+
"logos",
|
|
6928
|
+
"stats",
|
|
6929
|
+
"cards",
|
|
6930
|
+
"marquee",
|
|
6931
|
+
"text",
|
|
6932
|
+
"other",
|
|
6933
|
+
"cta-band",
|
|
6934
|
+
"footer-cta",
|
|
6935
|
+
"pricing",
|
|
6936
|
+
"testimonials",
|
|
6937
|
+
"faq",
|
|
6938
|
+
"process",
|
|
6939
|
+
"features",
|
|
6940
|
+
"team",
|
|
6941
|
+
"contact"
|
|
6942
|
+
];
|
|
6943
|
+
var POSITIONS = /* @__PURE__ */ new Set([...SECTION_ROLES, "header", "sticky"]);
|
|
6944
|
+
function considerationTied(because, job) {
|
|
6945
|
+
const text = because.toLowerCase();
|
|
6946
|
+
const level = job.consideration?.value;
|
|
6947
|
+
if (level && new RegExp(`\\b${level}\\b`).test(text)) return true;
|
|
6948
|
+
const said = contentWords(because);
|
|
6949
|
+
return (Array.isArray(job.objections) ? job.objections : []).some((o) => typeof o === "string" && [...contentWords(o)].some((w) => said.has(w)));
|
|
6950
|
+
}
|
|
6951
|
+
function validateHierarchy(hierarchy, job, sources) {
|
|
6952
|
+
const problems = [];
|
|
6953
|
+
const err = (at, message) => problems.push({ severity: "error", at: `hierarchy${at}`, message });
|
|
6954
|
+
const pages = [];
|
|
6955
|
+
if (!hierarchy || typeof hierarchy !== "object" || Array.isArray(hierarchy)) {
|
|
6956
|
+
err("", "no hierarchy: decide what the home page leads with, where it asks, and the order, each from the job");
|
|
6957
|
+
return { problems, pages };
|
|
6958
|
+
}
|
|
6959
|
+
const h = hierarchy;
|
|
6960
|
+
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(", ")}`);
|
|
6961
|
+
if (!h.home) err(".home", "no home page hierarchy: the home page is where most visitors start");
|
|
6962
|
+
for (const page of PAGE_TYPES) {
|
|
6963
|
+
const p = h[page];
|
|
6964
|
+
if (!p) continue;
|
|
6965
|
+
let decided = 0;
|
|
6966
|
+
const at = (f4) => `hierarchy.${page}.${f4}`;
|
|
6967
|
+
const reason = (where, because, evidence) => checkReason({ at: where, because, evidence, sources, tie: "job", job });
|
|
6968
|
+
const count4 = (found) => {
|
|
6969
|
+
problems.push(...found);
|
|
6970
|
+
if (!found.some((x) => x.severity === "error")) decided += 1;
|
|
6971
|
+
};
|
|
6972
|
+
if (!p.primaryAction) err(`.${page}.primaryAction`, "not decided yet");
|
|
6973
|
+
else {
|
|
6974
|
+
const a = p.primaryAction;
|
|
6975
|
+
const found = [];
|
|
6976
|
+
if (typeof a.value !== "string" || !a.value.trim()) found.push({ severity: "error", at: `${at("primaryAction")}.value`, message: "what the visitor is asked to do" });
|
|
6977
|
+
const positions = Array.isArray(a.positions) ? a.positions : [];
|
|
6978
|
+
if (positions.length === 0) found.push({ severity: "error", at: `${at("primaryAction")}.positions`, message: 'where the ask sits: one place, or several ("hero", "footer-cta")' });
|
|
6979
|
+
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"` });
|
|
6980
|
+
const r = reason(at("primaryAction"), a.because, a.evidence);
|
|
6981
|
+
found.push(...r);
|
|
6982
|
+
if (r.length === 0 && job && typeof a.because === "string" && !considerationTied(a.because, job)) {
|
|
6983
|
+
found.push({
|
|
6984
|
+
severity: "error",
|
|
6985
|
+
at: `${at("primaryAction")}.because`,
|
|
6986
|
+
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.`
|
|
6987
|
+
});
|
|
6988
|
+
}
|
|
6989
|
+
count4(found);
|
|
6990
|
+
}
|
|
6991
|
+
const order = Array.isArray(p.order) ? p.order : [];
|
|
6992
|
+
if (order.length < 2) err(`.${page}.order`, "the running order, two sections or more, each with why it sits there");
|
|
6993
|
+
else {
|
|
6994
|
+
const found = [];
|
|
6995
|
+
order.forEach((o, i) => {
|
|
6996
|
+
if (!o || !SECTION_ROLES.includes(o.role)) found.push({ severity: "error", at: `${at("order")}[${i}].role`, message: `a section role: ${SECTION_ROLES.join(", ")}` });
|
|
6997
|
+
found.push(...reason(`${at("order")}[${i}]`, o?.because));
|
|
6998
|
+
});
|
|
6999
|
+
count4(found);
|
|
7000
|
+
}
|
|
7001
|
+
if (!p.lead) err(`.${page}.lead`, "not decided yet");
|
|
7002
|
+
else {
|
|
7003
|
+
const found = [];
|
|
7004
|
+
if (typeof p.lead.value !== "string" || !p.lead.value.trim()) found.push({ severity: "error", at: `${at("lead")}.value`, message: "what the page leads with" });
|
|
7005
|
+
found.push(...reason(at("lead"), p.lead.because));
|
|
7006
|
+
count4(found);
|
|
7007
|
+
}
|
|
7008
|
+
const journey = Array.isArray(p.journey) ? p.journey : [];
|
|
7009
|
+
if (journey.length < 2) err(`.${page}.journey`, "the stages the visitor goes through, two or more, each with why");
|
|
7010
|
+
else {
|
|
7011
|
+
const found = [];
|
|
7012
|
+
journey.forEach((s, i) => {
|
|
7013
|
+
if (!s || typeof s.stage !== "string" || !s.stage.trim()) found.push({ severity: "error", at: `${at("journey")}[${i}].stage`, message: "name the stage" });
|
|
7014
|
+
found.push(...reason(`${at("journey")}[${i}]`, s?.because));
|
|
7015
|
+
});
|
|
7016
|
+
count4(found);
|
|
7017
|
+
}
|
|
7018
|
+
pages.push({ page, decided, total: HIERARCHY_FIELDS.length });
|
|
7019
|
+
}
|
|
7020
|
+
return { problems, pages };
|
|
7021
|
+
}
|
|
7022
|
+
function renderedOrder(snapshot) {
|
|
7023
|
+
const sections = snapshot.sections ?? [];
|
|
7024
|
+
if (sections.length === 0 || !sections.every((s) => s.role)) return null;
|
|
7025
|
+
return sections.map((s) => s.role).filter((r) => r !== "other");
|
|
7026
|
+
}
|
|
7027
|
+
function checkOrder(declared, page, snapshot, strict = false) {
|
|
7028
|
+
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" }];
|
|
7029
|
+
const rendered = renderedOrder(snapshot);
|
|
7030
|
+
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" }];
|
|
7031
|
+
const said = declared.order.map((o) => o.role).filter((r) => r !== "other");
|
|
7032
|
+
if (said.join(",") === rendered.join(",")) return [];
|
|
7033
|
+
return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
|
|
7034
|
+
}
|
|
7035
|
+
|
|
7036
|
+
// src/direction/research.ts
|
|
7037
|
+
function tradeOf(brief) {
|
|
7038
|
+
const text = brief.toLowerCase();
|
|
7039
|
+
return TRADE_NOUNS.filter((t) => new RegExp(`\\b${t.replace(/ /g, "\\s+")}s?\\b`).test(text));
|
|
7040
|
+
}
|
|
7041
|
+
function categoryMatchesBrief(category, brief) {
|
|
7042
|
+
const known = tradeOf(category);
|
|
7043
|
+
if (tradeOf(brief).some((t) => known.includes(t))) return true;
|
|
7044
|
+
const label = category.trim().replace(/^(?:(?:a|an|the|local|independent)\s+)+/i, "");
|
|
7045
|
+
if (!label) return false;
|
|
7046
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
7047
|
+
return new RegExp(`\\b${escaped}s?\\b`, "i").test(brief);
|
|
7048
|
+
}
|
|
7049
|
+
function researchPrompt(input) {
|
|
7050
|
+
const trade = tradeOf(input.brief);
|
|
7051
|
+
const category = trade.length ? trade.join(", ") : "this kind of business";
|
|
7052
|
+
const who = input.client ? `${input.client}: ${input.brief}` : input.brief;
|
|
7053
|
+
const lines = [
|
|
7054
|
+
"# Research brief: the customer's job",
|
|
7055
|
+
"",
|
|
7056
|
+
`The business: ${who}`,
|
|
7057
|
+
"",
|
|
7058
|
+
"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.",
|
|
7059
|
+
"",
|
|
7060
|
+
"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?",
|
|
7061
|
+
'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?',
|
|
7062
|
+
"3. **Objections.** What do they worry about before getting in touch? What has gone wrong for them, or people they know, before?",
|
|
7063
|
+
"4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
|
|
7064
|
+
"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?",
|
|
7065
|
+
"6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
|
|
7066
|
+
"7. **Where they look.** What do they search for, and where do they ask?",
|
|
7067
|
+
"",
|
|
7068
|
+
"## Do not",
|
|
7069
|
+
"",
|
|
7070
|
+
`- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
|
|
7071
|
+
'- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
|
|
7072
|
+
"- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
|
|
7073
|
+
"- Do not invent quotes. If you cannot find a source for something, say so."
|
|
7074
|
+
];
|
|
7075
|
+
if (input.visual) {
|
|
7076
|
+
lines.push(
|
|
7077
|
+
"",
|
|
7078
|
+
"# Visual references, from outside the category",
|
|
7079
|
+
"",
|
|
7080
|
+
`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.`,
|
|
7081
|
+
"",
|
|
7082
|
+
`Nothing from a ${category} website, and nothing from a web design gallery.`
|
|
7083
|
+
);
|
|
7084
|
+
}
|
|
7085
|
+
return `${lines.join("\n")}
|
|
7086
|
+
`;
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
// src/estate/direction.ts
|
|
7090
|
+
function summariseDirection(d) {
|
|
7091
|
+
const c = d.choices ?? {};
|
|
7092
|
+
const home = d.hierarchy?.home;
|
|
7093
|
+
const s = {};
|
|
7094
|
+
if (typeof c.accent?.value === "string" && c.accent.value) s.accent = c.accent.value;
|
|
7095
|
+
if (typeof c.ground?.value === "string" && c.ground.value) s.ground = c.ground.value;
|
|
7096
|
+
if (typeof c.display?.value === "string" && c.display.value) s.display = normaliseFamily(c.display.value);
|
|
7097
|
+
if (typeof c.body?.value === "string" && c.body.value) s.body = normaliseFamily(c.body.value);
|
|
7098
|
+
if (typeof c.shape?.value === "string" && c.shape.value) s.shape = c.shape.value;
|
|
7099
|
+
if (Array.isArray(home?.order) && home.order.length) s.order = home.order.filter((o) => o && typeof o.role === "string").map((o) => o.role);
|
|
7100
|
+
if (typeof home?.primaryAction?.value === "string" && home.primaryAction.value) {
|
|
7101
|
+
s.primaryAction = home.primaryAction.value;
|
|
7102
|
+
s.positions = Array.isArray(home.primaryAction.positions) ? [...home.primaryAction.positions] : [];
|
|
7103
|
+
}
|
|
7104
|
+
return s;
|
|
7105
|
+
}
|
|
7106
|
+
var delta = (a, b) => {
|
|
7107
|
+
const x = a ? parseColour(a) : null;
|
|
7108
|
+
const y = b ? parseColour(b) : null;
|
|
7109
|
+
return x && y ? deltaEOk(x.oklch, y.oklch) : null;
|
|
7110
|
+
};
|
|
7111
|
+
var same = (a, b) => Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase());
|
|
7112
|
+
function compareDirections(a, b) {
|
|
7113
|
+
const shared = [];
|
|
7114
|
+
const accentDelta = delta(a.accent, b.accent);
|
|
7115
|
+
const groundDelta = delta(a.ground, b.ground);
|
|
7116
|
+
if (accentDelta !== null && accentDelta <= ACCENT_SHARED) shared.push(`accent (\u0394E ${accentDelta.toFixed(3)})`);
|
|
7117
|
+
if (groundDelta !== null && groundDelta <= GROUND_SHARED) shared.push(`ground (\u0394E ${groundDelta.toFixed(3)})`);
|
|
7118
|
+
if (a.display && b.display && same(normaliseFamily(a.display), normaliseFamily(b.display))) shared.push(`${normaliseFamily(a.display)} headline`);
|
|
7119
|
+
if (a.body && b.body && same(normaliseFamily(a.body), normaliseFamily(b.body))) shared.push(`${normaliseFamily(a.body)} body`);
|
|
7120
|
+
if (same(a.shape, b.shape)) shared.push(`shape "${a.shape}"`);
|
|
7121
|
+
let orderDistance = null;
|
|
7122
|
+
if (a.order?.length && b.order?.length) {
|
|
7123
|
+
orderDistance = levenshtein(a.order, b.order) / Math.max(a.order.length, b.order.length);
|
|
7124
|
+
if (orderDistance <= SHARED_PART) shared.push(orderDistance === 0 ? "the same section order" : "nearly the same section order");
|
|
7125
|
+
}
|
|
7126
|
+
if (same(a.primaryAction, b.primaryAction) && [...a.positions ?? []].sort().join(",") === [...b.positions ?? []].sort().join(",")) shared.push(`"${a.primaryAction}" asked in the same places`);
|
|
7127
|
+
const surface = shared.filter((s) => /^(?:accent|ground|shape)|headline$/.test(s)).length;
|
|
7128
|
+
const flagged = surface >= 2 || orderDistance !== null && orderDistance <= SHARED_PART;
|
|
7129
|
+
return { shared, accentDelta, groundDelta, orderDistance, flagged };
|
|
7130
|
+
}
|
|
7131
|
+
function compareToDeclared(target2, sites, exclude) {
|
|
7132
|
+
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));
|
|
7133
|
+
}
|
|
7134
|
+
function formatDirectionMatches(label, matches) {
|
|
7135
|
+
const lines = [`${label}: declared directions in the estate (ranked, not judged: no sibling line yet)`];
|
|
7136
|
+
for (const m of matches) lines.push(` ${m.id.padEnd(20)} ${m.flagged ? "LOOK " : " "}${m.shared.length ? `shares ${m.shared.join(", ")}` : "nothing shared"}`);
|
|
7137
|
+
if (matches.length === 0) lines.push(" no site in the register has a declared direction yet: craft estate add --direction <file>");
|
|
7138
|
+
return lines.join("\n");
|
|
7139
|
+
}
|
|
6482
7140
|
|
|
6483
7141
|
// src/direction/licences.ts
|
|
6484
7142
|
var LICENCES = [
|
|
@@ -6592,13 +7250,6 @@ function licencesJson() {
|
|
|
6592
7250
|
}
|
|
6593
7251
|
|
|
6594
7252
|
// 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
7253
|
function tellFor(key, value) {
|
|
6603
7254
|
if (key === "accent") {
|
|
6604
7255
|
const c = parseColour(value);
|
|
@@ -6615,44 +7266,58 @@ function tellFor(key, value) {
|
|
|
6615
7266
|
}
|
|
6616
7267
|
return null;
|
|
6617
7268
|
}
|
|
7269
|
+
var MIN_TOKENS = 5;
|
|
7270
|
+
var emptyLayers = () => ({ job: false, hierarchy: [], tokens: { decided: 0, total: CHOICE_KEYS.length }, complete: false });
|
|
6618
7271
|
function validateDirection(input, ctx = {}) {
|
|
6619
7272
|
const problems = [];
|
|
6620
7273
|
const err = (at, message) => problems.push({ severity: "error", at, message });
|
|
6621
7274
|
const warn = (at, message) => problems.push({ severity: "warn", at, message });
|
|
6622
7275
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
6623
7276
|
err("", "art-direction.json must be a JSON object");
|
|
6624
|
-
return { valid: false, problems, decided: 0 };
|
|
7277
|
+
return { valid: false, problems, decided: 0, layers: emptyLayers() };
|
|
6625
7278
|
}
|
|
6626
7279
|
const d = input;
|
|
6627
7280
|
const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
|
|
6628
7281
|
if (legacy) {
|
|
6629
7282
|
warn("", "exceptions only: run craft direction init to record the choices and their reasons");
|
|
6630
|
-
return { valid: true, problems, decided: 0 };
|
|
7283
|
+
return { valid: true, problems, decided: 0, layers: emptyLayers() };
|
|
6631
7284
|
}
|
|
6632
|
-
if (d.version
|
|
7285
|
+
if (!DIRECTION_VERSIONS.includes(d.version)) err("version", `version must be ${DIRECTION_VERSIONS.join(" or ")}`);
|
|
7286
|
+
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
7287
|
if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
|
|
6634
7288
|
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
|
|
7289
|
+
const rawSources = Array.isArray(d.sources) ? d.sources : [];
|
|
7290
|
+
const sources = rawSources.filter((s) => Boolean(s && typeof s === "object" && !Array.isArray(s)));
|
|
6636
7291
|
if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
|
|
6637
7292
|
if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
|
|
6638
7293
|
const ids = /* @__PURE__ */ new Set();
|
|
6639
|
-
|
|
7294
|
+
rawSources.forEach((s, i) => {
|
|
6640
7295
|
const at = `sources[${i}]`;
|
|
6641
|
-
if (!s || typeof s !== "object") return err(at, "a source must be an object");
|
|
7296
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) return err(at, "a source must be an object");
|
|
6642
7297
|
if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
|
|
6643
7298
|
else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
|
|
6644
7299
|
else ids.add(s.id);
|
|
6645
7300
|
if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
|
|
6646
7301
|
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
|
|
6648
|
-
|
|
7302
|
+
if (s.path !== void 0 && typeof s.path !== "string") err(`${at}.path`, "a path must be text");
|
|
7303
|
+
if (typeof s.path === "string" && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
|
|
7304
|
+
if (s.colours !== void 0 && !Array.isArray(s.colours)) err(`${at}.colours`, "colours must be a list");
|
|
7305
|
+
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`);
|
|
7306
|
+
if (s.kind === "reference") {
|
|
7307
|
+
if (typeof s.category !== "string" || !s.category.trim()) err(`${at}.category`, "say where the reference comes from: print, a place, another trade");
|
|
7308
|
+
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 : "")) {
|
|
7309
|
+
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.`);
|
|
7310
|
+
}
|
|
7311
|
+
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");
|
|
7312
|
+
}
|
|
7313
|
+
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
7314
|
});
|
|
6650
7315
|
const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
|
|
6651
7316
|
if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
|
|
6652
7317
|
const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
|
|
6653
7318
|
let decided = 0;
|
|
6654
7319
|
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(", ")}.
|
|
7320
|
+
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
7321
|
}
|
|
6657
7322
|
for (const key of CHOICE_KEYS) {
|
|
6658
7323
|
const c = choices[key];
|
|
@@ -6669,44 +7334,9 @@ function validateDirection(input, ctx = {}) {
|
|
|
6669
7334
|
err(`${at}.value`, `"${c.value}" is not a colour`);
|
|
6670
7335
|
ok = false;
|
|
6671
7336
|
}
|
|
6672
|
-
const
|
|
6673
|
-
|
|
6674
|
-
if (
|
|
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
|
-
}
|
|
7337
|
+
const reasons = checkReason({ at, because: c.because, evidence: c.evidence, sources, tie: "source" });
|
|
7338
|
+
problems.push(...reasons);
|
|
7339
|
+
if (reasons.length) ok = false;
|
|
6710
7340
|
if (typeof c.value === "string") {
|
|
6711
7341
|
const tell = tellFor(key, c.value);
|
|
6712
7342
|
if (tell && !exceptions.some((e) => e.tell === tell)) {
|
|
@@ -6742,7 +7372,33 @@ function validateDirection(input, ctx = {}) {
|
|
|
6742
7372
|
}
|
|
6743
7373
|
if (ok) decided += 1;
|
|
6744
7374
|
}
|
|
6745
|
-
|
|
7375
|
+
let job = false;
|
|
7376
|
+
let hierarchy = [];
|
|
7377
|
+
if (d.version === 2) {
|
|
7378
|
+
const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
|
|
7379
|
+
problems.push(...j.problems);
|
|
7380
|
+
job = j.decided && d.job !== void 0;
|
|
7381
|
+
const h = validateHierarchy(d.hierarchy, d.job, sources);
|
|
7382
|
+
problems.push(...h.problems);
|
|
7383
|
+
hierarchy = h.pages;
|
|
7384
|
+
if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
|
|
7385
|
+
}
|
|
7386
|
+
if (ctx.estate?.length) {
|
|
7387
|
+
for (const m of compareToDeclared(summariseDirection(d), ctx.estate, ctx.estateId)) {
|
|
7388
|
+
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.`);
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
if (ctx.strict) {
|
|
7392
|
+
for (const p of problems) if (p.severity === "warn" && /the page (?:does not show|sets|runs)/.test(p.message)) p.severity = "error";
|
|
7393
|
+
}
|
|
7394
|
+
const home = hierarchy.find((p) => p.page === "home");
|
|
7395
|
+
const complete = job && Boolean(home) && hierarchy.every((p) => p.decided === p.total) && decided >= MIN_TOKENS;
|
|
7396
|
+
return {
|
|
7397
|
+
valid: problems.every((p) => p.severity !== "error"),
|
|
7398
|
+
problems,
|
|
7399
|
+
decided,
|
|
7400
|
+
layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
|
|
7401
|
+
};
|
|
6746
7402
|
}
|
|
6747
7403
|
|
|
6748
7404
|
// src/direction/init.ts
|
|
@@ -6761,11 +7417,31 @@ function initDirection(input) {
|
|
|
6761
7417
|
body: empty(fp.body.family),
|
|
6762
7418
|
...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
7419
|
} : {};
|
|
7420
|
+
const part = () => ({ value: "", evidence: [] });
|
|
6764
7421
|
return {
|
|
6765
7422
|
$schema: "https://unpkg.com/@domandigital/craft/art-direction.schema.json",
|
|
6766
7423
|
version: DIRECTION_VERSION,
|
|
6767
7424
|
client: input.client ?? "",
|
|
6768
7425
|
brief: input.brief ?? "",
|
|
7426
|
+
// Empty on purpose, like the reasons: the first validate lists the job and the structure as undecided.
|
|
7427
|
+
job: {
|
|
7428
|
+
statement: { verb: "", object: "", context: "" },
|
|
7429
|
+
functional: part(),
|
|
7430
|
+
emotional: part(),
|
|
7431
|
+
social: part(),
|
|
7432
|
+
consideration: { value: "low", because: "", evidence: [] },
|
|
7433
|
+
objections: [],
|
|
7434
|
+
language: [],
|
|
7435
|
+
evidence: []
|
|
7436
|
+
},
|
|
7437
|
+
hierarchy: {
|
|
7438
|
+
home: {
|
|
7439
|
+
primaryAction: { value: "", positions: [], because: "" },
|
|
7440
|
+
order: fp?.sections?.length ? fp.sections.map((s) => ({ role: s.role, because: "" })) : [],
|
|
7441
|
+
lead: { value: "", because: "" },
|
|
7442
|
+
journey: []
|
|
7443
|
+
}
|
|
7444
|
+
},
|
|
6769
7445
|
sources: [],
|
|
6770
7446
|
choices,
|
|
6771
7447
|
...input.exceptions?.length ? { exceptions: input.exceptions } : {}
|
|
@@ -6773,7 +7449,6 @@ function initDirection(input) {
|
|
|
6773
7449
|
}
|
|
6774
7450
|
|
|
6775
7451
|
// src/direction/propose.ts
|
|
6776
|
-
var PROPOSED = "PROPOSED:";
|
|
6777
7452
|
function srgbToLinear2(c) {
|
|
6778
7453
|
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
6779
7454
|
}
|
|
@@ -6835,7 +7510,13 @@ function accentCandidates(input) {
|
|
|
6835
7510
|
const parsed = parseColour(c);
|
|
6836
7511
|
if (!parsed || !chromatic(parsed.oklch)) continue;
|
|
6837
7512
|
const o = parsed.oklch;
|
|
6838
|
-
const siblings =
|
|
7513
|
+
const siblings = [
|
|
7514
|
+
...(input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 })),
|
|
7515
|
+
...(input.declared ?? []).flatMap((e) => {
|
|
7516
|
+
const p = parseColour(e.accent);
|
|
7517
|
+
return p ? [{ id: `${e.id} (declared)`, d: deltaEOk(o, p.oklch) }] : [];
|
|
7518
|
+
})
|
|
7519
|
+
];
|
|
6839
7520
|
const nearest2 = siblings.sort((a, b) => a.d - b.d)[0];
|
|
6840
7521
|
const penalty = isAiViolet(o) ? 1 : 0;
|
|
6841
7522
|
out.push({
|
|
@@ -6902,7 +7583,7 @@ function addSite(register, site) {
|
|
|
6902
7583
|
}
|
|
6903
7584
|
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
6904
7585
|
const out = [];
|
|
6905
|
-
const close = (k) => d.parts[k] <=
|
|
7586
|
+
const close = (k) => d.parts[k] <= SHARED_PART;
|
|
6906
7587
|
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
6907
7588
|
if (close("type")) {
|
|
6908
7589
|
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
@@ -6917,14 +7598,14 @@ function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
|
6917
7598
|
}
|
|
6918
7599
|
function compareToEstate(target2, register, options = {}) {
|
|
6919
7600
|
const at = options.siblingAt ?? SIBLING_AT;
|
|
6920
|
-
return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
|
|
7601
|
+
return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
|
|
6921
7602
|
const d = fingerprintDistance(target2, s.fingerprint);
|
|
6922
7603
|
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
|
|
6923
7604
|
}).sort((a, b) => a.distance - b.distance);
|
|
6924
7605
|
}
|
|
6925
7606
|
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
6926
7607
|
const out = [];
|
|
6927
|
-
const s = register.sites;
|
|
7608
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
6928
7609
|
for (let i = 0; i < s.length; i += 1) {
|
|
6929
7610
|
for (let j = i + 1; j < s.length; j += 1) {
|
|
6930
7611
|
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
@@ -7044,8 +7725,16 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
|
|
|
7044
7725
|
}
|
|
7045
7726
|
if (input.direction) {
|
|
7046
7727
|
const d = input.direction;
|
|
7047
|
-
const
|
|
7048
|
-
|
|
7728
|
+
const layers = d.layers;
|
|
7729
|
+
const raised2 = !d.valid || d.decided < MIN_DECIDED || !layers.complete;
|
|
7730
|
+
const pages = layers.hierarchy.map((p) => `${p.page} ${p.decided} of ${p.total}`).join(", ");
|
|
7731
|
+
const detail = [
|
|
7732
|
+
`job ${layers.job ? "decided" : "not decided"}`,
|
|
7733
|
+
`hierarchy ${pages || "not decided"}`,
|
|
7734
|
+
`${d.decided} of 7 choices decided with a reason`
|
|
7735
|
+
].join("; ");
|
|
7736
|
+
signals.push({ id: "reasons", raised: raised2, detail: `${detail}${d.valid ? "" : ", art-direction.json has errors"}` });
|
|
7737
|
+
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
7738
|
for (const p of d.problems) {
|
|
7050
7739
|
const key = p.at.match(/^choices\.(\w+)/)?.[1];
|
|
7051
7740
|
if (p.severity === "error" || p.message === "not decided yet") {
|
|
@@ -7063,11 +7752,11 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
|
|
|
7063
7752
|
const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
|
|
7064
7753
|
const merged = [];
|
|
7065
7754
|
for (const a of actions) {
|
|
7066
|
-
const
|
|
7067
|
-
if (
|
|
7755
|
+
const same2 = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
|
|
7756
|
+
if (same2) {
|
|
7068
7757
|
const fromTell = /\(\S+, gen \d\)/.test(a.why);
|
|
7069
|
-
|
|
7070
|
-
|
|
7758
|
+
same2.why = `${same2.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
|
|
7759
|
+
same2.priority = Math.min(same2.priority, a.priority);
|
|
7071
7760
|
} else merged.push({ ...a });
|
|
7072
7761
|
}
|
|
7073
7762
|
const unique2 = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
|
|
@@ -7145,6 +7834,8 @@ function retrofitPlan(report2, direction) {
|
|
|
7145
7834
|
}
|
|
7146
7835
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7147
7836
|
0 && (module.exports = {
|
|
7837
|
+
ACCENT_SCALE,
|
|
7838
|
+
ACCENT_SHARED,
|
|
7148
7839
|
AI_PHRASES,
|
|
7149
7840
|
AI_SOURCE_TYPES,
|
|
7150
7841
|
AI_VIOLET,
|
|
@@ -7155,10 +7846,14 @@ function retrofitPlan(report2, direction) {
|
|
|
7155
7846
|
CATALOGUE_VERSION,
|
|
7156
7847
|
CHOICE_KEYS,
|
|
7157
7848
|
COMPONENT_ROLES,
|
|
7849
|
+
CONSIDERATION,
|
|
7850
|
+
CONVENTION,
|
|
7851
|
+
CUSTOMER_KINDS,
|
|
7158
7852
|
DEFAULT_IGNORE,
|
|
7159
7853
|
DENSITIES,
|
|
7160
7854
|
DENSITY_NAMES,
|
|
7161
7855
|
DIRECTION_VERSION,
|
|
7856
|
+
DIRECTION_VERSIONS,
|
|
7162
7857
|
DISTANCE_WEIGHTS,
|
|
7163
7858
|
DURATION_MS,
|
|
7164
7859
|
DURATION_S,
|
|
@@ -7168,6 +7863,10 @@ function retrofitPlan(report2, direction) {
|
|
|
7168
7863
|
ESTATE_VERSION,
|
|
7169
7864
|
EXIT_RATIO,
|
|
7170
7865
|
FINGERPRINT_VERSION,
|
|
7866
|
+
FUNCTION_WORDS,
|
|
7867
|
+
GROUND_SCALE,
|
|
7868
|
+
GROUND_SHARED,
|
|
7869
|
+
HIERARCHY_FIELDS,
|
|
7171
7870
|
HOME_FILE,
|
|
7172
7871
|
HOME_URL,
|
|
7173
7872
|
HOUSE,
|
|
@@ -7180,10 +7879,13 @@ function retrofitPlan(report2, direction) {
|
|
|
7180
7879
|
LIGHTNESS_CURVE,
|
|
7181
7880
|
MIN_DECIDED,
|
|
7182
7881
|
MIN_RUNS,
|
|
7882
|
+
MOOD,
|
|
7183
7883
|
NEGATIVE_REASSURANCE,
|
|
7184
7884
|
NULL_VERSION,
|
|
7885
|
+
PAGE_TYPES,
|
|
7185
7886
|
PILL_LIMIT,
|
|
7186
7887
|
PLAINER_WORDS,
|
|
7888
|
+
PREFERENCE,
|
|
7187
7889
|
PROMO_BADGE,
|
|
7188
7890
|
PROOF_NAMES,
|
|
7189
7891
|
PROPOSED,
|
|
@@ -7197,8 +7899,10 @@ function retrofitPlan(report2, direction) {
|
|
|
7197
7899
|
REVEAL_LIMIT,
|
|
7198
7900
|
REVIEW_PHRASES,
|
|
7199
7901
|
SCALE,
|
|
7902
|
+
SECTION_ROLES,
|
|
7200
7903
|
SHADCN_LIMIT,
|
|
7201
7904
|
SHADCN_PRIMITIVES,
|
|
7905
|
+
SHARED_PART,
|
|
7202
7906
|
SIBLING_AT,
|
|
7203
7907
|
SNAPSHOT_VERSION,
|
|
7204
7908
|
SOURCE_KINDS,
|
|
@@ -7227,21 +7931,28 @@ function retrofitPlan(report2, direction) {
|
|
|
7227
7931
|
catalogueTable,
|
|
7228
7932
|
characterReport,
|
|
7229
7933
|
checkCopy,
|
|
7934
|
+
checkOrder,
|
|
7230
7935
|
checkPair,
|
|
7936
|
+
checkReason,
|
|
7231
7937
|
checkRestraint,
|
|
7232
7938
|
choicesOf,
|
|
7939
|
+
clauseDepth,
|
|
7233
7940
|
closestComponents,
|
|
7234
7941
|
compareCompetitor,
|
|
7942
|
+
compareDirections,
|
|
7235
7943
|
compareFacts,
|
|
7944
|
+
compareToDeclared,
|
|
7236
7945
|
compareToEstate,
|
|
7237
7946
|
componentDistance,
|
|
7238
7947
|
componentShared,
|
|
7239
7948
|
componentTypicalities,
|
|
7240
7949
|
componentTypicality,
|
|
7241
7950
|
componentsOf,
|
|
7951
|
+
contentWords,
|
|
7242
7952
|
countWords,
|
|
7243
7953
|
craftTokens,
|
|
7244
7954
|
createBaseline,
|
|
7955
|
+
cv,
|
|
7245
7956
|
deltaEOk,
|
|
7246
7957
|
deltaEOkHex,
|
|
7247
7958
|
densityCss,
|
|
@@ -7265,11 +7976,14 @@ function retrofitPlan(report2, direction) {
|
|
|
7265
7976
|
formatClaims,
|
|
7266
7977
|
formatComparison,
|
|
7267
7978
|
formatCompetitor,
|
|
7979
|
+
formatDirectionMatches,
|
|
7268
7980
|
formatHex,
|
|
7269
7981
|
formatReport,
|
|
7270
7982
|
globToRegExp,
|
|
7271
7983
|
groundName,
|
|
7272
7984
|
harvest,
|
|
7985
|
+
hedgeCount,
|
|
7986
|
+
hedgedClose,
|
|
7273
7987
|
hexToOklch,
|
|
7274
7988
|
holdHouseBlocks,
|
|
7275
7989
|
houseRule,
|
|
@@ -7280,28 +7994,37 @@ function retrofitPlan(report2, direction) {
|
|
|
7280
7994
|
inSrgbGamut,
|
|
7281
7995
|
initDirection,
|
|
7282
7996
|
isAiViolet,
|
|
7997
|
+
isBlurb,
|
|
7998
|
+
isCategoryLabel,
|
|
7283
7999
|
isCream,
|
|
8000
|
+
jobWords,
|
|
7284
8001
|
layoutDistance,
|
|
7285
8002
|
licencesJson,
|
|
7286
8003
|
makeSnapshot,
|
|
7287
8004
|
motionTokens,
|
|
7288
8005
|
namedSources,
|
|
7289
8006
|
nearest,
|
|
8007
|
+
nominalisations,
|
|
7290
8008
|
normaliseFamily,
|
|
7291
8009
|
nullPrompt,
|
|
7292
8010
|
oklchToHex,
|
|
7293
8011
|
oklchToRgb,
|
|
7294
8012
|
pageCopy,
|
|
7295
8013
|
paletteFromPixels,
|
|
8014
|
+
paragraphUniformity,
|
|
7296
8015
|
parseBaseline,
|
|
7297
8016
|
parseColour,
|
|
7298
8017
|
parseCraftConfig,
|
|
7299
8018
|
parseHex,
|
|
8019
|
+
participialClauses,
|
|
8020
|
+
phrasalCoordinations,
|
|
7300
8021
|
propose,
|
|
7301
8022
|
protectedFacts,
|
|
7302
8023
|
ramp,
|
|
7303
8024
|
rampFromAnchors,
|
|
7304
8025
|
readSnapshot,
|
|
8026
|
+
renderedOrder,
|
|
8027
|
+
researchPrompt,
|
|
7305
8028
|
retrofitPlan,
|
|
7306
8029
|
runTell,
|
|
7307
8030
|
scanProvenance,
|
|
@@ -7310,24 +8033,36 @@ function retrofitPlan(report2, direction) {
|
|
|
7310
8033
|
semantic,
|
|
7311
8034
|
sentencesOf,
|
|
7312
8035
|
severityExceptions,
|
|
8036
|
+
shapeMoves,
|
|
7313
8037
|
shapeName,
|
|
8038
|
+
shapeOf,
|
|
8039
|
+
shapeWords,
|
|
7314
8040
|
sharedParts,
|
|
7315
8041
|
shouldAnimate,
|
|
7316
8042
|
siblingLine,
|
|
8043
|
+
skeleton,
|
|
8044
|
+
skeletonRepeat,
|
|
7317
8045
|
specificity,
|
|
7318
8046
|
specifics,
|
|
8047
|
+
splitSentences,
|
|
8048
|
+
stackedConditional,
|
|
7319
8049
|
stockAvatarSource,
|
|
7320
8050
|
stockSource,
|
|
8051
|
+
summariseDirection,
|
|
7321
8052
|
tailwindV3Preset,
|
|
7322
8053
|
tailwindV4Theme,
|
|
7323
8054
|
tellById,
|
|
7324
8055
|
toGamut,
|
|
7325
8056
|
toJson,
|
|
7326
8057
|
toSarif,
|
|
8058
|
+
tradeOf,
|
|
8059
|
+
tricolons,
|
|
7327
8060
|
typeFeatureTokens,
|
|
7328
8061
|
typicality,
|
|
7329
8062
|
unprovenClaims,
|
|
7330
8063
|
validateDirection,
|
|
8064
|
+
validateHierarchy,
|
|
8065
|
+
validateJob,
|
|
7331
8066
|
visibleText,
|
|
7332
8067
|
visualMeasures,
|
|
7333
8068
|
wcagContrast,
|