@domandigital/craft 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/CHARACTER.md +254 -11
- package/dist/audit-YNODYB3U.js +20 -0
- package/dist/audit.cjs +45 -17
- package/dist/audit.d.cts +14 -1
- package/dist/audit.d.ts +14 -1
- package/dist/audit.js +43 -16
- package/dist/{chunk-GWMSSRIQ.js → chunk-3AP4PJMX.js} +480 -670
- package/dist/chunk-CU5TZHOV.js +163 -0
- package/dist/chunk-GV3QOTQU.js +458 -0
- package/dist/chunk-IQTZWNEK.js +112 -0
- package/dist/chunk-J2BOAIHJ.js +175 -0
- package/dist/{cli-7ME4VIRG.js → chunk-OA7SGPN4.js} +50 -101
- package/dist/{chunk-HYGFP3LB.js → chunk-Q54JSNC5.js} +40 -3
- package/dist/chunk-YYC4EGER.js +249 -0
- package/dist/{cli-F7W4F3ER.js → cli-6Y5E32FS.js} +14 -168
- package/dist/cli-IQT6QZ6U.js +16 -0
- package/dist/cli-MW3JAGX3.js +21 -0
- package/dist/cli-PD43TWX7.js +15 -0
- package/dist/cli-TM2HWYN7.js +312 -0
- package/dist/cli.js +2 -1
- package/dist/index.cjs +572 -12
- package/dist/index.d.cts +286 -1
- package/dist/index.d.ts +286 -1
- package/dist/index.js +544 -9
- package/package.json +3 -2
- package/word-lists.json +155 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dbdef56: Signal 2, the counterfactual. `craft null build --brief "..." --out <dir>` asks `claude -p` for about 20 home pages from the brief alone, renders and fingerprints each, and writes `null.json`. `craft audit <url> --null <dir>` scores how typical a page is against them: the share of the model's own pages that sit further out than this one, typical at 0.10 or above, with the choices it shares with most of them. `craft tells harvest <dir>` lists what recurs across null pages and which tell already catches it, so the next generation of tells comes from what the model builds now. `snapshotUrls` in `@domandigital/craft/audit` snapshots several pages through one browser. The rendered reveal check now also reads the text blocks inside a section, so reveals on cards two levels down are no longer read as a still page.
|
|
8
|
+
- f765c69: Signal 3, the estate. `craft estate add <url | snapshot.json> --id <id>` fingerprints a shipped site into `estate.json`; `craft estate compare` lists every pair, or one site against the rest, closest first, and marks siblings: two sites closer together than two pages Claude builds for one brief (0.31, or measured again from `--null`). What a pair shares is named in plain words ("Fraunces headline", "no accent"). `--strict` exits 1 on a sibling. `craft direction propose --estate estate.json` reads the register. The fingerprint no longer takes a small painted patch, such as a floating chat button, for the accent: a painted colour needs a panel's worth of area.
|
|
9
|
+
- f3c3dc0: The character report. `craft report <url | snapshot.json>` reads a site against all three signals and the reason rule (`--repo`, `--null`, `--estate`, `--direction`) and gives a verdict: default, mixed, decided, or unproven when something was not measured. The actions come in retrofit order, one per choice, with every reason that points at it. `craft retrofit` writes them as a Markdown checklist that names the art-direction choice settling each change, shows the decided value, and ends with what to leave alone. `characterReport` and `retrofitPlan` are exported. System font keywords such as `ui-monospace` no longer read as CSS variable names.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 235b558: Export the copy catalogue's word and phrase lists as portable JSON
|
|
14
|
+
(`wordListsJson()`, checked in at `word-lists.json`), so a non-TypeScript
|
|
15
|
+
consumer can source from them instead of duplicating them by hand.
|
|
16
|
+
|
|
17
|
+
`claude-kit`'s `copy_check.py` hardcoded its own copies of these same lists
|
|
18
|
+
and had drifted from this package (documented in claude-kit's
|
|
19
|
+
`house-style/copy-rules.md`, "Note the enforcement gap"). `word-lists.json` is
|
|
20
|
+
the file that ends the drift: `install/sync-copy-wordlists.sh` on that side
|
|
21
|
+
copies it in verbatim, the same shape `sync-craft-standard.sh` already uses
|
|
22
|
+
for `STANDARD.md`.
|
|
23
|
+
|
|
24
|
+
No tell's detection or severity changed. Regenerate the checked-in file with
|
|
25
|
+
`pnpm --filter @domandigital/craft run docs` after editing any list in
|
|
26
|
+
`tells/copy.ts`.
|
|
27
|
+
|
|
3
28
|
## 0.6.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/CHARACTER.md
CHANGED
|
@@ -29,8 +29,8 @@ model defaults move.
|
|
|
29
29
|
| Signal | Question | Status |
|
|
30
30
|
| --- | --- | --- |
|
|
31
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? |
|
|
33
|
-
| 3. Distance from the estate | Does it look like the agency's other sites? |
|
|
32
|
+
| 2. Counterfactual typicality | Would Claude have built this anyway, for this brief? | **Shipped**: `craft null build` generates about 20 pages from the brief; `craft audit --null` scores a page against them. |
|
|
33
|
+
| 3. Distance from the estate | Does it look like the agency's other sites? | **Shipped**: `craft estate add` fingerprints each shipped site into `estate.json`; `craft estate compare` marks siblings. The agency's biggest tell is its clients looking like each other. |
|
|
34
34
|
|
|
35
35
|
**The reason rule.** Every expressive choice records a `because`, with evidence
|
|
36
36
|
from the client's real world: the shopfront, the van, the trade's own look, the
|
|
@@ -43,7 +43,7 @@ adds character rather than only removing tells. It lives in `art-direction.json`
|
|
|
43
43
|
| --- | --- |
|
|
44
44
|
| 1 | The first wave: indigo, blue-to-purple gradients, Inter, glass, three icon cards. |
|
|
45
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
|
|
46
|
+
| 3 | Whatever `craft tells harvest` finds next in the null models, once a person has read it. |
|
|
47
47
|
|
|
48
48
|
A report counts findings by generation, so it can say *which* wave a site was
|
|
49
49
|
built in, not only that it has tells.
|
|
@@ -185,6 +185,98 @@ against different sets.
|
|
|
185
185
|
Known limit: a computed font family is the family the stylesheet asked for.
|
|
186
186
|
A face that failed to load is still reported under its name.
|
|
187
187
|
|
|
188
|
+
## The counterfactual
|
|
189
|
+
|
|
190
|
+
A list of tells goes stale; the model does not. So signal 2 asks the model
|
|
191
|
+
directly: given this brief and nothing else, what would you build?
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
craft null build --brief "RMP Electrical, electricians based in Uxbridge ..." --out null/rmp
|
|
195
|
+
craft audit https://www.rmp-electrical.co.uk --null null/rmp
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`null build` runs `claude -p` about 20 times (`--runs`), from an empty folder
|
|
199
|
+
with no tools, settings, skills or MCP servers, so each page is what the model
|
|
200
|
+
builds from the brief alone. The prompt is recorded in `null.json` and says
|
|
201
|
+
nothing about how the page should look. Each page is rendered, snapshotted and
|
|
202
|
+
fingerprinted like any other. A reply that is a note instead of a page is kept
|
|
203
|
+
as `NN.reply.txt` and generated again. The build is resumable: run the same
|
|
204
|
+
command to fill in anything that failed. `--brief` can come from
|
|
205
|
+
`art-direction.json` with `--direction`.
|
|
206
|
+
|
|
207
|
+
**Typicality** is a page's mean fingerprint distance to its three nearest null
|
|
208
|
+
pages, ranked against the same measure for every null page against the
|
|
209
|
+
others. A score of 0.40 means 40% of the model's own pages sit further from
|
|
210
|
+
the rest than this one does. A page is typical at 0.10 or above, so a page the
|
|
211
|
+
model built is flagged nine times in ten by construction, and the threshold
|
|
212
|
+
comes from the model's own spread rather than a number picked here. The report
|
|
213
|
+
lists what the page shares with most of the null ("display Inter, 14 of 20"),
|
|
214
|
+
which is where to start changing it.
|
|
215
|
+
|
|
216
|
+
**The harvest** reads null models the other way round. `craft tells harvest
|
|
217
|
+
calibration/null` lists the choices that recur across the null pages (faces,
|
|
218
|
+
accent families, grounds, button shapes, openings, phrases) and says which
|
|
219
|
+
tell already catches each. The ones nothing catches are the candidates for
|
|
220
|
+
generation 3. A phrase has to turn up across more than one brief, so a trade's
|
|
221
|
+
own words do not count, and navigation labels, form labels and the legal footer
|
|
222
|
+
are left out as page grammar. Nothing is added to the catalogue automatically:
|
|
223
|
+
a candidate becomes a tell with a flag case and a pass case like every other.
|
|
224
|
+
|
|
225
|
+
Known limits: a user-level `CLAUDE.md` on the machine that builds the null
|
|
226
|
+
still reaches the model. The null is only as current as its build date; rebuild
|
|
227
|
+
it when the model changes.
|
|
228
|
+
|
|
229
|
+
## The estate
|
|
230
|
+
|
|
231
|
+
The agency's biggest tell is not on any one site. It is its clients looking
|
|
232
|
+
like each other. `estate.json` holds one fingerprint per shipped site:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
craft estate add https://www.rmp-electrical.co.uk --id rmp --client "RMP Electrical"
|
|
236
|
+
craft estate compare # every pair, closest first
|
|
237
|
+
craft estate compare rmp # one site against the rest
|
|
238
|
+
craft estate compare https://staging.example.test # a new build before it ships
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Siblings** are two sites closer together than two pages Claude builds for
|
|
242
|
+
the same brief usually are: the median of that distance over the null models,
|
|
243
|
+
0.31 on 2026-09-23 (`--null` measures it again from any null models given).
|
|
244
|
+
If a barber and an electrician look more alike than two drafts of one barber,
|
|
245
|
+
neither look was chosen. The report says what the pair shares in plain words
|
|
246
|
+
("Fraunces headline", "no accent", "pill buttons"), which is where to start
|
|
247
|
+
pulling them apart. A sibling is a warning; `--strict` exits 1 on one, for a
|
|
248
|
+
pipeline that has decided to hold the line.
|
|
249
|
+
|
|
250
|
+
`craft direction propose --estate estate.json` reads the register too, so a
|
|
251
|
+
proposed accent steers away from one a sibling already uses.
|
|
252
|
+
|
|
253
|
+
## The character report
|
|
254
|
+
|
|
255
|
+
`craft report` reads one site against everything above and gives a verdict:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
craft report https://www.rmp-electrical.co.uk --repo . --null null/rmp --estate ../estate.json
|
|
259
|
+
craft retrofit https://www.rmp-electrical.co.uk --repo . --null null/rmp --estate ../estate.json --out RETROFIT.md
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
| Verdict | When |
|
|
263
|
+
|---|---|
|
|
264
|
+
| default | Two or more signals raised: three or more design tells, typical of the brief's null, a sibling in the estate, or fewer than five choices decided with a reason. |
|
|
265
|
+
| mixed | One raised. |
|
|
266
|
+
| decided | None raised, and all four measured. |
|
|
267
|
+
| unproven | None raised, but something was not measured. Unverified is never a pass. |
|
|
268
|
+
|
|
269
|
+
The actions come in the order a retrofit works: decide first, then type,
|
|
270
|
+
colour, shape, effects, motion, how sections look, and copy. Where a tell, the
|
|
271
|
+
null model and a sibling all point at one choice, they are one action with
|
|
272
|
+
three reasons. Sharing a white page or a still one with the null is never
|
|
273
|
+
an action: that would reward strangeness.
|
|
274
|
+
|
|
275
|
+
`craft retrofit` writes the same as a checklist. Each change names the
|
|
276
|
+
art-direction choice that settles it, and shows the decided value and its
|
|
277
|
+
reason, or says it waits on that decision. It ends with what to leave alone:
|
|
278
|
+
navigation, the order of sections, where the call to action sits.
|
|
279
|
+
|
|
188
280
|
## The house copy rules live here
|
|
189
281
|
|
|
190
282
|
Since catalogue `2026.09.2` the rule lists of claude-kit's
|
|
@@ -212,9 +304,10 @@ reason that rule exists.
|
|
|
212
304
|
The targets, recorded here with the date once each has been run:
|
|
213
305
|
|
|
214
306
|
- **AI set** (pages generated by `claude -p` from about 20 briefs): 90% or more
|
|
215
|
-
flagged as typical or tell-heavy.
|
|
307
|
+
flagged as typical or tell-heavy. **First run 2026-09-23: 19 of 20 (95%)**, below.
|
|
216
308
|
- **Human set** (the ten references in claude-kit's `reference-set.md`): zero
|
|
217
|
-
block hits and low typicality.
|
|
309
|
+
block hits and low typicality. **First run 2026-09-23: zero block hits, none
|
|
310
|
+
typical**, below.
|
|
218
311
|
- **Estate baseline** (all seven client repos and live URLs): every hit read by
|
|
219
312
|
a person before any rule blocks. **First run 2026-09-23**, below.
|
|
220
313
|
|
|
@@ -240,7 +333,7 @@ blocks; it is for a person to read.
|
|
|
240
333
|
| `glass-panel` | 9 / · | 45 / · | 13 / · | | 1 / · | | |
|
|
241
334
|
| `hero-then-proof` | 9 / · | 1 / R | · / R | | 1 / · | 1 / · | 2 / R |
|
|
242
335
|
| `icon-tile-grid` | 1 / R | 9 / · | · / R | | · / R | | 1 / R |
|
|
243
|
-
| `reveal-everywhere` | 1 / · | 1 / · | | | |
|
|
336
|
+
| `reveal-everywhere` | 1 / · | 1 / · | | | | · / R | |
|
|
244
337
|
| `pill-everything` | 1 / R | 1 / · | 1 / · | | | | 1 / R |
|
|
245
338
|
| `shadcn-dump` | | 1 / · | | | | | |
|
|
246
339
|
| `cream-palette` | | 3 / · | | | | | 18 / R |
|
|
@@ -258,15 +351,17 @@ What it shows:
|
|
|
258
351
|
- **The three survey findings reproduce where they can.** Sensphere's violet
|
|
259
352
|
is found in source (18) and on its buttons; MMM's intro, glows and glass are
|
|
260
353
|
in source and its intro and glows on the page; MMM reveals 60% of the
|
|
261
|
-
sections below its fold on scroll, three of
|
|
262
|
-
|
|
354
|
+
sections below its fold on scroll, all three of them, one short of the
|
|
355
|
+
four the rendered rule needs, and its source is over the reveal limit. Rise & Bloom's glass
|
|
263
356
|
hero and icon grid could not be checked: that repo is not attached to this
|
|
264
357
|
session.
|
|
265
358
|
- **Every site ships a reflex face.** Twelve of fourteen site-face pairs are
|
|
266
359
|
on one of the two lists. Fraunces sets the headline on DD, HJ Beauty and RMP.
|
|
267
|
-
- **DD and RMP
|
|
268
|
-
|
|
269
|
-
|
|
360
|
+
- **DD and RMP were read as the closest pair** (0.46) on the first run, with
|
|
361
|
+
a green accent in common. DD's "green" was its floating WhatsApp button,
|
|
362
|
+
48px square, the only saturated paint on a near-black and white page. The
|
|
363
|
+
fingerprint now needs a panel's worth of area before a painted colour
|
|
364
|
+
counts as the accent. The corrected pairs are under *The estate*, below.
|
|
270
365
|
- **DD's violet is its brand.** Its 36 source hits wait for the exception in
|
|
271
366
|
its `art-direction.json`.
|
|
272
367
|
|
|
@@ -280,9 +375,157 @@ False positives found and fixed in this run:
|
|
|
280
375
|
`cormorantGaramond`) are now read back to the face's name, or every Next.js
|
|
281
376
|
site would have passed the font tells.
|
|
282
377
|
|
|
378
|
+
- The rendered reveal check read only a section and its direct children.
|
|
379
|
+
Found through the null models (below): a reveal on the cards two levels
|
|
380
|
+
down read as a still page. It now reads the text blocks inside a section
|
|
381
|
+
too, ignoring anything fixed. Re-run the same day: Chair and Blade, which
|
|
382
|
+
has a `RevealWrapper`, now shows `reveal-everywhere` on the page (six of
|
|
383
|
+
nine sections below the fold).
|
|
384
|
+
|
|
283
385
|
Not yet read by a person: the source counts for `icon-tile-stack` (MMM 93)
|
|
284
386
|
and `glass-panel` (MMM 45) look high and are the first to check.
|
|
285
387
|
|
|
388
|
+
### Signal 2 first read, 2026-09-23
|
|
389
|
+
|
|
390
|
+
Seven null models, one per estate brief, 20 pages each (140 pages), built
|
|
391
|
+
with `craft null build` from the briefs in `calibration/briefs.json`. Eight
|
|
392
|
+
replies were notes instead of pages and were generated again (kept as
|
|
393
|
+
`*.reply.txt`). Everything is in `calibration/null/`; `score.mjs` reproduces
|
|
394
|
+
the numbers and writes `results.json`.
|
|
395
|
+
|
|
396
|
+
**AI set.** Twenty briefs the null never saw (a dentist, a bakery, a
|
|
397
|
+
landscaper and so on), one page each, scored against all 140 null pages.
|
|
398
|
+
Flagged means typical (0.10 or above) or tell-heavy (three or more distinct
|
|
399
|
+
design tells), a rule fixed before any result was read.
|
|
400
|
+
|
|
401
|
+
| | Pages | Share |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| Flagged | 19 of 20 | 95% (target 90%) |
|
|
404
|
+
| Typical on its own | 13 of 20 | 65% |
|
|
405
|
+
| Tell-heavy on its own | 16 of 20 | 80% |
|
|
406
|
+
|
|
407
|
+
The one miss is the landscaper: dark forest green on a cream ground,
|
|
408
|
+
Cormorant Garamond over Karla, square buttons. Two tells (`cream-palette`,
|
|
409
|
+
`reflex-font-2`), one short of tell-heavy, and a score of 0.01 against the
|
|
410
|
+
pooled null because no estate brief is a garden business. Neither signal alone reaches the
|
|
411
|
+
target; together they do, which is the reason for having more than one.
|
|
412
|
+
|
|
413
|
+
**The model's look depends on the brief.** Each brief's null pages scored
|
|
414
|
+
against the other six briefs' pages are typical only 2 to 13 times in 20.
|
|
415
|
+
A barber and an electrician get different pages. So a site is scored against
|
|
416
|
+
the null built from its own brief, and pooling briefs is a weaker test.
|
|
417
|
+
|
|
418
|
+
**The estate against its own brief's null.** Score, then what it shares with
|
|
419
|
+
most of the null.
|
|
420
|
+
|
|
421
|
+
| Site | Score | Typical | Shares with the null |
|
|
422
|
+
|---|---|---|---|
|
|
423
|
+
| DD | 0.15 | yes | pill buttons (19 of 20), Fraunces headline (13 of 20) |
|
|
424
|
+
| Harrison James | 0.10 | yes, at the line | Inter body (18 of 20), yellow accent (16 of 20), white ground |
|
|
425
|
+
| MMM | 0.05 | no | white ground, radial glow (13 of 20), Cormorant Garamond (9 of 20) |
|
|
426
|
+
| HJ Beauty | 0.05 | no | white ground, Fraunces (5 of 20), marquee (5 of 20) |
|
|
427
|
+
| Chair and Blade | 0.05 | no | square buttons (18 of 20), Bebas Neue (9 of 20), dark ground |
|
|
428
|
+
| RMP | 0.05 | no | eyebrow chip (14 of 20), Manrope body |
|
|
429
|
+
| Sensphere | 0.00 | no | radial glow (10 of 20), Inter body |
|
|
430
|
+
|
|
431
|
+
Read with care: "not typical" says the site is not what Claude builds from
|
|
432
|
+
the brief today. It does not say the site was decided. Sensphere scores 0.00
|
|
433
|
+
and still carries five catalogue tells on the page, violet among them: Claude
|
|
434
|
+
no longer builds that look for this brief, but it is still a known tell. That
|
|
435
|
+
is what signal 1 is for. DD and Harrison James are the
|
|
436
|
+
two a model would most nearly have built unprompted.
|
|
437
|
+
|
|
438
|
+
**The harvest** (`calibration/null/harvest.txt`). No face outside the two
|
|
439
|
+
reflex lists is chosen by a quarter of the null pages, so the font lists still
|
|
440
|
+
cover what the model picks today: Inter 69 of 140, Fraunces 51, Poppins 36.
|
|
441
|
+
The catalogue tells the model still produces most: `reflex-font-2` 103 of
|
|
442
|
+
140, `glass-panel` 100, `reflex-font` 94, `cream-palette` 81,
|
|
443
|
+
`pill-everything` 79. Candidates nothing catches yet, for a person to read:
|
|
444
|
+
|
|
445
|
+
- A coral or terracotta accent (around `#e77a63`): 66 of 140, five briefs.
|
|
446
|
+
- Phrases across five or more briefs: "done properly" (33 pages, six briefs),
|
|
447
|
+
"exactly what" (27, all seven), "who actually" (24, five), "within one
|
|
448
|
+
working day" (32, five), "rather than" (27, six).
|
|
449
|
+
- The opening "hero > hero > cards" (54 of 140) is how the section reader
|
|
450
|
+
splits a nav band from the hero, not a model habit. Not a candidate.
|
|
451
|
+
|
|
452
|
+
`glass-panel` at 100 of 140 includes source hits on blurred sticky navs, the
|
|
453
|
+
same false positive fixed on the rendered path. The source rule needs the same
|
|
454
|
+
fix before that count means anything.
|
|
455
|
+
|
|
456
|
+
### Signal 3 first read, 2026-09-23
|
|
457
|
+
|
|
458
|
+
`craft estate add` on the seven live snapshots, then `craft estate compare
|
|
459
|
+
--null calibration/null/*` (the line measured again: 0.31). Register and output
|
|
460
|
+
in `calibration/estate/`.
|
|
461
|
+
|
|
462
|
+
| Pair | Distance | Shares |
|
|
463
|
+
|---|---|---|
|
|
464
|
+
| DD + HJ Beauty | **0.28, siblings** | no accent, Fraunces headline, no reveals |
|
|
465
|
+
| DD + RMP | 0.49 | Fraunces headline, grey ground, pill buttons |
|
|
466
|
+
| Harrison James + Sensphere | 0.50 | white ground, square buttons, no reveals |
|
|
467
|
+
| HJ Beauty + RMP | 0.56 | Fraunces headline |
|
|
468
|
+
| Chair and Blade + anything | 0.60 to 0.86 | at most a marquee (with HJ Beauty) |
|
|
469
|
+
|
|
470
|
+
One sibling pair in twenty-one, and it is the agency's own site with one of
|
|
471
|
+
its clients: the same Fraunces headline over a near-black and white page with
|
|
472
|
+
no colour of its own. Fraunces sets the headline on three of the seven. Chair
|
|
473
|
+
and Blade shares nothing with anyone but a marquee. Not yet read by a person.
|
|
474
|
+
|
|
475
|
+
### Human set, 2026-09-23
|
|
476
|
+
|
|
477
|
+
The references in claude-kit's `reference-set.md`, live home pages, scored as
|
|
478
|
+
the AI set is: typicality against all seven null models pooled, and the
|
|
479
|
+
design tells on the rendered page. The tenth reference is "any
|
|
480
|
+
generated-looking site" and is left out by definition. Snapshots and
|
|
481
|
+
`score.mjs` in `calibration/human/`.
|
|
482
|
+
|
|
483
|
+
| Site | Score | Tells on the page | Flagged |
|
|
484
|
+
|---|---|---|---|
|
|
485
|
+
| Linear | 0.00 | 7: violet, grid, icon grid, marquee, glow, Inter, hairline shadow | tell-heavy |
|
|
486
|
+
| Stripe | 0.00 | 5: violet, blue-to-purple, glass, icon grid, glow | tell-heavy |
|
|
487
|
+
| DD | 0.00 | 4: icon grid, pills, glow, Fraunces | tell-heavy |
|
|
488
|
+
| animations.dev | 0.00 | 2: marquee, Inter | no |
|
|
489
|
+
| Utopia | 0.00 | 2: hero then proof, pills | no |
|
|
490
|
+
| APCA (Myndex) | 0.00 | 0 | no |
|
|
491
|
+
| rauno.me | 0.01 | 0 | no |
|
|
492
|
+
| StarrLuxury | 0.01 | not measured: a preview gate, not the site | |
|
|
493
|
+
| candy-aesthetics | 0.05 | not measured: "This preview has ended" | |
|
|
494
|
+
|
|
495
|
+
- **Zero block hits and none typical**: both targets met. Every score is 0.05
|
|
496
|
+
or under.
|
|
497
|
+
- **Three of the seven measured are tell-heavy.** Linear and Stripe are where
|
|
498
|
+
the first-wave look came from: the indigo, the glow, the gradient were
|
|
499
|
+
theirs before they were every model's. The tell list describes them too,
|
|
500
|
+
which is the reason no single signal decides the verdict.
|
|
501
|
+
- **The comparison is not like for like.** The null models are small-business
|
|
502
|
+
briefs; these are software and portfolio sites, so a low score is easier to
|
|
503
|
+
reach. A human set of small-business sites nobody generated is the better
|
|
504
|
+
test and is still to find.
|
|
505
|
+
- Both house demos had expired or gated previews on the day. They are not
|
|
506
|
+
measured, not clean.
|
|
507
|
+
|
|
508
|
+
### Character report first read, 2026-09-23
|
|
509
|
+
|
|
510
|
+
`craft report` on each live snapshot with its repo, its own brief's null and
|
|
511
|
+
the register. Reports and retrofit plans are in `calibration/report/`.
|
|
512
|
+
|
|
513
|
+
| Site | Verdict | Raised | Actions |
|
|
514
|
+
|---|---|---|---|
|
|
515
|
+
| DD | default | tells (12), typical (0.15), sibling of HJ Beauty | 26 |
|
|
516
|
+
| HJ Beauty | default | tells (6), sibling of DD | 18 |
|
|
517
|
+
| MMM | mixed | tells (18) | 27 |
|
|
518
|
+
| Sensphere | mixed | tells (9) | 18 |
|
|
519
|
+
| Chair and Blade | mixed | tells (7) | 11 |
|
|
520
|
+
| RMP | mixed | tells (8) | 18 |
|
|
521
|
+
| Harrison James | mixed | typical (0.10) | 9 |
|
|
522
|
+
|
|
523
|
+
No site has an `art-direction.json`, so the reasons are not measured on any of
|
|
524
|
+
them and none can be *decided* yet. The first change after deciding is the
|
|
525
|
+
same on all seven: the display face. It is the estate's common default, and
|
|
526
|
+
the one choice the retrofits (phase G) should settle first. Not yet read by a
|
|
527
|
+
person.
|
|
528
|
+
|
|
286
529
|
## The catalogue
|
|
287
530
|
|
|
288
531
|
Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
audit,
|
|
3
|
+
collectInPage,
|
|
4
|
+
loadPlaywright,
|
|
5
|
+
overlayInPage,
|
|
6
|
+
snapshotPage,
|
|
7
|
+
snapshotUrl,
|
|
8
|
+
snapshotUrls
|
|
9
|
+
} from "./chunk-OA7SGPN4.js";
|
|
10
|
+
import "./chunk-Q54JSNC5.js";
|
|
11
|
+
import "./chunk-3AP4PJMX.js";
|
|
12
|
+
export {
|
|
13
|
+
audit,
|
|
14
|
+
collectInPage,
|
|
15
|
+
loadPlaywright,
|
|
16
|
+
overlayInPage,
|
|
17
|
+
snapshotPage,
|
|
18
|
+
snapshotUrl,
|
|
19
|
+
snapshotUrls
|
|
20
|
+
};
|
package/dist/audit.cjs
CHANGED
|
@@ -25,7 +25,8 @@ __export(audit_exports, {
|
|
|
25
25
|
loadPlaywright: () => loadPlaywright,
|
|
26
26
|
overlayInPage: () => overlayInPage,
|
|
27
27
|
snapshotPage: () => snapshotPage,
|
|
28
|
-
snapshotUrl: () => snapshotUrl
|
|
28
|
+
snapshotUrl: () => snapshotUrl,
|
|
29
|
+
snapshotUrls: () => snapshotUrls
|
|
29
30
|
});
|
|
30
31
|
module.exports = __toCommonJS(audit_exports);
|
|
31
32
|
|
|
@@ -1208,7 +1209,7 @@ const inter = Inter({ subsets: ["latin"] });`),
|
|
|
1208
1209
|
severity: "warn",
|
|
1209
1210
|
surface: "source",
|
|
1210
1211
|
why: "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.",
|
|
1211
|
-
fix: "
|
|
1212
|
+
fix: "Pick the display face from the client's world (signage, packaging, the trade's lettering) and record why in art-direction.json. If this face really is the right one, declare an exception with the reason and the evidence.",
|
|
1212
1213
|
detect: fontTell(REFLEX_FONTS_2),
|
|
1213
1214
|
fixtures: {
|
|
1214
1215
|
flag: [f2("src/styles.css", `@theme {
|
|
@@ -1640,15 +1641,16 @@ function makeSnapshot(patch = {}) {
|
|
|
1640
1641
|
}
|
|
1641
1642
|
|
|
1642
1643
|
// src/snapshot/fonts.ts
|
|
1644
|
+
var SYSTEM = /^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|ui-rounded|sans-serif|serif|monospace|cursive)$/i;
|
|
1643
1645
|
function normaliseFamily(raw) {
|
|
1644
1646
|
let name = raw.replace(/["']/g, "").trim();
|
|
1645
1647
|
const next = name.match(/^__(.+?)(?:_Fallback)?_[0-9a-f]{5,8}$/i);
|
|
1646
1648
|
if (next) name = next[1].replace(/_/g, " ");
|
|
1647
1649
|
name = name.replace(/\s+Fallback$/i, "").replace(/\s+Variable$/i, "");
|
|
1650
|
+
if (SYSTEM.test(name)) return "system-ui";
|
|
1648
1651
|
if (!/\s/.test(name) && (/[a-z][A-Z]/.test(name) || /^[a-z]+(?:-[a-z]+)+$/.test(name))) {
|
|
1649
1652
|
name = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/-/g, " ").replace(/\b([a-z])/g, (c) => c.toUpperCase()).replace(/\bDm\b/, "DM");
|
|
1650
1653
|
}
|
|
1651
|
-
if (/^(?:system-ui|-apple-system|BlinkMacSystemFont|ui-sans-serif|ui-serif|ui-monospace|sans-serif|serif|monospace)$/i.test(name)) return "system-ui";
|
|
1652
1654
|
return name;
|
|
1653
1655
|
}
|
|
1654
1656
|
var KNOWN = {
|
|
@@ -2000,6 +2002,7 @@ function auditSnapshot(snapshot, options = {}) {
|
|
|
2000
2002
|
// src/fingerprint/index.ts
|
|
2001
2003
|
var FINGERPRINT_VERSION = 1;
|
|
2002
2004
|
var CHROMATIC = 0.03;
|
|
2005
|
+
var MIN_ACCENT_AREA = 2e4;
|
|
2003
2006
|
var oklch = (value) => {
|
|
2004
2007
|
const parsed = parseColour(value);
|
|
2005
2008
|
return parsed && parsed.alpha > 0.5 ? parsed.oklch : null;
|
|
@@ -2012,7 +2015,7 @@ function accentOf(s) {
|
|
|
2012
2015
|
}
|
|
2013
2016
|
const topButton = [...buttons].sort((a, b) => b[1] - a[1])[0];
|
|
2014
2017
|
if (topButton) return oklch(topButton[0]);
|
|
2015
|
-
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
2018
|
+
const bg = s.colours.backgrounds.map((b) => ({ o: oklch(b.value), w: b.area })).filter((x) => x.o && x.o.c >= CHROMATIC && x.w >= MIN_ACCENT_AREA).sort((a, b) => b.w - a.w)[0];
|
|
2016
2019
|
if (bg) return bg.o;
|
|
2017
2020
|
const text = s.colours.text.map((t) => ({ o: oklch(t.value), w: t.chars })).filter((x) => x.o && x.o.c >= CHROMATIC).sort((a, b) => b.w - a.w)[0];
|
|
2018
2021
|
return text ? text.o : null;
|
|
@@ -2207,13 +2210,18 @@ function collectInPage() {
|
|
|
2207
2210
|
pushBlocks(root, 0);
|
|
2208
2211
|
const header = document.querySelector("body > header, header");
|
|
2209
2212
|
const h1 = document.querySelector("h1");
|
|
2213
|
+
const waiting = (c) => {
|
|
2214
|
+
const cs = style(c);
|
|
2215
|
+
const op = parseFloat(cs.opacity);
|
|
2216
|
+
return op < 0.1 || op < 1 && cs.transform !== "none" || c.hasAttribute("data-aos") || /\breveal\b|\bfade-?(?:in|up)\b|\banimate-on-scroll\b/i.test(typeof c.className === "string" ? c.className : "");
|
|
2217
|
+
};
|
|
2210
2218
|
const hiddenAtLoad = (el) => {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2219
|
+
if ([el, ...Array.from(el.children).slice(0, 6)].some(waiting)) return true;
|
|
2220
|
+
const blocks2 = Array.from(el.querySelectorAll("div, article, li, figure, p, h2, h3")).slice(0, 200).filter((d) => {
|
|
2221
|
+
const b = box(d);
|
|
2222
|
+
return b.width > 100 && b.height > 40 && (d.textContent ?? "").trim().length > 12 && style(d).position !== "fixed";
|
|
2223
|
+
}).slice(0, 12);
|
|
2224
|
+
return blocks2.filter(waiting).length >= Math.min(2, blocks2.length) && blocks2.some(waiting);
|
|
2217
2225
|
};
|
|
2218
2226
|
const cardsIn = (section) => {
|
|
2219
2227
|
let best = { cards: 0, iconCards: 0 };
|
|
@@ -2412,19 +2420,38 @@ async function snapshotPage(page, url, options = {}) {
|
|
|
2412
2420
|
motion: { ...data.motion, introOverlay: early && !late }
|
|
2413
2421
|
};
|
|
2414
2422
|
}
|
|
2415
|
-
async function
|
|
2416
|
-
const pw = await loadPlaywright();
|
|
2423
|
+
async function launch(pw, url, options) {
|
|
2417
2424
|
const env = globalThis.process?.env ?? {};
|
|
2418
2425
|
const executablePath = options.executablePath ?? env.CRAFT_CHROMIUM ?? void 0;
|
|
2419
2426
|
const host = new URL(url).hostname;
|
|
2420
2427
|
const noProxy = [`localhost`, `127.0.0.1`, `::1`, ...(env.NO_PROXY ?? env.no_proxy ?? "").split(",")].map((h) => h.trim().replace(/^\*?\./, "")).filter(Boolean);
|
|
2421
2428
|
const direct = /^(?:localhost|127\.|\[?::1\]?$)/.test(host) || noProxy.some((h) => host === h || host.endsWith(`.${h}`));
|
|
2422
2429
|
const proxy = direct ? void 0 : env.HTTPS_PROXY ?? env.https_proxy;
|
|
2423
|
-
|
|
2430
|
+
return pw.chromium.launch({ headless: true, ...executablePath ? { executablePath } : {}, ...proxy ? { proxy: { server: proxy } } : {} });
|
|
2431
|
+
}
|
|
2432
|
+
async function snapshotUrl(url, options = {}) {
|
|
2433
|
+
const [result] = await snapshotUrls([url], options);
|
|
2434
|
+
if (!result.snapshot) throw new Error(result.error);
|
|
2435
|
+
return result.snapshot;
|
|
2436
|
+
}
|
|
2437
|
+
async function snapshotUrls(urls, options = {}) {
|
|
2438
|
+
if (urls.length === 0) return [];
|
|
2439
|
+
const pw = await loadPlaywright();
|
|
2440
|
+
const browser = await launch(pw, urls[0], options);
|
|
2441
|
+
const out = [];
|
|
2424
2442
|
try {
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2443
|
+
for (const url of urls) {
|
|
2444
|
+
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
2445
|
+
try {
|
|
2446
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
2447
|
+
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
2448
|
+
} catch (error) {
|
|
2449
|
+
out.push({ url, error: error.message });
|
|
2450
|
+
} finally {
|
|
2451
|
+
await page.close?.();
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return out;
|
|
2428
2455
|
} finally {
|
|
2429
2456
|
await browser.close();
|
|
2430
2457
|
}
|
|
@@ -2440,5 +2467,6 @@ async function audit(url, options = {}) {
|
|
|
2440
2467
|
loadPlaywright,
|
|
2441
2468
|
overlayInPage,
|
|
2442
2469
|
snapshotPage,
|
|
2443
|
-
snapshotUrl
|
|
2470
|
+
snapshotUrl,
|
|
2471
|
+
snapshotUrls
|
|
2444
2472
|
});
|
package/dist/audit.d.cts
CHANGED
|
@@ -329,6 +329,7 @@ interface SnapshotOptions {
|
|
|
329
329
|
timeoutMs?: number;
|
|
330
330
|
}
|
|
331
331
|
interface MinimalPage {
|
|
332
|
+
close?(): Promise<void>;
|
|
332
333
|
goto(url: string, options?: {
|
|
333
334
|
waitUntil?: string;
|
|
334
335
|
timeout?: number;
|
|
@@ -365,6 +366,18 @@ declare function loadPlaywright(): Promise<Launcher>;
|
|
|
365
366
|
declare function snapshotPage(page: MinimalPage, url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
366
367
|
/** Launch a browser, open `url`, and capture a snapshot. */
|
|
367
368
|
declare function snapshotUrl(url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
369
|
+
interface SnapshotResult {
|
|
370
|
+
url: string;
|
|
371
|
+
snapshot?: Snapshot;
|
|
372
|
+
/** Why this page has no snapshot. The others are still taken. */
|
|
373
|
+
error?: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Several pages through one browser, in order. A null model is twenty pages;
|
|
377
|
+
* launching Chromium twenty times is most of the wait. One page that will not
|
|
378
|
+
* load costs that page, not the batch.
|
|
379
|
+
*/
|
|
380
|
+
declare function snapshotUrls(urls: string[], options?: SnapshotOptions): Promise<SnapshotResult[]>;
|
|
368
381
|
interface AuditResult {
|
|
369
382
|
snapshot: Snapshot;
|
|
370
383
|
report: CheckReport;
|
|
@@ -373,4 +386,4 @@ interface AuditResult {
|
|
|
373
386
|
/** Snapshot a URL, judge it against the rendered tells, and fingerprint it. */
|
|
374
387
|
declare function audit(url: string, options?: SnapshotOptions & CheckOptions): Promise<AuditResult>;
|
|
375
388
|
|
|
376
|
-
export { type AuditResult, type SnapshotOptions, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl };
|
|
389
|
+
export { type AuditResult, type SnapshotOptions, type SnapshotResult, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl, snapshotUrls };
|
package/dist/audit.d.ts
CHANGED
|
@@ -329,6 +329,7 @@ interface SnapshotOptions {
|
|
|
329
329
|
timeoutMs?: number;
|
|
330
330
|
}
|
|
331
331
|
interface MinimalPage {
|
|
332
|
+
close?(): Promise<void>;
|
|
332
333
|
goto(url: string, options?: {
|
|
333
334
|
waitUntil?: string;
|
|
334
335
|
timeout?: number;
|
|
@@ -365,6 +366,18 @@ declare function loadPlaywright(): Promise<Launcher>;
|
|
|
365
366
|
declare function snapshotPage(page: MinimalPage, url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
366
367
|
/** Launch a browser, open `url`, and capture a snapshot. */
|
|
367
368
|
declare function snapshotUrl(url: string, options?: SnapshotOptions): Promise<Snapshot>;
|
|
369
|
+
interface SnapshotResult {
|
|
370
|
+
url: string;
|
|
371
|
+
snapshot?: Snapshot;
|
|
372
|
+
/** Why this page has no snapshot. The others are still taken. */
|
|
373
|
+
error?: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Several pages through one browser, in order. A null model is twenty pages;
|
|
377
|
+
* launching Chromium twenty times is most of the wait. One page that will not
|
|
378
|
+
* load costs that page, not the batch.
|
|
379
|
+
*/
|
|
380
|
+
declare function snapshotUrls(urls: string[], options?: SnapshotOptions): Promise<SnapshotResult[]>;
|
|
368
381
|
interface AuditResult {
|
|
369
382
|
snapshot: Snapshot;
|
|
370
383
|
report: CheckReport;
|
|
@@ -373,4 +386,4 @@ interface AuditResult {
|
|
|
373
386
|
/** Snapshot a URL, judge it against the rendered tells, and fingerprint it. */
|
|
374
387
|
declare function audit(url: string, options?: SnapshotOptions & CheckOptions): Promise<AuditResult>;
|
|
375
388
|
|
|
376
|
-
export { type AuditResult, type SnapshotOptions, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl };
|
|
389
|
+
export { type AuditResult, type SnapshotOptions, type SnapshotResult, audit, collectInPage, loadPlaywright, overlayInPage, snapshotPage, snapshotUrl, snapshotUrls };
|