@domandigital/craft 0.5.0 → 0.6.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,90 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8c6304b: Character, phase A: a tell catalogue, a source scanner, a copy engine, and the `craft` command.
8
+
9
+ The standard asks whether a surface is well made. The new CHARACTER.md asks
10
+ whether anyone decided how it looks. A site can pass every craft number and
11
+ still be the indigo-and-Inter answer a model gives any brief.
12
+
13
+ - `scanSource(files)` finds 21 design tells in markup, component code and CSS,
14
+ tagged by generation: the first wave (indigo, blue-to-purple gradients, Inter,
15
+ glass panels, hero straight into a trust strip, icon-card grids, reveal on
16
+ every section, pills everywhere, a stock shadcn dump) and the second (cream
17
+ grounds, italic serif display, eyebrow chips, icon tiles, bento grids, radial
18
+ glows, graph-paper backgrounds, marquees, hairline-border cards, intro
19
+ cinematics).
20
+ - It reads classes the way Tailwind does, through `cn()`, `clsx()` and `cva()`,
21
+ and colour in hex, `rgb()`, `hsl()` and `oklch()` in raw CSS, `@theme` blocks
22
+ and arbitrary values. Hue is judged in OKLCh, so a violet set as `#5a35d1` is
23
+ caught.
24
+ - `checkCopy(files)` finds 8 copy tells, including em dashes, "not just X, it's
25
+ Y", staccato triplets and "where X meets Y". The word and phrase lists are
26
+ exported data with a test per entry.
27
+ - `craft scan [--staged]`, `craft copy`, `craft tells list`. Exceptions are
28
+ declared in `art-direction.json` with a `because`; one without a reason is
29
+ not applied and is reported.
30
+ - Every tell ships as `warn`. Nothing blocks until its estate hits have been
31
+ read. Every entry has a flag case per detection path and a pass case, and
32
+ the test proves each one fires on its own.
33
+
34
+ STANDARD.md section 10 now hands layout defaults to CHARACTER.md. Still eleven sections.
35
+
36
+ - 161a771: The house copy rules move into craft, so `copy-check` can become a thin wrapper over `craft copy --json`.
37
+
38
+ - Ten tells carry the lists that claude-kit's `copy-check` hardcoded: `ai-phrase`,
39
+ `plainer-word`, `plain-english`, `buzzword`, `negative-reassurance`, `emoji`,
40
+ `no-x-no-y`, `no-x-badge`, `review-phrase` and `vague-word`. `not-just-but` now
41
+ covers every form of contrastive negation the house rules name, including the
42
+ parallel participle ("dispatched from here, not shipped in from Seoul").
43
+ Each word and phrase belongs to exactly one tell, with a test per entry.
44
+ Every tell still ships as `warn`. Catalogue `2026.09.2`, 39 tells.
45
+ - `copy-ok` or `craft-ok` on a finding's line, or the line above, suppresses it.
46
+ Suppressed findings are listed in `report.suppressed`, never hidden.
47
+ - `craft copy` never reads a comment as copy, reads JSX text that runs into an
48
+ expression, reads the text inside HTML held in a string, sees short strings
49
+ for the dash, emoji and badge tells, leaves a lone dash placeholder alone,
50
+ and reads `.jsonl` (a Sanity export) a document per line.
51
+ - Class lists written with arbitrary values (`bg-[var(--bg-elevated)]`) are no
52
+ longer read as prose.
53
+
54
+ - 8093ad7: Phase B: the rendered page. `craft snapshot`, `craft audit`, rendered detection paths and a fingerprint.
55
+
56
+ - `@domandigital/craft/audit` captures a snapshot of a live page in Chromium: faces by
57
+ text and headline, colours by painted area, button shape, sections in running order
58
+ and which wait for a scroll, glass, gradients, glows, grid backgrounds, marquees,
59
+ hairline-shadow cards, the eyebrow chip, and an intro that covers the page at load.
60
+ Playwright is an optional peer dependency; the core import never loads a browser.
61
+ `collectInPage` is exported for any caller already driving a page.
62
+ - 18 tells gain a rendered detection path, with their own flag and pass snapshots,
63
+ proved like every other path. `auditSnapshot(snapshot)` judges a saved snapshot
64
+ without a browser. Catalogue `2026.09.3`.
65
+ - `fingerprint(snapshot)` and `fingerprintDistance(a, b)`: accent and ground in
66
+ OKLCh, display and body faces, roundness, reveal density, effects and running order,
67
+ compared 0 to 1. `nearest()` ranks a set.
68
+ - `craft audit <url | snapshot.json> [--repo dir]` reports the page and its source
69
+ together, with the fingerprint. `HTTPS_PROXY` is honoured.
70
+ - Colour parsing reads `oklab()` and `color(srgb ...)`, which browsers return.
71
+ - First estate baseline recorded in CHARACTER.md and `calibration/estate/2026-09-23/`.
72
+
73
+ - ee6d6cd: Phase C: `art-direction.json`, the reason rule, and `craft direction init | validate | propose`.
74
+
75
+ - A schema for the seven expressive choices (accent, ground, display, body, shape, motif,
76
+ signature), the sources in the client's world they come from, and the tell exceptions
77
+ from phase A. Ships as `art-direction.schema.json` for editors.
78
+ - `validateDirection` applies the reason rule: a reason is a sentence, cites a declared
79
+ source and mentions what that source shows; preferences, mood boards and unconfirmed
80
+ proposals are rejected; a value on the tell catalogue needs an exception. Given a
81
+ fingerprint it warns where the page does not show what the file declares.
82
+ - `initDirection` writes today's choices with empty reasons. `propose` reads dominant
83
+ colours off PNG photos (`paletteFromPixels`, k-means in OKLab) and drafts choices ranked
84
+ away from the reflex band and the estate, every one marked `PROPOSED:` until a person
85
+ rewrites it.
86
+ - An exceptions-only file stays valid.
87
+
3
88
  ## 0.5.0
4
89
 
5
90
  ### Minor Changes
package/CHARACTER.md ADDED
@@ -0,0 +1,335 @@
1
+ # The character standard
2
+
3
+ [STANDARD.md](./STANDARD.md) asks whether a surface is well made. This document
4
+ asks a second question: **did anyone decide how it looks, or is it the answer a
5
+ model gives when nobody told it otherwise?**
6
+
7
+ A site can pass every craft number and still read as generated. Indigo accent,
8
+ Inter, a glass card over the hero, a strip of logos, three icon cards, every
9
+ section fading up on scroll. None of those is wrong. Together they say nobody
10
+ chose anything.
11
+
12
+ ## Change the look, not the page grammar
13
+
14
+ First impressions favour pages that look typical for their category (Tuch et
15
+ al., 2012). So this standard **never rewards strangeness**. Navigation, where
16
+ the call to action sits, reading order and legibility stay conventional. They
17
+ remain the job of STANDARD.md and the page audit.
18
+
19
+ Character goes into the surface: the display face, the accent, the shape
20
+ language, the motif, the one signature moment. That is where a model's defaults
21
+ show, and where a real business has something of its own to draw on.
22
+
23
+ ## Three signals, and a reason behind every choice
24
+
25
+ Any fixed list of tells goes stale. Ban Inter and a model moves to Space
26
+ Grotesk. So the list is one signal of three, and the other two keep working as
27
+ model defaults move.
28
+
29
+ | Signal | Question | Status |
30
+ | --- | --- | --- |
31
+ | 1. Known tells | Does it use a choice on the catalogue below? | **Shipped**: source, copy, and on the rendered page for 18 tells (`craft audit`). |
32
+ | 2. Counterfactual typicality | Would Claude have built this anyway, for this brief? | Planned. About 20 `claude -p` runs per brief; distance from those answers. |
33
+ | 3. Distance from the estate | Does it look like the agency's other sites? | Fingerprint and distance shipped; the estate register is planned. The agency's biggest tell is its clients looking like siblings. |
34
+
35
+ **The reason rule.** Every expressive choice records a `because`, with evidence
36
+ from the client's real world: the shopfront, the van, the trade's own look, the
37
+ place, the materials. A token with no reason is a default. This is the part that
38
+ adds character rather than only removing tells. It lives in `art-direction.json`.
39
+
40
+ ## Tells are tagged by generation
41
+
42
+ | Gen | What it is |
43
+ | --- | --- |
44
+ | 1 | The first wave: indigo, blue-to-purple gradients, Inter, glass, three icon cards. |
45
+ | 2 | What models moved to once the first wave was named: cream grounds and italic serifs, eyebrow chips, bento grids, marquees, intro cinematics. |
46
+ | 3 | Whatever the harvester finds next. |
47
+
48
+ A report counts findings by generation, so it can say *which* wave a site was
49
+ built in, not only that it has tells.
50
+
51
+ ## Every tell ships as a warning
52
+
53
+ Nothing blocks until its hits across the estate have been read by a person and
54
+ the rule tuned against them. A rule that blocks on day one gets switched off by
55
+ day three. `--strict` fails on any finding, for a surface that has been
56
+ cleaned and should stay clean.
57
+
58
+ ## Declaring the direction
59
+
60
+ `art-direction.json` at the site root records the seven expressive choices
61
+ (accent, ground, display face, body face, shape, motif, signature moment), the
62
+ sources in the client's world they come from, and why. Layout and navigation
63
+ are not choices here: they stay conventional.
64
+
65
+ ```json
66
+ {
67
+ "version": 1,
68
+ "client": "RMP Electrical",
69
+ "brief": "A two-person electrical contractor in Brackley doing rewires and EV chargers.",
70
+ "sources": [
71
+ { "id": "van", "kind": "livery", "note": "The Transit van, bottle green with cream sign-writing.", "path": "brand/van.png" }
72
+ ],
73
+ "choices": {
74
+ "accent": {
75
+ "value": "#1f4d3a",
76
+ "because": "The bottle green is the van's own paint, and the van is what people in Brackley recognise.",
77
+ "evidence": ["van"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ `craft direction validate` applies the reason rule. A choice is decided only
84
+ when its reason is at least a sentence, cites a declared source, and mentions
85
+ what that source shows (naming only the colour does not count). It rejects a
86
+ preference ("the client likes it"), a mood board (two or more of modern,
87
+ clean, premium, elegant and the rest), and a value on the tell catalogue
88
+ without an exception carrying the same reason. Given a snapshot, it warns
89
+ where the page does not show what the file declares.
90
+
91
+ `craft direction init` writes what the site does today with every reason
92
+ empty, so the first thing it produces is the list of things nobody decided.
93
+ Run on RMP's live home page (2026-09-23) it records Fraunces, Manrope, a cream
94
+ ground and pill buttons, three of them on the catalogue, and zero of seven
95
+ choices decided.
96
+
97
+ `craft direction propose` reads the dominant colours off each source's PNG
98
+ photo, ranks accent candidates away from the reflex violet band and from the
99
+ rest of the estate (`--estate` a folder of snapshots), and drafts choices.
100
+ Every draft reason starts `PROPOSED:`, which validate rejects: a person looks
101
+ at the van, agrees the green is the van's green, and says so in their own
102
+ words.
103
+
104
+ The editor schema ships as `@domandigital/craft/art-direction.schema.json`.
105
+ A file from phase A that holds only exceptions stays valid.
106
+
107
+ ## Declaring an exception
108
+
109
+ Doman Digital's own brand is violet. A hue rule with no way to say so flags the
110
+ agency's own site on every commit. So an exception is declared, with its reason,
111
+ in `art-direction.json` at the site root:
112
+
113
+ ```json
114
+ {
115
+ "exceptions": [
116
+ {
117
+ "tell": "ai-violet",
118
+ "because": "Violet has been the Doman Digital mark since 2019: the van, the cards, the signage.",
119
+ "evidence": "brand/van-livery.jpg"
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ An exception with no `because`, or one shorter than a sentence, is not applied
126
+ and is reported. An applied exception is still listed in every report with the
127
+ count it silenced, so it stays visible.
128
+
129
+ ## Using it
130
+
131
+ ```bash
132
+ craft scan # markup, components and stylesheets under .
133
+ craft scan --staged # the git index, for a pre-commit hook
134
+ craft copy app content # the prose a visitor reads
135
+ craft tells list # the catalogue this build judges against
136
+ craft audit https://example.co.uk --repo . # the rendered page and its source, one report
137
+ craft snapshot https://example.co.uk --out home.json
138
+ ```
139
+
140
+ `--json` gives the full report. Exit code is 0 on warnings, 1 on a block (or
141
+ any finding under `--strict`), 2 on a usage error.
142
+
143
+ `scan` reads classes the way Tailwind does: `className="..."`, the literals
144
+ inside `cn()`, `clsx()` and `twMerge()`, and each `cva()` variant separately.
145
+ It reads colour in hex, `rgb()`, `hsl()` and `oklch()`, in raw CSS, Tailwind v4
146
+ `@theme` blocks and arbitrary values, and judges hue in OKLCh. A framework's
147
+ own palette definitions are ignored: a colour counts when a site uses it, not
148
+ when the framework defines it.
149
+
150
+ `copy` reads Markdown outside code, text between tags (including text that
151
+ runs into an expression, such as a label followed by `{count}`), string literals that
152
+ read as sentences, and the text inside HTML held in a string, such as an email
153
+ template. Short strings are read too, for the tells that fit in a label: an em
154
+ dash, an emoji, a "No catch." badge. Comments are never copy. It reads `.json`
155
+ and `.jsonl`, so a Sanity export can be checked a document per line. It skips
156
+ READMEs, changelogs and `_`-prefixed folders when walking a directory, because
157
+ those are for the people who build the site.
158
+
159
+ A real person's own words, a review or a testimonial, must never be edited to
160
+ pass. Mark the line, or the line above it, with `copy-ok` or `craft-ok`. Marked
161
+ findings are listed in every report with where they are, never hidden. The
162
+ same markers work for `craft scan`.
163
+
164
+ ## The rendered page
165
+
166
+ `craft audit` opens the page in Chromium, waits five seconds, and records what
167
+ a visitor gets as a snapshot: the faces that set the text and the headline,
168
+ the colours by painted area, the buttons' shape, the sections in running
169
+ order and which ones wait for a scroll, and the effects (glass, glows,
170
+ marquees, an intro that covers the page at load). The same tells then judge
171
+ it, so a report can say "Inter in source and on the page", or catch the
172
+ violet a CMS sets that no source file names.
173
+
174
+ It needs Playwright, as an optional peer dependency: `npm i -D playwright`.
175
+ The core import never loads a browser. A saved snapshot can be judged again
176
+ anywhere with `craft audit home.json`. `HTTPS_PROXY` is honoured, and a
177
+ `localhost` or `NO_PROXY` host is reached directly.
178
+
179
+ Every audit also prints a **fingerprint**: accent and ground in OKLCh, the
180
+ display and body faces, button roundness, reveal density, the effects and the
181
+ running order. `fingerprintDistance` compares two of them, 0 to 1, weighted
182
+ towards accent and type. Signals 2 and 3 are both this distance, measured
183
+ against different sets.
184
+
185
+ Known limit: a computed font family is the family the stylesheet asked for.
186
+ A face that failed to load is still reported under its name.
187
+
188
+ ## The house copy rules live here
189
+
190
+ Since catalogue `2026.09.2` the rule lists of claude-kit's
191
+ `house-style/copy-rules.md` are carried by tells in this catalogue:
192
+ `ai-phrase`, `plainer-word`, `plain-english`, `buzzword`,
193
+ `negative-reassurance`, `em-dash`, `emoji`, `not-just-but`, `no-x-no-y` and
194
+ `no-x-badge` are its blocking tier; `review-phrase` and `vague-word` its review
195
+ tier. craft ships all of them as `warn`. `copy-check` in claude-kit is a thin
196
+ wrapper over `craft copy --json` and decides which tells fail a commit, so the
197
+ lists exist once, here, with a test per entry.
198
+
199
+ The swap was checked against the old checker on 2,570 files across the seven
200
+ client repos (2026-09-23). The old checker found 193 issues, craft finds every
201
+ one of them that is visitor copy. The rest, 29, were the old checker reading
202
+ code comments, asset keys (`brows-hero-seamless`), a regex that strips em
203
+ dashes, and three words (`truly`, `genuinely`, `deeply`) that the house rules
204
+ keep in the review tier and the old list had drifted into blocking. Four real
205
+ misses in craft were fixed on the way: em dashes in JSX text next to an
206
+ expression, a lone dash placeholder in a table cell, prose inside HTML held in
207
+ a string, and the contrastive negation in a live welcome email that is the
208
+ reason that rule exists.
209
+
210
+ ## Calibration
211
+
212
+ The targets, recorded here with the date once each has been run:
213
+
214
+ - **AI set** (pages generated by `claude -p` from about 20 briefs): 90% or more
215
+ flagged as typical or tell-heavy. *Not yet run.*
216
+ - **Human set** (the ten references in claude-kit's `reference-set.md`): zero
217
+ block hits and low typicality. *Not yet run.*
218
+ - **Estate baseline** (all seven client repos and live URLs): every hit read by
219
+ a person before any rule blocks. **First run 2026-09-23**, below.
220
+
221
+ A first read of one internal repo (dd-library, 157 source files, 2026-09-22)
222
+ found Inter Tight and Fraunces in the base template, 21 scroll reveals and 31
223
+ pills, and one false positive: Tailwind's `amber-50`, the stock warning-notice
224
+ fill, read as a cream page ground. That rule now matches named cream tokens and
225
+ measured colour only.
226
+
227
+ ### Estate baseline, 2026-09-23
228
+
229
+ `craft scan` over each repo and `craft audit` on each live home page at
230
+ 1440 x 900. Snapshots and the table are in `calibration/estate/2026-09-23/`.
231
+ Cells read *source findings / R if seen on the rendered page*. Nothing here
232
+ blocks; it is for a person to read.
233
+
234
+ | Tell | DD | MMM | Sensphere | Harrison James | HJ Beauty | Chair and Blade | RMP |
235
+ |---|---|---|---|---|---|---|---|
236
+ | `reflex-font` | 15 / · | 2 / · | 3 / R | 2 / R | 2 / · | 1 / · | |
237
+ | `reflex-font-2` | 7 / R | 34 / R | 2 / · | 1 / R | 3 / R | 4 / R | 12 / R |
238
+ | `ai-violet` | 36 / · | 9 / · | 18 / R | | | | |
239
+ | `gradient-text` | | 2 / · | | | | | |
240
+ | `glass-panel` | 9 / · | 45 / · | 13 / · | | 1 / · | | |
241
+ | `hero-then-proof` | 9 / · | 1 / R | · / R | | 1 / · | 1 / · | 2 / R |
242
+ | `icon-tile-grid` | 1 / R | 9 / · | · / R | | · / R | | 1 / R |
243
+ | `reveal-everywhere` | 1 / · | 1 / · | | | | | |
244
+ | `pill-everything` | 1 / R | 1 / · | 1 / · | | | | 1 / R |
245
+ | `shadcn-dump` | | 1 / · | | | | | |
246
+ | `cream-palette` | | 3 / · | | | | | 18 / R |
247
+ | `italic-serif-display` | | 4 / · | | | | 8 / · | 1 / R |
248
+ | `hero-eyebrow-chip` | 1 / · | 1 / · | | | | | · / R |
249
+ | `icon-tile-stack` | 1 / · | 93 / · | 4 / · | | | | 18 / · |
250
+ | `radial-spotlight-glow` | 6 / R | 14 / R | 5 / R | | | | |
251
+ | `grid-background` | | 2 / · | | | | | |
252
+ | `marquee` | 1 / · | | | | 3 / R | · / R | |
253
+ | `thin-border-wide-shadow` | | 2 / · | 4 / R | | | | |
254
+ | `intro-cinematic` | | · / R | | | | 2 / · | |
255
+
256
+ What it shows:
257
+
258
+ - **The three survey findings reproduce where they can.** Sensphere's violet
259
+ is found in source (18) and on its buttons; MMM's intro, glows and glass are
260
+ in source and its intro and glows on the page; MMM reveals 60% of the
261
+ sections below its fold on scroll, three of five, one short of the rendered
262
+ threshold, and its source is over the reveal limit. Rise & Bloom's glass
263
+ hero and icon grid could not be checked: that repo is not attached to this
264
+ session.
265
+ - **Every site ships a reflex face.** Twelve of fourteen site-face pairs are
266
+ on one of the two lists. Fraunces sets the headline on DD, HJ Beauty and RMP.
267
+ - **DD and RMP are the closest pair** (fingerprint distance 0.46: Fraunces
268
+ headlines, pill buttons, a green accent). Chair and Blade is furthest from
269
+ everything (0.59 to 0.77). This is signal 3's first measurement.
270
+ - **DD's violet is its brand.** Its 36 source hits wait for the exception in
271
+ its `art-direction.json`.
272
+
273
+ False positives found and fixed in this run:
274
+
275
+ - `glass-panel` on the page fired on five of seven sites. Every one was a
276
+ sticky or fixed nav bar with a blurred background, which is ordinary chrome.
277
+ The rendered path now ignores anything in a `header` or `nav`, fixed or
278
+ sticky, and anything smaller than a panel.
279
+ - `next/font` and CSS-variable family names (`__Inter_d65c78`,
280
+ `cormorantGaramond`) are now read back to the face's name, or every Next.js
281
+ site would have passed the font tells.
282
+
283
+ Not yet read by a person: the source counts for `icon-tile-stack` (MMM 93)
284
+ and `glass-panel` (MMM 45) look high and are the first to check.
285
+
286
+ ## The catalogue
287
+
288
+ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
289
+ after changing an entry; a test fails until you do.
290
+
291
+ <!-- craft:catalogue:start -->
292
+ Catalogue version `2026.09.3`, 39 tells.
293
+
294
+ | Id | Gen | Surface | Severity | Tell | Why it is a default |
295
+ | --- | --- | --- | --- | --- | --- |
296
+ | `reflex-font` | 1 | source + rendered | warn | Reflex font | Inter, Roboto, Poppins and friends are what a model sets when nothing in the brief names a face. Every site in the estate ships one. |
297
+ | `reflex-font-2` | 2 | source + rendered | warn | Second-wave reflex font | Ban Inter and a model reaches for Inter Tight, DM Sans, Manrope, Space Grotesk or an Instrument/Fraunces serif. A swap inside the reflex list is not a decision. |
298
+ | `ai-violet` | 1 | source + rendered | warn | Indigo-violet accent | Tailwind's indigo and violet are the accent a model picks for any brand it knows nothing about. Hue is read in OKLCh, so a hex violet is caught as well as a class name. |
299
+ | `blue-purple-gradient` | 1 | source + rendered | warn | Blue-to-purple gradient | The first-wave hero background. It says 'software product' on a plumber's site. |
300
+ | `gradient-text` | 1 | source + rendered | warn | Gradient-filled heading | Gradient text is the model's way of making a heading look designed without deciding what it should look like. |
301
+ | `glass-panel` | 1 | source + rendered | warn | Glass panel | A translucent blurred card over a busy background is the stock way to put text on a hero image. It costs contrast and says nothing about the client. |
302
+ | `hero-then-proof` | 1 | source + rendered | warn | Hero straight into a trust strip | Six of seven estate sites go hero, then logos or stats. It is the landing-page template's order, not the client's story. |
303
+ | `icon-tile-grid` | 1 | source + rendered | warn | Three-column icon-card grid | Three cards, each an icon, a heading and two lines, is what a model builds for any list of services. |
304
+ | `reveal-everywhere` | 1 | source + rendered | warn | Reveal on every section | When every block fades up on scroll, the motion stops meaning anything and the page feels slow. MMM had 70, DD 40, sen-sphere 27. |
305
+ | `pill-everything` | 1 | source + rendered | warn | Pills everywhere | rounded-full on every button, badge and tag is the framework's friendliest default. MMM had 162, DD 125. A shape used everywhere is not a shape language. |
306
+ | `shadcn-dump` | 1 | source | warn | Stock component dump | A generator exports the whole shadcn/ui kit whether the site uses it or not. Rise & Bloom ships a full set under its own book, vine and petal assets. |
307
+ | `cream-palette` | 2 | source + rendered | warn | Cream ground | Once white-and-violet was named, models moved to a warm off-white: parchment, linen, oat. It reads as 'tasteful' in the same way everywhere. |
308
+ | `italic-serif-display` | 2 | source + rendered | warn | Italic serif display | A serif heading with one italic word ('Beauty, reimagined') is the second wave's signature move. |
309
+ | `hero-eyebrow-chip` | 2 | source + rendered | warn | Eyebrow chip above the hero | A small bordered pill ('New · Now booking') above the headline is in almost every generated hero. |
310
+ | `icon-tile-stack` | 2 | source | warn | Icon in a tinted tile | A library icon in a soft rounded square, stacked above a heading, is the stock card header. |
311
+ | `bento-grid` | 2 | source | warn | Bento grid | Mixed-size tiles in a grid became the default 'features' layout of the second wave. |
312
+ | `radial-spotlight-glow` | 2 | source + rendered | warn | Radial glow | A soft blurred blob or radial spotlight behind the hero is atmosphere without a subject. |
313
+ | `grid-background` | 2 | source + rendered | warn | Graph-paper background | A faint 1px line or dot grid behind the hero is the developer-tool look, borrowed by everyone. |
314
+ | `marquee` | 2 | source + rendered | warn | Scrolling marquee | An endless strip of logos or words is the second wave's trust strip. It moves so it looks alive. |
315
+ | `thin-border-wide-shadow` | 2 | source + rendered | warn | Hairline border, wide shadow | A near-invisible border under a large soft shadow is the stock 'elevated card'. |
316
+ | `intro-cinematic` | 2 | source + rendered | warn | Intro cinematic | A logo animation that runs before the page is something a model adds to make a site feel premium. Three estate sites open with one. It delays the page for every visitor. |
317
+ | `ai-vocabulary` | 1 | copy | warn | AI vocabulary | Tapestry, elevate, nestled, unparalleled: words that appear in generated copy far more than in anything a business owner writes. |
318
+ | `stock-phrase` | 1 | copy | warn | Stock phrase | Phrases every generated services page uses. They fill space where a fact should be. |
319
+ | `ai-phrase` | 1 | copy | warn | AI phrase | Let's dive in, here's the thing, at its core, seamless, rest assured: the phrase list of the house copy rules. A reader has seen each one in a thousand generated pages. |
320
+ | `plainer-word` | 1 | copy | warn | A plainer word exists | Empower, leverage, harness, delve: each stands in for a plainer verb, and the swap is a reliable sign nobody chose the word. |
321
+ | `plain-english` | 2 | copy | warn | 'Plain English' | Told to avoid jargon, a model announces that it is avoiding jargon. 'Explained plainly' swaps a synonym and keeps the tell. |
322
+ | `buzzword` | 1 | copy | warn | Buzzword | Cutting-edge, world-class, reach out, turnkey: corporate filler older than any model, and still the first thing a model writes about a business it knows nothing about. |
323
+ | `negative-reassurance` | 1 | copy | warn | Negative reassurance | 'No hidden fees', 'no surprises', 'never locked in' reassure by naming the fear, and plant it in a reader who did not have it. |
324
+ | `vague-word` | 1 | copy | warn | Vague word | Innovative, scalable, end-to-end, solutions: each can be true, and each is used where the writer had nothing specific to say. |
325
+ | `review-phrase` | 1 | copy | warn | Review-tier phrase | Truly, genuinely, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking. |
326
+ | `hollow-imperative` | 1 | copy | warn | Hollow imperative | 'Discover', 'Experience', 'Transform your' as the opening verb of a heading or button is a call to action with nothing in it. |
327
+ | `rhetorical-opener` | 1 | copy | warn | Rhetorical question opener | 'Looking for...?', 'Tired of...?', 'Ready to...?' opens with the model guessing at the reader instead of telling them something. |
328
+ | `em-dash` | 2 | copy | warn | Em dash | Generated copy leans on the em dash to join clauses. House copy does not use it. |
329
+ | `emoji` | 1 | copy | warn | Emoji in copy | A sparkle or a rocket beside a heading is decoration a model adds to seem friendly. It dates the page and reads as a social post. |
330
+ | `not-just-but` | 2 | copy | warn | Contrastive negation | 'It's not just a haircut, it's an experience', 'dispatched from here, not shipped in': the contrast-and-reveal template is the most recognisable construction in generated copy. |
331
+ | `no-x-no-y` | 2 | copy | warn | 'No X, no Y' list | 'No obligation, no spam.' Defining the business by what it is not, itemised, is the same template as contrastive negation. |
332
+ | `no-x-badge` | 2 | copy | warn | 'No X' badge | 'No catch.', 'NO JARGON GUIDE': a short standalone 'No X' reads as a slapped-on kicker, and worse when the same one is reused across pieces. |
333
+ | `staccato-triplet` | 2 | copy | warn | Staccato triplet | Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm. |
334
+ | `where-x-meets-y` | 2 | copy | warn | 'Where X meets Y' | 'Where luxury meets comfort' is a tagline shape that fits every business and so describes none. |
335
+ <!-- craft:catalogue:end -->
package/PRINCIPLES.md CHANGED
@@ -91,3 +91,17 @@ instead: a piecewise-linear map pinned at 0 and 1 that passes through
91
91
  (curve value, actual value) for the anchor. Monotonic by construction, and every
92
92
  other step keeps its relative position. The anchors define the ramp; the curve
93
93
  only sets the spacing between them.
94
+
95
+ ## Well made is not the same as decided
96
+
97
+ A surface can clear every number in the standard and still read as generated.
98
+ The craft rules measure quality; they cannot see that the accent, the face and
99
+ the section order are the ones a model picks for every brief it knows nothing
100
+ about. That is a different question, so it is a different document:
101
+ [CHARACTER.md](./CHARACTER.md).
102
+
103
+ Two things keep it from becoming a taste police. It only ever changes the look,
104
+ never the page grammar, because a page that looks typical for its category is
105
+ the one visitors trust first. And it never asks for novelty. It asks for a
106
+ reason: every expressive choice records where it came from in the client's own
107
+ world, and a choice with no reason is the one flagged.
package/README.md CHANGED
@@ -4,7 +4,8 @@ The Doman Digital house craft standard, as numbers.
4
4
 
5
5
  OKLCh colour ramps that snap to colours you already ship, semantic tokens derived
6
6
  with measured WCAG **and** APCA contrast, one motion vocabulary shared by CSS and
7
- JS, and a base stylesheet. Zero runtime dependencies.
7
+ JS, a base stylesheet, and a scanner that finds the AI look in source and copy.
8
+ Zero runtime dependencies.
8
9
 
9
10
  Read [PRINCIPLES.md](./PRINCIPLES.md) for why each rule is a rule.
10
11
 
@@ -71,6 +72,37 @@ rule cited, including a section on what is deliberately *not* a rule. Its
71
72
  canonical numbers are compared against the code on every test run, so the
72
73
  document cannot drift from what ships.
73
74
 
75
+ ## Character: finding the AI look
76
+
77
+ The standard asks whether a surface is well made. [CHARACTER.md](./CHARACTER.md)
78
+ asks whether anyone decided how it looks, or whether it is what a model builds
79
+ when nobody told it otherwise.
80
+
81
+ ```bash
82
+ npx craft scan # markup, components, stylesheets
83
+ npx craft scan --staged # pre-commit: the git index only
84
+ npx craft copy app content
85
+ npx craft tells list
86
+ ```
87
+
88
+ Every finding names the tell, its generation (1: indigo, Inter, glass; 2: cream,
89
+ italic serif, eyebrow chips, bento), the line, and what to do instead. Every
90
+ tell ships as a warning. A brand that really is violet declares it, with a
91
+ reason, in `art-direction.json`:
92
+
93
+ ```json
94
+ { "exceptions": [{ "tell": "ai-violet", "because": "Violet is on the van, the cards and the fascia." }] }
95
+ ```
96
+
97
+ The same checks run in code:
98
+
99
+ ```ts
100
+ import { checkCopy, formatReport, scanSource } from "@domandigital/craft";
101
+
102
+ const report = scanSource([{ path: "app/page.tsx", text }]);
103
+ console.log(formatReport(report, "craft scan"));
104
+ ```
105
+
74
106
  ## The anchor guarantee
75
107
 
76
108
  Every hex you pass in comes back out unchanged — the same string, byte for byte.
@@ -99,6 +131,9 @@ checkable rather than asserted.
99
131
  | Restraint | `checkRestraint`, `HOUSE_BUDGET` |
100
132
  | Tailwind | `tailwindV3Preset` (v3), `tailwindV4Theme` / `craft.tailwind.css` (v4) |
101
133
  | Emit | `craftTokens`, `emitCss`, `motionTokens` |
134
+ | Character | `scanSource`, `checkCopy`, `formatReport`, `CATALOGUE`, `CATALOGUE_VERSION`, `catalogueTable`, `runTell`, `tellById` |
135
+ | Character colour | `parseColour`, `findColours`, `isAiViolet`, `isCream`, `AI_VIOLET` |
136
+ | Character lists | `REFLEX_FONTS_1`, `REFLEX_FONTS_2`, `SHADCN_PRIMITIVES`, `AI_WORDS`, `STOCK_PHRASES`, `REVEAL_LIMIT`, `PILL_LIMIT`, `SHADCN_LIMIT` |
102
137
 
103
138
  ## Licence
104
139
 
package/STANDARD.md CHANGED
@@ -164,7 +164,10 @@ The package emits **plain CSS custom properties** precisely so these stay free:
164
164
  so Framer Motion, GSAP and CSS agree without the package choosing one.
165
165
  - **Where tokens live.** A `:root` block, a generated file, a Tailwind theme —
166
166
  the package does not care.
167
- - **Layout, grid and component structure.** Out of scope.
167
+ - **Layout, grid and component structure.** Not a rule. Layout *defaults*
168
+ (hero straight into a trust strip, three icon cards, a reveal on every
169
+ section) are measured as character in [CHARACTER.md](./CHARACTER.md), as
170
+ warnings with a declared-exception path, never as rules here.
168
171
  - **Brand voice, copy, and photography grading.** Separate standards.
169
172
 
170
173
  A rule here has to be one that is wrong to break. Everything else is a default.
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://unpkg.com/@domandigital/craft/art-direction.schema.json",
4
+ "title": "art-direction.json",
5
+ "description": "What a site decided about how it looks, and why. Every choice carries a reason from the client's real world and cites a source that shows it. The reason rule itself is enforced by `craft direction validate`; this schema only checks the shape.",
6
+ "type": "object",
7
+ "required": ["version", "client", "brief", "sources", "choices"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "client": { "type": "string", "minLength": 1 },
12
+ "brief": { "type": "string", "description": "Who, where, and what they do, in a sentence or two." },
13
+ "sources": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "object",
17
+ "required": ["id", "kind", "note"],
18
+ "properties": {
19
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
20
+ "kind": { "enum": ["livery", "shopfront", "signage", "packaging", "product", "uniform", "interior", "place", "material", "trade", "print", "photo", "founder"] },
21
+ "note": { "type": "string" },
22
+ "path": { "type": "string" },
23
+ "colours": { "type": "array", "items": { "type": "string" } },
24
+ "lettering": { "type": "string" }
25
+ },
26
+ "additionalProperties": false
27
+ }
28
+ },
29
+ "choices": {
30
+ "type": "object",
31
+ "propertyNames": { "enum": ["accent", "ground", "display", "body", "shape", "motif", "signature"] },
32
+ "additionalProperties": {
33
+ "type": "object",
34
+ "required": ["value", "because", "evidence"],
35
+ "properties": {
36
+ "value": { "type": "string" },
37
+ "because": { "type": "string" },
38
+ "evidence": { "type": "array", "items": { "type": "string" } }
39
+ },
40
+ "additionalProperties": false
41
+ }
42
+ },
43
+ "exceptions": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "object",
47
+ "required": ["tell", "because"],
48
+ "properties": {
49
+ "tell": { "type": "string" },
50
+ "because": { "type": "string" },
51
+ "evidence": { "type": "string" },
52
+ "path": { "type": "string" }
53
+ },
54
+ "additionalProperties": false
55
+ }
56
+ }
57
+ },
58
+ "additionalProperties": false
59
+ }