@domandigital/craft 0.9.0 → 0.11.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 +26 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +6 -0
- package/README.md +41 -0
- package/dist/{audit-5P65UMFL.js → audit-QIPXL7E4.js} +3 -3
- package/dist/audit.cjs +67 -11
- package/dist/audit.js +67 -11
- package/dist/chunk-2AJB27BJ.js +743 -0
- package/dist/{chunk-ZT3WLN7R.js → chunk-3NGWXZZE.js} +64 -10
- package/dist/{chunk-VKQDOUHO.js → chunk-3OZQ4C5R.js} +7 -6
- package/dist/{chunk-5LI2VFN5.js → chunk-HOHHQB4N.js} +9 -8
- package/dist/{chunk-BYY32NIN.js → chunk-JP75P2DU.js} +5 -3
- package/dist/{chunk-JRMU6GYX.js → chunk-JWHC67ZS.js} +1 -1
- package/dist/chunk-MSQ57PJU.js +206 -0
- package/dist/{chunk-EHNEGKFR.js → chunk-QMYERCBB.js} +1 -1
- package/dist/{cli-4VAOH7ST.js → cli-4ESXOLKV.js} +4 -4
- package/dist/{cli-V55MTIOP.js → cli-DBZMVYYV.js} +20 -14
- package/dist/cli-T5RD2BTK.js +17 -0
- package/dist/{cli-3XPEGBUR.js → cli-U3SZVHSK.js} +7 -6
- package/dist/cli-VREYWCT4.js +16 -0
- package/dist/cli.js +2 -2
- package/dist/index.cjs +400 -10
- package/dist/index.d.cts +227 -2
- package/dist/index.d.ts +227 -2
- package/dist/index.js +383 -10
- package/package.json +13 -6
- package/word-lists.json +1 -1
- package/dist/chunk-DAENYJUO.js +0 -112
- package/dist/chunk-YLQE35PN.js +0 -364
- package/dist/cli-J7C6TI2B.js +0 -16
- package/dist/cli-WDSVVHKG.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e2ea0e8: Phase I: craft in CI.
|
|
8
|
+
|
|
9
|
+
- `craft.config.json`: `ignore` globs, `copyPaths` (where `craft copy` looks when given no path), and `severity` changes per tell. Every severity change needs a `because` of at least a sentence; `off` is applied as an exception, so the report still lists what it silenced. `--config <file>` points elsewhere.
|
|
10
|
+
- `.claude`, `.agents` and `.cursor` are never walked (a path named on the command line is still read).
|
|
11
|
+
- `--baseline <file>` reports only findings the baseline does not hold; `--update-baseline` writes it. Findings are keyed on tell, path and excerpt, not line, and counted.
|
|
12
|
+
- `--sarif <file>` writes SARIF 2.1.0 for GitHub code scanning.
|
|
13
|
+
- Every `--json` output carries `schemaVersion: 1`. The audit JSON adds `pages` and `failed`.
|
|
14
|
+
- `craft audit --pages <sitemap.xml | urls.txt>` and `--viewport 390,768,1440`. A page that will not load is listed as not measured and fails `--strict`.
|
|
15
|
+
- **Behaviour change:** a page that answers with an HTTP error (404, 500) is no longer snapshotted as if it were the page. `snapshotUrls` reports it as an error and `snapshotUrl` throws `HTTP <status>`. Before, a missing page was measured, and reported clean.
|
|
16
|
+
- New exports: `createBaseline`, `applyBaseline`, `parseBaseline`, `findingKey`, `parseCraftConfig`, `applySeverity`, `severityExceptions`, `globToRegExp`, `ignoreMatcher`, `DEFAULT_IGNORE`, `toSarif`, `toJson`, `JSON_SCHEMA_VERSION`.
|
|
17
|
+
|
|
18
|
+
- 3eb1560: CommonJS consumers now get the CommonJS type declarations. The `exports` map put a top-level `types` (the ESM `.d.ts`) ahead of `require`, so TypeScript resolving a `require` under `node16` stopped at the ESM file ("Masquerading as ESM" in @arethetypeswrong/cli). `types` now sits inside `import` and `require` separately. Runtime behaviour is unchanged.
|
|
19
|
+
|
|
20
|
+
Requires Node 22.12 or later (`engines.node`). Node 20 reached end of life on 2026-04-30. Also declares `sideEffects` so bundlers can tree-shake (craft keeps its CSS).
|
|
21
|
+
|
|
22
|
+
## 0.10.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- cd13c94: `craft copy claims <paths>` lists every sentence holding a price, figure, date or named source, marked sourced or UNSOURCED, for a person to check against the primary source. `findClaims` and `formatClaims` do the same in code. No tell can tell a true figure from a false one; this makes the checking a list instead of a hunt.
|
|
27
|
+
- cd13c94: Catalogue 2026.09.8, from reading all 144 review findings on the live DD site. New tell `prompt-context` (review): the model naming its own inputs, "the attached Perplexity research file", 24 real hits in five live articles. Tuned: `inline-label-list` leaves reference and data lists alone (0 of 11 hits were real), `staccato-triplet` no longer crosses a paragraph break, `ing-tail` passes a list of tasks, `repeated-sentence` ignores footnotes repeating a source title.
|
|
28
|
+
|
|
3
29
|
## 0.9.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/CHARACTER.md
CHANGED
|
@@ -534,7 +534,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
|
|
534
534
|
after changing an entry; a test fails until you do.
|
|
535
535
|
|
|
536
536
|
<!-- craft:catalogue:start -->
|
|
537
|
-
Catalogue version `2026.09.
|
|
537
|
+
Catalogue version `2026.09.8`, 55 tells.
|
|
538
538
|
|
|
539
539
|
| Id | Gen | Surface | Severity | Tell | Why it is a default |
|
|
540
540
|
| --- | --- | --- | --- | --- | --- |
|
|
@@ -578,6 +578,7 @@ Catalogue version `2026.09.7`, 54 tells.
|
|
|
578
578
|
| `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. |
|
|
579
579
|
| `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. |
|
|
580
580
|
| `chatbot-residue` | 1 | copy | warn | Chatbot residue | 'Certainly! Here's a revised version', 'as an AI language model', a citeturn0search0 token, a link tracked utm_source=chatgpt.com: text that exists only because a chat reply was pasted whole. This is evidence, not style. |
|
|
581
|
+
| `prompt-context` | 1 | copy | warn | Prompt context | 'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell. |
|
|
581
582
|
| `placeholder` | 1 | copy | warn | Unfilled placeholder | '[Insert testimonial]', '[Your Name]', lorem ipsum, TODO_PLACEHOLDER: scaffolding a reader must never see, and the clearest sign a page shipped before anyone read it. |
|
|
582
583
|
| `question-reveal` | 2 | copy | warn | Staged reveal | 'The result? Faster growth.', 'What does this mean for you? It means...': the writer asks the reader's question for them and answers it. An infomercial hook, and a favourite of generated copy. |
|
|
583
584
|
| `inline-label-list` | 2 | copy | warn | Bold-label bullets | '**Speed:** Faster pages' three times in a row: a short label restated in a few words, the list shape generated copy defaults to. A definition list, with a real explanation after each label, is not this. |
|
package/COPY.md
CHANGED
|
@@ -100,6 +100,7 @@ person reading decides which instance earns its place.
|
|
|
100
100
|
| `vague-word` | innovative, scalable, end-to-end, streamline, solutions. Reported only when a file is named outright |
|
|
101
101
|
| `question-reveal` | "The result? Twice the bookings.", "What does this mean for you? It means...": a staged reveal |
|
|
102
102
|
| `inline-label-list` | three or more bullets in a row opening "**Label:**", where the label repeats the line |
|
|
103
|
+
| `prompt-context` | "This article uses the attached Perplexity research file as the primary source", "the attached research is right to say", "the attached brief asks": the model naming what it was given. A web page has no attachment. **Zero in anything published** |
|
|
103
104
|
| `placeholder` | "[Insert client name]", lorem ipsum, `TODO_PLACEHOLDER`. Fine in a draft commit; **zero in anything delivered** |
|
|
104
105
|
|
|
105
106
|
### Style is not evidence
|
|
@@ -212,6 +213,11 @@ Hyphenated compounds count as one word. Not automated.
|
|
|
212
213
|
`TODO_PLACEHOLDER: <the exact field needed>` and say so. Never fill it.
|
|
213
214
|
- A quotation under a named person must be theirs. Copy drafted for someone,
|
|
214
215
|
in quote marks with their job title beneath it, is a fabricated attribution.
|
|
216
|
+
- **Every price, figure, date and named source is checked against its primary
|
|
217
|
+
source before it ships.** `craft copy claims <paths>` lists them, each marked
|
|
218
|
+
sourced or UNSOURCED. A claim is sourced, corrected or cut, never re-cited
|
|
219
|
+
from memory. The copy check cannot do this part: a false figure in clean
|
|
220
|
+
prose passes every tell.
|
|
215
221
|
|
|
216
222
|
## CTA rule
|
|
217
223
|
|
package/README.md
CHANGED
|
@@ -94,6 +94,41 @@ reason, in `art-direction.json`:
|
|
|
94
94
|
{ "exceptions": [{ "tell": "ai-violet", "because": "Violet is on the van, the cards and the fascia." }] }
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
### In CI
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx craft scan --baseline craft-baseline.json --update-baseline # once: adopt today's findings
|
|
101
|
+
npx craft scan --baseline craft-baseline.json --strict --sarif craft.sarif
|
|
102
|
+
npx craft audit --pages https://example.com/sitemap.xml --viewport 390,1440
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `--baseline` reports only findings the baseline does not already hold. A
|
|
106
|
+
finding is keyed on its tell, path and excerpt, never its line, so an edit
|
|
107
|
+
above a known finding does not make it new. The output says how many known
|
|
108
|
+
findings it left out.
|
|
109
|
+
- `--sarif <file>` writes SARIF 2.1.0 for GitHub code scanning. Findings on a
|
|
110
|
+
rendered page carry the URL, not a file, so code scanning does not show them.
|
|
111
|
+
- `--json` output always carries `schemaVersion` (currently 1).
|
|
112
|
+
- `--pages` audits every URL in a sitemap (or a sitemap index, one level deep)
|
|
113
|
+
or a file of URLs; `--viewport` audits each at those widths. A page that
|
|
114
|
+
will not load is listed as not measured, and fails `--strict`.
|
|
115
|
+
|
|
116
|
+
`craft.config.json` in the repo root holds what craft should know about the
|
|
117
|
+
repo. `.claude`, `.agents` and `.cursor` are never walked, config or not.
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"ignore": ["docs/archive/**", "*.stories.tsx"],
|
|
122
|
+
"copyPaths": ["content", "src/app"],
|
|
123
|
+
"severity": {
|
|
124
|
+
"em-dash": { "level": "block", "because": "This client's style guide bans them outright." }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A severity change needs a `because`, like an exception. `off` is applied as an
|
|
130
|
+
exception, so the report lists what it silenced.
|
|
131
|
+
|
|
97
132
|
### Copy
|
|
98
133
|
|
|
99
134
|
[COPY.md](./COPY.md) is the house copy standard: the blocking tier, the review
|
|
@@ -104,8 +139,14 @@ run. A test fails if a phrase the standard blocks does not block.
|
|
|
104
139
|
```bash
|
|
105
140
|
npx craft copy --gate content # the house gate: exit 1 on the blocking tier
|
|
106
141
|
npx craft copy compare draft.md rewrite.md # facts a rewrite lost or added
|
|
142
|
+
npx craft copy claims content # every price, figure, date and named source, to check
|
|
107
143
|
```
|
|
108
144
|
|
|
145
|
+
No tell can say whether a figure is true. `craft copy claims` lists each
|
|
146
|
+
sentence a reader would take as a checkable fact, marked `sourced` (a source is
|
|
147
|
+
named) or `UNSOURCED`, for a person to check against the primary source. It
|
|
148
|
+
always exits 0: a checklist, not a verdict.
|
|
149
|
+
|
|
109
150
|
The same checks run in code:
|
|
110
151
|
|
|
111
152
|
```ts
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
snapshotPage,
|
|
7
7
|
snapshotUrl,
|
|
8
8
|
snapshotUrls
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-JP75P2DU.js";
|
|
10
|
+
import "./chunk-QMYERCBB.js";
|
|
11
|
+
import "./chunk-3NGWXZZE.js";
|
|
12
12
|
export {
|
|
13
13
|
audit,
|
|
14
14
|
collectInPage,
|
package/dist/audit.cjs
CHANGED
|
@@ -564,10 +564,14 @@ var COPY_TELLS = [
|
|
|
564
564
|
surface: "copy",
|
|
565
565
|
why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
|
|
566
566
|
fix: "Write one sentence that says which of the three is true, and how you know.",
|
|
567
|
-
|
|
567
|
+
// The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
|
|
568
|
+
detect: phrases([/(?:\b[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?:[ \t]+|[ \t]*\n(?![ \t]*\n)[ \t]*)){2}[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?=\s|$)/], quoted),
|
|
568
569
|
fixtures: {
|
|
569
|
-
flag: [f("content/home.md", "No fuss. No jargon. Just results.")],
|
|
570
|
-
pass: [
|
|
570
|
+
flag: [f("content/home.md", "No fuss. No jargon. Just results."), f("content/home.md", "No fuss.\nNo jargon.\nJust results.")],
|
|
571
|
+
pass: [
|
|
572
|
+
f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
|
|
573
|
+
f("content/guide.md", "Your competitor appears in the top three. You don't. Again.\n\nIt's frustrating.")
|
|
574
|
+
]
|
|
571
575
|
}
|
|
572
576
|
},
|
|
573
577
|
{
|
|
@@ -586,6 +590,14 @@ var COPY_TELLS = [
|
|
|
586
590
|
}
|
|
587
591
|
];
|
|
588
592
|
var ING_TAIL = /,\s+(?:ensuring|highlighting|underscoring|showcasing|reflecting|demonstrating|emphasi[sz]ing|fostering|cementing|solidifying|reinforcing|symboli[sz]ing|signalling|signaling)\b[^.!?\n]{0,80}/i;
|
|
593
|
+
var PROMPT_CONTEXT = [
|
|
594
|
+
/\b(?:the|this|that)\s+(?:attached|uploaded|provided)\s+(?:(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\s+)?(?:research|research file|research pack|source file|sources)\b/i,
|
|
595
|
+
/\b(?:attached|uploaded)\s+(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\b/i,
|
|
596
|
+
/\bthe\s+(?:attached|uploaded|provided)\s+brief\s+(?:asks|says|wants|requests|suggests|mentions)\b/i
|
|
597
|
+
];
|
|
598
|
+
function gerundList(block, _match, index) {
|
|
599
|
+
return /(?:^|[(,;:]\s*)\w+ing\b[^,.;()!?]{0,40}$/.test(block.text.slice(Math.max(0, index - 60), index));
|
|
600
|
+
}
|
|
589
601
|
var VAGUE_ATTRIBUTION = [
|
|
590
602
|
/\b(?:studies|research|experts|scientists|surveys|statistics|the data)\s+(?:show|shows|suggest|suggests|agree|say|says|confirm|confirms|prove|proves|indicate|indicates)\b/i,
|
|
591
603
|
/\b(?:many|most)\s+(?:experts|people|customers|professionals|homeowners)\s+(?:agree|say|believe)\b/i,
|
|
@@ -671,8 +683,10 @@ function inlineLabelHits(ctx) {
|
|
|
671
683
|
run = [];
|
|
672
684
|
};
|
|
673
685
|
for (const line of lines) {
|
|
674
|
-
const m = /^\s*
|
|
675
|
-
|
|
686
|
+
const m = /^\s*[-*+]\s+\*\*([^*]{1,40}?):?\*\*:?\s+(\S.*)$/.exec(line);
|
|
687
|
+
const data = m && /[\d£$€%`]|https?:|^\*\*/.test(m[2]);
|
|
688
|
+
const named = m && /\S[A-Z]|[./]/.test(m[1]);
|
|
689
|
+
if (m && !data && !named && m[2].trim().split(/\s+/).length <= 6) run.push(offset);
|
|
676
690
|
else flush();
|
|
677
691
|
offset += line.length + 1;
|
|
678
692
|
}
|
|
@@ -710,6 +724,29 @@ var RESEARCH_TELLS = [
|
|
|
710
724
|
]
|
|
711
725
|
}
|
|
712
726
|
},
|
|
727
|
+
{
|
|
728
|
+
id: "prompt-context",
|
|
729
|
+
name: "Prompt context",
|
|
730
|
+
generation: 1,
|
|
731
|
+
severity: "warn",
|
|
732
|
+
surface: "copy",
|
|
733
|
+
why: "'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell.",
|
|
734
|
+
fix: "Delete the reference to the research or brief. Where it carried a claim, name the real source or cut the claim.",
|
|
735
|
+
detect: phrases(PROMPT_CONTEXT, quoted),
|
|
736
|
+
fixtures: {
|
|
737
|
+
flag: [
|
|
738
|
+
f("content/guide.md", "This article uses the attached Perplexity research file as the primary source."),
|
|
739
|
+
f("content/guide.md", "The attached research is right to say these platforms suit early-stage businesses."),
|
|
740
|
+
f("content/guide.md", "That is exactly how the uploaded research frames the category."),
|
|
741
|
+
f("content/guide.md", "The attached brief asks for the point that most tradespeople miss.")
|
|
742
|
+
],
|
|
743
|
+
pass: [
|
|
744
|
+
f("content/guide.md", "We research every quote before we send it."),
|
|
745
|
+
f("app/email.tsx", `export const Invoice = () => <p>Your invoice is attached.</p>;`),
|
|
746
|
+
f("content/guide.md", "BrightLocal's research shows that reviews drive 20% of local ranking.")
|
|
747
|
+
]
|
|
748
|
+
}
|
|
749
|
+
},
|
|
713
750
|
{
|
|
714
751
|
id: "placeholder",
|
|
715
752
|
name: "Unfilled placeholder",
|
|
@@ -762,7 +799,11 @@ var RESEARCH_TELLS = [
|
|
|
762
799
|
pass: [
|
|
763
800
|
f("content/home.md", "- Pages load in under a second\n- Card details never touch our server\n- Support by phone until 8pm\n"),
|
|
764
801
|
f("content/home.md", "- **Speed:** Faster pages\n- **Security:** Safer data\n"),
|
|
765
|
-
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n")
|
|
802
|
+
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n"),
|
|
803
|
+
f("content/guide.md", "- **Per page**: \xA3100-\xA3200 per page\n- **Hourly**: \xA350-\xA375/hour\n- **Day rate**: \xA3350-\xA3500\n"),
|
|
804
|
+
f("content/guide.md", "- **Law Society Find a Solicitor:** For solicitors\n- **AccountingWEB / ICAEW directories:** For accountants\n- **NHS.uk:** For NHS services\n"),
|
|
805
|
+
f("content/guide.md", "1. **Visit PageSpeed Insights** and paste the address\n2. **Enter their URL** and run it\n3. **Note their scores** for later\n"),
|
|
806
|
+
f("content/guide.md", "- **Structural problems:** **REBUILD.**\n- **Messaging issues:** **REFRESH.**\n- **Neither:** **LEAVE IT.**\n")
|
|
766
807
|
]
|
|
767
808
|
}
|
|
768
809
|
},
|
|
@@ -774,7 +815,7 @@ var RESEARCH_TELLS = [
|
|
|
774
815
|
surface: "copy",
|
|
775
816
|
why: "'..., ensuring peace of mind', '..., highlighting our commitment': a participle tacked on the end that claims significance and names no mechanism. One of the most common shapes in the Wikipedia guide to AI writing.",
|
|
776
817
|
fix: "Cut the tail, or replace it with the mechanism: 'so you can drop the car off before work'.",
|
|
777
|
-
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, ""))),
|
|
818
|
+
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, "")), void 0, gerundList),
|
|
778
819
|
fixtures: {
|
|
779
820
|
flag: [
|
|
780
821
|
f("content/home.md", "Every engineer is Gas Safe registered, ensuring complete peace of mind."),
|
|
@@ -782,7 +823,8 @@ var RESEARCH_TELLS = [
|
|
|
782
823
|
],
|
|
783
824
|
pass: [
|
|
784
825
|
f("content/home.md", "We open at 7am, so you can drop the car off before work."),
|
|
785
|
-
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers.")
|
|
826
|
+
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers."),
|
|
827
|
+
f("content/guide.md", "Many items (claiming your GBP, ensuring NAP consistency, writing title tags) are straightforward.")
|
|
786
828
|
]
|
|
787
829
|
}
|
|
788
830
|
},
|
|
@@ -1032,6 +1074,7 @@ function repeatHits(ctx) {
|
|
|
1032
1074
|
for (const s of sentences(doc2)) {
|
|
1033
1075
|
const words = normal(s.text);
|
|
1034
1076
|
if (words.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
1077
|
+
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
1035
1078
|
const key = words.join(" ");
|
|
1036
1079
|
const earlier = whole.get(key);
|
|
1037
1080
|
if (earlier) {
|
|
@@ -1229,7 +1272,15 @@ var DENSITY_TELLS = [
|
|
|
1229
1272
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nMore here.\n\nWe check every valve before the engineer leaves the house.")),
|
|
1230
1273
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nBy then we check every valve before the engineer leaves the site."))
|
|
1231
1274
|
],
|
|
1232
|
-
pass: [
|
|
1275
|
+
pass: [
|
|
1276
|
+
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nThe engineer leaves once the customer has signed the sheet.")),
|
|
1277
|
+
f2(
|
|
1278
|
+
"content/guide.md",
|
|
1279
|
+
doc(
|
|
1280
|
+
"The Independent Healthcare Providers Network Private Patient Insight Report 2024 found that private demand rose.[^1]\n\n[^1]: Independent Healthcare Providers Network, Private Patient Insight Report 2024, published online."
|
|
1281
|
+
)
|
|
1282
|
+
)
|
|
1283
|
+
]
|
|
1233
1284
|
}
|
|
1234
1285
|
},
|
|
1235
1286
|
{
|
|
@@ -2309,6 +2360,9 @@ var HOUSE = {
|
|
|
2309
2360
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2310
2361
|
"false-range": { tier: "review", label: "false range" },
|
|
2311
2362
|
"placeholder": { tier: "review", label: "unfilled placeholder" },
|
|
2363
|
+
// Every estate hit so far is real (38 in five DD articles, 2026-09-24), but
|
|
2364
|
+
// a proposal can honestly say "the attached research". Review until measured there.
|
|
2365
|
+
"prompt-context": { tier: "review", label: "prompt context: the model naming its own inputs" },
|
|
2312
2366
|
"question-reveal": { tier: "review", label: "staged reveal" },
|
|
2313
2367
|
"inline-label-list": { tier: "review", label: "bold-label bullets" },
|
|
2314
2368
|
"phrase-density": { tier: "review", label: "density" },
|
|
@@ -2675,7 +2729,7 @@ var RENDERED_PATHS = {
|
|
|
2675
2729
|
};
|
|
2676
2730
|
|
|
2677
2731
|
// src/character/check.ts
|
|
2678
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
2732
|
+
var CATALOGUE_VERSION = "2026.09.8";
|
|
2679
2733
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2680
2734
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2681
2735
|
);
|
|
@@ -3216,7 +3270,9 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
3216
3270
|
for (const url of urls) {
|
|
3217
3271
|
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
3218
3272
|
try {
|
|
3219
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3273
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3274
|
+
const status = typeof response?.status === "function" ? response.status() : 200;
|
|
3275
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
3220
3276
|
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
3221
3277
|
} catch (error) {
|
|
3222
3278
|
out.push({ url, error: error.message });
|
package/dist/audit.js
CHANGED
|
@@ -532,10 +532,14 @@ var COPY_TELLS = [
|
|
|
532
532
|
surface: "copy",
|
|
533
533
|
why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
|
|
534
534
|
fix: "Write one sentence that says which of the three is true, and how you know.",
|
|
535
|
-
|
|
535
|
+
// The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
|
|
536
|
+
detect: phrases([/(?:\b[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?:[ \t]+|[ \t]*\n(?![ \t]*\n)[ \t]*)){2}[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?=\s|$)/], quoted),
|
|
536
537
|
fixtures: {
|
|
537
|
-
flag: [f("content/home.md", "No fuss. No jargon. Just results.")],
|
|
538
|
-
pass: [
|
|
538
|
+
flag: [f("content/home.md", "No fuss. No jargon. Just results."), f("content/home.md", "No fuss.\nNo jargon.\nJust results.")],
|
|
539
|
+
pass: [
|
|
540
|
+
f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
|
|
541
|
+
f("content/guide.md", "Your competitor appears in the top three. You don't. Again.\n\nIt's frustrating.")
|
|
542
|
+
]
|
|
539
543
|
}
|
|
540
544
|
},
|
|
541
545
|
{
|
|
@@ -554,6 +558,14 @@ var COPY_TELLS = [
|
|
|
554
558
|
}
|
|
555
559
|
];
|
|
556
560
|
var ING_TAIL = /,\s+(?:ensuring|highlighting|underscoring|showcasing|reflecting|demonstrating|emphasi[sz]ing|fostering|cementing|solidifying|reinforcing|symboli[sz]ing|signalling|signaling)\b[^.!?\n]{0,80}/i;
|
|
561
|
+
var PROMPT_CONTEXT = [
|
|
562
|
+
/\b(?:the|this|that)\s+(?:attached|uploaded|provided)\s+(?:(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\s+)?(?:research|research file|research pack|source file|sources)\b/i,
|
|
563
|
+
/\b(?:attached|uploaded)\s+(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\b/i,
|
|
564
|
+
/\bthe\s+(?:attached|uploaded|provided)\s+brief\s+(?:asks|says|wants|requests|suggests|mentions)\b/i
|
|
565
|
+
];
|
|
566
|
+
function gerundList(block, _match, index) {
|
|
567
|
+
return /(?:^|[(,;:]\s*)\w+ing\b[^,.;()!?]{0,40}$/.test(block.text.slice(Math.max(0, index - 60), index));
|
|
568
|
+
}
|
|
557
569
|
var VAGUE_ATTRIBUTION = [
|
|
558
570
|
/\b(?:studies|research|experts|scientists|surveys|statistics|the data)\s+(?:show|shows|suggest|suggests|agree|say|says|confirm|confirms|prove|proves|indicate|indicates)\b/i,
|
|
559
571
|
/\b(?:many|most)\s+(?:experts|people|customers|professionals|homeowners)\s+(?:agree|say|believe)\b/i,
|
|
@@ -639,8 +651,10 @@ function inlineLabelHits(ctx) {
|
|
|
639
651
|
run = [];
|
|
640
652
|
};
|
|
641
653
|
for (const line of lines) {
|
|
642
|
-
const m = /^\s*
|
|
643
|
-
|
|
654
|
+
const m = /^\s*[-*+]\s+\*\*([^*]{1,40}?):?\*\*:?\s+(\S.*)$/.exec(line);
|
|
655
|
+
const data = m && /[\d£$€%`]|https?:|^\*\*/.test(m[2]);
|
|
656
|
+
const named = m && /\S[A-Z]|[./]/.test(m[1]);
|
|
657
|
+
if (m && !data && !named && m[2].trim().split(/\s+/).length <= 6) run.push(offset);
|
|
644
658
|
else flush();
|
|
645
659
|
offset += line.length + 1;
|
|
646
660
|
}
|
|
@@ -678,6 +692,29 @@ var RESEARCH_TELLS = [
|
|
|
678
692
|
]
|
|
679
693
|
}
|
|
680
694
|
},
|
|
695
|
+
{
|
|
696
|
+
id: "prompt-context",
|
|
697
|
+
name: "Prompt context",
|
|
698
|
+
generation: 1,
|
|
699
|
+
severity: "warn",
|
|
700
|
+
surface: "copy",
|
|
701
|
+
why: "'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell.",
|
|
702
|
+
fix: "Delete the reference to the research or brief. Where it carried a claim, name the real source or cut the claim.",
|
|
703
|
+
detect: phrases(PROMPT_CONTEXT, quoted),
|
|
704
|
+
fixtures: {
|
|
705
|
+
flag: [
|
|
706
|
+
f("content/guide.md", "This article uses the attached Perplexity research file as the primary source."),
|
|
707
|
+
f("content/guide.md", "The attached research is right to say these platforms suit early-stage businesses."),
|
|
708
|
+
f("content/guide.md", "That is exactly how the uploaded research frames the category."),
|
|
709
|
+
f("content/guide.md", "The attached brief asks for the point that most tradespeople miss.")
|
|
710
|
+
],
|
|
711
|
+
pass: [
|
|
712
|
+
f("content/guide.md", "We research every quote before we send it."),
|
|
713
|
+
f("app/email.tsx", `export const Invoice = () => <p>Your invoice is attached.</p>;`),
|
|
714
|
+
f("content/guide.md", "BrightLocal's research shows that reviews drive 20% of local ranking.")
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
},
|
|
681
718
|
{
|
|
682
719
|
id: "placeholder",
|
|
683
720
|
name: "Unfilled placeholder",
|
|
@@ -730,7 +767,11 @@ var RESEARCH_TELLS = [
|
|
|
730
767
|
pass: [
|
|
731
768
|
f("content/home.md", "- Pages load in under a second\n- Card details never touch our server\n- Support by phone until 8pm\n"),
|
|
732
769
|
f("content/home.md", "- **Speed:** Faster pages\n- **Security:** Safer data\n"),
|
|
733
|
-
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n")
|
|
770
|
+
f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n"),
|
|
771
|
+
f("content/guide.md", "- **Per page**: \xA3100-\xA3200 per page\n- **Hourly**: \xA350-\xA375/hour\n- **Day rate**: \xA3350-\xA3500\n"),
|
|
772
|
+
f("content/guide.md", "- **Law Society Find a Solicitor:** For solicitors\n- **AccountingWEB / ICAEW directories:** For accountants\n- **NHS.uk:** For NHS services\n"),
|
|
773
|
+
f("content/guide.md", "1. **Visit PageSpeed Insights** and paste the address\n2. **Enter their URL** and run it\n3. **Note their scores** for later\n"),
|
|
774
|
+
f("content/guide.md", "- **Structural problems:** **REBUILD.**\n- **Messaging issues:** **REFRESH.**\n- **Neither:** **LEAVE IT.**\n")
|
|
734
775
|
]
|
|
735
776
|
}
|
|
736
777
|
},
|
|
@@ -742,7 +783,7 @@ var RESEARCH_TELLS = [
|
|
|
742
783
|
surface: "copy",
|
|
743
784
|
why: "'..., ensuring peace of mind', '..., highlighting our commitment': a participle tacked on the end that claims significance and names no mechanism. One of the most common shapes in the Wikipedia guide to AI writing.",
|
|
744
785
|
fix: "Cut the tail, or replace it with the mechanism: 'so you can drop the car off before work'.",
|
|
745
|
-
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, ""))),
|
|
786
|
+
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, "")), void 0, gerundList),
|
|
746
787
|
fixtures: {
|
|
747
788
|
flag: [
|
|
748
789
|
f("content/home.md", "Every engineer is Gas Safe registered, ensuring complete peace of mind."),
|
|
@@ -750,7 +791,8 @@ var RESEARCH_TELLS = [
|
|
|
750
791
|
],
|
|
751
792
|
pass: [
|
|
752
793
|
f("content/home.md", "We open at 7am, so you can drop the car off before work."),
|
|
753
|
-
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers.")
|
|
794
|
+
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers."),
|
|
795
|
+
f("content/guide.md", "Many items (claiming your GBP, ensuring NAP consistency, writing title tags) are straightforward.")
|
|
754
796
|
]
|
|
755
797
|
}
|
|
756
798
|
},
|
|
@@ -1000,6 +1042,7 @@ function repeatHits(ctx) {
|
|
|
1000
1042
|
for (const s of sentences(doc2)) {
|
|
1001
1043
|
const words = normal(s.text);
|
|
1002
1044
|
if (words.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
1045
|
+
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
1003
1046
|
const key = words.join(" ");
|
|
1004
1047
|
const earlier = whole.get(key);
|
|
1005
1048
|
if (earlier) {
|
|
@@ -1197,7 +1240,15 @@ var DENSITY_TELLS = [
|
|
|
1197
1240
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nMore here.\n\nWe check every valve before the engineer leaves the house.")),
|
|
1198
1241
|
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nBy then we check every valve before the engineer leaves the site."))
|
|
1199
1242
|
],
|
|
1200
|
-
pass: [
|
|
1243
|
+
pass: [
|
|
1244
|
+
f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nThe engineer leaves once the customer has signed the sheet.")),
|
|
1245
|
+
f2(
|
|
1246
|
+
"content/guide.md",
|
|
1247
|
+
doc(
|
|
1248
|
+
"The Independent Healthcare Providers Network Private Patient Insight Report 2024 found that private demand rose.[^1]\n\n[^1]: Independent Healthcare Providers Network, Private Patient Insight Report 2024, published online."
|
|
1249
|
+
)
|
|
1250
|
+
)
|
|
1251
|
+
]
|
|
1201
1252
|
}
|
|
1202
1253
|
},
|
|
1203
1254
|
{
|
|
@@ -2277,6 +2328,9 @@ var HOUSE = {
|
|
|
2277
2328
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2278
2329
|
"false-range": { tier: "review", label: "false range" },
|
|
2279
2330
|
"placeholder": { tier: "review", label: "unfilled placeholder" },
|
|
2331
|
+
// Every estate hit so far is real (38 in five DD articles, 2026-09-24), but
|
|
2332
|
+
// a proposal can honestly say "the attached research". Review until measured there.
|
|
2333
|
+
"prompt-context": { tier: "review", label: "prompt context: the model naming its own inputs" },
|
|
2280
2334
|
"question-reveal": { tier: "review", label: "staged reveal" },
|
|
2281
2335
|
"inline-label-list": { tier: "review", label: "bold-label bullets" },
|
|
2282
2336
|
"phrase-density": { tier: "review", label: "density" },
|
|
@@ -2643,7 +2697,7 @@ var RENDERED_PATHS = {
|
|
|
2643
2697
|
};
|
|
2644
2698
|
|
|
2645
2699
|
// src/character/check.ts
|
|
2646
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
2700
|
+
var CATALOGUE_VERSION = "2026.09.8";
|
|
2647
2701
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2648
2702
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2649
2703
|
);
|
|
@@ -3184,7 +3238,9 @@ async function snapshotUrls(urls, options = {}) {
|
|
|
3184
3238
|
for (const url of urls) {
|
|
3185
3239
|
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
|
3186
3240
|
try {
|
|
3187
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3241
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
|
|
3242
|
+
const status = typeof response?.status === "function" ? response.status() : 200;
|
|
3243
|
+
if (status >= 400) throw new Error(`HTTP ${status}`);
|
|
3188
3244
|
out.push({ url, snapshot: await snapshotPage(page, url, options) });
|
|
3189
3245
|
} catch (error) {
|
|
3190
3246
|
out.push({ url, error: error.message });
|