@domandigital/craft 0.15.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ef257c9: Add `craft register`: briefs for writing in four registers (plain, persuasive, warm, literary), and a review-only check that places a document of five paragraphs or more against human baselines for the chosen register. New exports: `registerOf`, `REGISTER_FEATURES`, `REGISTERS`, `registerBrief`, `checkRegister`, `PROFILE`. `REGISTERS.md` ships in the package.
8
+
9
+ ### Patch Changes
10
+
11
+ - 000f345: `craft calibrate` now says plainly that "flagged" is a look-closer signal, not a verdict that a site is AI-made. No threshold changed.
12
+ - 539b760: A labelled page, a null page and an imported page now count as a home page, so `generic-hero-claim` can fire on them. Before, every local `file://` page read as an inner page and the tell could never fire on a labelled or null set. `Snapshot` gains an optional `home`; absent, the URL decides as before. `craft calibrate` takes `home: false` on a label entry, and its per-tell table shows each tell's catalogue generation.
13
+
14
+ ## 0.16.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 6545c2e: Calibration tooling. `craft null import <dir> --builder <name> --brief "<text>"` builds a null model from pages another builder made (v0, Lovable), each an HTML file or a folder holding a built `index.html`, served on loopback so root-relative assets resolve. `craft null prompt` prints the prompt to give that builder. `craft calibrate <labels.json>` measures a labelled set (`ai`, `human`, `ai-looking`) and reports precision and recall per tell and CHARACTER.md's first three targets; a page it cannot measure is listed and never counts as a pass. `NullModel` gains an optional `builder`. The blind-test protocol for target 5 is in `docs/blind-test.md`.
19
+ - cb7317e: Lexical and specificity measurement. New exports `lexicalOf`, `vocabulary`, `vocabularyCounts`, `LEXICAL_FEATURES`, `STUDY_WORDS` and the `LexicalFeatures` and `StudyWord` types compute how often a block uses craft's own tell words and the published excess-use words, and how particular it is (names, places, figures, trade nouns). Every study word carries its source paper. Measured against the frozen human baselines and the Claude set, no feature met the ship rule, so no tell is added and `CATALOGUE_VERSION` is unchanged; the result is in `calibration/copy-shape/report-lexical.md`.
20
+
3
21
  ## 0.15.0
4
22
 
5
23
  ### Minor Changes
package/CHARACTER.md CHANGED
@@ -826,6 +826,53 @@ generated-looking site" and is left out by definition. Snapshots and
826
826
  - Both house demos had expired or gated previews on the day. They are not
827
827
  measured, not clean.
828
828
 
829
+ ### Calibration tooling, 2026-09-24
830
+
831
+ `craft calibrate calibration/labels.json` now does what the `score.mjs`
832
+ scripts did by hand. `labels.json` lists the set:
833
+
834
+ - the 20 AI-set pages;
835
+ - the seven measured human references, with the two gated previews listed as
836
+ skipped;
837
+ - wiredge.studio as the first `ai-looking` page: made by people, and it reads
838
+ as generated.
839
+
840
+ `calibrate` reports precision and recall per tell and checks targets 1 to 3.
841
+ Its rules:
842
+
843
+ - A hit on an `ai` or `ai-looking` page counts for a tell, and a hit on a
844
+ `human` page counts against it, because a tell detects a look, not a maker.
845
+ - Flagged keeps the rule fixed on 2026-09-23: typical against the pooled null,
846
+ or three or more distinct design tells.
847
+ - A page it cannot measure is listed by name and counted neither way.
848
+
849
+ `craft null import` builds a null model from another builder's pages, so the
850
+ AI set and the null can include more than Claude. Give each builder the prompt
851
+ `craft null prompt` prints, so its pages answer the same brief.
852
+
853
+ Target 5, the blind test, is a protocol for people, not a command: see
854
+ `docs/blind-test.md`.
855
+
856
+ **First run, 2026-09-29** (`craft calibrate calibration/labels.json --fresh`, Playwright driving a headless Chrome through `CRAFT_CHROMIUM`, every page re-snapshotted on the current snapshot version; results in `calibration/results/2026-09-29/`). Against 140 null pages from 7 briefs:
857
+
858
+ | Target | Result |
859
+ | --- | --- |
860
+ | 1. Generated pages get caught | met: 20 of 20 flagged |
861
+ | 2. Good human sites don't get caught | met on its definition: 0 block hits, 0 of 6 typical. 3 pages not measured: two preview gates and myndex.com, unreachable from this machine |
862
+ | 3. AI-looking sites get caught | met: 1 of 1 (wiredge.studio), one page, so weak evidence |
863
+
864
+ Read the second row with care. The flagged rule (typical against the null, or three or more distinct design tells) flagged 5 of the 6 measured human sites as tell-heavy, though none is typical and none has a blocking tell. Target 2 as written does not count that. If the rule is meant to say "this looks AI-made", 5 of 6 is a false-positive problem to fix before anything gates on it. It would want a decision on the target, not a quiet threshold change. The set is also small (20 AI, 6 human, 1 AI-looking), all AI pages are Claude's, and 46 of 68 tells never fired on it. Target 4 (the estate) and target 5 (the blind test) are not measured by this command.
865
+
866
+ **Wider human set, 2026-09-29** (`calibration/results/2026-09-29-wider/`). The human set grew from 6 to 36 measured sites: 20 professionally built well-known sites and 12 live UK small-business sites from the Wayback seed list, plus the original six. The AI and AI-looking sets are unchanged (20 and 1).
867
+
868
+ | Target | Result |
869
+ | --- | --- |
870
+ | 1. Generated pages get caught | met: 20 of 20 |
871
+ | 2. Good human sites don't get caught | met on its definition: 0 block hits, 0 of 36 typical. 5 not measured (two preview gates, myndex.com unreachable, screwfix.com 403, mubi.com 503) |
872
+ | 3. AI-looking sites get caught | met: 1 of 1, still one page |
873
+
874
+ The caveat from the first run holds and is now firmer. The flagged rule marked **14 of 36 human sites (39%)** as tell-heavy. It needs either a tighter rule or to stop being read as "looks AI-made". No threshold was changed; that is a decision on the target. Per-tell precision on this set also separates the tells that earn their keep from the ones that do not: `icon-tile-grid` 0.17, `generic-hero-claim` 0.14, `marquee` 0.14, `grid-background` 0.20 and `unproven-claim` 0.36 fire more on human sites than on the AI set, while `cream-palette`, `reflex-font-2`, `italic-serif-display` and `pricing-trio-popular` are at 0.93 or better. 42 of 68 tells never fired. The human sites are not a random sample: they are chosen, well-made and mostly large-brand, and the AI pages are all Claude's. `generic-hero-claim` is 0 of 20 on the AI set because the briefs name a business and a place, so the pages open with a specific line ("Exeter's sole traders", "Chandos Road, Redland"), and the tell is right not to fire on them. It is not a home-page bug: after dd-packages #76 made labelled and null pages count as home pages, the same snapshots re-scored on 2026-09-30 give 0 of 20 on the AI set and 7 of 36 on the human set (one more human hit than the 6 above), and the flagged counts are unchanged (20 of 20 AI, 14 of 36 human). Still to do: an AI set from other builders (`craft null import`), and a decision on the flagged rule.
875
+
829
876
  ### Character report first read, 2026-09-23
830
877
 
831
878
  `craft report` on each live snapshot with its repo, its own brief's null and
@@ -926,3 +973,7 @@ Catalogue version `2026.09.12`, 68 tells.
926
973
  | `no-real-imagery` | 1 | rendered | warn | No photograph on the page | A page with no photograph at all, only icons and illustrations, is the page a model builds when it has no assets. Icon tiles stand in for the evidence a buyer looks for: the work, the premises, the people. |
927
974
  | `unproven-claim` | 1 | rendered | warn | Claim with no proof next to it | "Trusted", "fully qualified" and "award-winning" ask the visitor to take the business's word for it. A page built with no reviews and no registration number makes the claim anyway, with nothing beside it. |
928
975
  <!-- craft:catalogue:end -->
976
+
977
+ ### Decision on the flagged rule (2026-09-30)
978
+
979
+ Closed without changing the threshold. The rule was fixed before any result was read, and retuning it on the 56 pages it was measured on would break that. "Flagged" is a **look-closer signal**, not a verdict that a site is AI-made: it catches AI pages reliably (20 of 20) and also marks well-made human sites (14 of 36) that share a few common design habits. Nothing gates on it. Any change to the rule or to the five weak tells (`icon-tile-grid`, `generic-hero-claim`, `marquee`, `grid-background`, `unproven-claim`) must be judged on a fresh human set, not this one. Follow-up, not blocking: an AI set from other builders via `craft null import` (v0, Lovable and similar), which needs pages from those builders.
package/COPY.md CHANGED
@@ -194,6 +194,11 @@ and confident, never matey: no over-familiar asides ("with me nowhere near
194
194
  it"), no American idiom ("shallow wins", "looks busy"), no "honestly" as an
195
195
  intensifier. Do not change a brand name, domain or legal entity wording.
196
196
 
197
+ For a register other than the house one (plain, persuasive, warm, literary),
198
+ see `REGISTERS.md`: a brief to read before drafting, and a check that places
199
+ a document of five paragraphs or more against human baselines. It reports
200
+ where a draft sits; a person still decides whether the register is right.
201
+
197
202
  ## Word limits
198
203
 
199
204
  | Element | Limit |
package/README.md CHANGED
@@ -170,6 +170,14 @@ sentence a reader would take as a checkable fact, marked `sourced` (a source is
170
170
  named) or `UNSOURCED`, for a person to check against the primary source. It
171
171
  always exits 0: a checklist, not a verdict.
172
172
 
173
+ To write in a chosen register (plain, persuasive, warm, literary), read the
174
+ brief before drafting and check a finished document after. See `REGISTERS.md`.
175
+
176
+ ```bash
177
+ npx craft register brief warm # what to do before drafting
178
+ npx craft register check letter.md --as warm # five paragraphs or more; never fails
179
+ ```
180
+
173
181
  The same checks run in code:
174
182
 
175
183
  ```ts
package/REGISTERS.md ADDED
@@ -0,0 +1,163 @@
1
+ # Writing in a register
2
+
3
+ **Canonical.** How to write in a chosen register, and how to check where a
4
+ draft sits. `COPY.md` says what every piece of house copy avoids; a register
5
+ says what a draft reaches for.
6
+
7
+ | Register | For |
8
+ | --- | --- |
9
+ | `plain` | forms, policies, instructions, professional services |
10
+ | `persuasive` | landing pages, service pages, adverts, sales emails |
11
+ | `warm` | customer emails, enquiry replies, newsletters |
12
+ | `literary` | premium brands, makers, hospitality |
13
+
14
+ ## Using it
15
+
16
+ 1. **Before drafting**, read the brief: `craft register brief warm`. Paste it
17
+ into a prompt if a model is drafting.
18
+ 2. **After drafting**, run `craft copy <file>` as always. The blocking tier
19
+ and the proof rule hold in every register.
20
+ 3. **For a document of five paragraphs or more**, run
21
+ `craft register check <file> --as warm`. It lists the style features that
22
+ fall outside the usual range for that register, and which human baseline
23
+ the draft reads closest to. It never fails a run.
24
+
25
+ ## What the check can and cannot do
26
+
27
+ It measures style, not content: sentence length, commas, clauses,
28
+ nominalisations, pronoun openers, paired phrases, contractions, person and
29
+ passive voice. Names, figures and specifics are left out, because they say
30
+ what a text is about, not how it is written.
31
+
32
+ It needs five paragraphs of two to eight sentences each. In
33
+ `calibration/copy-shape/report-registers.md`, single passages from different
34
+ registers overlap too much to place (26 of 258 pair tests separate), while
35
+ five-paragraph averages separate every register pair (78 of 258). Anything
36
+ shorter gets the brief, not a measurement.
37
+
38
+ Each range is the middle 80% of human documents, so one in five human
39
+ documents falls outside any single range. Read the findings as a
40
+ read-through. They are not a grade.
41
+
42
+ Each brief is compared with the nearest free human baseline, and none is
43
+ exact:
44
+
45
+ | Brief | Baseline | Not quite |
46
+ | --- | --- | --- |
47
+ | `plain` | Hansard Commons, 2010 to 2019 | edited debate, not plain writing |
48
+ | `persuasive` | UK small-business websites before 2021 | 715 passages only |
49
+ | `warm` | Enron sent email, 1999 to 2002 | corporate and American |
50
+ | `literary` | English fiction and essays, 1726 to 1925 | older than any brand |
51
+
52
+ The bands in `src/register/profile.ts` are generated by
53
+ `node scripts/shape/registers.mjs --profile`. Rerun it after adding a
54
+ baseline; never edit the file by hand.
55
+
56
+ ## The briefs
57
+
58
+ Generated from `src/register/registers.ts` by
59
+ `pnpm --filter @domandigital/craft run docs`. Edit the data, not this block.
60
+
61
+ <!-- craft:registers:start -->
62
+ ## Plain and trustworthy (`plain`)
63
+
64
+ **Use for:** Forms, policies, terms, instructions, and professional services where the reader must act correctly.
65
+
66
+ **Before you draft:**
67
+
68
+ 1. Put the most important information first, then taper down to the detail. *(GOV.UK writing guidelines)*
69
+ 2. Split any sentence over 25 words into two. *(GOV.UK writing guidelines)*
70
+ 3. Choose the short, common word: buy, help, about. *(GOV.UK writing guidelines)*
71
+ 4. Write in the active voice, so the reader sees who does what. *(GOV.UK writing guidelines)*
72
+ 5. Address the reader as "you", as one person you can actually help. *(GOV.UK writing guidelines)*
73
+ 6. Use positive contractions such as "you'll", and write negatives out in full: "do not", "cannot". *(GOV.UK writing guidelines)*
74
+ 7. Write headings that say what the section tells the reader, starting with a verb where you can. *(GOV.UK writing guidelines)*
75
+ 8. Give each paragraph one job. *(house judgement)*
76
+
77
+ **Example (fictional):**
78
+
79
+ - 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.
80
+ - After: Apply for a residents' parking permit before you need it. Fill in the form online. It takes about 10 minutes.
81
+ - Why: The action comes first, the council is no longer the subject, and each sentence does one thing.
82
+
83
+ **Sources:**
84
+
85
+ - [GOV.UK writing guidelines](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/): 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.
86
+
87
+ ## Persuasive and concise (`persuasive`)
88
+
89
+ **Use for:** Landing pages, service pages, adverts and sales emails: copy whose job is one action.
90
+
91
+ **Before you draft:**
92
+
93
+ 1. Put the promise in the headline: what the reader gets, in words they would use. *(Ogilvy, Ogilvy on Advertising (1983))*
94
+ 2. Back each claim with something the reader can check: a number, a name, a date, a price. *(COPY.md)*
95
+ 3. Write to one reader, as "you", about their problem before your service. *(Caples, Tested Advertising Methods)*
96
+ 4. Name the mechanism: how it works, how long it takes, what it costs. *(COPY.md)*
97
+ 5. Answer the objection the reader already has, with evidence, in the sentence that follows the claim. *(house judgement)*
98
+ 6. Let the copy run as long as the facts last, and cut each sentence that carries none. *(Ogilvy, Ogilvy on Advertising (1983))*
99
+ 7. Close with one specific action and what happens straight after it. *(COPY.md)*
100
+
101
+ **Example (fictional):**
102
+
103
+ - Before: Transform your garden with our premium landscaping solutions. Quality you can trust.
104
+ - After: We lay a 40 square metre Indian sandstone patio in three days, for £4,800 fitted. Every job since 2014 is on our reviews page. Book a free site visit and get a fixed quote within 48 hours.
105
+ - 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.
106
+
107
+ **Sources:**
108
+
109
+ - Ogilvy, Ogilvy on Advertising (1983): 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.
110
+ - Caples, Tested Advertising Methods: 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.
111
+ - COPY.md: Writing directives (lead with the fact, name the mechanism, persuade with something checkable) and the CTA rule.
112
+
113
+ ## Warm and conversational (`warm`)
114
+
115
+ **Use for:** Customer emails, replies to enquiries, newsletters and small-business updates.
116
+
117
+ **Before you draft:**
118
+
119
+ 1. Write the way you would talk to a customer you like: contractions, everyday words. *(Mailchimp Content Style Guide)*
120
+ 2. Keep words and sentences simple; being clear matters more than being entertaining. *(Mailchimp Content Style Guide)*
121
+ 3. Say what the reader can do, in positive language. *(Mailchimp Content Style Guide)*
122
+ 4. Use the active voice. *(Mailchimp Content Style Guide)*
123
+ 5. Match the tone to how the reader feels right now: lighter for good news, calm and direct for a problem. *(Mailchimp Content Style Guide)*
124
+ 6. Keep humour dry and occasional, and only where it comes naturally. *(Mailchimp Content Style Guide)*
125
+ 7. Sign off as a person, with a name. *(house judgement)*
126
+
127
+ **Example (fictional):**
128
+
129
+ - Before: Dear Valued Customer, We wish to inform you that your order has been dispatched and will be delivered in due course.
130
+ - 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
131
+ - Why: A name at each end, a real day and place instead of "in due course", and a next step. Fictional business.
132
+
133
+ **Sources:**
134
+
135
+ - [Mailchimp Content Style Guide](https://styleguide.mailchimp.com/voice-and-tone/): 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.
136
+
137
+ ## Elegant and literary (`literary`)
138
+
139
+ **Use for:** Premium brands, makers, hospitality and anything sold on craft and taste rather than price.
140
+
141
+ **Before you draft:**
142
+
143
+ 1. Choose one precise noun over two adjectives. *(house judgement)*
144
+ 2. Let sentence length follow the thought: a long sentence to build, a short one to land. *(COPY.md)*
145
+ 3. Carry the detail in concrete facts: material, place, process, time. *(house judgement)*
146
+ 4. Trust the reader with one image, state it once, and move on. *(house judgement)*
147
+ 5. Keep claims quiet and exact, and let the facts carry the prestige. *(house judgement)*
148
+ 6. Draw any metaphor from the maker's own work, and hold it for the whole piece. *(house judgement)*
149
+ 7. Read the draft aloud and rework each line that trips the breath. *(house judgement)*
150
+
151
+ **Example (fictional):**
152
+
153
+ - Before: Our luxurious, handcrafted candles create an unforgettable ambience of elegance and sophistication.
154
+ - 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.
155
+ - Why: Place, material, scent and burn time do the work the adjectives claimed. Fictional business.
156
+
157
+ **Sources:**
158
+
159
+ - house judgement: No public style authority exists for premium-brand prose. These directives are house judgement, to be revised as the estate grows.
160
+ - [Register separation, 2026-09-29](calibration/copy-shape/report-registers.md): 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.
161
+
162
+ **Always:** Every register keeps the COPY.md blocking tier and the proof rule. Draft first, then run `craft copy <file>` on the draft.
163
+ <!-- craft:registers:end -->
@@ -9,9 +9,10 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-OUQ25LY2.js";
12
+ } from "./chunk-SL5VZJ2J.js";
13
13
  import "./chunk-JYNXGCYZ.js";
14
- import "./chunk-LFNSB6W7.js";
14
+ import "./chunk-WFB6F3BN.js";
15
+ import "./chunk-4QWEAL4G.js";
15
16
  import "./chunk-QYBAVU3P.js";
16
17
  export {
17
18
  PROVENANCE_BUDGET_MS,
package/dist/audit.cjs CHANGED
@@ -2914,7 +2914,7 @@ var SPECIFICITY_COPY_TELLS = [
2914
2914
  },
2915
2915
  rendered: {
2916
2916
  detect: (s) => {
2917
- if (!s.firstScreenText || !HOME_URL.test(s.url)) return [];
2917
+ if (!s.firstScreenText || !(s.home ?? HOME_URL.test(s.url))) return [];
2918
2918
  const text = s.firstScreenText.join("\n");
2919
2919
  if (!text.trim() || !specificity(text).generic) return [];
2920
2920
  const h1 = s.headings.find((h) => h.level === 1 && h.top < s.viewport.height);
package/dist/audit.d.cts CHANGED
@@ -230,6 +230,13 @@ interface Snapshot {
230
230
  * claim, not its menu or its calls to action.
231
231
  */
232
232
  firstScreenText?: string[];
233
+ /**
234
+ * Whether this is a site's home page, set by whoever knows: `craft
235
+ * calibrate` from its labels, and `craft null build` and `null import`,
236
+ * which ask for a home page. Absent, the URL decides (`HOME_URL`), and a
237
+ * local file such as `pages/01.html` never reads as one.
238
+ */
239
+ home?: boolean;
233
240
  /** Set when a version 1 snapshot was read and upgraded in memory. */
234
241
  migratedFrom?: 1;
235
242
  /** The design-system measures trawl has collected since v7, kept so it can switch to this. */
package/dist/audit.d.ts CHANGED
@@ -230,6 +230,13 @@ interface Snapshot {
230
230
  * claim, not its menu or its calls to action.
231
231
  */
232
232
  firstScreenText?: string[];
233
+ /**
234
+ * Whether this is a site's home page, set by whoever knows: `craft
235
+ * calibrate` from its labels, and `craft null build` and `null import`,
236
+ * which ask for a home page. Absent, the URL decides (`HOME_URL`), and a
237
+ * local file such as `pages/01.html` never reads as one.
238
+ */
239
+ home?: boolean;
233
240
  /** Set when a version 1 snapshot was read and upgraded in memory. */
234
241
  migratedFrom?: 1;
235
242
  /** The design-system measures trawl has collected since v7, kept so it can switch to this. */
package/dist/audit.js CHANGED
@@ -2879,7 +2879,7 @@ var SPECIFICITY_COPY_TELLS = [
2879
2879
  },
2880
2880
  rendered: {
2881
2881
  detect: (s) => {
2882
- if (!s.firstScreenText || !HOME_URL.test(s.url)) return [];
2882
+ if (!s.firstScreenText || !(s.home ?? HOME_URL.test(s.url))) return [];
2883
2883
  const text = s.firstScreenText.join("\n");
2884
2884
  if (!text.trim() || !specificity(text).generic) return [];
2885
2885
  const h1 = s.headings.find((h) => h.level === 1 && h.top < s.viewport.height);