@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/CHANGELOG.md +25 -0
- package/CHARACTER.md +247 -28
- package/README.md +19 -1
- package/art-direction.schema.json +428 -25
- package/dist/{audit-6WHR7ZTV.js → audit-UJCUWORL.js} +2 -2
- package/dist/audit.cjs +5 -0
- package/dist/audit.js +5 -0
- package/dist/{chunk-GQVEYTBY.js → chunk-B2MJLGF6.js} +14 -3
- package/dist/{chunk-PBK6SYH5.js → chunk-CVG5GSMN.js} +46 -11
- package/dist/{chunk-XVBR2UPH.js → chunk-LWVGIJ5A.js} +175 -37
- package/dist/{chunk-3OTKMKJQ.js → chunk-LXDNIJFG.js} +36 -15
- package/dist/{cli-WG3UJNUF.js → chunk-NTIPJ5U7.js} +12 -172
- package/dist/chunk-OQXTQQAU.js +27 -0
- package/dist/{chunk-7T5WR74C.js → chunk-TQKDCA4V.js} +4 -4
- package/dist/chunk-WAV6AKR4.js +711 -0
- package/dist/{chunk-Q7ILL2HV.js → chunk-YK2FFL6Z.js} +1 -1
- package/dist/cli-3GQROFCB.js +241 -0
- package/dist/{cli-WSHPSWVU.js → cli-52M62QBN.js} +6 -5
- package/dist/{cli-Z2IQG3LC.js → cli-AVYNHBFJ.js} +12 -5
- package/dist/cli-CWORJYOU.js +190 -0
- package/dist/{cli-O3CDSONO.js → cli-K4LSRYBE.js} +86 -20
- package/dist/{cli-3CGUKXY4.js → cli-PEATEW57.js} +6 -4
- package/dist/cli.js +2 -2
- package/dist/generate.js +6 -0
- package/dist/index.cjs +1142 -208
- package/dist/index.d.cts +603 -57
- package/dist/index.d.ts +603 -57
- package/dist/index.js +1084 -208
- 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,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
|
|
763
|
+
const lower2 = Math.min(minRem, maxRem);
|
|
706
764
|
const upper = Math.max(minRem, maxRem);
|
|
707
|
-
return `clamp(${
|
|
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
|
|
1030
|
+
const pattern2 = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
|
|
973
1031
|
while (i < css.length) {
|
|
974
|
-
|
|
975
|
-
const match =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1111
|
+
const pattern2 = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
|
|
1054
1112
|
const seen = /* @__PURE__ */ new Set();
|
|
1055
|
-
for (const match of css.matchAll(
|
|
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(
|
|
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
|
|
1481
|
-
const global = new RegExp(
|
|
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 = (
|
|
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
|
|
2340
|
-
if (
|
|
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
|
|
2485
|
-
const
|
|
2486
|
-
const
|
|
2487
|
-
if (
|
|
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 ${
|
|
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
|
|
2506
|
-
if (
|
|
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 =
|
|
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 (${
|
|
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 <=
|
|
2517
|
-
const run =
|
|
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
|
|
2575
|
-
if (
|
|
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,
|
|
2578
|
-
if (opening.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(
|
|
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 <
|
|
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,
|
|
2869
|
-
return bases(unit).some((c) =>
|
|
2926
|
+
function has(unit, pattern2) {
|
|
2927
|
+
return bases(unit).some((c) => pattern2.test(c));
|
|
2870
2928
|
}
|
|
2871
|
-
function find(unit,
|
|
2872
|
-
return bases(unit).find((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,
|
|
2889
|
-
const m =
|
|
2890
|
-
|
|
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
|
|
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,
|
|
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
|
|
3987
|
-
if (looksLikeCode(
|
|
3988
|
-
return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(
|
|
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(
|
|
3994
|
-
return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(
|
|
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,
|
|
4179
|
-
for (const m of [...rest.matchAll(
|
|
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
|
|
4398
|
-
const letters =
|
|
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
|
|
4409
|
-
if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(
|
|
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
|
|
4490
|
+
const words3 = f4.value.split(/\s+/);
|
|
4433
4491
|
const named = (w) => !NOT_NAMES.has(w.toLowerCase()) && w !== "&";
|
|
4434
|
-
const first =
|
|
4492
|
+
const first = words3.findIndex(named);
|
|
4435
4493
|
if (first === -1) continue;
|
|
4436
|
-
const last =
|
|
4437
|
-
add("name",
|
|
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
|
|
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:
|
|
4519
|
+
words: words3,
|
|
4462
4520
|
specifics: list,
|
|
4463
4521
|
hard,
|
|
4464
4522
|
trade,
|
|
4465
|
-
perHundred:
|
|
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 = (
|
|
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
|
|
4939
|
+
const share2 = font.chars / total;
|
|
4882
4940
|
const displayShare = font.displayChars / display;
|
|
4883
|
-
if (
|
|
4941
|
+
if (share2 >= 0.15 || displayShare >= 0.3) {
|
|
4884
4942
|
seen.add(name);
|
|
4885
|
-
hits.push(hit(s, `${name} sets ${pct(
|
|
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) /
|
|
5176
|
-
const ground = clamp(deltaEOk(a.ground, b.ground) /
|
|
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
|
|
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 <=
|
|
5314
|
-
const g =
|
|
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
|
|
6076
|
-
while (
|
|
6077
|
-
return
|
|
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((
|
|
6084
|
-
for (const m of sentence.matchAll(
|
|
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
|
|
6617
|
+
const pattern2 = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
|
|
6271
6618
|
let body = "";
|
|
6272
|
-
for (let i = 0; i <
|
|
6273
|
-
const c =
|
|
6274
|
-
if (c === "*" &&
|
|
6275
|
-
const slash =
|
|
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 = !
|
|
6629
|
+
const anywhere = !pattern2.includes("/");
|
|
6283
6630
|
return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
|
|
6284
6631
|
}
|
|
6285
|
-
function ignoreMatcher(
|
|
6286
|
-
const res =
|
|
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
|
|
6800
|
+
const round3 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
6454
6801
|
return {
|
|
6455
|
-
colourfulness:
|
|
6456
|
-
edgeDensity:
|
|
6457
|
-
symmetry:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
6648
|
-
|
|
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(", ")}.
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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 =
|
|
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
|
|
7048
|
-
|
|
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
|
|
7067
|
-
if (
|
|
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
|
-
|
|
7070
|
-
|
|
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
|
});
|