@domandigital/craft 0.8.1 → 0.10.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 +13 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +12 -2
- package/README.md +6 -0
- package/dist/{audit-C62GULQR.js → audit-EGDNWSQB.js} +3 -3
- package/dist/audit.cjs +67 -13
- package/dist/audit.js +67 -13
- package/dist/{chunk-2VLN6JLN.js → chunk-2NQFIDLE.js} +5 -5
- package/dist/{chunk-OBVWDPQQ.js → chunk-3NGWXZZE.js} +67 -13
- package/dist/{chunk-XEH4GK5K.js → chunk-APO4BT2F.js} +4 -4
- package/dist/{chunk-X6PHP22P.js → chunk-JKIDWHG3.js} +133 -7
- package/dist/{chunk-JNFHQGKR.js → chunk-JWHC67ZS.js} +1 -1
- package/dist/{chunk-DWZY5MWD.js → chunk-QMYERCBB.js} +1 -1
- package/dist/{chunk-UVBVYM5D.js → chunk-TSKRBIOC.js} +2 -2
- package/dist/{chunk-6LPMM752.js → chunk-U2JTFQKO.js} +5 -5
- package/dist/{cli-BWERSVPW.js → cli-2WBGTBKD.js} +4 -4
- package/dist/{cli-UMQNCHAQ.js → cli-ADQQURNR.js} +9 -9
- package/dist/cli-EK3GSAOW.js +15 -0
- package/dist/{cli-C5RLL7AW.js → cli-PRWTNFVP.js} +4 -4
- package/dist/cli-VQUNNIZM.js +16 -0
- package/dist/cli.js +2 -2
- package/dist/index.cjs +187 -13
- package/dist/index.d.cts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +184 -13
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/cli-D6VYGCOE.js +0 -16
- package/dist/cli-I4TQMCUP.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
## 0.9.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- bbf1f54: `chatbot-residue` moves to the house blocking tier. A pasted chat reply (citation tokens, entity markers, "as an AI language model") now fails `craft copy --gate`, `copy-check` and `sanity-copy`. Its estate hits were read first: 15 of 15 real, no false alarms.
|
|
15
|
+
|
|
3
16
|
## 0.8.1
|
|
4
17
|
|
|
5
18
|
### Patch 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
|
@@ -45,6 +45,11 @@ A single hit fails the check. These are not judgment calls.
|
|
|
45
45
|
a signed-in route (`/admin/`, `/portal/`, `/dashboard/`).
|
|
46
46
|
- **"No X, no Y" as a list**, anywhere in a sentence. "No obligation, no
|
|
47
47
|
spam." Defining the business by what it is not, itemised.
|
|
48
|
+
- **Chatbot residue.** "Certainly! Here's a revised version", "as an AI
|
|
49
|
+
language model", "let me know if you'd like me to", ChatGPT citation tokens
|
|
50
|
+
and entity markers, links tracked `utm_source=chatgpt.com`. This is not
|
|
51
|
+
style: it is proof a chat reply was pasted in whole. Remove it, then source
|
|
52
|
+
or cut any claim a citation token was backing. Never re-cite from memory.
|
|
48
53
|
- The phrase list: `let's dive in`, `let's unpack`, `let's break this down`,
|
|
49
54
|
`here's the thing`, `it's important to note`, `at its core`,
|
|
50
55
|
`the key takeaway`, `the reality is`, `the truth is`,
|
|
@@ -95,15 +100,15 @@ person reading decides which instance earns its place.
|
|
|
95
100
|
| `vague-word` | innovative, scalable, end-to-end, streamline, solutions. Reported only when a file is named outright |
|
|
96
101
|
| `question-reveal` | "The result? Twice the bookings.", "What does this mean for you? It means...": a staged reveal |
|
|
97
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** |
|
|
98
104
|
| `placeholder` | "[Insert client name]", lorem ipsum, `TODO_PLACEHOLDER`. Fine in a draft commit; **zero in anything delivered** |
|
|
99
|
-
| `chatbot-residue` | "Certainly! Here's a revised version", "as an AI language model", "let me know if you'd like me to", chat citation tokens, links tracked `utm_source=chatgpt.com`. **Zero in anything delivered**, and the first candidate for the blocking tier |
|
|
100
105
|
|
|
101
106
|
### Style is not evidence
|
|
102
107
|
|
|
103
108
|
Most tells here are house style: they make copy generic, whoever wrote it. The
|
|
104
109
|
research is clear that an em dash, a rule of three or a rhetorical question
|
|
105
110
|
proves nothing about authorship, and em dash rates even run in opposite
|
|
106
|
-
directions between models. Only
|
|
111
|
+
directions between models. Only chatbot residue, in the blocking tier, is evidence that a chat reply
|
|
107
112
|
was pasted. Report findings as an editorial diagnosis ("three benefit claims
|
|
108
113
|
lack a mechanism; two stock phrases recur"), never as a score or an
|
|
109
114
|
accusation.
|
|
@@ -208,6 +213,11 @@ Hyphenated compounds count as one word. Not automated.
|
|
|
208
213
|
`TODO_PLACEHOLDER: <the exact field needed>` and say so. Never fill it.
|
|
209
214
|
- A quotation under a named person must be theirs. Copy drafted for someone,
|
|
210
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.
|
|
211
221
|
|
|
212
222
|
## CTA rule
|
|
213
223
|
|
package/README.md
CHANGED
|
@@ -104,8 +104,14 @@ run. A test fails if a phrase the standard blocks does not block.
|
|
|
104
104
|
```bash
|
|
105
105
|
npx craft copy --gate content # the house gate: exit 1 on the blocking tier
|
|
106
106
|
npx craft copy compare draft.md rewrite.md # facts a rewrite lost or added
|
|
107
|
+
npx craft copy claims content # every price, figure, date and named source, to check
|
|
107
108
|
```
|
|
108
109
|
|
|
110
|
+
No tell can say whether a figure is true. `craft copy claims` lists each
|
|
111
|
+
sentence a reader would take as a checkable fact, marked `sourced` (a source is
|
|
112
|
+
named) or `UNSOURCED`, for a person to check against the primary source. It
|
|
113
|
+
always exits 0: a checklist, not a verdict.
|
|
114
|
+
|
|
109
115
|
The same checks run in code:
|
|
110
116
|
|
|
111
117
|
```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-TSKRBIOC.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
|
{
|
|
@@ -2294,6 +2345,9 @@ var HOUSE = {
|
|
|
2294
2345
|
"plain-english": { tier: "block", label: 'use "properly explained"' },
|
|
2295
2346
|
buzzword: { tier: "block", label: "forbidden phrase" },
|
|
2296
2347
|
"negative-reassurance": { tier: "block", label: "negative reassurance, say what they get" },
|
|
2348
|
+
// Evidence of a pasted chat reply, not a matter of style. Moved from review
|
|
2349
|
+
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2350
|
+
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2297
2351
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
2298
2352
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2299
2353
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
@@ -2305,10 +2359,10 @@ var HOUSE = {
|
|
|
2305
2359
|
"vague-attribution": { tier: "review", label: "unnamed source" },
|
|
2306
2360
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2307
2361
|
"false-range": { tier: "review", label: "false range" },
|
|
2308
|
-
// The first candidate for block: evidence of a pasted chat reply, not a
|
|
2309
|
-
// matter of style. Blocks once its estate hits have been read (ROADMAP.md).
|
|
2310
|
-
"chatbot-residue": { tier: "review", label: "chatbot residue" },
|
|
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
|
);
|
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
|
{
|
|
@@ -2262,6 +2313,9 @@ var HOUSE = {
|
|
|
2262
2313
|
"plain-english": { tier: "block", label: 'use "properly explained"' },
|
|
2263
2314
|
buzzword: { tier: "block", label: "forbidden phrase" },
|
|
2264
2315
|
"negative-reassurance": { tier: "block", label: "negative reassurance, say what they get" },
|
|
2316
|
+
// Evidence of a pasted chat reply, not a matter of style. Moved from review
|
|
2317
|
+
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2318
|
+
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2265
2319
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
2266
2320
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2267
2321
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
@@ -2273,10 +2327,10 @@ var HOUSE = {
|
|
|
2273
2327
|
"vague-attribution": { tier: "review", label: "unnamed source" },
|
|
2274
2328
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2275
2329
|
"false-range": { tier: "review", label: "false range" },
|
|
2276
|
-
// The first candidate for block: evidence of a pasted chat reply, not a
|
|
2277
|
-
// matter of style. Blocks once its estate hits have been read (ROADMAP.md).
|
|
2278
|
-
"chatbot-residue": { tier: "review", label: "chatbot residue" },
|
|
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
|
);
|
|
@@ -3,18 +3,18 @@ import {
|
|
|
3
3
|
hueName,
|
|
4
4
|
loadNull,
|
|
5
5
|
shapeName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-APO4BT2F.js";
|
|
7
7
|
import {
|
|
8
8
|
parseFlags
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JKIDWHG3.js";
|
|
10
10
|
import {
|
|
11
11
|
fingerprint,
|
|
12
12
|
fingerprintDistance
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-QMYERCBB.js";
|
|
14
14
|
import {
|
|
15
15
|
SNAPSHOT_VERSION,
|
|
16
16
|
auditSnapshot
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-3NGWXZZE.js";
|
|
18
18
|
|
|
19
19
|
// src/estate/cli.ts
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -92,7 +92,7 @@ async function snapshotOf(target, cwd) {
|
|
|
92
92
|
return snap;
|
|
93
93
|
}
|
|
94
94
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
95
|
-
const { snapshotUrl } = await import("./audit-
|
|
95
|
+
const { snapshotUrl } = await import("./audit-EGDNWSQB.js");
|
|
96
96
|
return snapshotUrl(target);
|
|
97
97
|
}
|
|
98
98
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -866,10 +866,14 @@ var COPY_TELLS = [
|
|
|
866
866
|
surface: "copy",
|
|
867
867
|
why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
|
|
868
868
|
fix: "Write one sentence that says which of the three is true, and how you know.",
|
|
869
|
-
|
|
869
|
+
// The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
|
|
870
|
+
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),
|
|
870
871
|
fixtures: {
|
|
871
|
-
flag: [f("content/home.md", "No fuss. No jargon. Just results.")],
|
|
872
|
-
pass: [
|
|
872
|
+
flag: [f("content/home.md", "No fuss. No jargon. Just results."), f("content/home.md", "No fuss.\nNo jargon.\nJust results.")],
|
|
873
|
+
pass: [
|
|
874
|
+
f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
|
|
875
|
+
f("content/guide.md", "Your competitor appears in the top three. You don't. Again.\n\nIt's frustrating.")
|
|
876
|
+
]
|
|
873
877
|
}
|
|
874
878
|
},
|
|
875
879
|
{
|
|
@@ -888,6 +892,14 @@ var COPY_TELLS = [
|
|
|
888
892
|
}
|
|
889
893
|
];
|
|
890
894
|
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;
|
|
895
|
+
var PROMPT_CONTEXT = [
|
|
896
|
+
/\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,
|
|
897
|
+
/\b(?:attached|uploaded)\s+(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\b/i,
|
|
898
|
+
/\bthe\s+(?:attached|uploaded|provided)\s+brief\s+(?:asks|says|wants|requests|suggests|mentions)\b/i
|
|
899
|
+
];
|
|
900
|
+
function gerundList(block, _match, index) {
|
|
901
|
+
return /(?:^|[(,;:]\s*)\w+ing\b[^,.;()!?]{0,40}$/.test(block.text.slice(Math.max(0, index - 60), index));
|
|
902
|
+
}
|
|
891
903
|
var VAGUE_ATTRIBUTION = [
|
|
892
904
|
/\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,
|
|
893
905
|
/\b(?:many|most)\s+(?:experts|people|customers|professionals|homeowners)\s+(?:agree|say|believe)\b/i,
|
|
@@ -973,8 +985,10 @@ function inlineLabelHits(ctx) {
|
|
|
973
985
|
run = [];
|
|
974
986
|
};
|
|
975
987
|
for (const line of lines) {
|
|
976
|
-
const m = /^\s*
|
|
977
|
-
|
|
988
|
+
const m = /^\s*[-*+]\s+\*\*([^*]{1,40}?):?\*\*:?\s+(\S.*)$/.exec(line);
|
|
989
|
+
const data = m && /[\d£$€%`]|https?:|^\*\*/.test(m[2]);
|
|
990
|
+
const named = m && /\S[A-Z]|[./]/.test(m[1]);
|
|
991
|
+
if (m && !data && !named && m[2].trim().split(/\s+/).length <= 6) run.push(offset);
|
|
978
992
|
else flush();
|
|
979
993
|
offset += line.length + 1;
|
|
980
994
|
}
|
|
@@ -1012,6 +1026,29 @@ var RESEARCH_TELLS = [
|
|
|
1012
1026
|
]
|
|
1013
1027
|
}
|
|
1014
1028
|
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "prompt-context",
|
|
1031
|
+
name: "Prompt context",
|
|
1032
|
+
generation: 1,
|
|
1033
|
+
severity: "warn",
|
|
1034
|
+
surface: "copy",
|
|
1035
|
+
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.",
|
|
1036
|
+
fix: "Delete the reference to the research or brief. Where it carried a claim, name the real source or cut the claim.",
|
|
1037
|
+
detect: phrases(PROMPT_CONTEXT, quoted),
|
|
1038
|
+
fixtures: {
|
|
1039
|
+
flag: [
|
|
1040
|
+
f("content/guide.md", "This article uses the attached Perplexity research file as the primary source."),
|
|
1041
|
+
f("content/guide.md", "The attached research is right to say these platforms suit early-stage businesses."),
|
|
1042
|
+
f("content/guide.md", "That is exactly how the uploaded research frames the category."),
|
|
1043
|
+
f("content/guide.md", "The attached brief asks for the point that most tradespeople miss.")
|
|
1044
|
+
],
|
|
1045
|
+
pass: [
|
|
1046
|
+
f("content/guide.md", "We research every quote before we send it."),
|
|
1047
|
+
f("app/email.tsx", `export const Invoice = () => <p>Your invoice is attached.</p>;`),
|
|
1048
|
+
f("content/guide.md", "BrightLocal's research shows that reviews drive 20% of local ranking.")
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1015
1052
|
{
|
|
1016
1053
|
id: "placeholder",
|
|
1017
1054
|
name: "Unfilled placeholder",
|
|
@@ -1064,7 +1101,11 @@ var RESEARCH_TELLS = [
|
|
|
1064
1101
|
pass: [
|
|
1065
1102
|
f("content/home.md", "- Pages load in under a second\n- Card details never touch our server\n- Support by phone until 8pm\n"),
|
|
1066
1103
|
f("content/home.md", "- **Speed:** Faster pages\n- **Security:** Safer data\n"),
|
|
1067
|
-
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")
|
|
1104
|
+
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"),
|
|
1105
|
+
f("content/guide.md", "- **Per page**: \xA3100-\xA3200 per page\n- **Hourly**: \xA350-\xA375/hour\n- **Day rate**: \xA3350-\xA3500\n"),
|
|
1106
|
+
f("content/guide.md", "- **Law Society Find a Solicitor:** For solicitors\n- **AccountingWEB / ICAEW directories:** For accountants\n- **NHS.uk:** For NHS services\n"),
|
|
1107
|
+
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"),
|
|
1108
|
+
f("content/guide.md", "- **Structural problems:** **REBUILD.**\n- **Messaging issues:** **REFRESH.**\n- **Neither:** **LEAVE IT.**\n")
|
|
1068
1109
|
]
|
|
1069
1110
|
}
|
|
1070
1111
|
},
|
|
@@ -1076,7 +1117,7 @@ var RESEARCH_TELLS = [
|
|
|
1076
1117
|
surface: "copy",
|
|
1077
1118
|
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.",
|
|
1078
1119
|
fix: "Cut the tail, or replace it with the mechanism: 'so you can drop the car off before work'.",
|
|
1079
|
-
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, ""))),
|
|
1120
|
+
detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, "")), void 0, gerundList),
|
|
1080
1121
|
fixtures: {
|
|
1081
1122
|
flag: [
|
|
1082
1123
|
f("content/home.md", "Every engineer is Gas Safe registered, ensuring complete peace of mind."),
|
|
@@ -1084,7 +1125,8 @@ var RESEARCH_TELLS = [
|
|
|
1084
1125
|
],
|
|
1085
1126
|
pass: [
|
|
1086
1127
|
f("content/home.md", "We open at 7am, so you can drop the car off before work."),
|
|
1087
|
-
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers.")
|
|
1128
|
+
f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers."),
|
|
1129
|
+
f("content/guide.md", "Many items (claiming your GBP, ensuring NAP consistency, writing title tags) are straightforward.")
|
|
1088
1130
|
]
|
|
1089
1131
|
}
|
|
1090
1132
|
},
|
|
@@ -1334,6 +1376,7 @@ function repeatHits(ctx) {
|
|
|
1334
1376
|
for (const s of sentences(doc2)) {
|
|
1335
1377
|
const words = normal(s.text);
|
|
1336
1378
|
if (words.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
|
|
1379
|
+
if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
|
|
1337
1380
|
const key = words.join(" ");
|
|
1338
1381
|
const earlier = whole.get(key);
|
|
1339
1382
|
if (earlier) {
|
|
@@ -1531,7 +1574,15 @@ var DENSITY_TELLS = [
|
|
|
1531
1574
|
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.")),
|
|
1532
1575
|
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."))
|
|
1533
1576
|
],
|
|
1534
|
-
pass: [
|
|
1577
|
+
pass: [
|
|
1578
|
+
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.")),
|
|
1579
|
+
f2(
|
|
1580
|
+
"content/guide.md",
|
|
1581
|
+
doc(
|
|
1582
|
+
"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."
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
]
|
|
1535
1586
|
}
|
|
1536
1587
|
},
|
|
1537
1588
|
{
|
|
@@ -2629,6 +2680,9 @@ var HOUSE = {
|
|
|
2629
2680
|
"plain-english": { tier: "block", label: 'use "properly explained"' },
|
|
2630
2681
|
buzzword: { tier: "block", label: "forbidden phrase" },
|
|
2631
2682
|
"negative-reassurance": { tier: "block", label: "negative reassurance, say what they get" },
|
|
2683
|
+
// Evidence of a pasted chat reply, not a matter of style. Moved from review
|
|
2684
|
+
// on 2026-09-24 after its estate hits were read (ROADMAP.md): 15 of 15 real.
|
|
2685
|
+
"chatbot-residue": { tier: "block", label: "chatbot residue. A chat reply was pasted in: remove it, and source or cut the claim it backed" },
|
|
2632
2686
|
"review-phrase": { tier: "review", label: "review tier" },
|
|
2633
2687
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2634
2688
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
@@ -2640,10 +2694,10 @@ var HOUSE = {
|
|
|
2640
2694
|
"vague-attribution": { tier: "review", label: "unnamed source" },
|
|
2641
2695
|
"closing-summary": { tier: "review", label: "closing summary" },
|
|
2642
2696
|
"false-range": { tier: "review", label: "false range" },
|
|
2643
|
-
// The first candidate for block: evidence of a pasted chat reply, not a
|
|
2644
|
-
// matter of style. Blocks once its estate hits have been read (ROADMAP.md).
|
|
2645
|
-
"chatbot-residue": { tier: "review", label: "chatbot residue" },
|
|
2646
2697
|
"placeholder": { tier: "review", label: "unfilled placeholder" },
|
|
2698
|
+
// Every estate hit so far is real (38 in five DD articles, 2026-09-24), but
|
|
2699
|
+
// a proposal can honestly say "the attached research". Review until measured there.
|
|
2700
|
+
"prompt-context": { tier: "review", label: "prompt context: the model naming its own inputs" },
|
|
2647
2701
|
"question-reveal": { tier: "review", label: "staged reveal" },
|
|
2648
2702
|
"inline-label-list": { tier: "review", label: "bold-label bullets" },
|
|
2649
2703
|
"phrase-density": { tier: "review", label: "density" },
|
|
@@ -2992,7 +3046,7 @@ var RENDERED_PATHS = {
|
|
|
2992
3046
|
};
|
|
2993
3047
|
|
|
2994
3048
|
// src/character/check.ts
|
|
2995
|
-
var CATALOGUE_VERSION = "2026.09.
|
|
3049
|
+
var CATALOGUE_VERSION = "2026.09.8";
|
|
2996
3050
|
var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
|
|
2997
3051
|
(t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
|
|
2998
3052
|
);
|