@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.js
CHANGED
|
@@ -490,9 +490,9 @@ function fluidClamp(minPx, maxPx, minViewport, maxViewport, rootPx = 16) {
|
|
|
490
490
|
const interceptRem = round((minPx - slope * minViewport) / rootPx);
|
|
491
491
|
const slopeVw = round(slope * 100);
|
|
492
492
|
const preferred = interceptRem === 0 ? `${slopeVw}vw` : `${interceptRem}rem + ${slopeVw}vw`;
|
|
493
|
-
const
|
|
493
|
+
const lower2 = Math.min(minRem, maxRem);
|
|
494
494
|
const upper = Math.max(minRem, maxRem);
|
|
495
|
-
return `clamp(${
|
|
495
|
+
return `clamp(${lower2}rem, ${preferred}, ${upper}rem)`;
|
|
496
496
|
}
|
|
497
497
|
function fluidType(options = {}) {
|
|
498
498
|
const {
|
|
@@ -757,10 +757,10 @@ function stripLayers(css, ignored) {
|
|
|
757
757
|
const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
|
|
758
758
|
let out = "";
|
|
759
759
|
let i = 0;
|
|
760
|
-
const
|
|
760
|
+
const pattern2 = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
|
|
761
761
|
while (i < css.length) {
|
|
762
|
-
|
|
763
|
-
const match =
|
|
762
|
+
pattern2.lastIndex = i;
|
|
763
|
+
const match = pattern2.exec(css);
|
|
764
764
|
if (match === null) {
|
|
765
765
|
out += css.slice(i);
|
|
766
766
|
break;
|
|
@@ -790,12 +790,12 @@ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
790
790
|
]);
|
|
791
791
|
function atRuleBodies(css, prelude) {
|
|
792
792
|
const out = [];
|
|
793
|
-
const
|
|
793
|
+
const pattern2 = new RegExp(
|
|
794
794
|
prelude.source,
|
|
795
795
|
prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
|
|
796
796
|
);
|
|
797
797
|
let match;
|
|
798
|
-
while ((match =
|
|
798
|
+
while ((match = pattern2.exec(css)) !== null) {
|
|
799
799
|
const start = match.index + match[0].length;
|
|
800
800
|
let depth = 1;
|
|
801
801
|
let i = start;
|
|
@@ -805,7 +805,7 @@ function atRuleBodies(css, prelude) {
|
|
|
805
805
|
i += 1;
|
|
806
806
|
}
|
|
807
807
|
out.push(css.slice(start, i - 1));
|
|
808
|
-
|
|
808
|
+
pattern2.lastIndex = i;
|
|
809
809
|
}
|
|
810
810
|
return out;
|
|
811
811
|
}
|
|
@@ -838,9 +838,9 @@ function isVarOnlyList(value) {
|
|
|
838
838
|
return parts2.length > 1 && parts2.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
|
|
839
839
|
}
|
|
840
840
|
function distinctValues(css, property, props, unresolved) {
|
|
841
|
-
const
|
|
841
|
+
const pattern2 = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
|
|
842
842
|
const seen = /* @__PURE__ */ new Set();
|
|
843
|
-
for (const match of css.matchAll(
|
|
843
|
+
for (const match of css.matchAll(pattern2)) {
|
|
844
844
|
const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
|
|
845
845
|
if (CSS_WIDE_KEYWORDS.has(raw)) continue;
|
|
846
846
|
const value = unwrapShadowColor(raw);
|
|
@@ -1259,14 +1259,14 @@ function linkSpans(text) {
|
|
|
1259
1259
|
return [...text.matchAll(LINK)].map((m) => [m.index ?? 0, (m.index ?? 0) + m[0].length]);
|
|
1260
1260
|
}
|
|
1261
1261
|
var insideLink = (spans, at) => spans.some(([a, b]) => at >= a && at < b);
|
|
1262
|
-
function phrases(
|
|
1262
|
+
function phrases(patterns2, label, source = (ctx) => ctx.blocks, skip = () => false) {
|
|
1263
1263
|
return (ctx) => {
|
|
1264
1264
|
const hits = [];
|
|
1265
1265
|
for (const block of source(ctx)) {
|
|
1266
1266
|
const spans = [];
|
|
1267
1267
|
const links = linkSpans(block.text);
|
|
1268
|
-
for (const
|
|
1269
|
-
const global = new RegExp(
|
|
1268
|
+
for (const pattern2 of patterns2) {
|
|
1269
|
+
const global = new RegExp(pattern2.source, pattern2.flags.includes("g") ? pattern2.flags : `${pattern2.flags}g`);
|
|
1270
1270
|
for (const m of block.text.matchAll(global)) {
|
|
1271
1271
|
if (skip(block, m[0], m.index ?? 0)) continue;
|
|
1272
1272
|
if (insideLink(links, m.index ?? 0)) continue;
|
|
@@ -1286,7 +1286,7 @@ function phrases(patterns, label, source = (ctx) => ctx.blocks, skip = () => fal
|
|
|
1286
1286
|
};
|
|
1287
1287
|
}
|
|
1288
1288
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/'/g, "['\u2019]?").replace(/ /g, "\\s+");
|
|
1289
|
-
var wordList = (
|
|
1289
|
+
var wordList = (words3) => new RegExp(`\\b(?:${[...words3].sort((a, b) => b.length - a.length).map(escape).join("|")})\\b`, "i");
|
|
1290
1290
|
var quoted = (m) => `"${m}"`;
|
|
1291
1291
|
var AI_WORDS = [
|
|
1292
1292
|
"tapestry",
|
|
@@ -2124,8 +2124,8 @@ function documents(ctx) {
|
|
|
2124
2124
|
const docs = [];
|
|
2125
2125
|
for (const [path, blocks] of byPath) {
|
|
2126
2126
|
const text = blocks.map((b) => withoutHeadings(b.text)).join("\n");
|
|
2127
|
-
const
|
|
2128
|
-
if (
|
|
2127
|
+
const words3 = countWords(text);
|
|
2128
|
+
if (words3 >= MIN_WORDS) docs.push({ path, blocks, words: words3 });
|
|
2129
2129
|
}
|
|
2130
2130
|
return docs;
|
|
2131
2131
|
}
|
|
@@ -2269,14 +2269,14 @@ function rhythmHits(ctx) {
|
|
|
2269
2269
|
const list = sentences(doc2);
|
|
2270
2270
|
if (list.length < 40) continue;
|
|
2271
2271
|
const lengths = list.map((s) => countWords(s.text));
|
|
2272
|
-
const
|
|
2273
|
-
const
|
|
2274
|
-
const
|
|
2275
|
-
if (
|
|
2272
|
+
const mean2 = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
2273
|
+
const sd2 = Math.sqrt(lengths.reduce((a, b) => a + (b - mean2) ** 2, 0) / lengths.length);
|
|
2274
|
+
const cv2 = sd2 / mean2;
|
|
2275
|
+
if (cv2 < 0.42) {
|
|
2276
2276
|
hits.push({
|
|
2277
2277
|
path: doc2.path,
|
|
2278
2278
|
offset: list[0].offset,
|
|
2279
|
-
message: `${list.length} sentences averaging ${
|
|
2279
|
+
message: `${list.length} sentences averaging ${mean2.toFixed(1)} words with little variation (spread ${cv2.toFixed(2)}, reads metronomic below 0.42)`
|
|
2280
2280
|
});
|
|
2281
2281
|
}
|
|
2282
2282
|
}
|
|
@@ -2290,19 +2290,19 @@ function repeatHits(ctx) {
|
|
|
2290
2290
|
const whole = /* @__PURE__ */ new Map();
|
|
2291
2291
|
const runs = /* @__PURE__ */ new Map();
|
|
2292
2292
|
for (const s of sentences(doc2)) {
|
|
2293
|
-
const
|
|
2294
|
-
if (
|
|
2293
|
+
const words3 = normal(s.text);
|
|
2294
|
+
if (words3.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
2295
2295
|
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
2296
|
-
const key =
|
|
2296
|
+
const key = words3.join(" ");
|
|
2297
2297
|
const earlier = whole.get(key);
|
|
2298
2298
|
if (earlier) {
|
|
2299
|
-
hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${
|
|
2299
|
+
hits.push({ path: doc2.path, offset: s.offset, message: `the same sentence appears twice (${words3.length} words)`, excerpt: s.text.slice(0, 120) });
|
|
2300
2300
|
continue;
|
|
2301
2301
|
}
|
|
2302
2302
|
whole.set(key, s);
|
|
2303
2303
|
let reported = false;
|
|
2304
|
-
for (let i = 0; i + RUN <=
|
|
2305
|
-
const run =
|
|
2304
|
+
for (let i = 0; i + RUN <= words3.length && !reported; i += 1) {
|
|
2305
|
+
const run = words3.slice(i, i + RUN).join(" ");
|
|
2306
2306
|
const seen = runs.get(run);
|
|
2307
2307
|
if (seen && seen.key !== key) {
|
|
2308
2308
|
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) });
|
|
@@ -2359,11 +2359,11 @@ function headingEchoHits(ctx) {
|
|
|
2359
2359
|
const hits = [];
|
|
2360
2360
|
for (const { file } of longFiles(ctx)) {
|
|
2361
2361
|
for (const heading of headings(file)) {
|
|
2362
|
-
const
|
|
2363
|
-
if (
|
|
2362
|
+
const words3 = normal(heading.text);
|
|
2363
|
+
if (words3.length < 4) continue;
|
|
2364
2364
|
const after = file.text.slice(heading.end, heading.end + 600).trimStart().replace(/^\s*(?:<\/?(?:p|div|section)\b[^>]*>\s*)*/i, "");
|
|
2365
|
-
const opening = normal(after.split(/\n\s*\n/)[0] ?? "").slice(0,
|
|
2366
|
-
if (opening.join(" ") ===
|
|
2365
|
+
const opening = normal(after.split(/\n\s*\n/)[0] ?? "").slice(0, words3.length);
|
|
2366
|
+
if (opening.join(" ") === words3.join(" ")) {
|
|
2367
2367
|
hits.push({ path: file.path, offset: heading.offset, message: `the heading "${heading.text}" is repeated as the first words under it` });
|
|
2368
2368
|
}
|
|
2369
2369
|
}
|
|
@@ -2371,7 +2371,7 @@ function headingEchoHits(ctx) {
|
|
|
2371
2371
|
return hits;
|
|
2372
2372
|
}
|
|
2373
2373
|
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(" ");
|
|
2374
|
-
function filler(
|
|
2374
|
+
function filler(words3, seed = 1, options = {}) {
|
|
2375
2375
|
const { contractions = true, uniform } = options;
|
|
2376
2376
|
const shape = [5, 17, 9, 24, 6, 13, 31, 8, 11, 4, 20, 15];
|
|
2377
2377
|
let state = seed;
|
|
@@ -2382,7 +2382,7 @@ function filler(words2, seed = 1, options = {}) {
|
|
|
2382
2382
|
const out = [];
|
|
2383
2383
|
let count4 = 0;
|
|
2384
2384
|
let i = 0;
|
|
2385
|
-
while (count4 <
|
|
2385
|
+
while (count4 < words3) {
|
|
2386
2386
|
const length = uniform ?? shape[i % shape.length];
|
|
2387
2387
|
const s = [];
|
|
2388
2388
|
for (let w = 0; w < length; w += 1) s.push(FILLER_WORDS[next(FILLER_WORDS.length)]);
|
|
@@ -2653,11 +2653,11 @@ function baseClass(cls) {
|
|
|
2653
2653
|
function bases(unit) {
|
|
2654
2654
|
return unit.classes.map(baseClass);
|
|
2655
2655
|
}
|
|
2656
|
-
function has(unit,
|
|
2657
|
-
return bases(unit).some((c) =>
|
|
2656
|
+
function has(unit, pattern2) {
|
|
2657
|
+
return bases(unit).some((c) => pattern2.test(c));
|
|
2658
2658
|
}
|
|
2659
|
-
function find(unit,
|
|
2660
|
-
return bases(unit).find((c) =>
|
|
2659
|
+
function find(unit, pattern2) {
|
|
2660
|
+
return bases(unit).find((c) => pattern2.test(c));
|
|
2661
2661
|
}
|
|
2662
2662
|
function arbitrary(cls) {
|
|
2663
2663
|
const m = cls.match(/\[(.*)\]$/);
|
|
@@ -2673,9 +2673,9 @@ function eachUnit(ctx, fn) {
|
|
|
2673
2673
|
}
|
|
2674
2674
|
return hits;
|
|
2675
2675
|
}
|
|
2676
|
-
function firstMatch(text,
|
|
2677
|
-
const m =
|
|
2678
|
-
|
|
2676
|
+
function firstMatch(text, pattern2) {
|
|
2677
|
+
const m = pattern2.exec(text);
|
|
2678
|
+
pattern2.lastIndex = 0;
|
|
2679
2679
|
return m ? m.index : -1;
|
|
2680
2680
|
}
|
|
2681
2681
|
var FRAMEWORK_PALETTE = /^--(?:color-)?(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+$|^--tw-/;
|
|
@@ -3119,9 +3119,9 @@ function shadcnCardHits(ctx) {
|
|
|
3119
3119
|
}
|
|
3120
3120
|
function marqueeHits(ctx) {
|
|
3121
3121
|
const hits = [];
|
|
3122
|
-
const
|
|
3122
|
+
const pattern2 = /@keyframes\s+[\w-]*marquee|animate-(?:marquee|scroll-x|infinite-scroll)\b|<Marquee\b|["']react-fast-marquee["']|ui\/marquee["']|animation(?:-name)?\s*:\s*[^;]*marquee/i;
|
|
3123
3123
|
for (const file of ctx.files) {
|
|
3124
|
-
const at = firstMatch(file.text,
|
|
3124
|
+
const at = firstMatch(file.text, pattern2);
|
|
3125
3125
|
if (at !== -1) hits.push({ path: file.path, offset: at, message: "scrolling marquee" });
|
|
3126
3126
|
}
|
|
3127
3127
|
return hits;
|
|
@@ -3771,15 +3771,15 @@ var blank = (s) => s.replace(/[^\n]/g, " ");
|
|
|
3771
3771
|
function readsAsProse(value) {
|
|
3772
3772
|
if (value.length < 12 || !/\s/.test(value)) return false;
|
|
3773
3773
|
if (looksLikeClasses(value) || looksLikeAddress(value)) return false;
|
|
3774
|
-
const
|
|
3775
|
-
if (looksLikeCode(
|
|
3776
|
-
return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(
|
|
3774
|
+
const words3 = withoutMarkup(value);
|
|
3775
|
+
if (looksLikeCode(words3) || /[<>]/.test(words3)) return false;
|
|
3776
|
+
return /[a-zA-Z]{3,}\s+[a-zA-Z]{2,}/.test(words3);
|
|
3777
3777
|
}
|
|
3778
3778
|
function withoutMarkup(value) {
|
|
3779
3779
|
return value.replace(/<\/?[a-zA-Z][^<>]*>/g, " ").replace(/\$\{[^}]*\}/g, " ").replace(/\{\{[^}]*\}\}/g, " ").replace(/&(?:#\d+|#x[0-9a-f]+|[a-z]+);/gi, "'");
|
|
3780
3780
|
}
|
|
3781
|
-
function looksLikeCode(
|
|
3782
|
-
return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(
|
|
3781
|
+
function looksLikeCode(words3) {
|
|
3782
|
+
return /[{}]|;\s*(?:\n|$)|\s=\s|===?|=>|\b(?:const|let|var|function|import|export)\s/.test(words3);
|
|
3783
3783
|
}
|
|
3784
3784
|
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)$/;
|
|
3785
3785
|
function looksLikeClasses(value) {
|
|
@@ -3963,8 +3963,8 @@ function protectedFacts(text) {
|
|
|
3963
3963
|
const mask = (start, length) => {
|
|
3964
3964
|
rest = rest.slice(0, start) + " ".repeat(length) + rest.slice(start + length);
|
|
3965
3965
|
};
|
|
3966
|
-
for (const [kind,
|
|
3967
|
-
for (const m of [...rest.matchAll(
|
|
3966
|
+
for (const [kind, pattern2] of PATTERNS) {
|
|
3967
|
+
for (const m of [...rest.matchAll(pattern2)]) {
|
|
3968
3968
|
const value = m[0].replace(/[.,;:]+$/, "");
|
|
3969
3969
|
facts.push({ kind, value, key: `${kind}:${normalise(kind, value)}` });
|
|
3970
3970
|
mask(m.index ?? 0, m[0].length);
|
|
@@ -4182,8 +4182,8 @@ var termPattern = (term) => new RegExp(`(?<![\\w\xA3$\u20AC])${term.split(/\s+/)
|
|
|
4182
4182
|
var TRADE_PATTERNS = TRADE_NOUNS.map((t) => [t, termPattern(t)]);
|
|
4183
4183
|
var SMALL_WORDS = /* @__PURE__ */ new Set(["a", "an", "the", "and", "or", "of", "in", "on", "at", "to", "for", "by", "with", "from"]);
|
|
4184
4184
|
function caseless(line) {
|
|
4185
|
-
const
|
|
4186
|
-
const letters =
|
|
4185
|
+
const words3 = line.match(/[A-Za-z][A-Za-z’'-]*/g) ?? [];
|
|
4186
|
+
const letters = words3.filter((w) => w.length >= 2);
|
|
4187
4187
|
if (letters.length >= 3 && letters.filter((w) => w === w.toUpperCase()).length / letters.length >= 0.6) return "shout";
|
|
4188
4188
|
const content = letters.filter((w) => !SMALL_WORDS.has(w.toLowerCase()));
|
|
4189
4189
|
if (content.length >= 3 && content.filter((w) => /^[A-Z]/.test(w)).length / content.length >= 0.8) return "title";
|
|
@@ -4193,8 +4193,8 @@ var maskStock = (text) => STOCK_FIGURES.reduce((t, re) => t.replace(re, (m) => "
|
|
|
4193
4193
|
function briefTerms(brief) {
|
|
4194
4194
|
const terms = /* @__PURE__ */ new Set();
|
|
4195
4195
|
for (const w of brief.match(/[A-Za-z][A-Za-z’'&-]*/g) ?? []) {
|
|
4196
|
-
const
|
|
4197
|
-
if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(
|
|
4196
|
+
const lower2 = w.toLowerCase();
|
|
4197
|
+
if (/^[A-Z]/.test(w) && w.length >= 3 && !BRIEF_STOP.has(lower2) && !NOT_NAMES.has(lower2)) terms.add(lower2);
|
|
4198
4198
|
}
|
|
4199
4199
|
for (const [term, re] of TRADE_PATTERNS) if (re.test(brief)) terms.add(term);
|
|
4200
4200
|
for (const f4 of protectedFacts(maskStock(brief))) if (f4.kind !== "name") terms.add(f4.value.toLowerCase());
|
|
@@ -4217,12 +4217,12 @@ function specifics(text, brief) {
|
|
|
4217
4217
|
for (const acronym of f4.value.match(/\b[A-Z]{2,}\b/g) ?? []) add("name", acronym);
|
|
4218
4218
|
continue;
|
|
4219
4219
|
}
|
|
4220
|
-
const
|
|
4220
|
+
const words3 = f4.value.split(/\s+/);
|
|
4221
4221
|
const named = (w) => !NOT_NAMES.has(w.toLowerCase()) && w !== "&";
|
|
4222
|
-
const first =
|
|
4222
|
+
const first = words3.findIndex(named);
|
|
4223
4223
|
if (first === -1) continue;
|
|
4224
|
-
const last =
|
|
4225
|
-
add("name",
|
|
4224
|
+
const last = words3.length - 1 - [...words3].reverse().findIndex(named);
|
|
4225
|
+
add("name", words3.slice(first, last + 1).join(" "));
|
|
4226
4226
|
continue;
|
|
4227
4227
|
}
|
|
4228
4228
|
add(f4.kind, f4.value);
|
|
@@ -4242,15 +4242,15 @@ function specifics(text, brief) {
|
|
|
4242
4242
|
}
|
|
4243
4243
|
function specificity(text, brief) {
|
|
4244
4244
|
const list = specifics(text, brief);
|
|
4245
|
-
const
|
|
4245
|
+
const words3 = countWords2(text);
|
|
4246
4246
|
const trade = list.filter((s) => s.kind === "trade").length;
|
|
4247
4247
|
const hard = list.length - trade;
|
|
4248
4248
|
return {
|
|
4249
|
-
words:
|
|
4249
|
+
words: words3,
|
|
4250
4250
|
specifics: list,
|
|
4251
4251
|
hard,
|
|
4252
4252
|
trade,
|
|
4253
|
-
perHundred:
|
|
4253
|
+
perHundred: words3 ? Math.round(list.length / words3 * 1e3) / 10 : 0,
|
|
4254
4254
|
generic: hard === 0 && trade < 2
|
|
4255
4255
|
};
|
|
4256
4256
|
}
|
|
@@ -4290,7 +4290,7 @@ function side(page, other, brief) {
|
|
|
4290
4290
|
function compareCompetitor(ours, competitor, brief) {
|
|
4291
4291
|
return { ours: side(ours, competitor, brief), competitor: side(competitor, ours, brief), brief: Boolean(brief) };
|
|
4292
4292
|
}
|
|
4293
|
-
var pctOf = (
|
|
4293
|
+
var pctOf = (share2) => share2 === null ? "no sentences" : `${Math.round(share2 * 100)}%`;
|
|
4294
4294
|
function formatCompetitor(result) {
|
|
4295
4295
|
const row = (label, s) => ` ${label.padEnd(11)} ${s.interchangeable.length} of ${s.sentences} (${pctOf(s.share)}), ${s.perHundred} specifics per 100 words`;
|
|
4296
4296
|
const lines = [
|
|
@@ -4666,11 +4666,11 @@ function reflexFonts(list) {
|
|
|
4666
4666
|
for (const font of s.fonts) {
|
|
4667
4667
|
const name = wanted.get(normaliseFamily(font.family).toLowerCase());
|
|
4668
4668
|
if (!name || seen.has(name)) continue;
|
|
4669
|
-
const
|
|
4669
|
+
const share2 = font.chars / total;
|
|
4670
4670
|
const displayShare = font.displayChars / display;
|
|
4671
|
-
if (
|
|
4671
|
+
if (share2 >= 0.15 || displayShare >= 0.3) {
|
|
4672
4672
|
seen.add(name);
|
|
4673
|
-
hits.push(hit(s, `${name} sets ${pct(
|
|
4673
|
+
hits.push(hit(s, `${name} sets ${pct(share2)} of the text on the page${displayShare >= 0.3 ? ` and ${pct(displayShare)} of the headings` : ""}`));
|
|
4674
4674
|
}
|
|
4675
4675
|
}
|
|
4676
4676
|
return hits;
|
|
@@ -4959,9 +4959,14 @@ function faceDistance(a, b) {
|
|
|
4959
4959
|
if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
|
|
4960
4960
|
return a.class === b.class ? 0.5 : 1;
|
|
4961
4961
|
}
|
|
4962
|
+
var ACCENT_SCALE = 0.3;
|
|
4963
|
+
var GROUND_SCALE = 0.15;
|
|
4964
|
+
var SHARED_PART = 0.2;
|
|
4965
|
+
var ACCENT_SHARED = ACCENT_SCALE * SHARED_PART;
|
|
4966
|
+
var GROUND_SHARED = GROUND_SCALE * SHARED_PART;
|
|
4962
4967
|
function fingerprintDistance(a, b) {
|
|
4963
|
-
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) /
|
|
4964
|
-
const ground = clamp(deltaEOk(a.ground, b.ground) /
|
|
4968
|
+
const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / ACCENT_SCALE) : a.accent || b.accent ? 1 : 0;
|
|
4969
|
+
const ground = clamp(deltaEOk(a.ground, b.ground) / GROUND_SCALE);
|
|
4965
4970
|
const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
|
|
4966
4971
|
const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
|
|
4967
4972
|
const motion = clamp(Math.abs(a.motion - b.motion));
|
|
@@ -5096,10 +5101,10 @@ var STOP = new Set(
|
|
|
5096
5101
|
function grams(text) {
|
|
5097
5102
|
const out = /* @__PURE__ */ new Set();
|
|
5098
5103
|
for (const clause of text.toLowerCase().replace(/[’']/g, "'").split(/[.!?;:|()\u2013\u2014\u00b7•]+|,\s/)) {
|
|
5099
|
-
const
|
|
5104
|
+
const words3 = clause.match(/[a-z][a-z'-]*/g) ?? [];
|
|
5100
5105
|
for (let n = 2; n <= 4; n += 1) {
|
|
5101
|
-
for (let i = 0; i + n <=
|
|
5102
|
-
const g =
|
|
5106
|
+
for (let i = 0; i + n <= words3.length; i += 1) {
|
|
5107
|
+
const g = words3.slice(i, i + n);
|
|
5103
5108
|
if (STOP.has(g[0]) || STOP.has(g[n - 1])) continue;
|
|
5104
5109
|
if (g.filter((w) => !STOP.has(w)).length < 2) continue;
|
|
5105
5110
|
out.add(g.join(" "));
|
|
@@ -5266,7 +5271,7 @@ function closestComponents(a, b, role) {
|
|
|
5266
5271
|
}
|
|
5267
5272
|
function estateComponentPairs(register, role) {
|
|
5268
5273
|
const out = [];
|
|
5269
|
-
const s = register.sites;
|
|
5274
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
5270
5275
|
for (let i = 0; i < s.length; i += 1) {
|
|
5271
5276
|
for (let j = i + 1; j < s.length; j += 1) {
|
|
5272
5277
|
const c = closestComponents(s[i].fingerprint, s[j].fingerprint, role);
|
|
@@ -5860,16 +5865,16 @@ var ATTRIBUTION = [
|
|
|
5860
5865
|
];
|
|
5861
5866
|
var LINK2 = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|\bhttps?:\/\/[^\s<>"')\]]+/g;
|
|
5862
5867
|
function cleanName(raw) {
|
|
5863
|
-
const
|
|
5864
|
-
while (
|
|
5865
|
-
return
|
|
5868
|
+
const words3 = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
|
|
5869
|
+
while (words3.length && NOT_A_SOURCE.test(words3[0])) words3.shift();
|
|
5870
|
+
return words3.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
|
|
5866
5871
|
}
|
|
5867
5872
|
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)$/;
|
|
5868
5873
|
var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
|
|
5869
5874
|
function namedSources(sentence) {
|
|
5870
5875
|
const found = /* @__PURE__ */ new Set();
|
|
5871
|
-
ATTRIBUTION.forEach((
|
|
5872
|
-
for (const m of sentence.matchAll(
|
|
5876
|
+
ATTRIBUTION.forEach((pattern2, i) => {
|
|
5877
|
+
for (const m of sentence.matchAll(pattern2)) {
|
|
5873
5878
|
const raw = m[1].trim();
|
|
5874
5879
|
const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
|
|
5875
5880
|
if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
|
|
@@ -5955,6 +5960,290 @@ function formatClaims(report2, label = "craft copy claims") {
|
|
|
5955
5960
|
return lines.join("\n");
|
|
5956
5961
|
}
|
|
5957
5962
|
|
|
5963
|
+
// src/character/shape.ts
|
|
5964
|
+
var ABBREVIATIONS = /\b(?:e\.g|i\.e|etc|vs|Mr|Mrs|Ms|Dr|St|No|approx|incl|Ltd|Co|Inc|Jr|Sr)\./g;
|
|
5965
|
+
var DOT = "\0";
|
|
5966
|
+
function splitSentences(text) {
|
|
5967
|
+
const guarded = text.replace(/\s+/g, " ").replace(ABBREVIATIONS, (m) => m.replace(/\./g, DOT)).replace(/(\d)\.(\d)/g, `$1${DOT}$2`);
|
|
5968
|
+
return guarded.split(/(?<=[.!?]["'’”)]*)\s+(?=["'‘“(]?[A-Z0-9£$€])/).map((s) => s.split(DOT).join(".").trim()).filter((s) => words(s).length > 0);
|
|
5969
|
+
}
|
|
5970
|
+
var WORD3 = /[A-Za-z0-9£$€][A-Za-z0-9£$€%'’-]*/g;
|
|
5971
|
+
function words(text) {
|
|
5972
|
+
return text.match(WORD3) ?? [];
|
|
5973
|
+
}
|
|
5974
|
+
var lower = (w) => w.toLowerCase().replace(/’/g, "'");
|
|
5975
|
+
var FUNCTION_WORDS = new Set(
|
|
5976
|
+
"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(" ")
|
|
5977
|
+
);
|
|
5978
|
+
var ITEM = "[A-Za-z0-9\xA3$\u20AC'\u2019-]+(?: [A-Za-z0-9\xA3$\u20AC'\u2019-]+){0,5}";
|
|
5979
|
+
var TRICOLON = new RegExp(`(${ITEM}), (${ITEM}),? (and|or) (${ITEM})(?=[,.;:!?)]|$)`, "g");
|
|
5980
|
+
function tricolons(sentence) {
|
|
5981
|
+
const s = sentence.replace(/\s+/g, " ");
|
|
5982
|
+
const out = [];
|
|
5983
|
+
for (const m of s.matchAll(TRICOLON)) {
|
|
5984
|
+
const [, aSpan, b, , c] = m;
|
|
5985
|
+
const bWords = words(b);
|
|
5986
|
+
const cWords = words(c);
|
|
5987
|
+
if (bWords.length === 0 || cWords.length === 0 || cWords.length > 6) continue;
|
|
5988
|
+
const aWords = words(aSpan);
|
|
5989
|
+
const a = aWords.slice(-Math.max(1, bWords.length));
|
|
5990
|
+
const lead = lower(bWords[0]);
|
|
5991
|
+
const parallel = bWords.length >= 2 && FUNCTION_WORDS.has(lead) && (lower(cWords[0]) === lead || lower(a[0]) === lead);
|
|
5992
|
+
out.push({ text: m[0], parallel });
|
|
5993
|
+
}
|
|
5994
|
+
return out;
|
|
5995
|
+
}
|
|
5996
|
+
var STACKED = /,\s+(?:and|but|so|or)\s+(?:if|when|where|once|unless|provided)\b[^,.;!?]{1,60},/i;
|
|
5997
|
+
function stackedConditional(sentence) {
|
|
5998
|
+
return STACKED.test(sentence);
|
|
5999
|
+
}
|
|
6000
|
+
var SUBORDINATOR = /\b(?:if|when|which|because|so|while|where)\b/gi;
|
|
6001
|
+
function clauseDepth(sentence) {
|
|
6002
|
+
return (sentence.match(/,/g) ?? []).length + (sentence.match(SUBORDINATOR) ?? []).length;
|
|
6003
|
+
}
|
|
6004
|
+
var HEDGES = [
|
|
6005
|
+
"if it's a fit",
|
|
6006
|
+
"if it is a fit",
|
|
6007
|
+
"where needed",
|
|
6008
|
+
"if needed",
|
|
6009
|
+
"as needed",
|
|
6010
|
+
"where appropriate",
|
|
6011
|
+
"if appropriate",
|
|
6012
|
+
"as appropriate",
|
|
6013
|
+
"as required",
|
|
6014
|
+
"where required",
|
|
6015
|
+
"if required",
|
|
6016
|
+
"where possible",
|
|
6017
|
+
"if possible",
|
|
6018
|
+
"where relevant",
|
|
6019
|
+
"if relevant",
|
|
6020
|
+
"depending on",
|
|
6021
|
+
"can help",
|
|
6022
|
+
"tend to",
|
|
6023
|
+
"tends to",
|
|
6024
|
+
"may",
|
|
6025
|
+
"might",
|
|
6026
|
+
"could",
|
|
6027
|
+
"typically",
|
|
6028
|
+
"usually",
|
|
6029
|
+
"generally",
|
|
6030
|
+
"often",
|
|
6031
|
+
"potentially",
|
|
6032
|
+
"likely"
|
|
6033
|
+
];
|
|
6034
|
+
var HEDGE = new RegExp(`\\b(?:${HEDGES.map((h) => h.replace(/'/g, "['\u2019]").replace(/ /g, "\\s+")).join("|")})\\b`, "gi");
|
|
6035
|
+
var CONDITIONAL_CLOSE = /\b(?:if|unless|where|provided|depending)\b/i;
|
|
6036
|
+
function hedgeCount(text) {
|
|
6037
|
+
return (text.match(HEDGE) ?? []).length;
|
|
6038
|
+
}
|
|
6039
|
+
function hedgedClose(sentences3) {
|
|
6040
|
+
const last = sentences3[sentences3.length - 1];
|
|
6041
|
+
if (!last) return false;
|
|
6042
|
+
HEDGE.lastIndex = 0;
|
|
6043
|
+
return HEDGE.test(last) || CONDITIONAL_CLOSE.test(last);
|
|
6044
|
+
}
|
|
6045
|
+
var NOMINAL = /^[a-z]{3,}(?:tion|sion|ment|ness|ity|ance|ence)s?$/;
|
|
6046
|
+
var NOT_NOMINAL = new Set(
|
|
6047
|
+
[
|
|
6048
|
+
"nation station ration lotion potion portion question moment cement comment element garment segment fragment apartment document ointment",
|
|
6049
|
+
"city pity entity quality sentence science audience silence balance finance distance instance entrance substance romance province"
|
|
6050
|
+
].join(" ").split(" ").flatMap((w) => [w, `${w}s`])
|
|
6051
|
+
);
|
|
6052
|
+
function nominalisations(text) {
|
|
6053
|
+
return words(text).filter((w) => {
|
|
6054
|
+
const l = lower(w);
|
|
6055
|
+
return NOMINAL.test(l) && !NOT_NOMINAL.has(l);
|
|
6056
|
+
}).length;
|
|
6057
|
+
}
|
|
6058
|
+
var PRONOUN_OPENER = /^["'‘“(]?(?:you|your|we|our)\b/i;
|
|
6059
|
+
var TRANSITION_OPENER = /^["'‘“(]?(?:however|moreover|additionally|furthermore|thus|therefore|consequently|in addition|as a result|ultimately|overall|importantly|notably)\b/i;
|
|
6060
|
+
var IMPERSONAL_OPENER = /^["'‘“(]?(?:it is|it's|it’s|there is|there are|there's|there’s)\b/i;
|
|
6061
|
+
var NOT_PARTICIPLE = new Set(
|
|
6062
|
+
"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(" ")
|
|
6063
|
+
);
|
|
6064
|
+
function participialClauses(sentence) {
|
|
6065
|
+
let n = 0;
|
|
6066
|
+
for (const m of sentence.matchAll(/,\s+([A-Za-z]{2,}ing)\b/g)) if (!NOT_PARTICIPLE.has(lower(m[1]))) n += 1;
|
|
6067
|
+
const open = sentence.match(/^["'‘“(]?([A-Z][a-z]{2,}ing)\b[^,]{0,60},/);
|
|
6068
|
+
if (open && !NOT_PARTICIPLE.has(lower(open[1]))) n += 1;
|
|
6069
|
+
return n;
|
|
6070
|
+
}
|
|
6071
|
+
function phrasalCoordinations(sentence) {
|
|
6072
|
+
let n = 0;
|
|
6073
|
+
for (const m of sentence.matchAll(/(^|[^,]\s)([A-Za-z][a-z'’-]{2,}) (?:and|or) ([A-Za-z][a-z'’-]{2,})\b/g)) {
|
|
6074
|
+
const a = lower(m[2]);
|
|
6075
|
+
const b = lower(m[3]);
|
|
6076
|
+
if (FUNCTION_WORDS.has(a) || FUNCTION_WORDS.has(b) || a.endsWith("ly") || b.endsWith("ly")) continue;
|
|
6077
|
+
n += 1;
|
|
6078
|
+
}
|
|
6079
|
+
return n;
|
|
6080
|
+
}
|
|
6081
|
+
function skeleton(sentence) {
|
|
6082
|
+
const out = [];
|
|
6083
|
+
for (const w of words(sentence)) {
|
|
6084
|
+
const l = lower(w);
|
|
6085
|
+
const token = FUNCTION_WORDS.has(l) ? l : "_";
|
|
6086
|
+
if (token === "_" && out[out.length - 1] === "_") continue;
|
|
6087
|
+
out.push(token);
|
|
6088
|
+
}
|
|
6089
|
+
return out;
|
|
6090
|
+
}
|
|
6091
|
+
function skeletonRepeat(sentences3) {
|
|
6092
|
+
const grams2 = /* @__PURE__ */ new Map();
|
|
6093
|
+
let total = 0;
|
|
6094
|
+
for (const s of sentences3) {
|
|
6095
|
+
const sk = skeleton(s);
|
|
6096
|
+
for (let i = 0; i + 4 <= sk.length; i += 1) {
|
|
6097
|
+
const g = sk.slice(i, i + 4).join(" ");
|
|
6098
|
+
grams2.set(g, (grams2.get(g) ?? 0) + 1);
|
|
6099
|
+
total += 1;
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
if (total === 0) return 0;
|
|
6103
|
+
let repeated = 0;
|
|
6104
|
+
for (const count4 of grams2.values()) if (count4 > 1) repeated += count4;
|
|
6105
|
+
return repeated / total;
|
|
6106
|
+
}
|
|
6107
|
+
var mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
6108
|
+
var sd = (xs) => {
|
|
6109
|
+
const m = mean(xs);
|
|
6110
|
+
return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / xs.length);
|
|
6111
|
+
};
|
|
6112
|
+
function cv(xs, min) {
|
|
6113
|
+
if (xs.length < min) return null;
|
|
6114
|
+
const m = mean(xs);
|
|
6115
|
+
return m === 0 ? null : sd(xs) / m;
|
|
6116
|
+
}
|
|
6117
|
+
function shapeOf(text) {
|
|
6118
|
+
const ss = splitSentences(text);
|
|
6119
|
+
const wordCount = words(text).length;
|
|
6120
|
+
const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
|
|
6121
|
+
const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
|
|
6122
|
+
const tri = ss.flatMap(tricolons);
|
|
6123
|
+
const commas = (text.match(/,/g) ?? []).length;
|
|
6124
|
+
return {
|
|
6125
|
+
sentences: ss.length,
|
|
6126
|
+
words: wordCount,
|
|
6127
|
+
tricolons: tri.length,
|
|
6128
|
+
parallelTriads: tri.filter((t) => t.parallel).length,
|
|
6129
|
+
stackedConditionals: ss.filter(stackedConditional).length,
|
|
6130
|
+
clauseDepth: ss.length === 0 ? 0 : mean(ss.map(clauseDepth)),
|
|
6131
|
+
sentenceCv: cv(ss.map((s) => words(s).length), 3),
|
|
6132
|
+
hedgeRate: per100(hedgeCount(text)),
|
|
6133
|
+
hedgedClose: hedgedClose(ss),
|
|
6134
|
+
nominalisationRate: per100(nominalisations(text)),
|
|
6135
|
+
pronounOpenerShare: share2(PRONOUN_OPENER),
|
|
6136
|
+
transitionOpenerShare: share2(TRANSITION_OPENER),
|
|
6137
|
+
impersonalOpenerShare: share2(IMPERSONAL_OPENER),
|
|
6138
|
+
commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
|
|
6139
|
+
commasPer100: per100(commas),
|
|
6140
|
+
participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
|
|
6141
|
+
coordinationRate: per100(ss.reduce((s, x) => s + phrasalCoordinations(x), 0)),
|
|
6142
|
+
skeletonRepeat: skeletonRepeat(ss)
|
|
6143
|
+
};
|
|
6144
|
+
}
|
|
6145
|
+
function shapeMoves(f4) {
|
|
6146
|
+
const out = [];
|
|
6147
|
+
if (f4.parallelTriads > 0) out.push("parallel triad");
|
|
6148
|
+
else if (f4.tricolons > 0) out.push("tricolon");
|
|
6149
|
+
if (f4.stackedConditionals > 0) out.push("stacked conditional");
|
|
6150
|
+
if (f4.hedgedClose) out.push("hedged close");
|
|
6151
|
+
if (f4.participialRate > 0) out.push("participial clause");
|
|
6152
|
+
return out;
|
|
6153
|
+
}
|
|
6154
|
+
function paragraphUniformity(paragraphs) {
|
|
6155
|
+
return cv(
|
|
6156
|
+
paragraphs.map((p) => words(p).length).filter((n) => n > 0),
|
|
6157
|
+
4
|
|
6158
|
+
);
|
|
6159
|
+
}
|
|
6160
|
+
function isBlurb(f4) {
|
|
6161
|
+
return f4.sentences >= 2 && f4.sentences <= 8 && f4.words >= 20 && f4.words <= 160;
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
// src/character/lexical.ts
|
|
6165
|
+
var JUZEK = "Juzek and Ward, arXiv:2412.11385, Appendix A (PubMed abstracts, 2020 to 2024)";
|
|
6166
|
+
var STUDY_WORDS = [
|
|
6167
|
+
"delves",
|
|
6168
|
+
"delved",
|
|
6169
|
+
"delving",
|
|
6170
|
+
"delve",
|
|
6171
|
+
"showcasing",
|
|
6172
|
+
"boasts",
|
|
6173
|
+
"underscores",
|
|
6174
|
+
"underscoring",
|
|
6175
|
+
"comprehending",
|
|
6176
|
+
"intricacies",
|
|
6177
|
+
"surpassing",
|
|
6178
|
+
"intricate"
|
|
6179
|
+
].map((word) => ({ word, source: JUZEK }));
|
|
6180
|
+
function vocabulary() {
|
|
6181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6182
|
+
const out = [];
|
|
6183
|
+
const add = (word, source) => {
|
|
6184
|
+
const w = word.toLowerCase();
|
|
6185
|
+
if (seen.has(w)) return;
|
|
6186
|
+
seen.add(w);
|
|
6187
|
+
out.push({ word: w, source });
|
|
6188
|
+
};
|
|
6189
|
+
for (const s of STUDY_WORDS) add(s.word, s.source);
|
|
6190
|
+
for (const w of AI_WORDS) add(w, "AI_WORDS");
|
|
6191
|
+
for (const w of PLAINER_WORDS) add(w, "PLAINER_WORDS");
|
|
6192
|
+
for (const w of AI_PHRASES) add(w, "AI_PHRASES");
|
|
6193
|
+
for (const w of STOCK_PHRASES) add(w, "STOCK_PHRASES");
|
|
6194
|
+
for (const w of BUZZWORDS) add(w, "BUZZWORDS");
|
|
6195
|
+
return out;
|
|
6196
|
+
}
|
|
6197
|
+
var escape4 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6198
|
+
var pattern = (w) => new RegExp(`(?<![\\w-])${escape4(w).replace(/\s+/g, "\\s+")}(?![\\w-])`, "gi");
|
|
6199
|
+
var compiled = null;
|
|
6200
|
+
var patterns = () => compiled ?? (compiled = vocabulary().map((v) => ({ ...v, re: pattern(v.word) })));
|
|
6201
|
+
var plain = (text) => text.replace(/[’‘]/g, "'").replace(/\s+/g, " ");
|
|
6202
|
+
function vocabularyCounts(text) {
|
|
6203
|
+
const t = plain(text);
|
|
6204
|
+
const out = /* @__PURE__ */ new Map();
|
|
6205
|
+
for (const { word, re } of patterns()) {
|
|
6206
|
+
const n = (t.match(re) ?? []).length;
|
|
6207
|
+
if (n) out.set(word, n);
|
|
6208
|
+
}
|
|
6209
|
+
return out;
|
|
6210
|
+
}
|
|
6211
|
+
var LEXICAL_FEATURES = [
|
|
6212
|
+
"listRate",
|
|
6213
|
+
"studyRate",
|
|
6214
|
+
"hardPer100",
|
|
6215
|
+
"tradePer100",
|
|
6216
|
+
"specificsPer100",
|
|
6217
|
+
"generic",
|
|
6218
|
+
"figures",
|
|
6219
|
+
"names"
|
|
6220
|
+
];
|
|
6221
|
+
var STUDY = new Set(STUDY_WORDS.map((s) => s.word));
|
|
6222
|
+
var LISTED = new Set([...AI_WORDS, ...PLAINER_WORDS, ...AI_PHRASES, ...STOCK_PHRASES, ...BUZZWORDS].map((w) => w.toLowerCase()));
|
|
6223
|
+
var r3 = (n) => Math.round(n * 1e3) / 1e3;
|
|
6224
|
+
function lexicalOf(text, brief) {
|
|
6225
|
+
const words3 = Math.max(1, countWords2(text));
|
|
6226
|
+
const counts = vocabularyCounts(text);
|
|
6227
|
+
let list = 0;
|
|
6228
|
+
let study = 0;
|
|
6229
|
+
for (const [w, n] of counts) {
|
|
6230
|
+
if (STUDY.has(w)) study += n;
|
|
6231
|
+
if (LISTED.has(w)) list += n;
|
|
6232
|
+
}
|
|
6233
|
+
const s = specificity(text, brief);
|
|
6234
|
+
const kind = (...k) => s.specifics.filter((x) => k.includes(x.kind)).length;
|
|
6235
|
+
return {
|
|
6236
|
+
listRate: r3(list / words3 * 100),
|
|
6237
|
+
studyRate: r3(study / words3 * 100),
|
|
6238
|
+
hardPer100: r3(s.hard / words3 * 100),
|
|
6239
|
+
tradePer100: r3(s.trade / words3 * 100),
|
|
6240
|
+
specificsPer100: r3(s.perHundred),
|
|
6241
|
+
generic: s.generic ? 1 : 0,
|
|
6242
|
+
figures: kind("money", "date", "time", "number"),
|
|
6243
|
+
names: kind("name", "brief")
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
|
|
5958
6247
|
// src/character/baseline.ts
|
|
5959
6248
|
var BASELINE_VERSION = 1;
|
|
5960
6249
|
function fnv1a(text) {
|
|
@@ -6055,23 +6344,23 @@ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
|
|
|
6055
6344
|
return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
|
|
6056
6345
|
}
|
|
6057
6346
|
function globToRegExp(glob) {
|
|
6058
|
-
const
|
|
6347
|
+
const pattern2 = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
|
|
6059
6348
|
let body = "";
|
|
6060
|
-
for (let i = 0; i <
|
|
6061
|
-
const c =
|
|
6062
|
-
if (c === "*" &&
|
|
6063
|
-
const slash =
|
|
6349
|
+
for (let i = 0; i < pattern2.length; i += 1) {
|
|
6350
|
+
const c = pattern2[i];
|
|
6351
|
+
if (c === "*" && pattern2[i + 1] === "*") {
|
|
6352
|
+
const slash = pattern2[i + 2] === "/";
|
|
6064
6353
|
body += slash ? "(?:.*/)?" : ".*";
|
|
6065
6354
|
i += slash ? 2 : 1;
|
|
6066
6355
|
} else if (c === "*") body += "[^/]*";
|
|
6067
6356
|
else if (c === "?") body += "[^/]";
|
|
6068
6357
|
else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
6069
6358
|
}
|
|
6070
|
-
const anywhere = !
|
|
6359
|
+
const anywhere = !pattern2.includes("/");
|
|
6071
6360
|
return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
|
|
6072
6361
|
}
|
|
6073
|
-
function ignoreMatcher(
|
|
6074
|
-
const res =
|
|
6362
|
+
function ignoreMatcher(patterns2) {
|
|
6363
|
+
const res = patterns2.map(globToRegExp);
|
|
6075
6364
|
return (path) => {
|
|
6076
6365
|
const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6077
6366
|
return res.some((re) => re.test(p));
|
|
@@ -6238,11 +6527,11 @@ function visualMeasures(img, maxSamples = 1e6) {
|
|
|
6238
6527
|
const sdRg = Math.sqrt(Math.max(0, sumRg2 / n - meanRg * meanRg));
|
|
6239
6528
|
const sdYb = Math.sqrt(Math.max(0, sumYb2 / n - meanYb * meanYb));
|
|
6240
6529
|
const colourfulness = Math.sqrt(sdRg ** 2 + sdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
|
|
6241
|
-
const
|
|
6530
|
+
const round3 = (v, d = 3) => Math.round(v * 10 ** d) / 10 ** d;
|
|
6242
6531
|
return {
|
|
6243
|
-
colourfulness:
|
|
6244
|
-
edgeDensity:
|
|
6245
|
-
symmetry:
|
|
6532
|
+
colourfulness: round3(colourfulness, 1),
|
|
6533
|
+
edgeDensity: round3(edgeN ? edges / edgeN : 0),
|
|
6534
|
+
symmetry: round3(asymN ? 1 - asym / asymN / 255 : 1),
|
|
6246
6535
|
width: w,
|
|
6247
6536
|
height: h,
|
|
6248
6537
|
sampled: n
|
|
@@ -6250,7 +6539,9 @@ function visualMeasures(img, maxSamples = 1e6) {
|
|
|
6250
6539
|
}
|
|
6251
6540
|
|
|
6252
6541
|
// src/direction/types.ts
|
|
6253
|
-
var DIRECTION_VERSION =
|
|
6542
|
+
var DIRECTION_VERSION = 2;
|
|
6543
|
+
var DIRECTION_VERSIONS = [1, 2];
|
|
6544
|
+
var CUSTOMER_KINDS = ["review", "enquiry", "conversation", "search"];
|
|
6254
6545
|
var SOURCE_KINDS = [
|
|
6255
6546
|
"livery",
|
|
6256
6547
|
"shopfront",
|
|
@@ -6264,9 +6555,413 @@ var SOURCE_KINDS = [
|
|
|
6264
6555
|
"trade",
|
|
6265
6556
|
"print",
|
|
6266
6557
|
"photo",
|
|
6267
|
-
"founder"
|
|
6558
|
+
"founder",
|
|
6559
|
+
...CUSTOMER_KINDS,
|
|
6560
|
+
"reference"
|
|
6268
6561
|
];
|
|
6269
6562
|
var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
|
|
6563
|
+
var CONSIDERATION = ["low", "considered", "high"];
|
|
6564
|
+
var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
|
|
6565
|
+
var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
|
|
6566
|
+
|
|
6567
|
+
// src/direction/reason.ts
|
|
6568
|
+
var PROPOSED = "PROPOSED:";
|
|
6569
|
+
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;
|
|
6570
|
+
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;
|
|
6571
|
+
var CONVENTION = new RegExp(
|
|
6572
|
+
[
|
|
6573
|
+
// authority
|
|
6574
|
+
String.raw`industry[- ]standard`,
|
|
6575
|
+
String.raw`best[- ]practices?`,
|
|
6576
|
+
String.raw`the norm\b`,
|
|
6577
|
+
String.raw`standard (?:layout|structure|practice|pattern|format)`,
|
|
6578
|
+
String.raw`tried[- ]and[- ]tested`,
|
|
6579
|
+
String.raw`proven (?:layout|structure|pattern|format)`,
|
|
6580
|
+
// the crowd
|
|
6581
|
+
String.raw`(?:most|other|typical|many|all|similar)\s+(?:\w+\s+){0,2}(?:web)?sites\b`,
|
|
6582
|
+
String.raw`competitors?(?:'|’)?s?\b`,
|
|
6583
|
+
String.raw`(?:like|as) (?:everyone|everybody|all the others|the rest)\b`,
|
|
6584
|
+
String.raw`commonly used`,
|
|
6585
|
+
String.raw`(?:what|how) (?:everyone|others|the industry) (?:does|do|uses?)`,
|
|
6586
|
+
// familiarity
|
|
6587
|
+
String.raw`familiar (?:layout|structure|pattern|format|order|shape|design)`,
|
|
6588
|
+
String.raw`recogni[sz]able (?:layout|structure|pattern|format|order)`,
|
|
6589
|
+
String.raw`conventional(?:ly)?\b`,
|
|
6590
|
+
String.raw`(?:visitors|users|people|customers|anyone) (?:are|is) used to`,
|
|
6591
|
+
String.raw`feels? (?:at home|comfortable|familiar)(?: with)?(?: the)? (?:layout|site|page|structure|design)?`,
|
|
6592
|
+
String.raw`intuitive(?:ly)?\b`,
|
|
6593
|
+
String.raw`as usual\b`,
|
|
6594
|
+
String.raw`what (?:visitors|users|people) expect\b`
|
|
6595
|
+
].join("|"),
|
|
6596
|
+
"i"
|
|
6597
|
+
);
|
|
6598
|
+
var GENERIC = new Set(
|
|
6599
|
+
"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(" ")
|
|
6600
|
+
);
|
|
6601
|
+
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"]);
|
|
6602
|
+
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"]);
|
|
6603
|
+
function contentWords(text) {
|
|
6604
|
+
return new Set(
|
|
6605
|
+
(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))
|
|
6606
|
+
);
|
|
6607
|
+
}
|
|
6608
|
+
var stem = (w) => w.replace(/(?:ing|ers|er|ed|es|s)$/, "");
|
|
6609
|
+
function shares(a, b) {
|
|
6610
|
+
const bs = new Set([...b].map(stem));
|
|
6611
|
+
return [...a].filter((w) => bs.has(stem(w)));
|
|
6612
|
+
}
|
|
6613
|
+
function jobWords(job) {
|
|
6614
|
+
return contentWords(
|
|
6615
|
+
[
|
|
6616
|
+
job.statement?.verb,
|
|
6617
|
+
job.statement?.object,
|
|
6618
|
+
job.statement?.context,
|
|
6619
|
+
job.functional?.value,
|
|
6620
|
+
job.emotional?.value,
|
|
6621
|
+
job.social?.value,
|
|
6622
|
+
...Array.isArray(job.objections) ? job.objections : [],
|
|
6623
|
+
...Array.isArray(job.language) ? job.language : []
|
|
6624
|
+
].filter((x) => typeof x === "string").join(" ")
|
|
6625
|
+
);
|
|
6626
|
+
}
|
|
6627
|
+
var isCustomerVoice = (s) => Boolean(s && CUSTOMER_KINDS.includes(s.kind));
|
|
6628
|
+
function checkReason(input) {
|
|
6629
|
+
const problems = [];
|
|
6630
|
+
const err = (field, message) => problems.push({ severity: "error", at: `${input.at}.${field}`, message });
|
|
6631
|
+
const because = typeof input.because === "string" ? input.because.trim() : "";
|
|
6632
|
+
const evidence = Array.isArray(input.evidence) ? input.evidence.filter((e) => typeof e === "string") : [];
|
|
6633
|
+
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");
|
|
6634
|
+
const ids = new Map(input.sources.filter((s) => s && typeof s.id === "string").map((s) => [s.id, s]));
|
|
6635
|
+
if (because.split(/\s+/).filter(Boolean).length < 8) {
|
|
6636
|
+
err("because", "a reason is at least a sentence. Without one this is a default, however good it looks.");
|
|
6637
|
+
return problems;
|
|
6638
|
+
}
|
|
6639
|
+
if (because.startsWith(PROPOSED)) {
|
|
6640
|
+
err("because", "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
|
|
6641
|
+
return problems;
|
|
6642
|
+
}
|
|
6643
|
+
if (PREFERENCE.test(because)) err("because", "that is a preference, not a reason. Say what in the client's world this comes from.");
|
|
6644
|
+
const moods = because.match(MOOD) ?? [];
|
|
6645
|
+
if (moods.length >= 2) err("because", `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
|
|
6646
|
+
const convention = because.match(CONVENTION);
|
|
6647
|
+
if (convention) {
|
|
6648
|
+
err("because", `"${convention[0]}" is the category's default speaking, not a reason. Say what in this client's job or world it answers.`);
|
|
6649
|
+
}
|
|
6650
|
+
const needsEvidence = input.tie !== "job";
|
|
6651
|
+
if (needsEvidence && evidence.length === 0) err("evidence", "cite at least one source");
|
|
6652
|
+
for (const id of evidence) if (!ids.has(id)) err("evidence", `no source called "${id}"`);
|
|
6653
|
+
if (input.tie === "customer") {
|
|
6654
|
+
for (const id of evidence) {
|
|
6655
|
+
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(", ")}.`);
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
if (input.tie === "source") {
|
|
6659
|
+
for (const id of evidence) {
|
|
6660
|
+
if (isCustomerVoice(ids.get(id))) err("evidence", `"${id}" is customer voice. Token decisions need a physical source or an outside-category reference.`);
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
if (problems.length) return problems;
|
|
6664
|
+
const words3 = contentWords(because);
|
|
6665
|
+
if (input.tie === "job") {
|
|
6666
|
+
if (!input.job) {
|
|
6667
|
+
err("because", "a structural reason is checked against the job, and the file has no job map yet");
|
|
6668
|
+
} else if (shares(words3, jobWords(input.job)).length === 0) {
|
|
6669
|
+
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.");
|
|
6670
|
+
}
|
|
6671
|
+
} else {
|
|
6672
|
+
const cited = evidence.map((id) => ids.get(id));
|
|
6673
|
+
const tied = cited.some((s) => words3.has(s.id) || shares(words3, contentWords(`${s.note} ${s.lettering ?? ""}`)).length > 0);
|
|
6674
|
+
if (!tied) err("because", `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
|
|
6675
|
+
}
|
|
6676
|
+
return problems;
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
// src/direction/job.ts
|
|
6680
|
+
var JOB_VERBS = new Set(
|
|
6681
|
+
"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(" ")
|
|
6682
|
+
);
|
|
6683
|
+
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"]);
|
|
6684
|
+
var NOUNISH = /(?:tion|sion|ment|ness|ity|site|page)$/;
|
|
6685
|
+
var words2 = (s) => s.toLowerCase().match(/[a-z][a-z'’-]*/g) ?? [];
|
|
6686
|
+
function isCategoryLabel(object, brief = "") {
|
|
6687
|
+
const trade = /* @__PURE__ */ new Set([...TRADE_NOUNS.flatMap((t) => t.split(" ")), ...words2(brief).filter((w) => w.length > 3)]);
|
|
6688
|
+
const ws = words2(object);
|
|
6689
|
+
return ws.length > 0 && ws.every((w) => LABEL_WORDS.has(w) || trade.has(w) || trade.has(w.replace(/s$/, "")));
|
|
6690
|
+
}
|
|
6691
|
+
var normalise2 = (s) => s.toLowerCase().replace(/[“”"‘’'.,!?;:()-]/g, " ").replace(/\s+/g, " ").trim();
|
|
6692
|
+
function validateJob(job, sources, ctx = {}) {
|
|
6693
|
+
const problems = [];
|
|
6694
|
+
const err = (at, message) => problems.push({ severity: "error", at: `job${at}`, message });
|
|
6695
|
+
const warn = (at, message) => problems.push({ severity: "warn", at: `job${at}`, message });
|
|
6696
|
+
if (!job || typeof job !== "object" || Array.isArray(job)) {
|
|
6697
|
+
err("", "no job map: say what the visitor is trying to get done, in their words, before deciding structure or look");
|
|
6698
|
+
return { problems, decided: false };
|
|
6699
|
+
}
|
|
6700
|
+
const j = job;
|
|
6701
|
+
const ids = new Map(sources.map((s) => [s.id, s]));
|
|
6702
|
+
const st = j.statement;
|
|
6703
|
+
if (!st || typeof st !== "object") err(".statement", "a job statement is verb, object and context");
|
|
6704
|
+
else {
|
|
6705
|
+
const verb = typeof st.verb === "string" ? st.verb.trim().toLowerCase() : "";
|
|
6706
|
+
if (!verb) err(".statement.verb", "what the visitor is trying to do, as a verb: find, choose, book, fix");
|
|
6707
|
+
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.`);
|
|
6708
|
+
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.`);
|
|
6709
|
+
const object = typeof st.object === "string" ? st.object.trim() : "";
|
|
6710
|
+
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");
|
|
6711
|
+
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".`);
|
|
6712
|
+
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");
|
|
6713
|
+
}
|
|
6714
|
+
for (const part of ["functional", "emotional", "social"]) {
|
|
6715
|
+
const p = j[part];
|
|
6716
|
+
if (!p || typeof p !== "object") {
|
|
6717
|
+
err(`.${part}`, `the ${part} side of the job, with the customer-voice source that shows it`);
|
|
6718
|
+
continue;
|
|
6719
|
+
}
|
|
6720
|
+
if (typeof p.value !== "string" || words2(p.value).length < 6) err(`.${part}.value`, "a sentence: what they need, in the terms they would use");
|
|
6721
|
+
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") })));
|
|
6722
|
+
}
|
|
6723
|
+
const c = j.consideration;
|
|
6724
|
+
if (!c || typeof c !== "object") err(".consideration", `how long and how carefully they decide: ${CONSIDERATION.join(", ")}`);
|
|
6725
|
+
else {
|
|
6726
|
+
if (!CONSIDERATION.includes(c.value)) err(".consideration.value", `one of ${CONSIDERATION.join(", ")}`);
|
|
6727
|
+
problems.push(...checkReason({ at: "job.consideration", because: c.because, evidence: c.evidence, sources, tie: "customer" }));
|
|
6728
|
+
}
|
|
6729
|
+
const objections = Array.isArray(j.objections) ? j.objections : [];
|
|
6730
|
+
if (objections.length === 0) err(".objections", "what stops them: the worries they bring to the page");
|
|
6731
|
+
objections.forEach((o, i) => {
|
|
6732
|
+
if (typeof o !== "string" || words2(o).length < 3) err(`.objections[${i}]`, "an objection is a few words: what they worry about");
|
|
6733
|
+
});
|
|
6734
|
+
const evidence = Array.isArray(j.evidence) ? j.evidence : [];
|
|
6735
|
+
const voice = evidence.map((id) => ids.get(id)).filter(isCustomerVoice);
|
|
6736
|
+
if (evidence.length === 0 || voice.length === 0) err(".evidence", "cite the reviews, enquiries or conversations the job comes from");
|
|
6737
|
+
for (const id of evidence) if (!ids.has(id)) err(".evidence", `no source called "${id}"`);
|
|
6738
|
+
const language = Array.isArray(j.language) ? j.language : [];
|
|
6739
|
+
if (language.length === 0) err(".language", "quote the customer: the words they use for the problem");
|
|
6740
|
+
const texts = voice.map((s) => normalise2(`${s.note} ${ctx.readSource?.(s) ?? ""}`));
|
|
6741
|
+
language.forEach((q, i) => {
|
|
6742
|
+
if (typeof q !== "string" || !q.trim()) return err(`.language[${i}]`, "an empty quote");
|
|
6743
|
+
const n = normalise2(q);
|
|
6744
|
+
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.`);
|
|
6745
|
+
});
|
|
6746
|
+
return { problems, decided: problems.every((p) => p.severity !== "error") };
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
// src/direction/hierarchy.ts
|
|
6750
|
+
var SECTION_ROLES = [
|
|
6751
|
+
"hero",
|
|
6752
|
+
"logos",
|
|
6753
|
+
"stats",
|
|
6754
|
+
"cards",
|
|
6755
|
+
"marquee",
|
|
6756
|
+
"text",
|
|
6757
|
+
"other",
|
|
6758
|
+
"cta-band",
|
|
6759
|
+
"footer-cta",
|
|
6760
|
+
"pricing",
|
|
6761
|
+
"testimonials",
|
|
6762
|
+
"faq",
|
|
6763
|
+
"process",
|
|
6764
|
+
"features",
|
|
6765
|
+
"team",
|
|
6766
|
+
"contact"
|
|
6767
|
+
];
|
|
6768
|
+
var POSITIONS = /* @__PURE__ */ new Set([...SECTION_ROLES, "header", "sticky"]);
|
|
6769
|
+
function considerationTied(because, job) {
|
|
6770
|
+
const text = because.toLowerCase();
|
|
6771
|
+
const level = job.consideration?.value;
|
|
6772
|
+
if (level && new RegExp(`\\b${level}\\b`).test(text)) return true;
|
|
6773
|
+
const said = contentWords(because);
|
|
6774
|
+
return (Array.isArray(job.objections) ? job.objections : []).some((o) => typeof o === "string" && [...contentWords(o)].some((w) => said.has(w)));
|
|
6775
|
+
}
|
|
6776
|
+
function validateHierarchy(hierarchy, job, sources) {
|
|
6777
|
+
const problems = [];
|
|
6778
|
+
const err = (at, message) => problems.push({ severity: "error", at: `hierarchy${at}`, message });
|
|
6779
|
+
const pages = [];
|
|
6780
|
+
if (!hierarchy || typeof hierarchy !== "object" || Array.isArray(hierarchy)) {
|
|
6781
|
+
err("", "no hierarchy: decide what the home page leads with, where it asks, and the order, each from the job");
|
|
6782
|
+
return { problems, pages };
|
|
6783
|
+
}
|
|
6784
|
+
const h = hierarchy;
|
|
6785
|
+
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(", ")}`);
|
|
6786
|
+
if (!h.home) err(".home", "no home page hierarchy: the home page is where most visitors start");
|
|
6787
|
+
for (const page of PAGE_TYPES) {
|
|
6788
|
+
const p = h[page];
|
|
6789
|
+
if (!p) continue;
|
|
6790
|
+
let decided = 0;
|
|
6791
|
+
const at = (f4) => `hierarchy.${page}.${f4}`;
|
|
6792
|
+
const reason = (where, because, evidence) => checkReason({ at: where, because, evidence, sources, tie: "job", job });
|
|
6793
|
+
const count4 = (found) => {
|
|
6794
|
+
problems.push(...found);
|
|
6795
|
+
if (!found.some((x) => x.severity === "error")) decided += 1;
|
|
6796
|
+
};
|
|
6797
|
+
if (!p.primaryAction) err(`.${page}.primaryAction`, "not decided yet");
|
|
6798
|
+
else {
|
|
6799
|
+
const a = p.primaryAction;
|
|
6800
|
+
const found = [];
|
|
6801
|
+
if (typeof a.value !== "string" || !a.value.trim()) found.push({ severity: "error", at: `${at("primaryAction")}.value`, message: "what the visitor is asked to do" });
|
|
6802
|
+
const positions = Array.isArray(a.positions) ? a.positions : [];
|
|
6803
|
+
if (positions.length === 0) found.push({ severity: "error", at: `${at("primaryAction")}.positions`, message: 'where the ask sits: one place, or several ("hero", "footer-cta")' });
|
|
6804
|
+
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"` });
|
|
6805
|
+
const r = reason(at("primaryAction"), a.because, a.evidence);
|
|
6806
|
+
found.push(...r);
|
|
6807
|
+
if (r.length === 0 && job && typeof a.because === "string" && !considerationTied(a.because, job)) {
|
|
6808
|
+
found.push({
|
|
6809
|
+
severity: "error",
|
|
6810
|
+
at: `${at("primaryAction")}.because`,
|
|
6811
|
+
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.`
|
|
6812
|
+
});
|
|
6813
|
+
}
|
|
6814
|
+
count4(found);
|
|
6815
|
+
}
|
|
6816
|
+
const order = Array.isArray(p.order) ? p.order : [];
|
|
6817
|
+
if (order.length < 2) err(`.${page}.order`, "the running order, two sections or more, each with why it sits there");
|
|
6818
|
+
else {
|
|
6819
|
+
const found = [];
|
|
6820
|
+
order.forEach((o, i) => {
|
|
6821
|
+
if (!o || !SECTION_ROLES.includes(o.role)) found.push({ severity: "error", at: `${at("order")}[${i}].role`, message: `a section role: ${SECTION_ROLES.join(", ")}` });
|
|
6822
|
+
found.push(...reason(`${at("order")}[${i}]`, o?.because));
|
|
6823
|
+
});
|
|
6824
|
+
count4(found);
|
|
6825
|
+
}
|
|
6826
|
+
if (!p.lead) err(`.${page}.lead`, "not decided yet");
|
|
6827
|
+
else {
|
|
6828
|
+
const found = [];
|
|
6829
|
+
if (typeof p.lead.value !== "string" || !p.lead.value.trim()) found.push({ severity: "error", at: `${at("lead")}.value`, message: "what the page leads with" });
|
|
6830
|
+
found.push(...reason(at("lead"), p.lead.because));
|
|
6831
|
+
count4(found);
|
|
6832
|
+
}
|
|
6833
|
+
const journey = Array.isArray(p.journey) ? p.journey : [];
|
|
6834
|
+
if (journey.length < 2) err(`.${page}.journey`, "the stages the visitor goes through, two or more, each with why");
|
|
6835
|
+
else {
|
|
6836
|
+
const found = [];
|
|
6837
|
+
journey.forEach((s, i) => {
|
|
6838
|
+
if (!s || typeof s.stage !== "string" || !s.stage.trim()) found.push({ severity: "error", at: `${at("journey")}[${i}].stage`, message: "name the stage" });
|
|
6839
|
+
found.push(...reason(`${at("journey")}[${i}]`, s?.because));
|
|
6840
|
+
});
|
|
6841
|
+
count4(found);
|
|
6842
|
+
}
|
|
6843
|
+
pages.push({ page, decided, total: HIERARCHY_FIELDS.length });
|
|
6844
|
+
}
|
|
6845
|
+
return { problems, pages };
|
|
6846
|
+
}
|
|
6847
|
+
function renderedOrder(snapshot) {
|
|
6848
|
+
const sections = snapshot.sections ?? [];
|
|
6849
|
+
if (sections.length === 0 || !sections.every((s) => s.role)) return null;
|
|
6850
|
+
return sections.map((s) => s.role).filter((r) => r !== "other");
|
|
6851
|
+
}
|
|
6852
|
+
function checkOrder(declared, page, snapshot, strict = false) {
|
|
6853
|
+
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" }];
|
|
6854
|
+
const rendered = renderedOrder(snapshot);
|
|
6855
|
+
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" }];
|
|
6856
|
+
const said = declared.order.map((o) => o.role).filter((r) => r !== "other");
|
|
6857
|
+
if (said.join(",") === rendered.join(",")) return [];
|
|
6858
|
+
return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6861
|
+
// src/direction/research.ts
|
|
6862
|
+
function tradeOf(brief) {
|
|
6863
|
+
const text = brief.toLowerCase();
|
|
6864
|
+
return TRADE_NOUNS.filter((t) => new RegExp(`\\b${t.replace(/ /g, "\\s+")}s?\\b`).test(text));
|
|
6865
|
+
}
|
|
6866
|
+
function categoryMatchesBrief(category, brief) {
|
|
6867
|
+
const known = tradeOf(category);
|
|
6868
|
+
if (tradeOf(brief).some((t) => known.includes(t))) return true;
|
|
6869
|
+
const label = category.trim().replace(/^(?:(?:a|an|the|local|independent)\s+)+/i, "");
|
|
6870
|
+
if (!label) return false;
|
|
6871
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
6872
|
+
return new RegExp(`\\b${escaped}s?\\b`, "i").test(brief);
|
|
6873
|
+
}
|
|
6874
|
+
function researchPrompt(input) {
|
|
6875
|
+
const trade = tradeOf(input.brief);
|
|
6876
|
+
const category = trade.length ? trade.join(", ") : "this kind of business";
|
|
6877
|
+
const who = input.client ? `${input.client}: ${input.brief}` : input.brief;
|
|
6878
|
+
const lines = [
|
|
6879
|
+
"# Research brief: the customer's job",
|
|
6880
|
+
"",
|
|
6881
|
+
`The business: ${who}`,
|
|
6882
|
+
"",
|
|
6883
|
+
"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.",
|
|
6884
|
+
"",
|
|
6885
|
+
"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?",
|
|
6886
|
+
'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?',
|
|
6887
|
+
"3. **Objections.** What do they worry about before getting in touch? What has gone wrong for them, or people they know, before?",
|
|
6888
|
+
"4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
|
|
6889
|
+
"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?",
|
|
6890
|
+
"6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
|
|
6891
|
+
"7. **Where they look.** What do they search for, and where do they ask?",
|
|
6892
|
+
"",
|
|
6893
|
+
"## Do not",
|
|
6894
|
+
"",
|
|
6895
|
+
`- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
|
|
6896
|
+
'- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
|
|
6897
|
+
"- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
|
|
6898
|
+
"- Do not invent quotes. If you cannot find a source for something, say so."
|
|
6899
|
+
];
|
|
6900
|
+
if (input.visual) {
|
|
6901
|
+
lines.push(
|
|
6902
|
+
"",
|
|
6903
|
+
"# Visual references, from outside the category",
|
|
6904
|
+
"",
|
|
6905
|
+
`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.`,
|
|
6906
|
+
"",
|
|
6907
|
+
`Nothing from a ${category} website, and nothing from a web design gallery.`
|
|
6908
|
+
);
|
|
6909
|
+
}
|
|
6910
|
+
return `${lines.join("\n")}
|
|
6911
|
+
`;
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
// src/estate/direction.ts
|
|
6915
|
+
function summariseDirection(d) {
|
|
6916
|
+
const c = d.choices ?? {};
|
|
6917
|
+
const home = d.hierarchy?.home;
|
|
6918
|
+
const s = {};
|
|
6919
|
+
if (typeof c.accent?.value === "string" && c.accent.value) s.accent = c.accent.value;
|
|
6920
|
+
if (typeof c.ground?.value === "string" && c.ground.value) s.ground = c.ground.value;
|
|
6921
|
+
if (typeof c.display?.value === "string" && c.display.value) s.display = normaliseFamily(c.display.value);
|
|
6922
|
+
if (typeof c.body?.value === "string" && c.body.value) s.body = normaliseFamily(c.body.value);
|
|
6923
|
+
if (typeof c.shape?.value === "string" && c.shape.value) s.shape = c.shape.value;
|
|
6924
|
+
if (Array.isArray(home?.order) && home.order.length) s.order = home.order.filter((o) => o && typeof o.role === "string").map((o) => o.role);
|
|
6925
|
+
if (typeof home?.primaryAction?.value === "string" && home.primaryAction.value) {
|
|
6926
|
+
s.primaryAction = home.primaryAction.value;
|
|
6927
|
+
s.positions = Array.isArray(home.primaryAction.positions) ? [...home.primaryAction.positions] : [];
|
|
6928
|
+
}
|
|
6929
|
+
return s;
|
|
6930
|
+
}
|
|
6931
|
+
var delta = (a, b) => {
|
|
6932
|
+
const x = a ? parseColour(a) : null;
|
|
6933
|
+
const y = b ? parseColour(b) : null;
|
|
6934
|
+
return x && y ? deltaEOk(x.oklch, y.oklch) : null;
|
|
6935
|
+
};
|
|
6936
|
+
var same = (a, b) => Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase());
|
|
6937
|
+
function compareDirections(a, b) {
|
|
6938
|
+
const shared = [];
|
|
6939
|
+
const accentDelta = delta(a.accent, b.accent);
|
|
6940
|
+
const groundDelta = delta(a.ground, b.ground);
|
|
6941
|
+
if (accentDelta !== null && accentDelta <= ACCENT_SHARED) shared.push(`accent (\u0394E ${accentDelta.toFixed(3)})`);
|
|
6942
|
+
if (groundDelta !== null && groundDelta <= GROUND_SHARED) shared.push(`ground (\u0394E ${groundDelta.toFixed(3)})`);
|
|
6943
|
+
if (a.display && b.display && same(normaliseFamily(a.display), normaliseFamily(b.display))) shared.push(`${normaliseFamily(a.display)} headline`);
|
|
6944
|
+
if (a.body && b.body && same(normaliseFamily(a.body), normaliseFamily(b.body))) shared.push(`${normaliseFamily(a.body)} body`);
|
|
6945
|
+
if (same(a.shape, b.shape)) shared.push(`shape "${a.shape}"`);
|
|
6946
|
+
let orderDistance = null;
|
|
6947
|
+
if (a.order?.length && b.order?.length) {
|
|
6948
|
+
orderDistance = levenshtein(a.order, b.order) / Math.max(a.order.length, b.order.length);
|
|
6949
|
+
if (orderDistance <= SHARED_PART) shared.push(orderDistance === 0 ? "the same section order" : "nearly the same section order");
|
|
6950
|
+
}
|
|
6951
|
+
if (same(a.primaryAction, b.primaryAction) && [...a.positions ?? []].sort().join(",") === [...b.positions ?? []].sort().join(",")) shared.push(`"${a.primaryAction}" asked in the same places`);
|
|
6952
|
+
const surface = shared.filter((s) => /^(?:accent|ground|shape)|headline$/.test(s)).length;
|
|
6953
|
+
const flagged = surface >= 2 || orderDistance !== null && orderDistance <= SHARED_PART;
|
|
6954
|
+
return { shared, accentDelta, groundDelta, orderDistance, flagged };
|
|
6955
|
+
}
|
|
6956
|
+
function compareToDeclared(target2, sites, exclude) {
|
|
6957
|
+
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));
|
|
6958
|
+
}
|
|
6959
|
+
function formatDirectionMatches(label, matches) {
|
|
6960
|
+
const lines = [`${label}: declared directions in the estate (ranked, not judged: no sibling line yet)`];
|
|
6961
|
+
for (const m of matches) lines.push(` ${m.id.padEnd(20)} ${m.flagged ? "LOOK " : " "}${m.shared.length ? `shares ${m.shared.join(", ")}` : "nothing shared"}`);
|
|
6962
|
+
if (matches.length === 0) lines.push(" no site in the register has a declared direction yet: craft estate add --direction <file>");
|
|
6963
|
+
return lines.join("\n");
|
|
6964
|
+
}
|
|
6270
6965
|
|
|
6271
6966
|
// src/direction/licences.ts
|
|
6272
6967
|
var LICENCES = [
|
|
@@ -6380,13 +7075,6 @@ function licencesJson() {
|
|
|
6380
7075
|
}
|
|
6381
7076
|
|
|
6382
7077
|
// src/direction/validate.ts
|
|
6383
|
-
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;
|
|
6384
|
-
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;
|
|
6385
|
-
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"]);
|
|
6386
|
-
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"]);
|
|
6387
|
-
function words(text) {
|
|
6388
|
-
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)));
|
|
6389
|
-
}
|
|
6390
7078
|
function tellFor(key, value) {
|
|
6391
7079
|
if (key === "accent") {
|
|
6392
7080
|
const c = parseColour(value);
|
|
@@ -6403,44 +7091,58 @@ function tellFor(key, value) {
|
|
|
6403
7091
|
}
|
|
6404
7092
|
return null;
|
|
6405
7093
|
}
|
|
7094
|
+
var MIN_TOKENS = 5;
|
|
7095
|
+
var emptyLayers = () => ({ job: false, hierarchy: [], tokens: { decided: 0, total: CHOICE_KEYS.length }, complete: false });
|
|
6406
7096
|
function validateDirection(input, ctx = {}) {
|
|
6407
7097
|
const problems = [];
|
|
6408
7098
|
const err = (at, message) => problems.push({ severity: "error", at, message });
|
|
6409
7099
|
const warn = (at, message) => problems.push({ severity: "warn", at, message });
|
|
6410
7100
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
6411
7101
|
err("", "art-direction.json must be a JSON object");
|
|
6412
|
-
return { valid: false, problems, decided: 0 };
|
|
7102
|
+
return { valid: false, problems, decided: 0, layers: emptyLayers() };
|
|
6413
7103
|
}
|
|
6414
7104
|
const d = input;
|
|
6415
7105
|
const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
|
|
6416
7106
|
if (legacy) {
|
|
6417
7107
|
warn("", "exceptions only: run craft direction init to record the choices and their reasons");
|
|
6418
|
-
return { valid: true, problems, decided: 0 };
|
|
7108
|
+
return { valid: true, problems, decided: 0, layers: emptyLayers() };
|
|
6419
7109
|
}
|
|
6420
|
-
if (d.version
|
|
7110
|
+
if (!DIRECTION_VERSIONS.includes(d.version)) err("version", `version must be ${DIRECTION_VERSIONS.join(" or ")}`);
|
|
7111
|
+
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.");
|
|
6421
7112
|
if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
|
|
6422
7113
|
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");
|
|
6423
|
-
const
|
|
7114
|
+
const rawSources = Array.isArray(d.sources) ? d.sources : [];
|
|
7115
|
+
const sources = rawSources.filter((s) => Boolean(s && typeof s === "object" && !Array.isArray(s)));
|
|
6424
7116
|
if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
|
|
6425
7117
|
if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
|
|
6426
7118
|
const ids = /* @__PURE__ */ new Set();
|
|
6427
|
-
|
|
7119
|
+
rawSources.forEach((s, i) => {
|
|
6428
7120
|
const at = `sources[${i}]`;
|
|
6429
|
-
if (!s || typeof s !== "object") return err(at, "a source must be an object");
|
|
7121
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) return err(at, "a source must be an object");
|
|
6430
7122
|
if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
|
|
6431
7123
|
else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
|
|
6432
7124
|
else ids.add(s.id);
|
|
6433
7125
|
if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
|
|
6434
7126
|
if (typeof s.note !== "string" || s.note.trim().split(/\s+/).length < 3) err(`${at}.note`, "say what it is and where");
|
|
6435
|
-
if (s.path
|
|
6436
|
-
|
|
7127
|
+
if (s.path !== void 0 && typeof s.path !== "string") err(`${at}.path`, "a path must be text");
|
|
7128
|
+
if (typeof s.path === "string" && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
|
|
7129
|
+
if (s.colours !== void 0 && !Array.isArray(s.colours)) err(`${at}.colours`, "colours must be a list");
|
|
7130
|
+
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`);
|
|
7131
|
+
if (s.kind === "reference") {
|
|
7132
|
+
if (typeof s.category !== "string" || !s.category.trim()) err(`${at}.category`, "say where the reference comes from: print, a place, another trade");
|
|
7133
|
+
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 : "")) {
|
|
7134
|
+
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.`);
|
|
7135
|
+
}
|
|
7136
|
+
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");
|
|
7137
|
+
}
|
|
7138
|
+
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");
|
|
6437
7139
|
});
|
|
6438
7140
|
const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
|
|
6439
7141
|
if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
|
|
6440
7142
|
const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
|
|
6441
7143
|
let decided = 0;
|
|
6442
7144
|
for (const key of Object.keys(choices)) {
|
|
6443
|
-
if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}.
|
|
7145
|
+
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.`);
|
|
6444
7146
|
}
|
|
6445
7147
|
for (const key of CHOICE_KEYS) {
|
|
6446
7148
|
const c = choices[key];
|
|
@@ -6457,44 +7159,9 @@ function validateDirection(input, ctx = {}) {
|
|
|
6457
7159
|
err(`${at}.value`, `"${c.value}" is not a colour`);
|
|
6458
7160
|
ok = false;
|
|
6459
7161
|
}
|
|
6460
|
-
const
|
|
6461
|
-
|
|
6462
|
-
if (
|
|
6463
|
-
err(`${at}.because`, "a reason is at least a sentence. Without one this is a default, however good it looks.");
|
|
6464
|
-
ok = false;
|
|
6465
|
-
} else if (because.startsWith("PROPOSED:")) {
|
|
6466
|
-
err(`${at}.because`, "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
|
|
6467
|
-
ok = false;
|
|
6468
|
-
} else {
|
|
6469
|
-
if (PREFERENCE.test(because)) {
|
|
6470
|
-
err(`${at}.because`, "that is a preference, not a reason. Say what in the client's world this comes from.");
|
|
6471
|
-
ok = false;
|
|
6472
|
-
}
|
|
6473
|
-
const moods = because.match(MOOD) ?? [];
|
|
6474
|
-
if (moods.length >= 2) {
|
|
6475
|
-
err(`${at}.because`, `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
|
|
6476
|
-
ok = false;
|
|
6477
|
-
}
|
|
6478
|
-
}
|
|
6479
|
-
if (evidence.length === 0) {
|
|
6480
|
-
err(`${at}.evidence`, "cite at least one source");
|
|
6481
|
-
ok = false;
|
|
6482
|
-
}
|
|
6483
|
-
for (const id of evidence) {
|
|
6484
|
-
if (!ids.has(id)) {
|
|
6485
|
-
err(`${at}.evidence`, `no source called "${id}"`);
|
|
6486
|
-
ok = false;
|
|
6487
|
-
}
|
|
6488
|
-
}
|
|
6489
|
-
if (ok) {
|
|
6490
|
-
const cited = sources.filter((s) => evidence.includes(s.id));
|
|
6491
|
-
const reasonWords = words(because);
|
|
6492
|
-
const tied = cited.some((s) => reasonWords.has(s.id) || [...words(`${s.note} ${s.lettering ?? ""}`)].some((w) => reasonWords.has(w)));
|
|
6493
|
-
if (!tied) {
|
|
6494
|
-
err(`${at}.because`, `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
|
|
6495
|
-
ok = false;
|
|
6496
|
-
}
|
|
6497
|
-
}
|
|
7162
|
+
const reasons = checkReason({ at, because: c.because, evidence: c.evidence, sources, tie: "source" });
|
|
7163
|
+
problems.push(...reasons);
|
|
7164
|
+
if (reasons.length) ok = false;
|
|
6498
7165
|
if (typeof c.value === "string") {
|
|
6499
7166
|
const tell = tellFor(key, c.value);
|
|
6500
7167
|
if (tell && !exceptions.some((e) => e.tell === tell)) {
|
|
@@ -6530,7 +7197,33 @@ function validateDirection(input, ctx = {}) {
|
|
|
6530
7197
|
}
|
|
6531
7198
|
if (ok) decided += 1;
|
|
6532
7199
|
}
|
|
6533
|
-
|
|
7200
|
+
let job = false;
|
|
7201
|
+
let hierarchy = [];
|
|
7202
|
+
if (d.version === 2) {
|
|
7203
|
+
const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
|
|
7204
|
+
problems.push(...j.problems);
|
|
7205
|
+
job = j.decided && d.job !== void 0;
|
|
7206
|
+
const h = validateHierarchy(d.hierarchy, d.job, sources);
|
|
7207
|
+
problems.push(...h.problems);
|
|
7208
|
+
hierarchy = h.pages;
|
|
7209
|
+
if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
|
|
7210
|
+
}
|
|
7211
|
+
if (ctx.estate?.length) {
|
|
7212
|
+
for (const m of compareToDeclared(summariseDirection(d), ctx.estate, ctx.estateId)) {
|
|
7213
|
+
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.`);
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
if (ctx.strict) {
|
|
7217
|
+
for (const p of problems) if (p.severity === "warn" && /the page (?:does not show|sets|runs)/.test(p.message)) p.severity = "error";
|
|
7218
|
+
}
|
|
7219
|
+
const home = hierarchy.find((p) => p.page === "home");
|
|
7220
|
+
const complete = job && Boolean(home) && hierarchy.every((p) => p.decided === p.total) && decided >= MIN_TOKENS;
|
|
7221
|
+
return {
|
|
7222
|
+
valid: problems.every((p) => p.severity !== "error"),
|
|
7223
|
+
problems,
|
|
7224
|
+
decided,
|
|
7225
|
+
layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
|
|
7226
|
+
};
|
|
6534
7227
|
}
|
|
6535
7228
|
|
|
6536
7229
|
// src/direction/init.ts
|
|
@@ -6549,11 +7242,31 @@ function initDirection(input) {
|
|
|
6549
7242
|
body: empty(fp.body.family),
|
|
6550
7243
|
...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`) } : {}
|
|
6551
7244
|
} : {};
|
|
7245
|
+
const part = () => ({ value: "", evidence: [] });
|
|
6552
7246
|
return {
|
|
6553
7247
|
$schema: "https://unpkg.com/@domandigital/craft/art-direction.schema.json",
|
|
6554
7248
|
version: DIRECTION_VERSION,
|
|
6555
7249
|
client: input.client ?? "",
|
|
6556
7250
|
brief: input.brief ?? "",
|
|
7251
|
+
// Empty on purpose, like the reasons: the first validate lists the job and the structure as undecided.
|
|
7252
|
+
job: {
|
|
7253
|
+
statement: { verb: "", object: "", context: "" },
|
|
7254
|
+
functional: part(),
|
|
7255
|
+
emotional: part(),
|
|
7256
|
+
social: part(),
|
|
7257
|
+
consideration: { value: "low", because: "", evidence: [] },
|
|
7258
|
+
objections: [],
|
|
7259
|
+
language: [],
|
|
7260
|
+
evidence: []
|
|
7261
|
+
},
|
|
7262
|
+
hierarchy: {
|
|
7263
|
+
home: {
|
|
7264
|
+
primaryAction: { value: "", positions: [], because: "" },
|
|
7265
|
+
order: fp?.sections?.length ? fp.sections.map((s) => ({ role: s.role, because: "" })) : [],
|
|
7266
|
+
lead: { value: "", because: "" },
|
|
7267
|
+
journey: []
|
|
7268
|
+
}
|
|
7269
|
+
},
|
|
6557
7270
|
sources: [],
|
|
6558
7271
|
choices,
|
|
6559
7272
|
...input.exceptions?.length ? { exceptions: input.exceptions } : {}
|
|
@@ -6561,7 +7274,6 @@ function initDirection(input) {
|
|
|
6561
7274
|
}
|
|
6562
7275
|
|
|
6563
7276
|
// src/direction/propose.ts
|
|
6564
|
-
var PROPOSED = "PROPOSED:";
|
|
6565
7277
|
function srgbToLinear2(c) {
|
|
6566
7278
|
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
6567
7279
|
}
|
|
@@ -6623,7 +7335,13 @@ function accentCandidates(input) {
|
|
|
6623
7335
|
const parsed = parseColour(c);
|
|
6624
7336
|
if (!parsed || !chromatic(parsed.oklch)) continue;
|
|
6625
7337
|
const o = parsed.oklch;
|
|
6626
|
-
const siblings =
|
|
7338
|
+
const siblings = [
|
|
7339
|
+
...(input.estate ?? []).map((e) => ({ id: e.id, d: e.fingerprint.accent ? deltaEOk(o, e.fingerprint.accent) : 1 })),
|
|
7340
|
+
...(input.declared ?? []).flatMap((e) => {
|
|
7341
|
+
const p = parseColour(e.accent);
|
|
7342
|
+
return p ? [{ id: `${e.id} (declared)`, d: deltaEOk(o, p.oklch) }] : [];
|
|
7343
|
+
})
|
|
7344
|
+
];
|
|
6627
7345
|
const nearest2 = siblings.sort((a, b) => a.d - b.d)[0];
|
|
6628
7346
|
const penalty = isAiViolet(o) ? 1 : 0;
|
|
6629
7347
|
out.push({
|
|
@@ -6677,62 +7395,6 @@ function propose(input) {
|
|
|
6677
7395
|
};
|
|
6678
7396
|
}
|
|
6679
7397
|
|
|
6680
|
-
// src/estate/index.ts
|
|
6681
|
-
var ESTATE_VERSION = 1;
|
|
6682
|
-
var SIBLING_AT = 0.31;
|
|
6683
|
-
function emptyEstate() {
|
|
6684
|
-
return { version: ESTATE_VERSION, sites: [] };
|
|
6685
|
-
}
|
|
6686
|
-
function addSite(register, site) {
|
|
6687
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
|
|
6688
|
-
const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
|
|
6689
|
-
return { version: ESTATE_VERSION, sites };
|
|
6690
|
-
}
|
|
6691
|
-
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
6692
|
-
const out = [];
|
|
6693
|
-
const close = (k) => d.parts[k] <= 0.2;
|
|
6694
|
-
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
6695
|
-
if (close("type")) {
|
|
6696
|
-
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
6697
|
-
if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
|
|
6698
|
-
}
|
|
6699
|
-
if (close("ground")) out.push(`${groundName(a.ground)} ground`);
|
|
6700
|
-
if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
|
|
6701
|
-
if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
|
|
6702
|
-
if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
|
|
6703
|
-
if (close("layout")) out.push("running order");
|
|
6704
|
-
return out;
|
|
6705
|
-
}
|
|
6706
|
-
function compareToEstate(target2, register, options = {}) {
|
|
6707
|
-
const at = options.siblingAt ?? SIBLING_AT;
|
|
6708
|
-
return register.sites.filter((s) => s.id !== options.exclude).map((s) => {
|
|
6709
|
-
const d = fingerprintDistance(target2, s.fingerprint);
|
|
6710
|
-
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
|
|
6711
|
-
}).sort((a, b) => a.distance - b.distance);
|
|
6712
|
-
}
|
|
6713
|
-
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
6714
|
-
const out = [];
|
|
6715
|
-
const s = register.sites;
|
|
6716
|
-
for (let i = 0; i < s.length; i += 1) {
|
|
6717
|
-
for (let j = i + 1; j < s.length; j += 1) {
|
|
6718
|
-
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
6719
|
-
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) });
|
|
6720
|
-
}
|
|
6721
|
-
}
|
|
6722
|
-
return out.sort((x, y) => x.distance - y.distance);
|
|
6723
|
-
}
|
|
6724
|
-
function siblingLine(briefs) {
|
|
6725
|
-
const d = [];
|
|
6726
|
-
for (const runs of briefs) {
|
|
6727
|
-
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);
|
|
6728
|
-
}
|
|
6729
|
-
if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
|
|
6730
|
-
d.sort((a, b) => a - b);
|
|
6731
|
-
const mid = Math.floor(d.length / 2);
|
|
6732
|
-
const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
|
|
6733
|
-
return Math.round(m * 1e3) / 1e3;
|
|
6734
|
-
}
|
|
6735
|
-
|
|
6736
7398
|
// src/report/index.ts
|
|
6737
7399
|
var TELL_HEAVY = 3;
|
|
6738
7400
|
var MIN_DECIDED = 5;
|
|
@@ -6832,8 +7494,16 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
|
|
|
6832
7494
|
}
|
|
6833
7495
|
if (input.direction) {
|
|
6834
7496
|
const d = input.direction;
|
|
6835
|
-
const
|
|
6836
|
-
|
|
7497
|
+
const layers = d.layers;
|
|
7498
|
+
const raised2 = !d.valid || d.decided < MIN_DECIDED || !layers.complete;
|
|
7499
|
+
const pages = layers.hierarchy.map((p) => `${p.page} ${p.decided} of ${p.total}`).join(", ");
|
|
7500
|
+
const detail = [
|
|
7501
|
+
`job ${layers.job ? "decided" : "not decided"}`,
|
|
7502
|
+
`hierarchy ${pages || "not decided"}`,
|
|
7503
|
+
`${d.decided} of 7 choices decided with a reason`
|
|
7504
|
+
].join("; ");
|
|
7505
|
+
signals.push({ id: "reasons", raised: raised2, detail: `${detail}${d.valid ? "" : ", art-direction.json has errors"}` });
|
|
7506
|
+
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 });
|
|
6837
7507
|
for (const p of d.problems) {
|
|
6838
7508
|
const key = p.at.match(/^choices\.(\w+)/)?.[1];
|
|
6839
7509
|
if (p.severity === "error" || p.message === "not decided yet") {
|
|
@@ -6851,11 +7521,11 @@ function characterReport(input, copyTells = /* @__PURE__ */ new Set()) {
|
|
|
6851
7521
|
const order = ["direction", "type", "colour", "shape", "effects", "motion", "layout", "copy"];
|
|
6852
7522
|
const merged = [];
|
|
6853
7523
|
for (const a of actions) {
|
|
6854
|
-
const
|
|
6855
|
-
if (
|
|
7524
|
+
const same2 = a.choice && a.area !== "direction" && a.area !== "copy" ? merged.find((m) => m.choice === a.choice && m.area === a.area) : void 0;
|
|
7525
|
+
if (same2) {
|
|
6856
7526
|
const fromTell = /\(\S+, gen \d\)/.test(a.why);
|
|
6857
|
-
|
|
6858
|
-
|
|
7527
|
+
same2.why = `${same2.why}; ${fromTell ? a.why : `${a.what.replace(/\.$/, "")} (${a.why})`}`;
|
|
7528
|
+
same2.priority = Math.min(same2.priority, a.priority);
|
|
6859
7529
|
} else merged.push({ ...a });
|
|
6860
7530
|
}
|
|
6861
7531
|
const unique2 = merged.filter((a, i) => merged.findIndex((b) => b.area === a.area && b.what === a.what) === i);
|
|
@@ -6880,6 +7550,154 @@ function choiceOfShared(part) {
|
|
|
6880
7550
|
return /running order/.test(part) ? void 0 : "motif";
|
|
6881
7551
|
}
|
|
6882
7552
|
|
|
7553
|
+
// src/calibrate/index.ts
|
|
7554
|
+
var LABELS = ["ai", "ai-looking", "human"];
|
|
7555
|
+
var FLAGGED_TARGET = 0.9;
|
|
7556
|
+
var HUMAN_TYPICAL_TARGET = 0.1;
|
|
7557
|
+
var SURFACE = new Map(CATALOGUE.map((t) => [t.id, t.surface]));
|
|
7558
|
+
function designTells(tells) {
|
|
7559
|
+
return tells.filter((t) => SURFACE.has(t) && SURFACE.get(t) !== "copy");
|
|
7560
|
+
}
|
|
7561
|
+
function isFlagged(page) {
|
|
7562
|
+
return Boolean(page.typicality?.typical) || designTells(page.tells).length >= TELL_HEAVY;
|
|
7563
|
+
}
|
|
7564
|
+
var positive = (label) => label !== "human";
|
|
7565
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
7566
|
+
var share = (n, of) => `${n} of ${of} (${Math.round(100 * n / of)}%)`;
|
|
7567
|
+
function summarise(label, pages, typicality2) {
|
|
7568
|
+
const mine = pages.filter((p) => p.label === label);
|
|
7569
|
+
return {
|
|
7570
|
+
label,
|
|
7571
|
+
pages: mine.length,
|
|
7572
|
+
// A set measured partly against the null would count the same page two ways.
|
|
7573
|
+
flagged: mine.filter((p) => typicality2 ? isFlagged(p) : designTells(p.tells).length >= TELL_HEAVY).length,
|
|
7574
|
+
typical: typicality2 ? mine.filter((p) => p.typicality?.typical).length : null,
|
|
7575
|
+
tellHeavy: mine.filter((p) => designTells(p.tells).length >= TELL_HEAVY).length,
|
|
7576
|
+
blocking: mine.reduce((s, p) => s + p.blocking, 0)
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
function flaggedTarget(target2, s, missing) {
|
|
7580
|
+
const note = missing ? `; ${missing} not measured, counted neither way` : "";
|
|
7581
|
+
if (s.pages === 0) return { target: target2, met: null, detail: `no page measured${note}` };
|
|
7582
|
+
return { target: target2, met: s.flagged / s.pages >= FLAGGED_TARGET, detail: `${share(s.flagged, s.pages)} flagged, target ${FLAGGED_TARGET * 100}% or more${note}` };
|
|
7583
|
+
}
|
|
7584
|
+
function calibrate(pages, unmeasured = []) {
|
|
7585
|
+
const typicality2 = pages.length > 0 && pages.every((p) => p.typicality !== void 0);
|
|
7586
|
+
const labels = LABELS.map((l) => summarise(l, pages, typicality2));
|
|
7587
|
+
const byLabel = Object.fromEntries(labels.map((s) => [s.label, s]));
|
|
7588
|
+
const missing = (l) => unmeasured.filter((u) => u.label === l).length;
|
|
7589
|
+
const positives = pages.filter((p) => positive(p.label)).length;
|
|
7590
|
+
const fired = new Set(pages.flatMap((p) => p.tells));
|
|
7591
|
+
const tells = [...fired].map((tell) => {
|
|
7592
|
+
const hits = Object.fromEntries(LABELS.map((l) => [l, pages.filter((p) => p.label === l && p.tells.includes(tell)).length]));
|
|
7593
|
+
const on = hits.ai + hits["ai-looking"];
|
|
7594
|
+
return { tell, hits, precision: round2(on / (on + hits.human)), recall: positives ? round2(on / positives) : null };
|
|
7595
|
+
}).sort((a, b) => b.hits.ai + b.hits["ai-looking"] + b.hits.human - (a.hits.ai + a.hits["ai-looking"] + a.hits.human) || a.tell.localeCompare(b.tell));
|
|
7596
|
+
const human = byLabel.human;
|
|
7597
|
+
const humanNote = missing("human") ? `; ${missing("human")} not measured, counted neither way` : "";
|
|
7598
|
+
const targets = [
|
|
7599
|
+
flaggedTarget("1. Generated pages get caught", byLabel.ai, missing("ai")),
|
|
7600
|
+
human.pages === 0 ? { target: "2. Good human sites don't get caught", met: null, detail: `no page measured${humanNote}` } : {
|
|
7601
|
+
target: "2. Good human sites don't get caught",
|
|
7602
|
+
// A block hit fails it outright; without null models the other half is not known.
|
|
7603
|
+
met: human.blocking > 0 ? false : human.typical === null ? null : human.typical / human.pages <= HUMAN_TYPICAL_TARGET,
|
|
7604
|
+
detail: `${human.blocking} block hits, target 0; ${human.typical === null ? "typicality not measured (no null models)" : `${share(human.typical, human.pages)} typical, target ${HUMAN_TYPICAL_TARGET * 100}% or fewer`}${humanNote}`
|
|
7605
|
+
},
|
|
7606
|
+
flaggedTarget("3. Sites a person labelled AI-looking get caught", byLabel["ai-looking"], missing("ai-looking"))
|
|
7607
|
+
];
|
|
7608
|
+
return {
|
|
7609
|
+
pages: pages.length,
|
|
7610
|
+
typicality: typicality2,
|
|
7611
|
+
labels,
|
|
7612
|
+
tells,
|
|
7613
|
+
silent: CATALOGUE.map((t) => t.id).filter((id) => !fired.has(id)),
|
|
7614
|
+
targets,
|
|
7615
|
+
unmeasured
|
|
7616
|
+
};
|
|
7617
|
+
}
|
|
7618
|
+
function formatCalibration(c, notes = []) {
|
|
7619
|
+
const counts = c.labels.map((s) => `${s.pages} ${s.label}`).join(", ");
|
|
7620
|
+
const lines = [`craft calibrate: ${c.pages} pages measured (${counts})`, ...notes];
|
|
7621
|
+
if (!c.typicality) lines.push("No null models: flagged means tell-heavy only.");
|
|
7622
|
+
if (c.unmeasured.length) {
|
|
7623
|
+
lines.push("", `Not measured: ${c.unmeasured.length}. A page not measured never counts as a pass.`);
|
|
7624
|
+
for (const u of c.unmeasured) lines.push(` ${u.id} (${u.label}): ${u.reason}`);
|
|
7625
|
+
}
|
|
7626
|
+
lines.push("", "Targets");
|
|
7627
|
+
for (const t of c.targets) lines.push(` ${t.target.padEnd(50)} ${t.met === null ? "not known" : t.met ? "met" : "NOT MET"}`, ` ${t.detail}`);
|
|
7628
|
+
lines.push("", ` ${"label".padEnd(11)} ${"pages".padStart(5)} ${"flagged".padStart(8)} ${"typical".padStart(8)} ${"tell-heavy".padStart(11)} ${"blocking".padStart(9)}`);
|
|
7629
|
+
for (const s of c.labels) {
|
|
7630
|
+
lines.push(` ${s.label.padEnd(11)} ${String(s.pages).padStart(5)} ${String(s.flagged).padStart(8)} ${String(s.typical ?? "-").padStart(8)} ${String(s.tellHeavy).padStart(11)} ${String(s.blocking).padStart(9)}`);
|
|
7631
|
+
}
|
|
7632
|
+
lines.push("", "Per tell. A hit on ai or ai-looking counts for the tell; a hit on human counts against it.");
|
|
7633
|
+
lines.push(` ${"tell".padEnd(28)} ${"ai".padStart(7)} ${"ai-looking".padStart(10)} ${"human".padStart(7)} ${"precision".padStart(9)} ${"recall".padStart(7)}`);
|
|
7634
|
+
const of = (l) => c.labels.find((s) => s.label === l)?.pages ?? 0;
|
|
7635
|
+
for (const t of c.tells) {
|
|
7636
|
+
lines.push(
|
|
7637
|
+
` ${t.tell.padEnd(28)} ${`${t.hits.ai}/${of("ai")}`.padStart(7)} ${`${t.hits["ai-looking"]}/${of("ai-looking")}`.padStart(10)} ${`${t.hits.human}/${of("human")}`.padStart(7)} ${t.precision.toFixed(2).padStart(9)} ${(t.recall === null ? "-" : t.recall.toFixed(2)).padStart(7)}`
|
|
7638
|
+
);
|
|
7639
|
+
}
|
|
7640
|
+
if (!c.tells.length) lines.push(" no tell fired");
|
|
7641
|
+
lines.push("", `Never fired on this set: ${c.silent.length} of ${CATALOGUE.length} catalogue tells.`);
|
|
7642
|
+
return lines.join("\n");
|
|
7643
|
+
}
|
|
7644
|
+
|
|
7645
|
+
// src/estate/index.ts
|
|
7646
|
+
var ESTATE_VERSION = 1;
|
|
7647
|
+
var SIBLING_AT = 0.31;
|
|
7648
|
+
function emptyEstate() {
|
|
7649
|
+
return { version: ESTATE_VERSION, sites: [] };
|
|
7650
|
+
}
|
|
7651
|
+
function addSite(register, site) {
|
|
7652
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(site.id)) throw new Error(`site id "${site.id}" must be short kebab-case`);
|
|
7653
|
+
const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
|
|
7654
|
+
return { version: ESTATE_VERSION, sites };
|
|
7655
|
+
}
|
|
7656
|
+
function sharedParts(a, b, d = fingerprintDistance(a, b)) {
|
|
7657
|
+
const out = [];
|
|
7658
|
+
const close = (k) => d.parts[k] <= SHARED_PART;
|
|
7659
|
+
if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
|
|
7660
|
+
if (close("type")) {
|
|
7661
|
+
if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
|
|
7662
|
+
if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
|
|
7663
|
+
}
|
|
7664
|
+
if (close("ground")) out.push(`${groundName(a.ground)} ground`);
|
|
7665
|
+
if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
|
|
7666
|
+
if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
|
|
7667
|
+
if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
|
|
7668
|
+
if (close("layout")) out.push("running order");
|
|
7669
|
+
return out;
|
|
7670
|
+
}
|
|
7671
|
+
function compareToEstate(target2, register, options = {}) {
|
|
7672
|
+
const at = options.siblingAt ?? SIBLING_AT;
|
|
7673
|
+
return register.sites.filter((s) => s.id !== options.exclude && s.fingerprint).map((s) => {
|
|
7674
|
+
const d = fingerprintDistance(target2, s.fingerprint);
|
|
7675
|
+
return { id: s.id, distance: d.total, sibling: d.total < at, shared: sharedParts(target2, s.fingerprint, d) };
|
|
7676
|
+
}).sort((a, b) => a.distance - b.distance);
|
|
7677
|
+
}
|
|
7678
|
+
function estatePairs(register, siblingAt = SIBLING_AT) {
|
|
7679
|
+
const out = [];
|
|
7680
|
+
const s = register.sites.filter((site) => site.fingerprint);
|
|
7681
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
7682
|
+
for (let j = i + 1; j < s.length; j += 1) {
|
|
7683
|
+
const d = fingerprintDistance(s[i].fingerprint, s[j].fingerprint);
|
|
7684
|
+
out.push({ a: s[i].id, b: s[j].id, distance: d.total, sibling: d.total < siblingAt, shared: sharedParts(s[i].fingerprint, s[j].fingerprint, d) });
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7687
|
+
return out.sort((x, y) => x.distance - y.distance);
|
|
7688
|
+
}
|
|
7689
|
+
function siblingLine(briefs) {
|
|
7690
|
+
const d = [];
|
|
7691
|
+
for (const runs of briefs) {
|
|
7692
|
+
for (let i = 0; i < runs.length; i += 1) for (let j = i + 1; j < runs.length; j += 1) d.push(fingerprintDistance(runs[i], runs[j]).total);
|
|
7693
|
+
}
|
|
7694
|
+
if (d.length === 0) throw new Error("no null pages to measure the sibling line from");
|
|
7695
|
+
d.sort((a, b) => a - b);
|
|
7696
|
+
const mid = Math.floor(d.length / 2);
|
|
7697
|
+
const m = d.length % 2 ? d[mid] : (d[mid - 1] + d[mid]) / 2;
|
|
7698
|
+
return Math.round(m * 1e3) / 1e3;
|
|
7699
|
+
}
|
|
7700
|
+
|
|
6883
7701
|
// src/report/retrofit.ts
|
|
6884
7702
|
var HEADINGS = {
|
|
6885
7703
|
direction: "Decide first",
|
|
@@ -6932,6 +7750,8 @@ function retrofitPlan(report2, direction) {
|
|
|
6932
7750
|
return lines.join("\n");
|
|
6933
7751
|
}
|
|
6934
7752
|
export {
|
|
7753
|
+
ACCENT_SCALE,
|
|
7754
|
+
ACCENT_SHARED,
|
|
6935
7755
|
AI_PHRASES,
|
|
6936
7756
|
AI_SOURCE_TYPES,
|
|
6937
7757
|
AI_VIOLET,
|
|
@@ -6942,10 +7762,14 @@ export {
|
|
|
6942
7762
|
CATALOGUE_VERSION,
|
|
6943
7763
|
CHOICE_KEYS,
|
|
6944
7764
|
COMPONENT_ROLES,
|
|
7765
|
+
CONSIDERATION,
|
|
7766
|
+
CONVENTION,
|
|
7767
|
+
CUSTOMER_KINDS,
|
|
6945
7768
|
DEFAULT_IGNORE,
|
|
6946
7769
|
DENSITIES,
|
|
6947
7770
|
DENSITY_NAMES,
|
|
6948
7771
|
DIRECTION_VERSION,
|
|
7772
|
+
DIRECTION_VERSIONS,
|
|
6949
7773
|
DISTANCE_WEIGHTS,
|
|
6950
7774
|
DURATION_MS,
|
|
6951
7775
|
DURATION_S,
|
|
@@ -6955,22 +7779,33 @@ export {
|
|
|
6955
7779
|
ESTATE_VERSION,
|
|
6956
7780
|
EXIT_RATIO,
|
|
6957
7781
|
FINGERPRINT_VERSION,
|
|
7782
|
+
FLAGGED_TARGET,
|
|
7783
|
+
FUNCTION_WORDS,
|
|
7784
|
+
GROUND_SCALE,
|
|
7785
|
+
GROUND_SHARED,
|
|
7786
|
+
HIERARCHY_FIELDS,
|
|
6958
7787
|
HOME_FILE,
|
|
6959
7788
|
HOME_URL,
|
|
6960
7789
|
HOUSE,
|
|
6961
7790
|
HOUSE_BUDGET,
|
|
6962
7791
|
HOUSE_LOCKED,
|
|
6963
7792
|
HOUSE_TYPE,
|
|
7793
|
+
HUMAN_TYPICAL_TARGET,
|
|
6964
7794
|
IMAGERY_TELLS,
|
|
6965
7795
|
JSON_SCHEMA_VERSION,
|
|
7796
|
+
LABELS,
|
|
7797
|
+
LEXICAL_FEATURES,
|
|
6966
7798
|
LICENCES,
|
|
6967
7799
|
LIGHTNESS_CURVE,
|
|
6968
7800
|
MIN_DECIDED,
|
|
6969
7801
|
MIN_RUNS,
|
|
7802
|
+
MOOD,
|
|
6970
7803
|
NEGATIVE_REASSURANCE,
|
|
6971
7804
|
NULL_VERSION,
|
|
7805
|
+
PAGE_TYPES,
|
|
6972
7806
|
PILL_LIMIT,
|
|
6973
7807
|
PLAINER_WORDS,
|
|
7808
|
+
PREFERENCE,
|
|
6974
7809
|
PROMO_BADGE,
|
|
6975
7810
|
PROOF_NAMES,
|
|
6976
7811
|
PROPOSED,
|
|
@@ -6984,8 +7819,10 @@ export {
|
|
|
6984
7819
|
REVEAL_LIMIT,
|
|
6985
7820
|
REVIEW_PHRASES,
|
|
6986
7821
|
SCALE,
|
|
7822
|
+
SECTION_ROLES,
|
|
6987
7823
|
SHADCN_LIMIT,
|
|
6988
7824
|
SHADCN_PRIMITIVES,
|
|
7825
|
+
SHARED_PART,
|
|
6989
7826
|
SIBLING_AT,
|
|
6990
7827
|
SNAPSHOT_VERSION,
|
|
6991
7828
|
SOURCE_KINDS,
|
|
@@ -6998,6 +7835,7 @@ export {
|
|
|
6998
7835
|
STOCK_FIGURES,
|
|
6999
7836
|
STOCK_PHOTO_HOSTS,
|
|
7000
7837
|
STOCK_PHRASES,
|
|
7838
|
+
STUDY_WORDS,
|
|
7001
7839
|
TELL_HEAVY,
|
|
7002
7840
|
TRADE_NOUNS,
|
|
7003
7841
|
TYPE_STEPS,
|
|
@@ -7011,28 +7849,37 @@ export {
|
|
|
7011
7849
|
applySeverity,
|
|
7012
7850
|
auditSnapshot,
|
|
7013
7851
|
briefTerms,
|
|
7852
|
+
calibrate,
|
|
7014
7853
|
catalogueTable,
|
|
7015
7854
|
characterReport,
|
|
7016
7855
|
checkCopy,
|
|
7856
|
+
checkOrder,
|
|
7017
7857
|
checkPair,
|
|
7858
|
+
checkReason,
|
|
7018
7859
|
checkRestraint,
|
|
7019
7860
|
choicesOf,
|
|
7861
|
+
clauseDepth,
|
|
7020
7862
|
closestComponents,
|
|
7021
7863
|
compareCompetitor,
|
|
7864
|
+
compareDirections,
|
|
7022
7865
|
compareFacts,
|
|
7866
|
+
compareToDeclared,
|
|
7023
7867
|
compareToEstate,
|
|
7024
7868
|
componentDistance,
|
|
7025
7869
|
componentShared,
|
|
7026
7870
|
componentTypicalities,
|
|
7027
7871
|
componentTypicality,
|
|
7028
7872
|
componentsOf,
|
|
7873
|
+
contentWords,
|
|
7029
7874
|
countWords2 as countWords,
|
|
7030
7875
|
craftTokens,
|
|
7031
7876
|
createBaseline,
|
|
7877
|
+
cv,
|
|
7032
7878
|
deltaEOk,
|
|
7033
7879
|
deltaEOkHex,
|
|
7034
7880
|
densityCss,
|
|
7035
7881
|
densityTokens,
|
|
7882
|
+
designTells,
|
|
7036
7883
|
emitCss,
|
|
7037
7884
|
emptyEstate,
|
|
7038
7885
|
estateComponentPairs,
|
|
@@ -7049,14 +7896,18 @@ export {
|
|
|
7049
7896
|
fluidSpace,
|
|
7050
7897
|
fluidType,
|
|
7051
7898
|
fontClass,
|
|
7899
|
+
formatCalibration,
|
|
7052
7900
|
formatClaims,
|
|
7053
7901
|
formatComparison,
|
|
7054
7902
|
formatCompetitor,
|
|
7903
|
+
formatDirectionMatches,
|
|
7055
7904
|
formatHex,
|
|
7056
7905
|
formatReport,
|
|
7057
7906
|
globToRegExp,
|
|
7058
7907
|
groundName,
|
|
7059
7908
|
harvest,
|
|
7909
|
+
hedgeCount,
|
|
7910
|
+
hedgedClose,
|
|
7060
7911
|
hexToOklch,
|
|
7061
7912
|
holdHouseBlocks,
|
|
7062
7913
|
houseRule,
|
|
@@ -7067,28 +7918,39 @@ export {
|
|
|
7067
7918
|
inSrgbGamut,
|
|
7068
7919
|
initDirection,
|
|
7069
7920
|
isAiViolet,
|
|
7921
|
+
isBlurb,
|
|
7922
|
+
isCategoryLabel,
|
|
7070
7923
|
isCream,
|
|
7924
|
+
isFlagged,
|
|
7925
|
+
jobWords,
|
|
7071
7926
|
layoutDistance,
|
|
7927
|
+
lexicalOf,
|
|
7072
7928
|
licencesJson,
|
|
7073
7929
|
makeSnapshot,
|
|
7074
7930
|
motionTokens,
|
|
7075
7931
|
namedSources,
|
|
7076
7932
|
nearest,
|
|
7933
|
+
nominalisations,
|
|
7077
7934
|
normaliseFamily,
|
|
7078
7935
|
nullPrompt,
|
|
7079
7936
|
oklchToHex,
|
|
7080
7937
|
oklchToRgb,
|
|
7081
7938
|
pageCopy,
|
|
7082
7939
|
paletteFromPixels,
|
|
7940
|
+
paragraphUniformity,
|
|
7083
7941
|
parseBaseline,
|
|
7084
7942
|
parseColour,
|
|
7085
7943
|
parseCraftConfig,
|
|
7086
7944
|
parseHex,
|
|
7945
|
+
participialClauses,
|
|
7946
|
+
phrasalCoordinations,
|
|
7087
7947
|
propose,
|
|
7088
7948
|
protectedFacts,
|
|
7089
7949
|
ramp,
|
|
7090
7950
|
rampFromAnchors,
|
|
7091
7951
|
readSnapshot,
|
|
7952
|
+
renderedOrder,
|
|
7953
|
+
researchPrompt,
|
|
7092
7954
|
retrofitPlan,
|
|
7093
7955
|
runTell,
|
|
7094
7956
|
scanProvenance,
|
|
@@ -7097,26 +7959,40 @@ export {
|
|
|
7097
7959
|
semantic,
|
|
7098
7960
|
sentencesOf,
|
|
7099
7961
|
severityExceptions,
|
|
7962
|
+
shapeMoves,
|
|
7100
7963
|
shapeName,
|
|
7964
|
+
shapeOf,
|
|
7965
|
+
words as shapeWords,
|
|
7101
7966
|
sharedParts,
|
|
7102
7967
|
shouldAnimate,
|
|
7103
7968
|
siblingLine,
|
|
7969
|
+
skeleton,
|
|
7970
|
+
skeletonRepeat,
|
|
7104
7971
|
specificity,
|
|
7105
7972
|
specifics,
|
|
7973
|
+
splitSentences,
|
|
7974
|
+
stackedConditional,
|
|
7106
7975
|
stockAvatarSource,
|
|
7107
7976
|
stockSource,
|
|
7977
|
+
summariseDirection,
|
|
7108
7978
|
tailwindV3Preset,
|
|
7109
7979
|
tailwindV4Theme,
|
|
7110
7980
|
tellById,
|
|
7111
7981
|
toGamut,
|
|
7112
7982
|
toJson,
|
|
7113
7983
|
toSarif,
|
|
7984
|
+
tradeOf,
|
|
7985
|
+
tricolons,
|
|
7114
7986
|
typeFeatureTokens,
|
|
7115
7987
|
typicality,
|
|
7116
7988
|
unprovenClaims,
|
|
7117
7989
|
validateDirection,
|
|
7990
|
+
validateHierarchy,
|
|
7991
|
+
validateJob,
|
|
7118
7992
|
visibleText,
|
|
7119
7993
|
visualMeasures,
|
|
7994
|
+
vocabulary,
|
|
7995
|
+
vocabularyCounts,
|
|
7120
7996
|
wcagContrast,
|
|
7121
7997
|
wordListsJson
|
|
7122
7998
|
};
|