@domandigital/craft 0.16.0 → 0.17.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.
@@ -0,0 +1,1573 @@
1
+ import {
2
+ toJson
3
+ } from "./chunk-QIEDBL4J.js";
4
+ import {
5
+ AI_PHRASES,
6
+ AI_WORDS,
7
+ BUZZWORDS,
8
+ PLAINER_WORDS,
9
+ STOCK_PHRASES,
10
+ countWords,
11
+ specificity
12
+ } from "./chunk-4QWEAL4G.js";
13
+
14
+ // src/register/cli.ts
15
+ import { existsSync, readFileSync } from "fs";
16
+ import { resolve } from "path";
17
+
18
+ // src/character/lexical.ts
19
+ var JUZEK = "Juzek and Ward, arXiv:2412.11385, Appendix A (PubMed abstracts, 2020 to 2024)";
20
+ var STUDY_WORDS = [
21
+ "delves",
22
+ "delved",
23
+ "delving",
24
+ "delve",
25
+ "showcasing",
26
+ "boasts",
27
+ "underscores",
28
+ "underscoring",
29
+ "comprehending",
30
+ "intricacies",
31
+ "surpassing",
32
+ "intricate"
33
+ ].map((word) => ({ word, source: JUZEK }));
34
+ function vocabulary() {
35
+ const seen = /* @__PURE__ */ new Set();
36
+ const out = [];
37
+ const add = (word, source) => {
38
+ const w = word.toLowerCase();
39
+ if (seen.has(w)) return;
40
+ seen.add(w);
41
+ out.push({ word: w, source });
42
+ };
43
+ for (const s of STUDY_WORDS) add(s.word, s.source);
44
+ for (const w of AI_WORDS) add(w, "AI_WORDS");
45
+ for (const w of PLAINER_WORDS) add(w, "PLAINER_WORDS");
46
+ for (const w of AI_PHRASES) add(w, "AI_PHRASES");
47
+ for (const w of STOCK_PHRASES) add(w, "STOCK_PHRASES");
48
+ for (const w of BUZZWORDS) add(w, "BUZZWORDS");
49
+ return out;
50
+ }
51
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+ var pattern = (w) => new RegExp(`(?<![\\w-])${escape(w).replace(/\s+/g, "\\s+")}(?![\\w-])`, "gi");
53
+ var compiled = null;
54
+ var patterns = () => compiled ?? (compiled = vocabulary().map((v) => ({ ...v, re: pattern(v.word) })));
55
+ var plain = (text) => text.replace(/[’‘]/g, "'").replace(/\s+/g, " ");
56
+ function vocabularyCounts(text) {
57
+ const t = plain(text);
58
+ const out = /* @__PURE__ */ new Map();
59
+ for (const { word, re } of patterns()) {
60
+ const n = (t.match(re) ?? []).length;
61
+ if (n) out.set(word, n);
62
+ }
63
+ return out;
64
+ }
65
+ var STUDY = new Set(STUDY_WORDS.map((s) => s.word));
66
+ var LISTED = new Set([...AI_WORDS, ...PLAINER_WORDS, ...AI_PHRASES, ...STOCK_PHRASES, ...BUZZWORDS].map((w) => w.toLowerCase()));
67
+ var r3 = (n) => Math.round(n * 1e3) / 1e3;
68
+ function lexicalOf(text, brief) {
69
+ const words2 = Math.max(1, countWords(text));
70
+ const counts = vocabularyCounts(text);
71
+ let list = 0;
72
+ let study = 0;
73
+ for (const [w, n] of counts) {
74
+ if (STUDY.has(w)) study += n;
75
+ if (LISTED.has(w)) list += n;
76
+ }
77
+ const s = specificity(text, brief);
78
+ const kind = (...k) => s.specifics.filter((x) => k.includes(x.kind)).length;
79
+ return {
80
+ listRate: r3(list / words2 * 100),
81
+ studyRate: r3(study / words2 * 100),
82
+ hardPer100: r3(s.hard / words2 * 100),
83
+ tradePer100: r3(s.trade / words2 * 100),
84
+ specificsPer100: r3(s.perHundred),
85
+ generic: s.generic ? 1 : 0,
86
+ figures: kind("money", "date", "time", "number"),
87
+ names: kind("name", "brief")
88
+ };
89
+ }
90
+
91
+ // src/character/shape.ts
92
+ var ABBREVIATIONS = /\b(?:e\.g|i\.e|etc|vs|Mr|Mrs|Ms|Dr|St|No|approx|incl|Ltd|Co|Inc|Jr|Sr)\./g;
93
+ var DOT = "\0";
94
+ function splitSentences(text) {
95
+ const guarded = text.replace(/\s+/g, " ").replace(ABBREVIATIONS, (m) => m.replace(/\./g, DOT)).replace(/(\d)\.(\d)/g, `$1${DOT}$2`);
96
+ return guarded.split(/(?<=[.!?]["'’”)]*)\s+(?=["'‘“(]?[A-Z0-9£$€])/).map((s) => s.split(DOT).join(".").trim()).filter((s) => words(s).length > 0);
97
+ }
98
+ var WORD = /[A-Za-z0-9£$€][A-Za-z0-9£$€%'’-]*/g;
99
+ function words(text) {
100
+ return text.match(WORD) ?? [];
101
+ }
102
+ var lower = (w) => w.toLowerCase().replace(/’/g, "'");
103
+ var FUNCTION_WORDS = new Set(
104
+ "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(" ")
105
+ );
106
+ var ITEM = "[A-Za-z0-9\xA3$\u20AC'\u2019-]+(?: [A-Za-z0-9\xA3$\u20AC'\u2019-]+){0,5}";
107
+ var TRICOLON = new RegExp(`(${ITEM}), (${ITEM}),? (and|or) (${ITEM})(?=[,.;:!?)]|$)`, "g");
108
+ function tricolons(sentence) {
109
+ const s = sentence.replace(/\s+/g, " ");
110
+ const out = [];
111
+ for (const m of s.matchAll(TRICOLON)) {
112
+ const [, aSpan, b, , c] = m;
113
+ const bWords = words(b);
114
+ const cWords = words(c);
115
+ if (bWords.length === 0 || cWords.length === 0 || cWords.length > 6) continue;
116
+ const aWords = words(aSpan);
117
+ const a = aWords.slice(-Math.max(1, bWords.length));
118
+ const lead = lower(bWords[0]);
119
+ const parallel = bWords.length >= 2 && FUNCTION_WORDS.has(lead) && (lower(cWords[0]) === lead || lower(a[0]) === lead);
120
+ out.push({ text: m[0], parallel });
121
+ }
122
+ return out;
123
+ }
124
+ var STACKED = /,\s+(?:and|but|so|or)\s+(?:if|when|where|once|unless|provided)\b[^,.;!?]{1,60},/i;
125
+ function stackedConditional(sentence) {
126
+ return STACKED.test(sentence);
127
+ }
128
+ var SUBORDINATOR = /\b(?:if|when|which|because|so|while|where)\b/gi;
129
+ function clauseDepth(sentence) {
130
+ return (sentence.match(/,/g) ?? []).length + (sentence.match(SUBORDINATOR) ?? []).length;
131
+ }
132
+ var HEDGES = [
133
+ "if it's a fit",
134
+ "if it is a fit",
135
+ "where needed",
136
+ "if needed",
137
+ "as needed",
138
+ "where appropriate",
139
+ "if appropriate",
140
+ "as appropriate",
141
+ "as required",
142
+ "where required",
143
+ "if required",
144
+ "where possible",
145
+ "if possible",
146
+ "where relevant",
147
+ "if relevant",
148
+ "depending on",
149
+ "can help",
150
+ "tend to",
151
+ "tends to",
152
+ "may",
153
+ "might",
154
+ "could",
155
+ "typically",
156
+ "usually",
157
+ "generally",
158
+ "often",
159
+ "potentially",
160
+ "likely"
161
+ ];
162
+ var HEDGE = new RegExp(`\\b(?:${HEDGES.map((h) => h.replace(/'/g, "['\u2019]").replace(/ /g, "\\s+")).join("|")})\\b`, "gi");
163
+ var CONDITIONAL_CLOSE = /\b(?:if|unless|where|provided|depending)\b/i;
164
+ function hedgeCount(text) {
165
+ return (text.match(HEDGE) ?? []).length;
166
+ }
167
+ function hedgedClose(sentences) {
168
+ const last = sentences[sentences.length - 1];
169
+ if (!last) return false;
170
+ HEDGE.lastIndex = 0;
171
+ return HEDGE.test(last) || CONDITIONAL_CLOSE.test(last);
172
+ }
173
+ var NOMINAL = /^[a-z]{3,}(?:tion|sion|ment|ness|ity|ance|ence)s?$/;
174
+ var NOT_NOMINAL = new Set(
175
+ [
176
+ "nation station ration lotion potion portion question moment cement comment element garment segment fragment apartment document ointment",
177
+ "city pity entity quality sentence science audience silence balance finance distance instance entrance substance romance province"
178
+ ].join(" ").split(" ").flatMap((w) => [w, `${w}s`])
179
+ );
180
+ function nominalisations(text) {
181
+ return words(text).filter((w) => {
182
+ const l = lower(w);
183
+ return NOMINAL.test(l) && !NOT_NOMINAL.has(l);
184
+ }).length;
185
+ }
186
+ var PRONOUN_OPENER = /^["'‘“(]?(?:you|your|we|our)\b/i;
187
+ var TRANSITION_OPENER = /^["'‘“(]?(?:however|moreover|additionally|furthermore|thus|therefore|consequently|in addition|as a result|ultimately|overall|importantly|notably)\b/i;
188
+ var IMPERSONAL_OPENER = /^["'‘“(]?(?:it is|it's|it’s|there is|there are|there's|there’s)\b/i;
189
+ var NOT_PARTICIPLE = new Set(
190
+ "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(" ")
191
+ );
192
+ function participialClauses(sentence) {
193
+ let n = 0;
194
+ for (const m of sentence.matchAll(/,\s+([A-Za-z]{2,}ing)\b/g)) if (!NOT_PARTICIPLE.has(lower(m[1]))) n += 1;
195
+ const open = sentence.match(/^["'‘“(]?([A-Z][a-z]{2,}ing)\b[^,]{0,60},/);
196
+ if (open && !NOT_PARTICIPLE.has(lower(open[1]))) n += 1;
197
+ return n;
198
+ }
199
+ function phrasalCoordinations(sentence) {
200
+ let n = 0;
201
+ for (const m of sentence.matchAll(/(^|[^,]\s)([A-Za-z][a-z'’-]{2,}) (?:and|or) ([A-Za-z][a-z'’-]{2,})\b/g)) {
202
+ const a = lower(m[2]);
203
+ const b = lower(m[3]);
204
+ if (FUNCTION_WORDS.has(a) || FUNCTION_WORDS.has(b) || a.endsWith("ly") || b.endsWith("ly")) continue;
205
+ n += 1;
206
+ }
207
+ return n;
208
+ }
209
+ function skeleton(sentence) {
210
+ const out = [];
211
+ for (const w of words(sentence)) {
212
+ const l = lower(w);
213
+ const token = FUNCTION_WORDS.has(l) ? l : "_";
214
+ if (token === "_" && out[out.length - 1] === "_") continue;
215
+ out.push(token);
216
+ }
217
+ return out;
218
+ }
219
+ function skeletonRepeat(sentences) {
220
+ const grams = /* @__PURE__ */ new Map();
221
+ let total = 0;
222
+ for (const s of sentences) {
223
+ const sk = skeleton(s);
224
+ for (let i = 0; i + 4 <= sk.length; i += 1) {
225
+ const g = sk.slice(i, i + 4).join(" ");
226
+ grams.set(g, (grams.get(g) ?? 0) + 1);
227
+ total += 1;
228
+ }
229
+ }
230
+ if (total === 0) return 0;
231
+ let repeated = 0;
232
+ for (const count of grams.values()) if (count > 1) repeated += count;
233
+ return repeated / total;
234
+ }
235
+ var mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
236
+ var sd = (xs) => {
237
+ const m = mean(xs);
238
+ return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / xs.length);
239
+ };
240
+ function cv(xs, min) {
241
+ if (xs.length < min) return null;
242
+ const m = mean(xs);
243
+ return m === 0 ? null : sd(xs) / m;
244
+ }
245
+ function shapeOf(text) {
246
+ const ss = splitSentences(text);
247
+ const wordCount = words(text).length;
248
+ const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
249
+ const share = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
250
+ const tri = ss.flatMap(tricolons);
251
+ const commas = (text.match(/,/g) ?? []).length;
252
+ return {
253
+ sentences: ss.length,
254
+ words: wordCount,
255
+ tricolons: tri.length,
256
+ parallelTriads: tri.filter((t) => t.parallel).length,
257
+ stackedConditionals: ss.filter(stackedConditional).length,
258
+ clauseDepth: ss.length === 0 ? 0 : mean(ss.map(clauseDepth)),
259
+ sentenceCv: cv(ss.map((s) => words(s).length), 3),
260
+ hedgeRate: per100(hedgeCount(text)),
261
+ hedgedClose: hedgedClose(ss),
262
+ nominalisationRate: per100(nominalisations(text)),
263
+ pronounOpenerShare: share(PRONOUN_OPENER),
264
+ transitionOpenerShare: share(TRANSITION_OPENER),
265
+ impersonalOpenerShare: share(IMPERSONAL_OPENER),
266
+ commasPerSentence: ss.length === 0 ? 0 : commas / ss.length,
267
+ commasPer100: per100(commas),
268
+ participialRate: ss.length === 0 ? 0 : ss.reduce((s, x) => s + participialClauses(x), 0) / ss.length,
269
+ coordinationRate: per100(ss.reduce((s, x) => s + phrasalCoordinations(x), 0)),
270
+ skeletonRepeat: skeletonRepeat(ss)
271
+ };
272
+ }
273
+ function isBlurb(f) {
274
+ return f.sentences >= 2 && f.sentences <= 8 && f.words >= 20 && f.words <= 160;
275
+ }
276
+
277
+ // src/register/features.ts
278
+ var norm = (w) => w.toLowerCase().replace(/’/g, "'");
279
+ var YOU = /* @__PURE__ */ new Set(["you", "your", "yours", "yourself", "yourselves", "you'll", "you're", "you've", "you'd"]);
280
+ var FIRST = /* @__PURE__ */ new Set(["i", "me", "my", "mine", "we", "us", "our", "ours", "i'm", "i'll", "i've", "i'd", "we're", "we'll", "we've", "we'd"]);
281
+ var CONTRACTION = /^[a-z]+(?:n't|'ll|'re|'ve|'d|'m)$|^(?:it|that|there|here|what|who|where|how|he|she|let)'s$/;
282
+ var BE = "(?:am|is|are|was|were|be|been|being|'s|'re|'m)";
283
+ var IRREGULAR = "(?:made|done|given|taken|seen|known|shown|written|built|sent|paid|held|told|found|kept|left|brought|bought|thought|taught|caught|sold|put|set|run|cut|chosen|driven|eaten|fallen|forgotten|hidden|broken|spoken|stolen|worn|torn|grown|drawn|thrown|flown|begun|sung|won|lost|met|led|read|heard|meant|spent|built|lent|dealt|felt|sought|fed|bred|laid|said|struck|hung|shut|hit|hurt|let|quit|spread|bound|wound|ground|understood|withdrawn|overseen|undertaken)";
284
+ var PASSIVE = new RegExp(`\\b${BE}\\s+(?:[a-z]+ly\\s+)?(?:[a-z]+ed|[a-z]+en|${IRREGULAR})\\b`, "i");
285
+ var ADJECTIVAL = /* @__PURE__ */ new Set([
286
+ "tired",
287
+ "interested",
288
+ "excited",
289
+ "pleased",
290
+ "worried",
291
+ "concerned",
292
+ "surprised",
293
+ "bored",
294
+ "scared",
295
+ "married",
296
+ "qualified",
297
+ "experienced",
298
+ "based",
299
+ "located",
300
+ "committed",
301
+ "dedicated",
302
+ "delighted",
303
+ "disappointed",
304
+ "involved",
305
+ "prepared",
306
+ "supposed",
307
+ "used",
308
+ "open",
309
+ "often",
310
+ "even",
311
+ "seven",
312
+ "eleven",
313
+ "listen",
314
+ "happen",
315
+ "then",
316
+ "when",
317
+ "ten",
318
+ "garden",
319
+ "kitchen",
320
+ "children",
321
+ "women",
322
+ "men",
323
+ "red",
324
+ "bed",
325
+ "need",
326
+ "indeed",
327
+ "shed",
328
+ "seed",
329
+ "feed",
330
+ "speed",
331
+ "hundred"
332
+ ]);
333
+ function isPassive(sentence) {
334
+ const re = new RegExp(PASSIVE.source, "gi");
335
+ for (const m of sentence.matchAll(re)) {
336
+ const last = norm(m[0].trim().split(/\s+/).pop() ?? "");
337
+ if (last === "given" && /given\s+that\b/i.test(sentence.slice(m.index ?? 0))) continue;
338
+ if (!ADJECTIVAL.has(last)) return true;
339
+ }
340
+ return false;
341
+ }
342
+ function registerOf(text) {
343
+ const ss = splitSentences(text);
344
+ const ws = words(text).map(norm);
345
+ const n = ws.length;
346
+ const per100 = (k) => n === 0 ? 0 : 100 * k / n;
347
+ const letters = ws.reduce((s, w) => s + w.replace(/[^a-z]/g, "").length, 0);
348
+ return {
349
+ meanSentenceWords: ss.length === 0 ? 0 : n / ss.length,
350
+ contractionsPer100: per100(ws.filter((w) => CONTRACTION.test(w)).length),
351
+ youPer100: per100(ws.filter((w) => YOU.has(w)).length),
352
+ firstPersonPer100: per100(ws.filter((w) => FIRST.has(w)).length),
353
+ passiveShare: ss.length === 0 ? 0 : ss.filter(isPassive).length / ss.length,
354
+ meanWordLength: n === 0 ? 0 : letters / n
355
+ };
356
+ }
357
+
358
+ // src/register/registers.ts
359
+ var GOVUK = "GOV.UK writing guidelines";
360
+ var MAILCHIMP = "Mailchimp Content Style Guide";
361
+ var OGILVY = "Ogilvy, Ogilvy on Advertising (1983)";
362
+ var CAPLES = "Caples, Tested Advertising Methods";
363
+ var HOUSE = "house judgement";
364
+ var ALWAYS = "Every register keeps the COPY.md blocking tier and the proof rule. Draft first, then run `craft copy <file>` on the draft.";
365
+ var REGISTERS = [
366
+ {
367
+ id: "plain",
368
+ name: "Plain and trustworthy",
369
+ useFor: "Forms, policies, terms, instructions, and professional services where the reader must act correctly.",
370
+ directives: [
371
+ { text: "Put the most important information first, then taper down to the detail.", from: GOVUK },
372
+ { text: "Split any sentence over 25 words into two.", from: GOVUK },
373
+ { text: "Choose the short, common word: buy, help, about.", from: GOVUK },
374
+ { text: "Write in the active voice, so the reader sees who does what.", from: GOVUK },
375
+ { text: 'Address the reader as "you", as one person you can actually help.', from: GOVUK },
376
+ { text: `Use positive contractions such as "you'll", and write negatives out in full: "do not", "cannot".`, from: GOVUK },
377
+ { text: "Write headings that say what the section tells the reader, starting with a verb where you can.", from: GOVUK },
378
+ { text: "Give each paragraph one job.", from: HOUSE }
379
+ ],
380
+ example: {
381
+ before: "Should you wish to apply for a residents' parking permit, an application form must be completed and submitted to the council prior to the commencement of the permit period.",
382
+ after: "Apply for a residents' parking permit before you need it. Fill in the form online. It takes about 10 minutes.",
383
+ why: "The action comes first, the council is no longer the subject, and each sentence does one thing."
384
+ },
385
+ sources: [
386
+ {
387
+ name: GOVUK,
388
+ url: "https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/",
389
+ note: 'Clear language (the 25-word split, short words, active voice, contractions), clear structure (most important first, headings) and right tone (address the user as "you"). Checked against the live pages on 2026-09-30.'
390
+ }
391
+ ]
392
+ },
393
+ {
394
+ id: "persuasive",
395
+ name: "Persuasive and concise",
396
+ useFor: "Landing pages, service pages, adverts and sales emails: copy whose job is one action.",
397
+ directives: [
398
+ { text: "Put the promise in the headline: what the reader gets, in words they would use.", from: OGILVY },
399
+ { text: "Back each claim with something the reader can check: a number, a name, a date, a price.", from: "COPY.md" },
400
+ { text: 'Write to one reader, as "you", about their problem before your service.', from: CAPLES },
401
+ { text: "Name the mechanism: how it works, how long it takes, what it costs.", from: "COPY.md" },
402
+ { text: "Answer the objection the reader already has, with evidence, in the sentence that follows the claim.", from: HOUSE },
403
+ { text: "Let the copy run as long as the facts last, and cut each sentence that carries none.", from: OGILVY },
404
+ { text: "Close with one specific action and what happens straight after it.", from: "COPY.md" }
405
+ ],
406
+ example: {
407
+ before: "Transform your garden with our premium landscaping solutions. Quality you can trust.",
408
+ after: "We lay a 40 square metre Indian sandstone patio in three days, for \xA34,800 fitted. Every job since 2014 is on our reviews page. Book a free site visit and get a fixed quote within 48 hours.",
409
+ why: "Size, material, time and price replace the adjectives, the proof sits next to the claim, and the close says what happens next. Fictional business."
410
+ },
411
+ sources: [
412
+ {
413
+ name: OGILVY,
414
+ note: "Headlines carry the promise because far more people read the headline than the body; specific, informative copy outsells vague copy. Taken from the widely quoted principles; a page-by-page check against the book is still to do."
415
+ },
416
+ {
417
+ name: CAPLES,
418
+ note: "Headlines that appeal to the reader's self-interest, and copy judged by tested response rather than taste. A page-by-page check against the book is still to do."
419
+ },
420
+ {
421
+ name: "COPY.md",
422
+ note: "Writing directives (lead with the fact, name the mechanism, persuade with something checkable) and the CTA rule."
423
+ }
424
+ ]
425
+ },
426
+ {
427
+ id: "warm",
428
+ name: "Warm and conversational",
429
+ useFor: "Customer emails, replies to enquiries, newsletters and small-business updates.",
430
+ directives: [
431
+ { text: "Write the way you would talk to a customer you like: contractions, everyday words.", from: MAILCHIMP },
432
+ { text: "Keep words and sentences simple; being clear matters more than being entertaining.", from: MAILCHIMP },
433
+ { text: "Say what the reader can do, in positive language.", from: MAILCHIMP },
434
+ { text: "Use the active voice.", from: MAILCHIMP },
435
+ { text: "Match the tone to how the reader feels right now: lighter for good news, calm and direct for a problem.", from: MAILCHIMP },
436
+ { text: "Keep humour dry and occasional, and only where it comes naturally.", from: MAILCHIMP },
437
+ { text: "Sign off as a person, with a name.", from: HOUSE }
438
+ ],
439
+ example: {
440
+ before: "Dear Valued Customer, We wish to inform you that your order has been dispatched and will be delivered in due course.",
441
+ after: "Hi Sam, your order's on its way. It left our Leicester workshop this morning and should reach you on Thursday. Any questions, just reply to this email. Priya",
442
+ why: 'A name at each end, a real day and place instead of "in due course", and a next step. Fictional business.'
443
+ },
444
+ sources: [
445
+ {
446
+ name: MAILCHIMP,
447
+ url: "https://styleguide.mailchimp.com/voice-and-tone/",
448
+ note: "Voice and tone (plainspoken, genuine, dry humour; clear before entertaining; adjust tone to the reader's state) and grammar and mechanics (active voice, contractions, positive language). Its allowance for emoji does not apply: emoji block under COPY.md. Checked against the live pages on 2026-09-30."
449
+ }
450
+ ]
451
+ },
452
+ {
453
+ id: "literary",
454
+ name: "Elegant and literary",
455
+ useFor: "Premium brands, makers, hospitality and anything sold on craft and taste rather than price.",
456
+ directives: [
457
+ { text: "Choose one precise noun over two adjectives.", from: HOUSE },
458
+ { text: "Let sentence length follow the thought: a long sentence to build, a short one to land.", from: "COPY.md" },
459
+ { text: "Carry the detail in concrete facts: material, place, process, time.", from: HOUSE },
460
+ { text: "Trust the reader with one image, state it once, and move on.", from: HOUSE },
461
+ { text: "Keep claims quiet and exact, and let the facts carry the prestige.", from: HOUSE },
462
+ { text: "Draw any metaphor from the maker's own work, and hold it for the whole piece.", from: HOUSE },
463
+ { text: "Read the draft aloud and rework each line that trips the breath.", from: HOUSE }
464
+ ],
465
+ example: {
466
+ before: "Our luxurious, handcrafted candles create an unforgettable ambience of elegance and sophistication.",
467
+ after: "Each candle is poured by hand in our Hebden Bridge workshop, forty to a batch. The wax is English rapeseed and the scent is cedar, cut with a little smoke. It burns for sixty hours, and the glass is yours to keep.",
468
+ why: "Place, material, scent and burn time do the work the adjectives claimed. Fictional business."
469
+ },
470
+ sources: [
471
+ {
472
+ name: HOUSE,
473
+ note: "No public style authority exists for premium-brand prose. These directives are house judgement, to be revised as the estate grows."
474
+ },
475
+ {
476
+ name: "Register separation, 2026-09-29",
477
+ url: "calibration/copy-shape/report-registers.md",
478
+ note: "Literary prose (Gutenberg) carries about twice the comma density of the other human registers, the one feature that separated it from all four. Longer, jointed sentences are normal here."
479
+ }
480
+ ]
481
+ }
482
+ ];
483
+ var REGISTER_IDS = REGISTERS.map((r) => r.id);
484
+ function getRegister(id) {
485
+ return REGISTERS.find((r) => r.id === id);
486
+ }
487
+ function registerBrief(id) {
488
+ const r = getRegister(id);
489
+ if (!r) throw new Error(`unknown register "${id}". Registers: ${REGISTER_IDS.join(", ")}`);
490
+ const lines = [`Register: ${r.name} (${r.id})`, `Use for: ${r.useFor}`, "", "Before you draft:"];
491
+ r.directives.forEach((d, i) => lines.push(` ${i + 1}. ${d.text} [${d.from}]`));
492
+ lines.push("", "Example (fictional):", ` Before: ${r.example.before}`, ` After: ${r.example.after}`, ` Why: ${r.example.why}`);
493
+ lines.push("", "Sources:");
494
+ for (const s of r.sources) lines.push(` - ${s.name}${s.url ? ` (${s.url})` : ""}: ${s.note}`);
495
+ lines.push("", `Always: ${ALWAYS}`);
496
+ return lines.join("\n");
497
+ }
498
+
499
+ // src/register/check.ts
500
+ var REGISTER_BASELINE = {
501
+ plain: {
502
+ baseline: "institutional",
503
+ note: "Compared with Hansard: edited parliamentary debate, the closest free UK source, not plain writing itself."
504
+ },
505
+ persuasive: {
506
+ baseline: "marketing",
507
+ note: "Compared with 715 passages from UK small-business websites before 2021: a small sample."
508
+ },
509
+ warm: {
510
+ baseline: "business-email",
511
+ note: "Compared with Enron sent email, 1999 to 2002: corporate and American, not small-business email."
512
+ },
513
+ literary: {
514
+ baseline: "literary",
515
+ note: "Compared with English fiction and essays, 1726 to 1925: older than any premium brand's copy."
516
+ }
517
+ };
518
+ var MIN_PARAGRAPHS = 5;
519
+ var CONTENT_FEATURES = /* @__PURE__ */ new Set(["hardPer100", "specificsPer100", "tradePer100", "figures", "names", "generic"]);
520
+ var usable = (f, b) => !CONTENT_FEATURES.has(f) && b.p90 > b.p10;
521
+ var LABELS = {
522
+ meanSentenceWords: "Words per sentence",
523
+ contractionsPer100: "Contractions per 100 words",
524
+ youPer100: '"You" per 100 words',
525
+ firstPersonPer100: '"I" and "we" per 100 words',
526
+ passiveShare: "Share of sentences in the passive",
527
+ meanWordLength: "Letters per word",
528
+ commasPer100: "Commas per 100 words",
529
+ commasPerSentence: "Commas per sentence",
530
+ clauseDepth: "Clauses per sentence",
531
+ nominalisationRate: 'Nominalisations ("the provision of") per 100 words',
532
+ pronounOpenerShare: "Share of sentences opening with a pronoun",
533
+ impersonalOpenerShare: 'Share of sentences opening impersonally ("It is", "There are")',
534
+ coordinationRate: 'Paired phrases ("fast and friendly") per 100 words',
535
+ participialRate: '"-ing" clauses per sentence',
536
+ tricolons: "Lists of three",
537
+ sentenceCv: "Spread of sentence lengths",
538
+ skeletonRepeat: "Repeated sentence patterns",
539
+ hardPer100: "Hard specifics (names, figures, dates) per 100 words",
540
+ specificsPer100: "Specifics of any kind per 100 words",
541
+ tradePer100: "Trade nouns per 100 words",
542
+ figures: "Figures per paragraph",
543
+ names: "Names per paragraph",
544
+ generic: "Share of paragraphs with no specific at all"
545
+ };
546
+ var featureLabel = (f) => LABELS[f] ?? f;
547
+ function documentParagraphs(text) {
548
+ return text.replace(/\r\n?/g, "\n").split(/\n\s*\n/).map((p) => p.replace(/\s*\n\s*/g, " ").trim()).filter((p) => p && !/^#{1,6}\s/.test(p));
549
+ }
550
+ function measure(paragraph) {
551
+ const out = {};
552
+ for (const [k, v] of Object.entries(shapeOf(paragraph))) out[k] = typeof v === "boolean" ? v ? 1 : 0 : v;
553
+ for (const [k, v] of Object.entries(lexicalOf(paragraph))) out[k] = v;
554
+ for (const [k, v] of Object.entries(registerOf(paragraph))) out[k] = v;
555
+ return out;
556
+ }
557
+ var fmt = (x) => Math.abs(x) >= 10 ? x.toFixed(0) : x.toFixed(Math.abs(x) >= 1 ? 1 : 2);
558
+ function checkRegister(text, id, profile) {
559
+ const reg = getRegister(id);
560
+ if (!reg) throw new Error(`unknown register "${id}"`);
561
+ const { baseline, note } = REGISTER_BASELINE[id];
562
+ const target = profile[baseline];
563
+ if (!target) throw new Error(`no baseline profile for "${baseline}"`);
564
+ const kept = documentParagraphs(text).filter((p) => isBlurb(shapeOf(p)));
565
+ const base = { register: id, baseline, paragraphs: kept.length, note };
566
+ if (kept.length < MIN_PARAGRAPHS) {
567
+ return {
568
+ ...base,
569
+ measured: false,
570
+ means: {},
571
+ outside: [],
572
+ nearest: [],
573
+ summary: `Only ${kept.length} paragraph${kept.length === 1 ? "" : "s"} of 2 to 8 sentences. The registers overlap too much to place anything shorter than ${MIN_PARAGRAPHS}; read it against the brief instead.`
574
+ };
575
+ }
576
+ const rows = kept.map(measure);
577
+ const features = [...new Set(Object.values(profile).flatMap((p) => Object.keys(p)))];
578
+ const means = {};
579
+ for (const f of features) {
580
+ const xs = rows.map((r) => r[f]).filter((x) => typeof x === "number" && Number.isFinite(x));
581
+ if (xs.length) means[f] = xs.reduce((s, x) => s + x, 0) / xs.length;
582
+ }
583
+ const outside = [];
584
+ for (const [f, b] of Object.entries(target)) {
585
+ const v = means[f];
586
+ if (v === void 0 || b.separates.length === 0 || !usable(f, b)) continue;
587
+ if (v >= b.p10 && v <= b.p90) continue;
588
+ const direction = v > b.p90 ? "higher" : "lower";
589
+ outside.push({
590
+ feature: f,
591
+ value: v,
592
+ band: [b.p10, b.p90],
593
+ direction,
594
+ sentence: `${featureLabel(f)}: ${fmt(v)}. ${reg.name} writing in the baseline runs ${fmt(b.p10)} to ${fmt(b.p90)}, so this is ${direction}.`
595
+ });
596
+ }
597
+ const nearest = Object.entries(profile).map(([register, bands]) => {
598
+ const zs = Object.entries(bands).filter(([f, b]) => means[f] !== void 0 && b.sd > 0 && usable(f, b)).map(([f, b]) => ((means[f] - b.p50) / b.sd) ** 2);
599
+ return { register, distance: zs.length ? Math.sqrt(zs.reduce((s, z) => s + z, 0) / zs.length) : Infinity };
600
+ }).filter((n) => Number.isFinite(n.distance)).sort((a, b) => a.distance - b.distance);
601
+ const closest = nearest[0]?.register;
602
+ const summary = (outside.length === 0 ? `All ${Object.entries(target).filter(([f, b]) => b.separates.length && usable(f, b)).length} measured features sit inside the usual range for ${reg.name.toLowerCase()} writing.` : `${outside.length} feature${outside.length === 1 ? "" : "s"} outside the usual range for ${reg.name.toLowerCase()} writing.`) + (closest && closest !== baseline ? ` It reads closest to the ${closest} baseline.` : "");
603
+ return { ...base, measured: true, means, outside, nearest, summary };
604
+ }
605
+
606
+ // src/register/profile.ts
607
+ var PROFILE_MEASURED = "2026-09-30";
608
+ var PROFILE = {
609
+ "business-email": {
610
+ "clauseDepth": {
611
+ "p10": 0.36,
612
+ "p50": 0.673,
613
+ "p90": 1.113,
614
+ "sd": 0.307,
615
+ "separates": [
616
+ "explanatory",
617
+ "institutional",
618
+ "literary"
619
+ ]
620
+ },
621
+ "commasPer100": {
622
+ "p10": 1.424,
623
+ "p50": 3.19,
624
+ "p90": 5.154,
625
+ "sd": 1.529,
626
+ "separates": [
627
+ "institutional",
628
+ "literary"
629
+ ]
630
+ },
631
+ "commasPerSentence": {
632
+ "p10": 0.2,
633
+ "p50": 0.467,
634
+ "p90": 0.826,
635
+ "sd": 0.262,
636
+ "separates": [
637
+ "explanatory",
638
+ "institutional",
639
+ "literary",
640
+ "marketing"
641
+ ]
642
+ },
643
+ "coordinationRate": {
644
+ "p10": 0,
645
+ "p50": 0.743,
646
+ "p90": 1.712,
647
+ "sd": 0.666,
648
+ "separates": [
649
+ "marketing"
650
+ ]
651
+ },
652
+ "figures": {
653
+ "p10": 0.2,
654
+ "p50": 0.8,
655
+ "p90": 1.8,
656
+ "sd": 0.725,
657
+ "separates": [
658
+ "literary"
659
+ ]
660
+ },
661
+ "firstPersonPer100": {
662
+ "p10": 3.718,
663
+ "p50": 5.968,
664
+ "p90": 8.375,
665
+ "sd": 1.825,
666
+ "separates": [
667
+ "explanatory"
668
+ ]
669
+ },
670
+ "generic": {
671
+ "p10": 0,
672
+ "p50": 0,
673
+ "p90": 0.2,
674
+ "sd": 0.138,
675
+ "separates": [
676
+ "explanatory"
677
+ ]
678
+ },
679
+ "hardPer100": {
680
+ "p10": 4.289,
681
+ "p50": 7.127,
682
+ "p90": 11.308,
683
+ "sd": 2.843,
684
+ "separates": [
685
+ "explanatory",
686
+ "institutional",
687
+ "literary"
688
+ ]
689
+ },
690
+ "impersonalOpenerShare": {
691
+ "p10": 0,
692
+ "p50": 0,
693
+ "p90": 0.067,
694
+ "sd": 0.031,
695
+ "separates": [
696
+ "explanatory"
697
+ ]
698
+ },
699
+ "meanSentenceWords": {
700
+ "p10": 11.417,
701
+ "p50": 14.25,
702
+ "p90": 17.87,
703
+ "sd": 2.656,
704
+ "separates": [
705
+ "explanatory",
706
+ "literary"
707
+ ]
708
+ },
709
+ "meanWordLength": {
710
+ "p10": 3.858,
711
+ "p50": 4.16,
712
+ "p90": 4.459,
713
+ "sd": 0.231,
714
+ "separates": [
715
+ "explanatory"
716
+ ]
717
+ },
718
+ "names": {
719
+ "p10": 1.2,
720
+ "p50": 2.4,
721
+ "p90": 3.6,
722
+ "sd": 0.965,
723
+ "separates": [
724
+ "explanatory",
725
+ "institutional"
726
+ ]
727
+ },
728
+ "nominalisationRate": {
729
+ "p10": 0.625,
730
+ "p50": 1.93,
731
+ "p90": 3.831,
732
+ "sd": 1.264,
733
+ "separates": [
734
+ "institutional",
735
+ "marketing"
736
+ ]
737
+ },
738
+ "participialRate": {
739
+ "p10": 0,
740
+ "p50": 0,
741
+ "p90": 0.05,
742
+ "sd": 0.035,
743
+ "separates": [
744
+ "literary"
745
+ ]
746
+ },
747
+ "pronounOpenerShare": {
748
+ "p10": 0,
749
+ "p50": 0.05,
750
+ "p90": 0.15,
751
+ "sd": 0.068,
752
+ "separates": [
753
+ "marketing"
754
+ ]
755
+ },
756
+ "sentenceCv": {
757
+ "p10": 0.344,
758
+ "p50": 0.468,
759
+ "p90": 0.6,
760
+ "sd": 0.099,
761
+ "separates": [
762
+ "marketing"
763
+ ]
764
+ },
765
+ "skeletonRepeat": {
766
+ "p10": 0,
767
+ "p50": 0.017,
768
+ "p90": 0.063,
769
+ "sd": 0.031,
770
+ "separates": [
771
+ "institutional"
772
+ ]
773
+ },
774
+ "specificsPer100": {
775
+ "p10": 4.22,
776
+ "p50": 7.34,
777
+ "p90": 11.46,
778
+ "sd": 2.892,
779
+ "separates": [
780
+ "explanatory",
781
+ "institutional",
782
+ "literary"
783
+ ]
784
+ },
785
+ "tradePer100": {
786
+ "p10": 0,
787
+ "p50": 0,
788
+ "p90": 0.588,
789
+ "sd": 0.287,
790
+ "separates": [
791
+ "marketing"
792
+ ]
793
+ },
794
+ "tricolons": {
795
+ "p10": 0,
796
+ "p50": 0,
797
+ "p90": 0.2,
798
+ "sd": 0.142,
799
+ "separates": [
800
+ "institutional",
801
+ "literary",
802
+ "marketing"
803
+ ]
804
+ }
805
+ },
806
+ "explanatory": {
807
+ "clauseDepth": {
808
+ "p10": 0.78,
809
+ "p50": 1.194,
810
+ "p90": 1.733,
811
+ "sd": 0.383,
812
+ "separates": [
813
+ "business-email",
814
+ "literary"
815
+ ]
816
+ },
817
+ "commasPer100": {
818
+ "p10": 2.579,
819
+ "p50": 4.164,
820
+ "p90": 6.089,
821
+ "sd": 1.423,
822
+ "separates": [
823
+ "literary"
824
+ ]
825
+ },
826
+ "commasPerSentence": {
827
+ "p10": 0.448,
828
+ "p50": 0.77,
829
+ "p90": 1.2,
830
+ "sd": 0.309,
831
+ "separates": [
832
+ "business-email",
833
+ "literary"
834
+ ]
835
+ },
836
+ "coordinationRate": {
837
+ "p10": 0.323,
838
+ "p50": 0.973,
839
+ "p90": 1.862,
840
+ "sd": 0.612,
841
+ "separates": [
842
+ "marketing"
843
+ ]
844
+ },
845
+ "figures": {
846
+ "p10": 0,
847
+ "p50": 0.4,
848
+ "p90": 1.4,
849
+ "sd": 0.631,
850
+ "separates": [
851
+ "literary"
852
+ ]
853
+ },
854
+ "firstPersonPer100": {
855
+ "p10": 0.27,
856
+ "p50": 1.282,
857
+ "p90": 3.114,
858
+ "sd": 1.181,
859
+ "separates": [
860
+ "business-email",
861
+ "literary"
862
+ ]
863
+ },
864
+ "generic": {
865
+ "p10": 0,
866
+ "p50": 0.4,
867
+ "p90": 0.6,
868
+ "sd": 0.214,
869
+ "separates": [
870
+ "business-email",
871
+ "institutional"
872
+ ]
873
+ },
874
+ "hardPer100": {
875
+ "p10": 0.988,
876
+ "p50": 2.62,
877
+ "p90": 5.171,
878
+ "sd": 1.713,
879
+ "separates": [
880
+ "business-email",
881
+ "institutional",
882
+ "marketing"
883
+ ]
884
+ },
885
+ "impersonalOpenerShare": {
886
+ "p10": 0,
887
+ "p50": 0.04,
888
+ "p90": 0.14,
889
+ "sd": 0.061,
890
+ "separates": [
891
+ "business-email"
892
+ ]
893
+ },
894
+ "meanSentenceWords": {
895
+ "p10": 14.973,
896
+ "p50": 18.31,
897
+ "p90": 22.533,
898
+ "sd": 3.013,
899
+ "separates": [
900
+ "business-email"
901
+ ]
902
+ },
903
+ "meanWordLength": {
904
+ "p10": 4.207,
905
+ "p50": 4.466,
906
+ "p90": 4.752,
907
+ "sd": 0.213,
908
+ "separates": [
909
+ "business-email",
910
+ "literary"
911
+ ]
912
+ },
913
+ "names": {
914
+ "p10": 0.4,
915
+ "p50": 1.2,
916
+ "p90": 2.6,
917
+ "sd": 0.941,
918
+ "separates": [
919
+ "business-email",
920
+ "institutional"
921
+ ]
922
+ },
923
+ "nominalisationRate": {
924
+ "p10": 0.766,
925
+ "p50": 1.823,
926
+ "p90": 3.416,
927
+ "sd": 1.061,
928
+ "separates": [
929
+ "institutional",
930
+ "marketing"
931
+ ]
932
+ },
933
+ "participialRate": {
934
+ "p10": 0,
935
+ "p50": 0,
936
+ "p90": 0.1,
937
+ "sd": 0.045,
938
+ "separates": []
939
+ },
940
+ "pronounOpenerShare": {
941
+ "p10": 0,
942
+ "p50": 0.033,
943
+ "p90": 0.124,
944
+ "sd": 0.056,
945
+ "separates": [
946
+ "marketing"
947
+ ]
948
+ },
949
+ "sentenceCv": {
950
+ "p10": 0.337,
951
+ "p50": 0.441,
952
+ "p90": 0.57,
953
+ "sd": 0.092,
954
+ "separates": []
955
+ },
956
+ "skeletonRepeat": {
957
+ "p10": 9e-3,
958
+ "p50": 0.039,
959
+ "p90": 0.091,
960
+ "sd": 0.036,
961
+ "separates": []
962
+ },
963
+ "specificsPer100": {
964
+ "p10": 1.04,
965
+ "p50": 2.7,
966
+ "p90": 5.36,
967
+ "sd": 1.731,
968
+ "separates": [
969
+ "business-email",
970
+ "institutional",
971
+ "marketing"
972
+ ]
973
+ },
974
+ "tradePer100": {
975
+ "p10": 0,
976
+ "p50": 0,
977
+ "p90": 0.427,
978
+ "sd": 0.216,
979
+ "separates": [
980
+ "marketing"
981
+ ]
982
+ },
983
+ "tricolons": {
984
+ "p10": 0,
985
+ "p50": 0.2,
986
+ "p90": 0.6,
987
+ "sd": 0.222,
988
+ "separates": []
989
+ }
990
+ },
991
+ "institutional": {
992
+ "clauseDepth": {
993
+ "p10": 0.792,
994
+ "p50": 1.233,
995
+ "p90": 1.76,
996
+ "sd": 0.377,
997
+ "separates": [
998
+ "business-email",
999
+ "literary"
1000
+ ]
1001
+ },
1002
+ "commasPer100": {
1003
+ "p10": 3.03,
1004
+ "p50": 4.435,
1005
+ "p90": 5.941,
1006
+ "sd": 1.165,
1007
+ "separates": [
1008
+ "business-email",
1009
+ "literary"
1010
+ ]
1011
+ },
1012
+ "commasPerSentence": {
1013
+ "p10": 0.61,
1014
+ "p50": 0.96,
1015
+ "p90": 1.375,
1016
+ "sd": 0.307,
1017
+ "separates": [
1018
+ "business-email",
1019
+ "literary"
1020
+ ]
1021
+ },
1022
+ "coordinationRate": {
1023
+ "p10": 0.323,
1024
+ "p50": 0.949,
1025
+ "p90": 1.752,
1026
+ "sd": 0.559,
1027
+ "separates": [
1028
+ "marketing"
1029
+ ]
1030
+ },
1031
+ "figures": {
1032
+ "p10": 0,
1033
+ "p50": 0.6,
1034
+ "p90": 1.4,
1035
+ "sd": 0.56,
1036
+ "separates": [
1037
+ "literary"
1038
+ ]
1039
+ },
1040
+ "generic": {
1041
+ "p10": 0,
1042
+ "p50": 0,
1043
+ "p90": 0.2,
1044
+ "sd": 0.114,
1045
+ "separates": [
1046
+ "explanatory",
1047
+ "literary",
1048
+ "marketing"
1049
+ ]
1050
+ },
1051
+ "hardPer100": {
1052
+ "p10": 3.41,
1053
+ "p50": 5.135,
1054
+ "p90": 7.304,
1055
+ "sd": 1.573,
1056
+ "separates": [
1057
+ "business-email",
1058
+ "explanatory",
1059
+ "literary"
1060
+ ]
1061
+ },
1062
+ "impersonalOpenerShare": {
1063
+ "p10": 0,
1064
+ "p50": 0.033,
1065
+ "p90": 0.109,
1066
+ "sd": 0.051,
1067
+ "separates": []
1068
+ },
1069
+ "names": {
1070
+ "p10": 2,
1071
+ "p50": 3.2,
1072
+ "p90": 4.8,
1073
+ "sd": 1.113,
1074
+ "separates": [
1075
+ "business-email",
1076
+ "explanatory",
1077
+ "literary",
1078
+ "marketing"
1079
+ ]
1080
+ },
1081
+ "nominalisationRate": {
1082
+ "p10": 1.992,
1083
+ "p50": 3.391,
1084
+ "p90": 5.008,
1085
+ "sd": 1.147,
1086
+ "separates": [
1087
+ "business-email",
1088
+ "explanatory",
1089
+ "literary"
1090
+ ]
1091
+ },
1092
+ "participialRate": {
1093
+ "p10": 0,
1094
+ "p50": 0,
1095
+ "p90": 0.1,
1096
+ "sd": 0.044,
1097
+ "separates": [
1098
+ "literary"
1099
+ ]
1100
+ },
1101
+ "pronounOpenerShare": {
1102
+ "p10": 0,
1103
+ "p50": 0.05,
1104
+ "p90": 0.164,
1105
+ "sd": 0.068,
1106
+ "separates": [
1107
+ "marketing"
1108
+ ]
1109
+ },
1110
+ "sentenceCv": {
1111
+ "p10": 0.386,
1112
+ "p50": 0.512,
1113
+ "p90": 0.639,
1114
+ "sd": 0.097,
1115
+ "separates": [
1116
+ "marketing"
1117
+ ]
1118
+ },
1119
+ "skeletonRepeat": {
1120
+ "p10": 0.018,
1121
+ "p50": 0.048,
1122
+ "p90": 0.087,
1123
+ "sd": 0.028,
1124
+ "separates": [
1125
+ "business-email"
1126
+ ]
1127
+ },
1128
+ "specificsPer100": {
1129
+ "p10": 3.36,
1130
+ "p50": 5.2,
1131
+ "p90": 7.4,
1132
+ "sd": 1.612,
1133
+ "separates": [
1134
+ "business-email",
1135
+ "explanatory",
1136
+ "literary"
1137
+ ]
1138
+ },
1139
+ "tradePer100": {
1140
+ "p10": 0,
1141
+ "p50": 0,
1142
+ "p90": 0.157,
1143
+ "sd": 0.101,
1144
+ "separates": [
1145
+ "marketing"
1146
+ ]
1147
+ },
1148
+ "tricolons": {
1149
+ "p10": 0,
1150
+ "p50": 0.2,
1151
+ "p90": 0.6,
1152
+ "sd": 0.233,
1153
+ "separates": [
1154
+ "business-email"
1155
+ ]
1156
+ }
1157
+ },
1158
+ "literary": {
1159
+ "clauseDepth": {
1160
+ "p10": 1.333,
1161
+ "p50": 2.017,
1162
+ "p90": 2.933,
1163
+ "sd": 0.626,
1164
+ "separates": [
1165
+ "business-email",
1166
+ "explanatory",
1167
+ "institutional",
1168
+ "marketing"
1169
+ ]
1170
+ },
1171
+ "commasPer100": {
1172
+ "p10": 5.997,
1173
+ "p50": 8.1,
1174
+ "p90": 10.586,
1175
+ "sd": 1.815,
1176
+ "separates": [
1177
+ "business-email",
1178
+ "explanatory",
1179
+ "institutional",
1180
+ "marketing"
1181
+ ]
1182
+ },
1183
+ "commasPerSentence": {
1184
+ "p10": 1.067,
1185
+ "p50": 1.667,
1186
+ "p90": 2.435,
1187
+ "sd": 0.541,
1188
+ "separates": [
1189
+ "business-email",
1190
+ "explanatory",
1191
+ "institutional",
1192
+ "marketing"
1193
+ ]
1194
+ },
1195
+ "coordinationRate": {
1196
+ "p10": 0.194,
1197
+ "p50": 0.814,
1198
+ "p90": 1.633,
1199
+ "sd": 0.572,
1200
+ "separates": [
1201
+ "marketing"
1202
+ ]
1203
+ },
1204
+ "figures": {
1205
+ "p10": 0,
1206
+ "p50": 0,
1207
+ "p90": 0,
1208
+ "sd": 0.079,
1209
+ "separates": [
1210
+ "business-email",
1211
+ "explanatory",
1212
+ "institutional",
1213
+ "marketing"
1214
+ ]
1215
+ },
1216
+ "firstPersonPer100": {
1217
+ "p10": 2.207,
1218
+ "p50": 4.67,
1219
+ "p90": 7.486,
1220
+ "sd": 2.027,
1221
+ "separates": [
1222
+ "explanatory"
1223
+ ]
1224
+ },
1225
+ "generic": {
1226
+ "p10": 0,
1227
+ "p50": 0.2,
1228
+ "p90": 0.4,
1229
+ "sd": 0.178,
1230
+ "separates": [
1231
+ "institutional"
1232
+ ]
1233
+ },
1234
+ "hardPer100": {
1235
+ "p10": 1.632,
1236
+ "p50": 3.076,
1237
+ "p90": 5.154,
1238
+ "sd": 1.367,
1239
+ "separates": [
1240
+ "business-email",
1241
+ "institutional"
1242
+ ]
1243
+ },
1244
+ "impersonalOpenerShare": {
1245
+ "p10": 0,
1246
+ "p50": 0,
1247
+ "p90": 0.067,
1248
+ "sd": 0.039,
1249
+ "separates": []
1250
+ },
1251
+ "meanSentenceWords": {
1252
+ "p10": 15.083,
1253
+ "p50": 20.35,
1254
+ "p90": 27.027,
1255
+ "sd": 4.744,
1256
+ "separates": [
1257
+ "business-email"
1258
+ ]
1259
+ },
1260
+ "meanWordLength": {
1261
+ "p10": 3.958,
1262
+ "p50": 4.186,
1263
+ "p90": 4.409,
1264
+ "sd": 0.175,
1265
+ "separates": [
1266
+ "explanatory"
1267
+ ]
1268
+ },
1269
+ "names": {
1270
+ "p10": 1,
1271
+ "p50": 1.8,
1272
+ "p90": 3,
1273
+ "sd": 0.816,
1274
+ "separates": [
1275
+ "institutional"
1276
+ ]
1277
+ },
1278
+ "nominalisationRate": {
1279
+ "p10": 0.444,
1280
+ "p50": 1.334,
1281
+ "p90": 2.486,
1282
+ "sd": 0.817,
1283
+ "separates": [
1284
+ "institutional",
1285
+ "marketing"
1286
+ ]
1287
+ },
1288
+ "participialRate": {
1289
+ "p10": 0,
1290
+ "p50": 0.067,
1291
+ "p90": 0.2,
1292
+ "sd": 0.089,
1293
+ "separates": [
1294
+ "business-email",
1295
+ "institutional"
1296
+ ]
1297
+ },
1298
+ "pronounOpenerShare": {
1299
+ "p10": 0,
1300
+ "p50": 0.025,
1301
+ "p90": 0.12,
1302
+ "sd": 0.058,
1303
+ "separates": [
1304
+ "marketing"
1305
+ ]
1306
+ },
1307
+ "sentenceCv": {
1308
+ "p10": 0.367,
1309
+ "p50": 0.485,
1310
+ "p90": 0.625,
1311
+ "sd": 0.1,
1312
+ "separates": [
1313
+ "marketing"
1314
+ ]
1315
+ },
1316
+ "skeletonRepeat": {
1317
+ "p10": 5e-3,
1318
+ "p50": 0.029,
1319
+ "p90": 0.07,
1320
+ "sd": 0.028,
1321
+ "separates": []
1322
+ },
1323
+ "specificsPer100": {
1324
+ "p10": 1.8,
1325
+ "p50": 3.28,
1326
+ "p90": 5.24,
1327
+ "sd": 1.365,
1328
+ "separates": [
1329
+ "business-email",
1330
+ "institutional",
1331
+ "marketing"
1332
+ ]
1333
+ },
1334
+ "tradePer100": {
1335
+ "p10": 0,
1336
+ "p50": 0,
1337
+ "p90": 0.508,
1338
+ "sd": 0.275,
1339
+ "separates": [
1340
+ "marketing"
1341
+ ]
1342
+ },
1343
+ "tricolons": {
1344
+ "p10": 0,
1345
+ "p50": 0.4,
1346
+ "p90": 0.8,
1347
+ "sd": 0.314,
1348
+ "separates": [
1349
+ "business-email"
1350
+ ]
1351
+ }
1352
+ },
1353
+ "marketing": {
1354
+ "clauseDepth": {
1355
+ "p10": 0.567,
1356
+ "p50": 0.967,
1357
+ "p90": 1.56,
1358
+ "sd": 0.413,
1359
+ "separates": [
1360
+ "literary"
1361
+ ]
1362
+ },
1363
+ "commasPer100": {
1364
+ "p10": 2.169,
1365
+ "p50": 4.087,
1366
+ "p90": 6.434,
1367
+ "sd": 1.667,
1368
+ "separates": [
1369
+ "literary"
1370
+ ]
1371
+ },
1372
+ "commasPerSentence": {
1373
+ "p10": 0.4,
1374
+ "p50": 0.8,
1375
+ "p90": 1.35,
1376
+ "sd": 0.402,
1377
+ "separates": [
1378
+ "business-email",
1379
+ "literary"
1380
+ ]
1381
+ },
1382
+ "coordinationRate": {
1383
+ "p10": 1.087,
1384
+ "p50": 2.236,
1385
+ "p90": 3.473,
1386
+ "sd": 0.931,
1387
+ "separates": [
1388
+ "business-email",
1389
+ "explanatory",
1390
+ "institutional",
1391
+ "literary"
1392
+ ]
1393
+ },
1394
+ "figures": {
1395
+ "p10": 0,
1396
+ "p50": 0.4,
1397
+ "p90": 1,
1398
+ "sd": 0.427,
1399
+ "separates": [
1400
+ "literary"
1401
+ ]
1402
+ },
1403
+ "generic": {
1404
+ "p10": 0,
1405
+ "p50": 0.2,
1406
+ "p90": 0.6,
1407
+ "sd": 0.206,
1408
+ "separates": [
1409
+ "institutional"
1410
+ ]
1411
+ },
1412
+ "hardPer100": {
1413
+ "p10": 1.899,
1414
+ "p50": 4.549,
1415
+ "p90": 8.227,
1416
+ "sd": 2.655,
1417
+ "separates": [
1418
+ "explanatory"
1419
+ ]
1420
+ },
1421
+ "impersonalOpenerShare": {
1422
+ "p10": 0,
1423
+ "p50": 0,
1424
+ "p90": 0.1,
1425
+ "sd": 0.047,
1426
+ "separates": []
1427
+ },
1428
+ "names": {
1429
+ "p10": 0.6,
1430
+ "p50": 1.8,
1431
+ "p90": 3.4,
1432
+ "sd": 1.21,
1433
+ "separates": [
1434
+ "institutional"
1435
+ ]
1436
+ },
1437
+ "nominalisationRate": {
1438
+ "p10": 1.576,
1439
+ "p50": 3.222,
1440
+ "p90": 5.534,
1441
+ "sd": 1.6,
1442
+ "separates": [
1443
+ "business-email",
1444
+ "explanatory",
1445
+ "literary"
1446
+ ]
1447
+ },
1448
+ "participialRate": {
1449
+ "p10": 0,
1450
+ "p50": 0,
1451
+ "p90": 0.133,
1452
+ "sd": 0.062,
1453
+ "separates": []
1454
+ },
1455
+ "pronounOpenerShare": {
1456
+ "p10": 0.067,
1457
+ "p50": 0.213,
1458
+ "p90": 0.4,
1459
+ "sd": 0.136,
1460
+ "separates": [
1461
+ "business-email",
1462
+ "explanatory",
1463
+ "institutional",
1464
+ "literary"
1465
+ ]
1466
+ },
1467
+ "sentenceCv": {
1468
+ "p10": 0.275,
1469
+ "p50": 0.377,
1470
+ "p90": 0.52,
1471
+ "sd": 0.096,
1472
+ "separates": [
1473
+ "business-email",
1474
+ "institutional",
1475
+ "literary"
1476
+ ]
1477
+ },
1478
+ "skeletonRepeat": {
1479
+ "p10": 0,
1480
+ "p50": 0.031,
1481
+ "p90": 0.081,
1482
+ "sd": 0.038,
1483
+ "separates": []
1484
+ },
1485
+ "specificsPer100": {
1486
+ "p10": 2.54,
1487
+ "p50": 5.2,
1488
+ "p90": 9.18,
1489
+ "sd": 2.733,
1490
+ "separates": [
1491
+ "explanatory",
1492
+ "literary"
1493
+ ]
1494
+ },
1495
+ "tradePer100": {
1496
+ "p10": 0,
1497
+ "p50": 0.592,
1498
+ "p90": 1.515,
1499
+ "sd": 0.612,
1500
+ "separates": [
1501
+ "business-email",
1502
+ "explanatory",
1503
+ "institutional",
1504
+ "literary"
1505
+ ]
1506
+ },
1507
+ "tricolons": {
1508
+ "p10": 0,
1509
+ "p50": 0.4,
1510
+ "p90": 0.8,
1511
+ "sd": 0.309,
1512
+ "separates": [
1513
+ "business-email"
1514
+ ]
1515
+ }
1516
+ }
1517
+ };
1518
+
1519
+ // src/register/cli.ts
1520
+ var USAGE = `usage: craft register list [--json]
1521
+ craft register brief <${REGISTER_IDS.join("|")}> [--json]
1522
+ craft register check <file> --as <${REGISTER_IDS.join("|")}> [--json]`;
1523
+ function formatCheck(c, file) {
1524
+ const name = getRegister(c.register).name;
1525
+ const lines = [`craft register check ${file} --as ${c.register} (${name})`, ""];
1526
+ if (!c.measured) return [...lines, c.summary].join("\n");
1527
+ lines.push(`${c.paragraphs} paragraphs measured. ${c.summary}`, "");
1528
+ for (const o of c.outside) lines.push(` ${o.sentence}`);
1529
+ if (c.outside.length) lines.push("");
1530
+ lines.push("Closest baselines:");
1531
+ for (const n of c.nearest.slice(0, 3)) lines.push(` ${n.register.padEnd(16)} ${n.distance.toFixed(2)}`);
1532
+ lines.push("", c.note, `Each range is the middle 80% of human documents, so one in five falls outside any single range. Bands measured ${PROFILE_MEASURED}, over ${MIN_PARAGRAPHS}-paragraph averages. A read-through, not a grade.`);
1533
+ return lines.join("\n");
1534
+ }
1535
+ function runRegister(args, io) {
1536
+ const [sub, ...rest] = args;
1537
+ const json = rest.includes("--json");
1538
+ const positional = rest.filter((a, i) => !a.startsWith("--") && rest[i - 1] !== "--as");
1539
+ const bad = (msg) => {
1540
+ io.err(`craft register: ${msg}
1541
+ ${USAGE}`);
1542
+ return 2;
1543
+ };
1544
+ if (sub === "list") {
1545
+ if (json) io.out(toJson({ registers: REGISTERS.map(({ id, name, useFor }) => ({ id, name, useFor })) }));
1546
+ else for (const r of REGISTERS) io.out(`${r.id.padEnd(11)} ${r.name}. ${r.useFor}`);
1547
+ return 0;
1548
+ }
1549
+ if (sub === "brief") {
1550
+ const id = positional[0];
1551
+ const r = id ? getRegister(id) : void 0;
1552
+ if (!r) return bad(id ? `unknown register "${id}". Registers: ${REGISTER_IDS.join(", ")}` : "name a register");
1553
+ io.out(json ? toJson({ register: r }) : registerBrief(r.id));
1554
+ return 0;
1555
+ }
1556
+ if (sub === "check") {
1557
+ const i = rest.indexOf("--as");
1558
+ const id = i === -1 ? void 0 : rest[i + 1];
1559
+ if (!id || !getRegister(id)) return bad(id ? `unknown register "${id}". Registers: ${REGISTER_IDS.join(", ")}` : "--as <register> is required");
1560
+ const file = positional[0];
1561
+ if (!file) return bad("name a file to check");
1562
+ const path = resolve(io.cwd, file);
1563
+ if (!existsSync(path)) return bad(`${file} does not exist`);
1564
+ const result = checkRegister(readFileSync(path, "utf8"), id, PROFILE);
1565
+ if (json) io.out(toJson({ ...result, labels: Object.fromEntries(Object.keys(result.means).map((f) => [f, featureLabel(f)])) }));
1566
+ else io.out(formatCheck(result, file));
1567
+ return 0;
1568
+ }
1569
+ return bad(sub ? `unknown subcommand "${sub}"` : "name a subcommand");
1570
+ }
1571
+ export {
1572
+ runRegister
1573
+ };