diff --git a/llm-cliche-highlighter.html b/llm-cliche-highlighter.html
index 721d79c8..7a59c585 100644
--- a/llm-cliche-highlighter.html
+++ b/llm-cliche-highlighter.html
@@ -489,7 +489,8 @@
Matches
// Each pattern: { id, name, description, find(text) -> [{ start, end, badge?, badgeTitle?, count? }] }
// Add new patterns to this array and they get a checkbox, per-pattern count, and highlighting for free.
// makeChainFinder builds a detector for "HEAD X, HEAD Y, ..." lists and counts the items;
-// makeRegexFinder wraps a plain regex (must use the g flag).
+// makeRegexFinder wraps a plain regex (must use the g flag); makeEchoFinder builds a
+// detector for runs of consecutive sentences repeating the same multi-word skeleton.
const CHAIN_BODY = String.raw`[^,.;:!?\n\u2013\u2014\u2026]*`;
const CHAIN_SEP = String.raw`(?:\s*,\s*(?:and\s+|or\s+)?|\s+(?:and|or)\s+|\s*[;&\u2013\u2014]\s*(?:and\s+|or\s+)?|\s+-{1,2}\s+)`;
@@ -526,6 +527,58 @@ Matches
};
}
+// makeEchoFinder builds a detector for runs of consecutive sentences that
+// repeat the same multi-word skeleton -- the "X does A. Y does B." triad.
+// The badge counts the echoing sentences.
+function makeEchoFinder({ minGram = 3, minRun = 2 } = {}) {
+ const SENT = /[^.!?\n]+[.!?]?/g;
+ const grams = (s, n) => {
+ const w = s.toLowerCase().match(/[a-z0-9'’-]+/g) || [];
+ const out = new Set();
+ for (let i = 0; i + n <= w.length; i++) out.add(w.slice(i, i + n).join(' '));
+ return out;
+ };
+ return function (text) {
+ const sents = [];
+ for (const m of text.matchAll(SENT)) {
+ if ((m[0].match(/\S+/g) || []).length >= 4) {
+ sents.push({ start: m.index, end: m.index + m[0].length, text: m[0] });
+ }
+ }
+ const found = [];
+ let i = 0;
+ while (i < sents.length) {
+ let j = i;
+ let shared = null;
+ while (j + 1 < sents.length) {
+ if (sents[j + 1].start - sents[j].end > 3) break; // adjacent prose only
+ const a = grams(sents[j].text, minGram);
+ const b = grams(sents[j + 1].text, minGram);
+ const common = [...a].filter(g => b.has(g));
+ if (!common.length) break;
+ shared = common.sort((x, y) => y.length - x.length)[0];
+ j += 1;
+ }
+ const run = j - i + 1;
+ if (run >= minRun && shared) {
+ let end = sents[j].end;
+ while (end > sents[i].start && /\s/.test(text[end - 1])) end -= 1;
+ found.push({
+ start: sents[i].start,
+ end,
+ count: run,
+ badge: String(run),
+ badgeTitle: run + ' sentences echoing “' + shared + '”'
+ });
+ i = j + 1;
+ } else {
+ i += 1;
+ }
+ }
+ return found;
+ };
+}
+
// Patterns in this group are adapted from Wikipedia's "Signs of AI writing"
// guide: https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing
const WIKI_GROUP = 'Signs of AI writing (Wikipedia)';
@@ -603,6 +656,54 @@ Matches
description: '\u201cThat is not nothing\u201d / \u201cthat\u2019s not nothing\u201d, plus the \u201cthis / it / which is not nothing\u201d variants.',
find: makeRegexFinder(/\b(?:that|this|it|which)(?:['\u2019]s|\s+(?:is|was))\s+not\s+nothing\b/gi)
},
+ {
+ id: 'is-the-whole',
+ name: '“Is the whole …”',
+ description: 'Any subject + “is the whole point / trick / pitch / idea”, plus the “here is the whole …” opener. The twin of “is the entire …”, and a generalisation of “That’s the whole …” to subjects other than that/this.',
+ find: makeRegexFinder(/(?:\b(?:is|was|are|were)|['’]s)\s+the\s+whole\b(?:\s+\w+)?|\bhere(?:['’]s|\s+is)\s+the\s+whole\b(?:\s+\w+)?/gi)
+ },
+ {
+ id: 'echo-triad',
+ name: 'Echoing sentence runs',
+ description: 'Consecutive sentences built on the same repeated skeleton — “A shopping cart is an object in the system. A chat room is an object in the system.” The badge counts the echoing sentences.',
+ find: makeEchoFinder({ minGram: 4, minRun: 2 })
+ },
+ {
+ id: 'performative-honesty',
+ name: 'Performative honesty',
+ description: 'Sincerity announced rather than demonstrated: “I won’t pretend”, “I’ll be honest”, “let’s be honest”, “to be clear”, and sentence-initial “Honestly,” or “Look,”.',
+ find: makeRegexFinder(/\bI\s+(?:will\s+not|won['’]t)\s+pretend\b|\b(?:I['’]ll|let['’]s|to)\s+be\s+(?:honest|clear|blunt|real)\b|(?:^|[.!?–—]\s+|\n)(?:Honestly|Look|Truthfully|Frankly)\s*,/gi)
+ },
+ {
+ id: 'thats-the-part',
+ name: '“That’s the part …”',
+ description: 'Gesturing at a favoured detail instead of stating it: “that is the part a counter can’t reach”, “the part that makes me trust the rest”, “my favourite part of …”.',
+ find: makeRegexFinder(/\b(?:that|this|it)(?:['’]s|\s+(?:is|was))\s+the\s+part\b|\bthe\s+part\s+that\s+(?:makes|made|gets|got|keeps|kept)\s+(?:me|you|us|it)\b|\bmy\s+favou?rite\s+part\s+of\b/gi)
+ },
+ {
+ id: 'the-only-i-trust',
+ name: '“The only X I trust”',
+ description: 'The narrowing superlative reveal: “the only marketing I trust”, “the only thing it needs”, “the only X that matters”.',
+ find: makeRegexFinder(/\bthe\s+only\s+[\w'’-]+(?:\s+[\w'’-]+){0,2}?\s+(?:I|you|we|it|he|she|they)\s+(?:trust|need|needs|care|want|wants|use|uses|believe)\b|\bthe\s+only\s+[\w'’-]+\s+that\s+(?:matters|counts|works|survives)\b/gi)
+ },
+ {
+ id: 'take-my-word',
+ name: '“Don’t take my word for it”',
+ description: 'The stock invitation to verify: “you don’t have to take my word for it”, “don’t take my word for any of this”.',
+ find: makeRegexFinder(/\b(?:you\s+)?(?:do\s+not|don['’]t)\s+(?:have\s+to\s+)?take\s+my\s+word\s+for\s+(?:it|any\s+of\s+(?:it|this|that))\b/gi)
+ },
+ {
+ id: 'turns-out',
+ name: '“Turns out …”',
+ description: 'The casual-revelation opener, almost always bolted to a tidy conclusion: “Turns out X”, “it turns out that X”.',
+ find: makeRegexFinder(/(?:^|[.!?–—]\s+|\n)Turns\s+out\b|\bit\s+turns\s+out\s+that\b/gi)
+ },
+ {
+ id: 'fits-in-your-head',
+ name: '“Fits in your head”',
+ description: 'Dev-blog boilerplate for simplicity: “small enough to hold in your head”, “batteries included”, “it just works”, “zero config”, “sane defaults”.',
+ find: makeRegexFinder(/\b(?:hold|fit|fits|holds|held)\s+(?:it\s+)?in\s+your\s+head\b|\bbatteries[-\s]included\b|\bit\s+just\s+works\b|\bzero[-\s]config(?:uration)?\b|\bsane\s+defaults\b/gi)
+ },
{
id: 'ai-vocab',
group: WIKI_GROUP,
@@ -804,6 +905,10 @@ Matches
Despite these challenges, the team kept shipping. They adapted to an ever-evolving landscape. As of my last update, the pricing page still said “coming soon”.
+The parser is a tiny state machine. The renderer is a tiny state machine.
+
+I won't pretend the rollout was smooth. It turns out that nobody reads the changelog. Here is the whole secret. The small core still fits in your head. That's the part a schedule can't capture. It's the only estimate I trust. You don't have to take my word for it.
+
This closing paragraph is deliberately ordinary, with no list patterns at all, so nothing here should light up.`;
// URL loading: the page URL's #fragment holds the article URL, and the text is
@@ -1288,6 +1393,39 @@ Matches
['not-nothing', 'The launch drew a small crowd, which was not nothing.', 1],
['not-nothing', 'She insisted that nothing was wrong.', 0],
['not-nothing', 'There is nothing left to say.', 0],
+ ['is-the-whole', 'Distribution is the whole game.', 1],
+ ['is-the-whole', "Here's the whole pitch in one slide.", 1],
+ ['is-the-whole', 'That was the whole point of the meeting.', 1],
+ ['is-the-whole', 'The whole team showed up.', 0],
+ ['echo-triad', 'A shopping cart is an object in the system. A chat room is an object in the system.', 1, [2]],
+ ['echo-triad', 'The parser is a state machine. The renderer is a state machine. The scheduler is a state machine.', 1, [3]],
+ ['echo-triad', 'The parser is fast today. The renderer is fast today.', 0, []],
+ ['echo-triad', 'The parser is fast. The tests are slow.', 0, []],
+ ['performative-honesty', "I won't pretend the migration was painless.", 1],
+ ['performative-honesty', "Let's be honest: nobody reads the docs.", 1],
+ ['performative-honesty', 'To be clear, the API is unchanged.', 1],
+ ['performative-honesty', 'Honestly, it was fine.', 1],
+ ['performative-honesty', 'She answered honestly.', 0],
+ ['performative-honesty', 'Look at the diagram.', 0],
+ ['thats-the-part', "That's the part a counter can't reach.", 1],
+ ['thats-the-part', 'The part that makes me trust the rest is the errata.', 1],
+ ['thats-the-part', 'My favorite part of the demo was the undo.', 1],
+ ['thats-the-part', 'He played the part of the villain.', 0],
+ ['the-only-i-trust', 'It’s the only marketing I trust.', 1],
+ ['the-only-i-trust', 'The only benchmark that matters is retention.', 1],
+ ['the-only-i-trust', 'The only thing it needs is a cache.', 1],
+ ['the-only-i-trust', 'She was the only engineer on call.', 0],
+ ['take-my-word', "You don't have to take my word for it.", 1],
+ ['take-my-word', "Don't take my word for any of this.", 1],
+ ['take-my-word', 'He kept his word.', 0],
+ ['turns-out', 'Turns out the cache was never warm.', 1],
+ ['turns-out', 'It turns out that nobody tested it.', 1],
+ ['turns-out', 'She turns out solid work every week.', 0],
+ ['fits-in-your-head', 'The design is small enough to hold in your head.', 1],
+ ['fits-in-your-head', 'It ships with sane defaults and zero config.', 2],
+ ['fits-in-your-head', 'Install it and it just works.', 1],
+ ['fits-in-your-head', 'We choose boring technology on purpose.', 0],
+ ['fits-in-your-head', 'The helmet fits your head.', 0],
['ai-vocab', 'We delve into the intricacies of the interplay.', 3],
['ai-vocab', 'Her vibrant tapestry hung in the bustling hall.', 3],
['ai-vocab', 'A meticulously curated, seamless experience.', 2],