@domandigital/craft 0.23.1 → 0.24.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +17 -5
  2. package/CHARACTER.md +98 -97
  3. package/COPY.md +3 -2
  4. package/PRINCIPLES.md +5 -5
  5. package/README.md +8 -8
  6. package/REGISTERS.md +3 -3
  7. package/design-review.schema.json +1 -1
  8. package/dist/{audit-DESBVHQJ.js → audit-Q73WGKMS.js} +3 -3
  9. package/dist/audit.cjs +88 -23
  10. package/dist/audit.js +88 -23
  11. package/dist/{chunk-GA3AJN67.js → chunk-2CCL5CO7.js} +17 -15
  12. package/dist/{chunk-M5QSMIT6.js → chunk-4HPAMB52.js} +4 -4
  13. package/dist/{chunk-LRWKKJIW.js → chunk-6TOY3FAT.js} +5 -5
  14. package/dist/{chunk-GDDBCHQR.js → chunk-GJZ4Y5PT.js} +5 -5
  15. package/dist/{chunk-ZV3FPQPM.js → chunk-OA2MBUJJ.js} +1 -1
  16. package/dist/{chunk-V3GJSISR.js → chunk-OOT4QLO3.js} +14 -14
  17. package/dist/{chunk-7DPB5G4G.js → chunk-PXJKYMXF.js} +2 -2
  18. package/dist/{chunk-EHMEMSM2.js → chunk-R4KPE4G5.js} +72 -9
  19. package/dist/{cli-FCFRPPHZ.js → cli-766R2BUR.js} +5 -5
  20. package/dist/{cli-3PFEOEQI.js → cli-CE7ENNNG.js} +6 -6
  21. package/dist/{cli-SOH4QMMW.js → cli-DTJOOTNM.js} +3 -3
  22. package/dist/{cli-36YQA6BP.js → cli-EQJITFVE.js} +4 -4
  23. package/dist/{cli-RXXGSKD6.js → cli-SD2BJSGP.js} +6 -6
  24. package/dist/{cli-TP5ZMJOL.js → cli-TBBXUECM.js} +5 -5
  25. package/dist/{cli-YOSHHLIV.js → cli-TV2IJ5TV.js} +2 -3
  26. package/dist/{cli-7IBKMUMM.js → cli-U3SPFILJ.js} +9 -9
  27. package/dist/cli.js +3 -3
  28. package/dist/index.cjs +89 -25
  29. package/dist/index.d.cts +10 -10
  30. package/dist/index.d.ts +10 -10
  31. package/dist/index.js +89 -25
  32. package/dist/{viewports-cli-ET664I2Y.js → viewports-cli-SOOHAD5D.js} +3 -3
  33. package/package.json +1 -1
  34. package/word-lists.json +1 -1
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-EHMEMSM2.js";
12
+ } from "./chunk-R4KPE4G5.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";
@@ -473,8 +473,7 @@ var REGISTERS = [
473
473
  note: "No public style authority exists for premium-brand prose. These directives are house judgement, to be revised as the estate grows."
474
474
  },
475
475
  {
476
- name: "Register separation, 2026-09-29",
477
- url: "calibration/copy-shape/report-registers.md",
476
+ name: "Register separation, 2026-09-29 (report in the private craft corpus)",
478
477
  note: "Literary prose (Gutenberg) carries about twice the comma density of the other human registers, the one feature that separated it from all four. Longer, jointed sentences are normal here."
479
478
  }
480
479
  ]
@@ -3,21 +3,21 @@ import {
3
3
  } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-M5QSMIT6.js";
7
- import "./chunk-ZV3FPQPM.js";
6
+ } from "./chunk-4HPAMB52.js";
7
+ import "./chunk-OA2MBUJJ.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-LRWKKJIW.js";
13
+ } from "./chunk-6TOY3FAT.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
- } from "./chunk-7DPB5G4G.js";
17
+ } from "./chunk-PXJKYMXF.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-GDDBCHQR.js";
20
+ } from "./chunk-GJZ4Y5PT.js";
21
21
  import "./chunk-OQXTQQAU.js";
22
22
  import {
23
23
  COPY_FILE,
@@ -28,7 +28,7 @@ import {
28
28
  loadExceptions,
29
29
  parseFlags,
30
30
  readPaths
31
- } from "./chunk-V3GJSISR.js";
31
+ } from "./chunk-OOT4QLO3.js";
32
32
  import {
33
33
  toJson
34
34
  } from "./chunk-QIEDBL4J.js";
@@ -42,8 +42,8 @@ import {
42
42
  fingerprint,
43
43
  scanSource,
44
44
  typicality
45
- } from "./chunk-GA3AJN67.js";
46
- import "./chunk-EHMEMSM2.js";
45
+ } from "./chunk-2CCL5CO7.js";
46
+ import "./chunk-R4KPE4G5.js";
47
47
  import "./chunk-QYBAVU3P.js";
48
48
 
49
49
  // src/report/cli.ts
@@ -126,7 +126,7 @@ async function runReport(command, args, io) {
126
126
  if (/\.json$/i.test(target2) && existsSync(local)) {
127
127
  snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
128
128
  } else {
129
- const { snapshotUrl } = await import("./audit-DESBVHQJ.js");
129
+ const { snapshotUrl } = await import("./audit-Q73WGKMS.js");
130
130
  snap = await snapshotUrl(target2);
131
131
  }
132
132
  const fp = fingerprint(snap);
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-V3GJSISR.js";
4
+ } from "./chunk-OOT4QLO3.js";
5
5
  import "./chunk-QIEDBL4J.js";
6
- import "./chunk-GA3AJN67.js";
7
- import "./chunk-EHMEMSM2.js";
6
+ import "./chunk-2CCL5CO7.js";
7
+ import "./chunk-R4KPE4G5.js";
8
8
  import "./chunk-QYBAVU3P.js";
9
9
 
10
10
  // src/cli.ts
package/dist/index.cjs CHANGED
@@ -2160,6 +2160,29 @@ var PARALLEL_PAIR = [
2160
2160
  // a verbless list as a tagline: "Service account, match-day reminders, analytics."
2161
2161
  /^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
2162
2162
  ];
2163
+ var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
2164
+ var ABSOLUTE_NEGATION = new RegExp(
2165
+ `${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
2166
+ "im"
2167
+ );
2168
+ var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
2169
+ var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
2170
+ var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
2171
+ function notAbsoluteFragment(block, match, index) {
2172
+ const [head, ...rest] = match.trim().split(/[ \t]+/);
2173
+ let words3 = rest;
2174
+ const lead = head.toLowerCase();
2175
+ if (lead === "no" && words3[0]?.toLowerCase() === "one") words3 = words3.slice(1);
2176
+ else if (lead === "no" && NO_IDIOM.test(words3[0] ?? "")) return true;
2177
+ if (words3.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
2178
+ if (lead === "never" && !PARTICIPLE.test(words3[0] ?? "")) return true;
2179
+ if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words3[0] ?? "")) return true;
2180
+ const sentence = lineAround(block.text, index);
2181
+ const line = sentence.trim().replace(/^[-*]\s+/, "");
2182
+ if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
2183
+ if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
2184
+ return emptyState(block, match, index);
2185
+ }
2163
2186
  var alreadyBlocked = (block, match, index) => {
2164
2187
  const before = block.text.slice(0, index);
2165
2188
  const start = Math.max(before.search(/[^.!?\n]*$/), 0);
@@ -2286,7 +2309,7 @@ var COPY_TELLS = [
2286
2309
  fixtures: {
2287
2310
  flag: [
2288
2311
  f("content/home.md", "Your options, in plain English."),
2289
- // Eunice Ordor's draft lines, Pastel 986559 #12 and #13 (her diagnosis taken, her wording not).
2312
+ // A reviewer's draft lines, Pastel 986559 #12 and #13 (the diagnosis taken, the wording not).
2290
2313
  f("content/clinics.md", "Tell us what's happening and we'll say plainly what we would take on."),
2291
2314
  f("content/pricing.md", "DD's payment terms, plainly stated."),
2292
2315
  f("content/clinics.md", "We explain our findings in plain terms."),
@@ -2412,7 +2435,7 @@ var COPY_TELLS = [
2412
2435
  fixtures: {
2413
2436
  flag: [
2414
2437
  // From the portal's seeded prospect follow-ups, 2026-10-19.
2415
- f("emails/followup.md", "Alright Dave, it's Dmitri. No pressure, have a look when you get a min."),
2438
+ f("emails/followup.md", "Alright Dave, it's Sam. No pressure, have a look when you get a min."),
2416
2439
  f("emails/followup.md", "All good if you're not fussed mate, I'll leave it there."),
2417
2440
  f("emails/followup.md", "Carlo, built the shop a proper site."),
2418
2441
  f("content/contact.md", "No worries, we'll get you sorted."),
@@ -2425,11 +2448,11 @@ var COPY_TELLS = [
2425
2448
  f("content/home.md", "Proper planning saves a second visit."),
2426
2449
  f("content/certificates.md", "Download a sorted list of certificates, sorted by date."),
2427
2450
  f("content/directions.md", "At the junction, take the right then left turn."),
2428
- // Chair and Blade's own approved line, and "mate" as a noun.
2451
+ // A barber client's own approved line, and "mate" as a noun.
2429
2452
  f("content/home.md", "Every cut starts with a proper chat about what you actually want."),
2430
2453
  f("content/home.md", "It feels like getting a cut from a mate who knows what he is doing."),
2431
2454
  f("emails/quote.md", "Have a look at the quote and ring me if Thursday does not work."),
2432
- f("content/contact.md", "Give us a ring on 01280 700123. We take a look at the fault before quoting."),
2455
+ f("content/contact.md", "Give us a ring on 01632 960123. We take a look at the fault before quoting."),
2433
2456
  f("content/home.md", "Built to last, and made for older houses.")
2434
2457
  ]
2435
2458
  }
@@ -2441,14 +2464,14 @@ var COPY_TELLS = [
2441
2464
  severity: "warn",
2442
2465
  surface: "copy",
2443
2466
  why: "'What this is and is not', 'we have not yet built', 'our experience so far is one practice', 'what we would and wouldn't take on': a model scopes a business by confessing its limits. It reads as small print, and it turns a buyer away before they make contact.",
2444
- fix: "Scope by being precise about what is true, and leave the door open: 'Built and run by us for Sensphere, a practice whose clinicians work across several sites.' Keep contractual facts (eligibility, who runs what) and say them as what the buyer gets.",
2467
+ fix: "Scope by being precise about what is true, and leave the door open: 'Built and run by us for a children's therapy practice whose clinicians work across several sites.' Keep contractual facts (eligibility, who runs what) and say them as what the buyer gets.",
2445
2468
  detect: phrases(CONFESSION, quoted),
2446
2469
  fixtures: {
2447
2470
  flag: [
2448
- // Each one from the DD site, as Eunice Ordor flagged it (Pastel 986559 #8, #12, #13).
2471
+ // Each one from the studio's own site, as a reviewer flagged it (Pastel 986559 #8, #12, #13).
2449
2472
  f("content/clinics.md", "What this is and is not: a practice portal we built and run for one practice."),
2450
2473
  f("content/clinics.md", "We have not yet built and run the digital systems of a practice with several clinicians."),
2451
- f("content/home.md", "Including the client portal we run for Sensphere. Our clinic experience so far is one practice."),
2474
+ f("content/home.md", "Including the client portal we run for a therapy practice. Our clinic experience so far is one practice."),
2452
2475
  f("content/clinics.md", "Tell us what's happening and we'll say what we would and wouldn't take on."),
2453
2476
  f("content/clinics.md", "We will say what carries over from this work and what we would need to learn first."),
2454
2477
  f("content/clinics.md", "We will tell you where our experience applies directly and where it is close."),
@@ -2457,8 +2480,8 @@ var COPY_TELLS = [
2457
2480
  f("content/mobile.md", "Mobile is our newest line, so we tell you what we have and haven't done.")
2458
2481
  ],
2459
2482
  pass: [
2460
- f("content/clinics.md", "Built and run by us for Sensphere, a growing practice whose clinicians work remotely."),
2461
- f("content/home.md", "The client portal we run for Sensphere."),
2483
+ f("content/clinics.md", "Built and run by us for a therapy practice, a growing one whose clinicians work remotely."),
2484
+ f("content/home.md", "The client portal we run for a children's therapy practice."),
2462
2485
  // Contractual facts, said as they are.
2463
2486
  f("content/pricing.md", "Pay monthly is open to UK residents aged 18 and over, subject to status."),
2464
2487
  f("content/pricing.md", "Your monthly payments do not buy the site; the buy-out price does."),
@@ -2698,6 +2721,46 @@ var COPY_TELLS = [
2698
2721
  ]
2699
2722
  }
2700
2723
  },
2724
+ {
2725
+ id: "absolute-negation",
2726
+ name: "Absolute negation fragment",
2727
+ generation: 2,
2728
+ severity: "warn",
2729
+ surface: "copy",
2730
+ why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
2731
+ fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
2732
+ detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
2733
+ fixtures: {
2734
+ flag: [
2735
+ // From a 92EL deck that passed the gate, October 2026.
2736
+ f("content/deck.md", "A guardian consents by email; never pre-ticked."),
2737
+ f("content/deck.md", "Pay per user per month; no servers."),
2738
+ f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
2739
+ f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
2740
+ f("content/deck.md", "Each player is checked against the list once, no duplicates."),
2741
+ f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
2742
+ ],
2743
+ pass: [
2744
+ f("content/report.md", "Players never open the lists."),
2745
+ f("content/report.md", "Nothing is copied by hand."),
2746
+ f("content/report.md", "It is not tested at national scale."),
2747
+ f("content/report.md", "Never share your password with anyone."),
2748
+ f("content/report.md", "No one knows the password but the club secretary."),
2749
+ // A Harvard citation with no date.
2750
+ f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
2751
+ f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
2752
+ // A form answer option.
2753
+ f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
2754
+ f("app/form.tsx", `export const options = ["Yes", "No"];`),
2755
+ f("content/form.md", "No, the player is 18 or over."),
2756
+ f("content/home.md", "We no longer post paper forms."),
2757
+ f("content/report.md", "If the check fails, nothing new appears in the list."),
2758
+ // The blocking tells report these.
2759
+ f("content/home.md", "We reply within a day. No obligation, no spam."),
2760
+ f("app/list.tsx", `export const empty = "No results found.";`)
2761
+ ]
2762
+ }
2763
+ },
2701
2764
  {
2702
2765
  id: "where-x-meets-y",
2703
2766
  name: "'Where X meets Y'",
@@ -4208,7 +4271,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4208
4271
  generation: 1,
4209
4272
  severity: "warn",
4210
4273
  surface: "source",
4211
- why: "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.",
4274
+ why: "When every block fades up on scroll, the motion stops meaning anything and the page feels slow. One salon client's site had 70, the studio's own 40, a therapy practice's 27.",
4212
4275
  fix: `Keep one gated signature moment per page (STANDARD section 7) and let the rest arrive still. The limit here is ${REVEAL_LIMIT} per scan.`,
4213
4276
  detect: revealHits,
4214
4277
  fixtures: {
@@ -4224,7 +4287,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4224
4287
  generation: 1,
4225
4288
  severity: "warn",
4226
4289
  surface: "source",
4227
- why: "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.",
4290
+ why: "rounded-full on every button, badge and tag is the framework's friendliest default. One salon client's site had 162, the studio's own 125. A shape used everywhere is not a shape language.",
4228
4291
  fix: `Choose the shape language on purpose (and record it), then use the pill only where it means something. The limit here is ${PILL_LIMIT} per scan.`,
4229
4292
  detect: pillHits,
4230
4293
  fixtures: {
@@ -4239,7 +4302,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4239
4302
  generation: 1,
4240
4303
  severity: "warn",
4241
4304
  surface: "source",
4242
- why: "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.",
4305
+ why: "A generator exports the whole shadcn/ui kit whether the site uses it or not. One client's journal site ships a full set under its own book, vine and petal assets.",
4243
4306
  fix: "Delete the primitives nothing imports. Restyle the ones that stay so they belong to this site.",
4244
4307
  detect: shadcnHits,
4245
4308
  fixtures: {
@@ -4297,7 +4360,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4297
4360
  f3("components/Hero.tsx", `<span className="font-serif italic">reimagined</span>`),
4298
4361
  f3("app/globals.css", `.hero h1 { font-style: italic; }`)
4299
4362
  ],
4300
- pass: [f3("components/Hero.tsx", `<h1 className="font-display text-6xl">Nails in Brackley</h1>
4363
+ pass: [f3("components/Hero.tsx", `<h1 className="font-display text-6xl">Nails in Kettering</h1>
4301
4364
  <p><em>Walk-ins</em> welcome.</p>`)]
4302
4365
  }
4303
4366
  },
@@ -4313,11 +4376,11 @@ const inter = Inter({ subsets: ["latin"] });`),
4313
4376
  fixtures: {
4314
4377
  flag: [
4315
4378
  f3("components/Hero.tsx", `<span className="inline-flex rounded-full border px-3 py-1 text-xs">Now booking</span>
4316
- <h1>Nails in Brackley</h1>`),
4379
+ <h1>Nails in Kettering</h1>`),
4317
4380
  f3("components/Hero.tsx", `<span className={cn("rounded-full px-3 text-xs", "bg-rose-500/10")}>New</span>
4318
4381
  <h1>Nails</h1>`)
4319
4382
  ],
4320
- pass: [f3("components/Hero.tsx", `<h1>Nails in Brackley</h1>
4383
+ pass: [f3("components/Hero.tsx", `<h1>Nails in Kettering</h1>
4321
4384
  <span className="inline-flex rounded-full border px-3 py-1 text-xs">Tag</span>`)]
4322
4385
  }
4323
4386
  },
@@ -4332,7 +4395,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4332
4395
  detect: heroCtaPairHits,
4333
4396
  fixtures: {
4334
4397
  flag: [
4335
- // Eunice Ordor's #5 and #36 (Pastel 986559): the DD site's own hero and process close.
4398
+ // A reviewer's #5 and #36 (Pastel 986559): the studio's own hero and process close.
4336
4399
  f3(
4337
4400
  "src/components/Hero.astro",
4338
4401
  `<section class="hero">
@@ -4341,20 +4404,20 @@ const inter = Inter({ subsets: ["latin"] });`),
4341
4404
  <a href="/work" class="btn btn-ghost">See our work</a>
4342
4405
  </section>`
4343
4406
  ),
4344
- // The null build's hero, verbatim in shape (calibration/null/rmp/pages/05.html).
4407
+ // The null build's hero, verbatim in shape (an electrician brief's null pages, in the private dd-craft-corpus).
4345
4408
  f3(
4346
4409
  "index.html",
4347
4410
  `<nav><a class="btn" href="#contact">Quote</a></nav>
4348
4411
  <section id="hero" class="hero">
4349
- <h1>Electricians in Uxbridge</h1>
4412
+ <h1>Electricians in Reading</h1>
4350
4413
  <a href="#contact" class="btn btn-primary">Book a Free Quote</a>
4351
- <a href="tel:+441895555210" class="btn btn-outline">Call 01895 555 210</a>
4414
+ <a href="tel:+441632960210" class="btn btn-outline">Call 01632 960 210</a>
4352
4415
  </section>`
4353
4416
  ),
4354
4417
  f3(
4355
4418
  "app/page.tsx",
4356
4419
  `<section className="relative py-24">
4357
- <h1 className="text-5xl">Nails in Brackley</h1>
4420
+ <h1 className="text-5xl">Nails in Kettering</h1>
4358
4421
  <div className="flex gap-4">
4359
4422
  <Button asChild><a href="/book">Book now</a></Button>
4360
4423
  <Button variant="outline">View treatments</Button>
@@ -4383,7 +4446,7 @@ const inter = Inter({ subsets: ["latin"] });`),
4383
4446
  </header>`
4384
4447
  ),
4385
4448
  f3("app/page.tsx", `<section className="py-24">
4386
- <h1 className="text-5xl">Nails in Brackley</h1>
4449
+ <h1 className="text-5xl">Nails in Kettering</h1>
4387
4450
  <a className="rounded bg-ink px-5 py-3" href="/book">Book now</a>
4388
4451
  </section>
4389
4452
  <section><a className="rounded bg-ink px-5 py-3" href="/prices">Prices</a></section>`)
@@ -5631,13 +5694,15 @@ var HOUSE = {
5631
5694
  "stock-email-line": { tier: "review", label: "stock email line, start with the reason for writing and end with the next step" },
5632
5695
  // New 2026-10-19 (founder): review first, like every new tell, until estate hits are read.
5633
5696
  "matey-register": { tier: "review", label: "matey register, write plain and complete British English" },
5634
- // New 2026-10-20 (founder, from Eunice Ordor's review): review first, like every new tell.
5697
+ // New 2026-10-20 (founder, from a reviewer's notes): review first, like every new tell.
5635
5698
  "confession-line": { tier: "review", label: "confession line, scope the work by saying precisely what is true" },
5636
5699
  // New 2026-10-23 (a 92EL report passed the gate with three): review, because a
5637
5700
  // genuine disclaimer has the same shape. The gerund pair joined not-just-but.
5638
5701
  // New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
5639
5702
  "parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
5640
5703
  "trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
5704
+ // New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
5705
+ "absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
5641
5706
  "ai-vocabulary": { tier: "review", label: "AI vocabulary" },
5642
5707
  "stock-phrase": { tier: "review", label: "stock phrase" },
5643
5708
  "hollow-imperative": { tier: "review", label: "hollow imperative" },
@@ -6870,7 +6935,7 @@ var IMAGERY_TELLS = [
6870
6935
  ];
6871
6936
 
6872
6937
  // src/character/check.ts
6873
- var CATALOGUE_VERSION = "2026.10.8";
6938
+ var CATALOGUE_VERSION = "2026.10.9";
6874
6939
  var CATALOGUE = [
6875
6940
  ...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
6876
6941
  ...SPECIFICITY_COPY_TELLS,
@@ -7664,8 +7729,7 @@ var REGISTERS = [
7664
7729
  note: "No public style authority exists for premium-brand prose. These directives are house judgement, to be revised as the estate grows."
7665
7730
  },
7666
7731
  {
7667
- name: "Register separation, 2026-09-29",
7668
- url: "calibration/copy-shape/report-registers.md",
7732
+ name: "Register separation, 2026-09-29 (report in the private craft corpus)",
7669
7733
  note: "Literary prose (Gutenberg) carries about twice the comma density of the other human registers, the one feature that separated it from all four. Longer, jointed sentences are normal here."
7670
7734
  }
7671
7735
  ]
package/dist/index.d.cts CHANGED
@@ -1252,7 +1252,7 @@ interface CheckReport {
1252
1252
  * Bumped whenever an entry is added, removed or its detection changes, so a
1253
1253
  * report can say which list it was judged against.
1254
1254
  */
1255
- declare const CATALOGUE_VERSION = "2026.10.8";
1255
+ declare const CATALOGUE_VERSION = "2026.10.9";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
@@ -1780,7 +1780,7 @@ declare function registersDoc(): string;
1780
1780
  * `craft register check`: where a draft sits against the human baselines.
1781
1781
  *
1782
1782
  * A single short passage cannot be placed: at blurb length the registers
1783
- * overlap too much (calibration/copy-shape/report-registers.md). Averaged over
1783
+ * overlap too much (the register separation report in the private craft corpus). Averaged over
1784
1784
  * five paragraphs, every register pair separates on several features. So the
1785
1785
  * check reads documents only, measures each blurb-sized paragraph with the
1786
1786
  * same functions the baselines used, averages them, and compares the average
@@ -2307,8 +2307,8 @@ interface Fingerprint {
2307
2307
  }
2308
2308
  declare function fingerprint(s: Snapshot): Fingerprint;
2309
2309
  /**
2310
- * Chroma at which a ground's tint is unmistakable. RMP's cream sits at 0.010
2311
- * and Bellerose's stone at 0.007, both plainly warm paper; Bellerose's
2310
+ * Chroma at which a ground's tint is unmistakable. An electrician's cream sits at 0.010
2311
+ * and a plumber's stone at 0.007, both warm paper; the plumber's
2312
2312
  * porcelain (#F1F3F4) at 0.003 is a cool hint. A page ground is the largest
2313
2313
  * area on the page and the eye adapts to it, so a tint far below what reads
2314
2314
  * as a difference between two buttons still reads as warm or cool paper.
@@ -2659,7 +2659,7 @@ type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
2659
2659
  declare const CHANGE_CLASSES: readonly ["confirmed-correction", "design-hypothesis", "optional-refinement", "no-change"];
2660
2660
  type ChangeClass = (typeof CHANGE_CLASSES)[number];
2661
2661
  /**
2662
- * What was done with a note in a reviewer's own record (`reviews/`).
2662
+ * What was done with a note in a reviewer's own record (kept in the private craft corpus).
2663
2663
  * take-reworded: the diagnosis is taken and the suggested wording rewritten,
2664
2664
  * because a reviewer's lines are copy-checked like anyone's. partly: the aim
2665
2665
  * is taken by a smaller or different change. known: already tracked.
@@ -2902,15 +2902,15 @@ declare function formatDirectionMatches(label: string, matches: DirectionMatch[]
2902
2902
  declare const ESTATE_VERSION = 1;
2903
2903
  /**
2904
2904
  * Median fingerprint distance between two null pages built from the same
2905
- * brief, across seven briefs and 1,330 pairs (calibration/null, 2026-09-23).
2905
+ * brief, across seven briefs and 1,330 pairs (the private corpus's calibration/null, 2026-09-23).
2906
2906
  */
2907
2907
  declare const SIBLING_AT = 0.31;
2908
2908
  interface EstateSite {
2909
- /** Short kebab-case id: "rmp", "chair-and-blade". */
2909
+ /** Short kebab-case id: "electrician", "barber-studio". */
2910
2910
  id: string;
2911
2911
  client: string;
2912
2912
  url: string;
2913
- /** Where the site's source lives, if known: "rmp474/RMP-Electrical". */
2913
+ /** Where the site's source lives, if known: "client-org/site-repo". */
2914
2914
  repo?: string;
2915
2915
  /** Absent for a direction registered before the site is built. */
2916
2916
  fingerprint?: Fingerprint;
@@ -3060,7 +3060,7 @@ declare function groundName(o: Oklch): string;
3060
3060
  /**
3061
3061
  * The ground as a person would name it, with its temperature: "cream",
3062
3062
  * "cool grey", "warm white", "cool dark". Every light neutral page used to be
3063
- * "grey", which put RMP's cream and Bellerose's porcelain under one word.
3063
+ * "grey", which put an electrician's cream and a plumber's porcelain under one word.
3064
3064
  */
3065
3065
  declare function groundLabel(o: Oklch): string;
3066
3066
  declare function shapeName(roundness: number | null): string;
@@ -3278,7 +3278,7 @@ declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolut
3278
3278
  *
3279
3279
  * A reason has to be specific enough that whoever reads the file next (a
3280
3280
  * person, Claude, a future tool) cannot drift back to the category default
3281
- * while following it. "Freight Display, because MMM's photography needs to be
3281
+ * while following it. "Freight Display, because the salon's photography needs to be
3282
3282
  * the hero and the type needs to recede" passes. "Freight Display" does not,
3283
3283
  * and neither does "a familiar structure so visitors feel comfortable".
3284
3284
  *
package/dist/index.d.ts CHANGED
@@ -1252,7 +1252,7 @@ interface CheckReport {
1252
1252
  * Bumped whenever an entry is added, removed or its detection changes, so a
1253
1253
  * report can say which list it was judged against.
1254
1254
  */
1255
- declare const CATALOGUE_VERSION = "2026.10.8";
1255
+ declare const CATALOGUE_VERSION = "2026.10.9";
1256
1256
  declare const CATALOGUE: readonly Tell[];
1257
1257
  declare function tellById(id: string): Tell | undefined;
1258
1258
  /**
@@ -1780,7 +1780,7 @@ declare function registersDoc(): string;
1780
1780
  * `craft register check`: where a draft sits against the human baselines.
1781
1781
  *
1782
1782
  * A single short passage cannot be placed: at blurb length the registers
1783
- * overlap too much (calibration/copy-shape/report-registers.md). Averaged over
1783
+ * overlap too much (the register separation report in the private craft corpus). Averaged over
1784
1784
  * five paragraphs, every register pair separates on several features. So the
1785
1785
  * check reads documents only, measures each blurb-sized paragraph with the
1786
1786
  * same functions the baselines used, averages them, and compares the average
@@ -2307,8 +2307,8 @@ interface Fingerprint {
2307
2307
  }
2308
2308
  declare function fingerprint(s: Snapshot): Fingerprint;
2309
2309
  /**
2310
- * Chroma at which a ground's tint is unmistakable. RMP's cream sits at 0.010
2311
- * and Bellerose's stone at 0.007, both plainly warm paper; Bellerose's
2310
+ * Chroma at which a ground's tint is unmistakable. An electrician's cream sits at 0.010
2311
+ * and a plumber's stone at 0.007, both warm paper; the plumber's
2312
2312
  * porcelain (#F1F3F4) at 0.003 is a cool hint. A page ground is the largest
2313
2313
  * area on the page and the eye adapts to it, so a tint far below what reads
2314
2314
  * as a difference between two buttons still reads as warm or cool paper.
@@ -2659,7 +2659,7 @@ type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
2659
2659
  declare const CHANGE_CLASSES: readonly ["confirmed-correction", "design-hypothesis", "optional-refinement", "no-change"];
2660
2660
  type ChangeClass = (typeof CHANGE_CLASSES)[number];
2661
2661
  /**
2662
- * What was done with a note in a reviewer's own record (`reviews/`).
2662
+ * What was done with a note in a reviewer's own record (kept in the private craft corpus).
2663
2663
  * take-reworded: the diagnosis is taken and the suggested wording rewritten,
2664
2664
  * because a reviewer's lines are copy-checked like anyone's. partly: the aim
2665
2665
  * is taken by a smaller or different change. known: already tracked.
@@ -2902,15 +2902,15 @@ declare function formatDirectionMatches(label: string, matches: DirectionMatch[]
2902
2902
  declare const ESTATE_VERSION = 1;
2903
2903
  /**
2904
2904
  * Median fingerprint distance between two null pages built from the same
2905
- * brief, across seven briefs and 1,330 pairs (calibration/null, 2026-09-23).
2905
+ * brief, across seven briefs and 1,330 pairs (the private corpus's calibration/null, 2026-09-23).
2906
2906
  */
2907
2907
  declare const SIBLING_AT = 0.31;
2908
2908
  interface EstateSite {
2909
- /** Short kebab-case id: "rmp", "chair-and-blade". */
2909
+ /** Short kebab-case id: "electrician", "barber-studio". */
2910
2910
  id: string;
2911
2911
  client: string;
2912
2912
  url: string;
2913
- /** Where the site's source lives, if known: "rmp474/RMP-Electrical". */
2913
+ /** Where the site's source lives, if known: "client-org/site-repo". */
2914
2914
  repo?: string;
2915
2915
  /** Absent for a direction registered before the site is built. */
2916
2916
  fingerprint?: Fingerprint;
@@ -3060,7 +3060,7 @@ declare function groundName(o: Oklch): string;
3060
3060
  /**
3061
3061
  * The ground as a person would name it, with its temperature: "cream",
3062
3062
  * "cool grey", "warm white", "cool dark". Every light neutral page used to be
3063
- * "grey", which put RMP's cream and Bellerose's porcelain under one word.
3063
+ * "grey", which put an electrician's cream and a plumber's porcelain under one word.
3064
3064
  */
3065
3065
  declare function groundLabel(o: Oklch): string;
3066
3066
  declare function shapeName(roundness: number | null): string;
@@ -3278,7 +3278,7 @@ declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolut
3278
3278
  *
3279
3279
  * A reason has to be specific enough that whoever reads the file next (a
3280
3280
  * person, Claude, a future tool) cannot drift back to the category default
3281
- * while following it. "Freight Display, because MMM's photography needs to be
3281
+ * while following it. "Freight Display, because the salon's photography needs to be
3282
3282
  * the hero and the type needs to recede" passes. "Freight Display" does not,
3283
3283
  * and neither does "a familiar structure so visitors feel comfortable".
3284
3284
  *