Four regexes rescanned a long whitespace or blank-line run from every
position, so a document ending in blank lines or carrying a large masked
comment block took seconds instead of milliseconds:
- the sentence splitter behind highlight regions, replaced with a
single-pass scan that yields the same spans as the former regex
- its boundary-whitespace trim in rendered-Markdown mode, now trimStart
and trimEnd arithmetic instead of a `\s*$` match
- the Markdown table delimiter test, now run on the trimmed line
- the line-anchored `Interesting part:` opener, whose leading `\s*`
crossed newlines under /m and is now `[ \t]*`
The #190 fixture still scaled quadratically after #196 because masked
comments become whitespace, which the sentence splitter then choked on.
64000 masked comments now analyze in under a second.
Regression tests compare sentence boundaries against the former regex on
every boundary shape and assert linear growth by ratio rather than by a
wall-clock budget. Bundled detector copies regenerated.
Closes#235
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmCsufeKSGpesBP7w4Xf4j
Run maskCode once per document and advance a single comment-open scan.
Refresh code or list-aware views only when a removed comment contained
backticks or list markers, preserving #123 edge cases.
Fixesconorbronsdon/avoid-ai-writing#190
Co-authored-by: Sharad. <sharadvc@users.noreply.github.com>
Remove the nonexistent marketing boost from patterns.js comments.
Document all four accepted modes in CATEGORIES.md; only technical
changes flagging today. Regenerate mirrored detector copies.
Fixesconorbronsdon/avoid-ai-writing#173
Co-authored-by: Sharad. <sharadvc@users.noreply.github.com>
* Add detector trio from #108: launch intros, crowd contrast, fake-casual props
Three regex-detectable patterns from the welttowelt merged-system diff,
each with a must-fire and a must-not-fire fixture:
- launch-intro: sentence-initial imperative product introductions
("Enter X.", "Meet X, your new...", "Think X meets Y"). "Say hello
to X" stays judgment-only ("Say hello to Grandma." is ordinary prose).
- crowd-contrast: the trailing "while everyone else..." clause, gated
on a dismissive verb so literal simultaneity never fires.
- fake-casual-prop: stage directions, wink asides, and "because of
course it does". Verdict closers and label-prefix openers stay
LLM-judgment per the precision-over-recall policy.
Catalog 62 -> 65, engine 48 -> 51, word table unchanged at 112.
* Apply review fixes to the #128 detector trio
Addresses the three detector-surface items in the CHANGES_REQUESTED review,
plus the prose-scoping follow-up comment. Each item was reproduced against
the PR head before being changed.
1. `launch-intro`: the bare `Enter X.` surface is judgment-only. It matched
"Enter Password." and "Enter Amount." verbatim, and "Enter Username —
your work email." through the dash terminator; neither a terminator
change nor a field-name denylist reaches the period-terminated class.
The `Meet X, your new favorite…` and `Think X meets Y` surfaces stay.
The three UI forms and "Enter Flowdesk." are pinned as must-not-fire.
2. `crowd-contrast`: the progressive branch is restricted to `-ing` forms.
`(?:e|es|ed|ing)` let the adjective ("was still deliberate about"), the
passive ("was still debated by pundits") and the bare present through;
all three are now must-not-fire fixtures.
3. `fake-casual-prop`: `because of course …` is judgment-only. The present
tense plus `did` gate does not hold — "The build failed because of
course it did." is the ordinary human grumble in the same form. Two
grumbles are pinned as must-not-fire.
Also scopes the prose claim: the "was still" marker gates the
debate/speculation branch only, and the two stereotyped variants
("wrote think-pieces", "played catch-up") match on their own wording.
SKILL.md, detector/CATEGORIES.md and the CHANGELOG say the same thing,
and the generated plugin/cursor copies are regenerated from SKILL.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Disclose the crowd-contrast residue that was measured, not assumed
Beyond the review. A fresh-eyes pass turned up sentences that fire and
that the entry's disclosure did not cover; every one was reproduced
against this branch before being written down.
- Branch one fires on any literal progressive use of its verbs, not only
on "was still debating": "while the market was still speculating about
the price" and "while others were still arguing about procedure" are
ordinary wire copy and both fire.
- Branches two and three fire on literal contrasts of their own: "while
everyone else wrote think-pieces from Washington" (a real reporting
contrast) and "while everyone else played catch-up in the spring"
(sports and classroom narrative).
- The wink-aside regex accepts all four of (yes|no) x (really|seriously);
the entry named two.
No regex changes: these are accepted under precision-over-recall, and
narrowing them is the maintainer's call, not a review item. The prose now
states what the engine does instead of a tighter gate it does not enforce.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Tighten two surfaces and make every entry state what its regex does
Cross-family review (Codex / GPT-5.6). Each item was reproduced against
`land-pr-128` before the change and re-measured after.
Regex changes, both false positives on ordinary prose:
1. `fake-casual-prop`: `chef['’]?s\s+kiss` made the apostrophe optional, so
"At midnight, *chefs kiss* their spouses goodbye" fired. The apostrophe
is now required, straight or curly.
2. `launch-intro`: `the\s+new\s+(?:standard|way|home)\b` let the head noun
swallow the first word of a compound noun — "Meet Rosa, the new home
secretary" and "Meet Emma, the new way station manager" both fired. Each
head now needs the tail launch copy actually has (`home of`, `way to`,
`standard in|for`) or an end of clause. "Meet Riverside, the new home of
our Saturday farmers market." still fires.
Not changed, disclosed instead: "Meet Alice, your new favorite aunt" and
"Think Alice meets Bob at noon" fire. They are person-name variants of the
two surfaces the maintainer asked to keep, so narrowing them would cost the
surfaces themselves.
Test change: the `(yes, really)` positive shared a fixture with
`*checks notes*` and only asserted that some `fake-casual-prop` existed, so
replacing the wink regex with a never-match left it green. It is now an
independent case asserting the matched text.
Prose: the three entries and the CATEGORIES rows now state the actual gates
— the four `Meet` heads and the one-token 2-30 character name; the
`was|were|is|are` + `still` + `-ing` branch, the `writing|wrote` branch and
the `play|plays|played|playing` branch; the six stage directions and four
parentheticals — plus the measured misses that follow from closed lists:
two-token names ("Meet North Star", "Think Google Docs meets Microsoft
Word"), crowds outside the list ("every competitor", "our rivals"), and
register neighbours ("*checks calendar*", "(yes, honestly)").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: welttowelt <welttowelt@users.noreply.github.com>
Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Add seven rhetorical-tic categories from Simon Willison's LLM cliche highlighter
Three with detector types (performed-insight, negation-chain,
dev-blog-boilerplate) plus fixtures; four as skill-only judgment rules
with reasons recorded in CATEGORIES.md. Counts bumped per the CI
contracts (69 pattern categories, 51 engine types); version 3.28.0.
* Address review: sync 3.28.0 release artifacts, narrow detector precision
- Bump package.json, plugin.json, and regenerate the bundled skill and
cursor rule (plugin-skill-sync now passes locally).
- negation-chain: the no-chain must open its sentence (mid-sentence
inventories stay clean); the did-not chain is comma-joined with the
subject elided (sequential narration stays clean).
- performed-insight: sit-with requires a boundary or 'for a moment'
after that/this; the 'only X I trust' branch is dropped; sentence-
initial 'Turns out' uses a lookbehind so the issue text and index no
longer consume the previous sentence boundary.
- dev-blog-boilerplate: 'it just works' no longer matches the phrasal
'it just works out'.
- Must-not-fire fixtures added for every reproduction in the review,
plus a text/index regression assertion for Turns out.
* Narrow rhetorical-tic detector precision
* Align narrowed detector docs and slogan coverage
---------
Co-authored-by: tech nasty <262050521+t3chnaztea@users.noreply.github.com>
Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com>
* feat: detect unnecessary hyphenation
* fix(detector): harden hyphenation masking
* fix(detector): mask long path components
* fix(detector): keep zero-weight copyedits out of AI highlights
buildSentenceRegions() mapped every text-bearing issue into
highlight_sentence_for_ai, so a copyedit-only document scored 0 and
classified HUMAN_ONLY while still reporting an AI-highlighted region.
Rename SUMMARY_ONLY_TYPES to NON_HIGHLIGHT_TYPES, since the set now
covers both anchorless summary signals and anchored style-only findings,
and add unnecessary-hyphenation to it. The finding stays in issues[] at
P2 with its weight of 0; scoring and trinary classification are
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrhAmTQkVxzX6UkmTL2TCf
---------
Co-authored-by: Conor Bronsdon <cmbronsdon@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
TIER1[token] / TIER2[token] are plain-object literals, so tokens naming
Object.prototype members resolved truthy: TIER1['constructor'] returned
the built-in constructor function and flagged ordinary prose (reported
with a minimal repro in #109). Both membership tests now use
Object.hasOwn (Node 16.9+; engines pins >=18).
Regression tests run the real analyzer both directions: prototype names
in prose produce zero tier hits, and 'delve' still fires with
'constructor' in the same sentence. Both go red on the unfixed code —
verified by reverting patterns.js and rerunning before committing.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* detector: stop counting non-tag "#" forms as hashtags (#90)
hashtag-stuff counted every #word and flagged at six, so a paragraph
citing six issue numbers scored as a stuffed tag block. Found when this
repo's own README linked issue #88 and the detector flagged the README.
The bug report cannot be written without triggering the bug either: the
prose of #90 scores hashtag-stuff on its own issue references.
Two subtractive changes, so the rule cannot start firing on anything it
does not already fire on.
maskCode() blanks fenced blocks and inline code spans, index-preserving,
before counting. fenceRanges() already existed but was wired only to
title-case-header, so a tag quoted in backticks counted as a tag used.
Fences are blanked before inline spans so a fence backtick cannot pair
with a later span and swallow the prose between them.
isSocialTag() subtracts forms that cannot be a tag: all-digit (#88), hex
colours of 3/4/6/8 digits, and C preprocessor directives. owner/repo#88,
URL fragments, shebangs, and Markdown headings already passed on the
rule's own anchor and are untouched.
Deliberately still counted: #main and #general. A CSS id, a channel, and
a tag are the same token. The only separator is an adjacency gate, and
measuring it showed it also stops the rule catching inline tag stuffing,
which is a real shape. Accepted cost of the hex carve-out: an all-hex
word tag (#dad, #cafe) stops counting, which only changes an outcome if
five real tags sit beside it.
Six fixtures: four that must not fire, two that must, per CONTRIBUTING.
Mutation-tested both halves -- disabling isSocialTag kills 3 tests,
disabling fence masking kills the suite. No new detector type, so
CATEGORIES.md is unchanged and the pattern count stays 61.
Gates green: npm test, check-pattern-count 61/112, self-scan SKILL.md
12/25, sync-plugin-skill 3.22.1.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address adversarial review: a false negative, fake coverage, and no version bump
Three real problems, one of them worse than the bug being fixed.
The hex carve-out covered 3- and 4-digit forms, which silently deleted
them cost true positives on exactly the stuffed-block shape the rule
exists to catch -- two verified blocks that flagged on main went silent.
Trading a false positive for a false negative is the wrong direction in a
repo whose stated bias is precision over recall. Narrowed to 6 and 8
digits. Real palettes are dominated by 6-digit values, so the CSS
paragraph still lands under the threshold without the short forms.
The fixture named for inline-code masking did not test it. Four of its
six spans quoted forms isSocialTag already subtracts, so with masking
deleted the count was 3, under the threshold, and the test passed.
Rewritten with six spans that all survive the carve-outs.
No CHANGELOG entry and no version bump on a behavior change to an engine
downstreams vendor byte-identical. Two different patterns.js byte-streams
would both have called themselves 3.22.1. Now 3.22.2 across package.json,
SKILL.md frontmatter, and plugin.json, with an entry that records the two
recall changes (unclosed fence, stray backtick) rather than only the win.
Also mask indented code blocks. A fence nested in a list item is indented
past the three spaces fenceRanges anchors, so a README config block still
counted -- the exact shape masking was added for.
Mutation-tested all nine mutants now killed, including the inline-masking
one that previously survived: remove inline / fence / indented masking,
isSocialTag always true, drop digit / hex / cpp carve-out, widen hex to
3-8, threshold 6 to 7.
Gates green: npm test, self-scan:check (SKILL 12/25, CHANGELOG 29/40),
check-pattern-count 61/112, sync-plugin-skill 3.22.2.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Third review pass: drop the indented-code pass, and stop eating a-f words
Two false negatives and one artifact that never got the correction.
SKILL.md still said CSS hex colours "(#fff, #1a2b3c)" are subtracted.
The narrowing to 6/8 digits landed in patterns.js and in the CHANGELOG
and never traveled to SKILL.md -- the file agents load and downstreams
vendor. Verified wrong: seven 3-digit colours still flag. This is the
same shape as the two claims the first review pass caught, arriving a
third time through a different door.
Dropped the indented-code masking pass entirely rather than patching it.
Four spaces is a code block only at top level; under a list marker it is
a paragraph continuation, so blanking it silenced real tag blocks:
- We shipped the detector ...
(blank)
#AI #Innovation #FutureOfWork #MachineLearning #Leadership #Growth
flagged on main and went silent on the branch. #90 reports fences and
inline spans; the indented pass was scope I added in response to review
pass two, and it was the sole source of eleven surviving mutants
including a real off-by-one in its offset arithmetic. Deleting it removes
the arithmetic and the state machine with it.
HEX_COLOUR now requires at least one digit. #decade, #facade, #deface
and #beaded are 6-8 char a-f words and real tags; every actual palette
value carries a digit. Same defect class as the 3/4-digit carve-out the
last pass removed, one size up, and not eliminated by that fix.
Fixtures for both regressions plus an 8-character RGBA case, which was
the one genuine coverage hole left: dropping the {8} alternative used to
survive. Two mutants remain undistinguishable (inline pass reading text
vs withoutFences, and an inclusive blank boundary); I probed for inputs
that separate them and found none, so they are equivalent rather than
uncovered.
Gates green: npm test, self-scan (CHANGELOG 29/40), check-pattern-count
61/112, sync-plugin-skill 3.22.2.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* detector: derive the engine type count instead of restating it (#91)
TYPE_LABELS has 47 keys. Six places said 45 or 46: both README count
lines, both CATEGORIES.md count sentences, the patterns.js header, and
the package.json description that ships to npm.
Three of the four commits that added a type edited README.md in the same
commit and still left the number alone. That is the argument for a guard
rather than a convention: the count sits inside a 90-word parenthetical
explaining why two counts deliberately differ, so it reads as prose and
survives editing. 660c95c fixed it by hand and added no check; #65
reopened it nine days later.
Corrected where the number does explanatory work (README.md:180,
CATEGORIES.md), deleted where it was a decorative restatement (the
"45-category engine" phrase, the patterns.js header, the npm
description). CLAUDE.md already says the count belongs in one place.
The survivors are now asserted in categories.test.js, which already
computes typeKeys.length and runs under detector-test.yml -- the only
workflow whose paths cover both detector/** and README.md. Extending
check-pattern-count.sh could not work: plugin-skill-sync.yml, which runs
it, does not watch detector/**, so a commit that only adds a key would
never trigger it.
Each site registers as a file plus a one-capture-group regex, and a regex
that stops matching FAILS rather than passes, so a reworded sentence has
to re-register instead of silently dropping its own guard.
Watched it fail, three ways:
README says 46 with 47 keys -> exit 1
48th TYPE_LABELS key added -> exit 1 (two checks)
sentence reworded -> exit 1, names the stale regex
Gates green: npm test, self-scan:check, check-pattern-count 61/112,
sync-plugin-skill 3.22.1.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address adversarial review: a seventh site, and prose the guard invalidated
The guard change described three more sentences than it updated.
CHANGELOG.md:37 is a seventh site and it is wrong rather than stale. The
v3.22.0 entry says "No new detection categories: the catalog stays at 61
and the engine at 46 types" -- that release added tier1-clarity, taking
the engine to 47. The audit trail recorded "no new categories" for the
release that added one, which is why the README then sat at 45 with
nothing to catch it. Corrected in place with a dated correction note,
following the precedent the 3.22.1 entry set for its own wrong claim.
CATEGORIES.md:19 said categories.test.js "enforces only the engine <->
this-file mapping." False two lines below the number this branch fixed,
since the test now also checks a README sentence. CONTRIBUTING.md:55-57
said "Both must pass" of what is now three checks.
patterns.js header said "type-per-category," which asserts a 1:1
type-to-category mapping that README.md:180 in this same commit denies --
the engine splits vocabulary tiers, so 47 types is not 61 categories. It
traded a stale checkable number for an unguarded wrong claim.
Also split the guard into one test per site, so breaking three sentences
reports three failures instead of aborting on the first, and added a
uniqueness assertion: a regex matching twice would silently verify the
wrong sentence. Failure messages now name the regex.
Watched it fail: three sites broken at once -> 3 failures, exit 1.
Gates green: npm test, self-scan:check, check-pattern-count 61/112.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Third review pass: fix a wrong causal claim, and a guard that reddens a clean repo
The correction note I added to the 3.22.0 entry asserted a causal story
that is false. It said the wrong entry "is why the README then sat at 45
for four days." Checked: the README was already stale at 45 as of
3a34c68 (v3.20.0, 7/29 19:28), roughly 27 hours before the v3.22.0 entry
existed -- and v3.20.0's own entry states its bump correctly ("the engine
goes from 45 to 46"). An accurate changelog sat beside a README that
rotted anyway, so a wrong one did not cause it. That is the stronger
version of this PR's argument and it is also the true one.
Writing an unguarded factual claim into the audit trail, in the PR whose
thesis is that unguarded factual claims rot, is the failure mode this
change exists to fix.
Dropped the uniqueness assertion. Review flagged that a blockquoted
historical number registers as a second match, so following this repo's
own correction convention would fail a repo with no drift in it. The
suggested fix -- filter blockquote lines before matching -- does not work
here either: README.md:180's live statement is itself inside a
blockquote, so filtering removes the site it is meant to guard. Verified
both ways. A guard that reddens a correct repo is worse than the hole it
closes, so the value assertion stands alone and the reasoning is recorded
where the next person will read it.
Watched it behave, four ways:
blockquoted historical number -> exit 0
CATEGORIES.md drift -> exit 1
README drift -> exit 1
README sentence reworded -> exit 1
Gates green: npm test, self-scan:check, check-pattern-count 61/112.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes#62.
The pattern was anchored `^[A-Z][a-z]+`, so it never fired on a Markdown heading — the first character is `#`. It caught the bare-line form while missing the commonest way a heading is written.
Fixing that surfaced a larger false-positive class, found by adversarial review: `matchPatterns` reports `match[0]`, so `##` counted as a token and silently lowered the proper-noun guard from four content words to three. Separately, the guard's function-word test had no position constraint, so a leading `The` satisfied a rule whose own comment specifies a *mid-sentence* "And".
Measured on 81 files that provably predate LLMs: 13 false positives against zero on main, every one opening with `The`. Requiring the function word to be interior eliminates all 13 and leaves the target case firing.
Also rewrote fence detection — counting delimiters inverts on a four-backtick fence wrapping a three-backtick example, which is how you document fences — and removed an unreproducible score claim from the changelog rather than correcting it.
10 fixtures; 6 mutations run, all killed. #67 and #69 verified already fixed in 3.22.0 and closable.
Adds a preservation validator, a self-scan with PROOF.md, over-correction guardrails, a false-positive intake, and a two-class control corpus with the project's first FPR, TPR, and AUC.
Splits Tier 1 into 1A frequency markers and 1B clarity edits, and fixes two detector defects the measurement found (#67, #69).
Findings and open decisions: #70, #71, #72, #73.
The share-post frame that claims a thing has occupied the writer's mind
instead of saying anything about the thing: "the line I keep coming back
to," "I can't stop thinking about this," "rattling around in my head all
week."
Separate category from emotional flatline: flatline claims a feeling,
this claims duration of attention, which is unfalsifiable and
self-flattering in a way a feeling is not.
Precision carve-out: the bare "I keep coming back to X" does not fire,
since a following reason clause makes it legitimate and is not reliably
regex-detectable. Only the noun-anchored frame matches.
- SKILL.md: new rule + P1 severity entry, version 3.20.0
- detector: lingering-attention type, weight 3, medium severity
- CATEGORIES.md: direct-mapping row, 45 -> 46 types
- fixtures: two true positives + the must-not-fire carve-out
- README: 59 -> 60 pattern categories, catalog row 54
- plugin manifest + bundled skill synced
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds "verbatim" to the word table at Tier 3, so it flags only on overuse rather than always. Contributed by @mrlsecurity at Tier 1; moved to Tier 3 on review since the case is redundancy, not AI-specific frequency, and always-flagging over-fires on ordinary human writing.
* v3.18.0: carve out definition-list separator em dashes
An em dash after a bolded lead term or markdown link opening a list item
("**Term** — description", "[label](url) — description") is typography,
not a prose splice, and no longer counts toward the 1-per-1,000-words
rate. The detector exclusion is line-anchored with an optional bullet
marker, so mid-sentence splices still count. Fixture pair added: a
definition-style feature list stays clean; the same dash density in
flowing prose still fires.
This repo's own README and changelog use the separator convention
throughout — that's the strict-context false positive in practice.
Same carve-out independently proposed upstream in blader/humanizer#190.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MYk8KLh4KovTFwW2idcqb
* Require the list marker in the em-dash separator carve-out
Review found the optional bullet marker excluded any line merely
starting with a bold phrase or link before the dash — including the
line-initial "**Bold lead** — full sentence" splice, which is itself
an AI tell and an evasion path for the rate rule. The marker is now
required, matching the SKILL.md prose ("list-item separator"), and
the prose spells out the boundary: only the list-item form qualifies,
mid-sentence and markerless line-initial splices still count, and the
-- substitute is never carved out.
Two boundary fixtures added: numbered-list separators stay clean
(must-not-fire) and markerless bold-lead splices still flag
(must-fire — this one fails without the regex change).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MYk8KLh4KovTFwW2idcqb
* Separator dashes no longer corroborate smart-punct-signature
The co-occurrence signature (curly quotes + em dash + Oxford comma +
zero typos) used bare em-dash presence for its dash leg, so a clean
definition list with separator-only dashes still completed the
signature the rate carve-out had just declared typography. The
separator regex is hoisted to a shared SEPARATOR_DASH_RE and the
signature's em-dash leg now requires a non-separator dash. Fixture:
a curly-quoted, Oxford-comma'd definition list (85 words, fired
before this change) no longer completes the signature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MYk8KLh4KovTFwW2idcqb
* Cover the paren-numbered list style (1)) in the separator carve-out
Review residual: the marker alternation matched 1. but not 1), so a
paren-numbered definition list still counted toward the rate. Numbered
branch is now \d+[.)]; fixture extended with a 4) line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MYk8KLh4KovTFwW2idcqb
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds metaphorical "load-bearing" as a Tier 1 phrase — an LLM tell for labeling any dependency an argument rests on ("load-bearing assumption/claim/invariant"). Matches the hyphenated compound only; unhyphenated "load bearing" is ordinary English.
Construction carve-out exempts attributive use before a literal structural noun (wall, beam, column, joist, truss, member, footing, slab, stud, partition, masonry, lintel, pier, rafter, girder, capacity), with an optional material/position adjective in between. Abstract-capable nouns (structure, element, frame, foundation) are excluded so the metaphor still fires on "the load-bearing structure of his argument".
Known gap: predicative use ("the wall is load-bearing") still flags — the tell lives in the subject, which a lookahead cannot reach. Carve-out design tracked in #56.
Sources: Marek Suppa, Yaniv Bernstein, Developers Digest, HN 48905248, r/ClaudeAI.
* v3.15.0: conversational-register patterns (wall-of-text, recap-flattery)
Adds three catalog entries prompted by a real-world GitHub issue exchange
where a maintainer called out an assisted-sounding reply with "I prefer
to talk human to human" (name/repo withheld):
- Wall-of-text replies: reply-length text delivered as one unbroken
paragraph with no line breaks, where humans in conversational registers
break at thought boundaries instead. Documented as an LLM-judgment
rule after a first-pass detector implementation broke an existing
calibration fixture and, on review, would have fired on any ordinary
short paragraph rather than anything AI-specific.
- Recap-flattery opener: summarizing the reader's own work back at them
with praise before getting to the point. LLM-judgment rule.
- Immaculate typography in casual registers: folded into the existing
Formatting section at the same weak-signal tier as curly quotes, plus
its inverse (preserve a human's typos when editing their casual text).
Catalog goes from 51 to 53 detection categories; the deterministic
engine stays at 45 types (no new type — see the CATEGORIES.md §C note
and the NOTE in patterns.js for why the detector attempt was reverted).
Also catches the Cursor port up from v3.12.0 to v3.15.0, closing a gap
where it had missed the 3.13.0 and 3.14.0 rule changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review fixups: recap-flattery dedup line + wall-of-text carve-out
Two neighbor-standard gaps found in adversarial review of the new
conversational-register rules:
- Recap-flattery opener now explicitly distinguishes from Sycophantic
tone and Acknowledgment loops (every adjacent category carries a
"Distinct from X" clause; without it the skill's audience would file
dupes against those two).
- Wall-of-text replies now carries an explicit legitimate-use carve-out
(a single dense paragraph is correct in blog/docs/formal registers),
per CONTRIBUTING's precision-over-recall rule. Notes why a tolerance
matrix row is the wrong scoping home: a plain issue comment
auto-detects to the `blog` profile, so a skip-on-blog cell would
suppress the rule in the exact register it targets.
Ported to cursor-rules/.mdc and synced to the plugin copy.
Contracts still green: npm test (45 detector types), pattern count 53.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add speculative-scenario-opener detector + tropes.fyi gap-fill (#45)
Reviews tropes.fyi/directory against the skill. Most of the 33 catalogued
tropes were already covered; this adds the gaps that survived a
false-positive review.
New detection category (49 -> 50) + detector type (44 -> 45):
- Speculative scenario openers ("Imagine a world where...") — gated to the
world/future/reality object so instructional "imagine you have an array"
and analytical "consider a scenario where..." stay clean.
LLM-judgment / vocabulary additions (no new category):
- "deeply" added to Tier 2 (magic-adverbs trope), mirrored into the engine.
- Multi-negation countdown folded into the "It's not X, it's Y" rule.
- Invented concept labels folded into Novelty inflation.
- Historical analogy stacking noted under Notability name-dropping.
Version 3.13.0; plugin.json + bundled skill synced; README count bumped;
CATEGORIES.md updated to 45 types; fixtures cover a true positive and a
must-not-fire case for the new detector.
Credit: tropes.fyi and https://gist.github.com/ossa-ma/f3baa9d25154c33095e22272c631f5a1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Gate "deeply" on significance collocations; review fixes
Adversarial review showed unconditional "deeply" in Tier 2 flags
clean human prose (deeply nested JSON + crucial, cares deeply +
nuanced) and can tip a borderline human document across the
HUMAN_ONLY boundary. New TIER2_CONDITIONAL mechanism: everyday
words join a cluster only when their AI significance collocation
matches; literal uses never count. Fixtures pin both sides.
Also: comma-interrupted speculative opener variant ("Imagine, for
a moment, a world where") + fixture; document the accepted
fiction/thought-experiment FP in the regex comment and changelog;
list the Sentence structure contrastive rule under Skill-only in
CATEGORIES.md and mark the invented-labels sub-rule judgment-only
(anti-drift contract gaps found in review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add quietly to Tier 2 word table
* Sync bundled plugin skill to 3.12.0
Regenerate plugins/.../SKILL.md from root SKILL.md via
scripts/sync-plugin-skill.sh and bump plugin.json to match.
Absorbs the missed 3.11.0 sync (plugin.json was still 3.10.0
on main) plus this PR's quietly addition. Fixes the red
plugin-skill-sync check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the "match inflected forms" prose rule. The TIER1 dict matches
exact tokens, so single-form verb entries missed their inflections:
"utilize" didn't catch utilizing/utilized, "showcasing" missed
showcase/showcases/showcased, "leverage" missed leveraging/leveraged,
"underscores" missed underscoring/underscored, and embark/commence/
ascertain/endeavor missed theirs.
Converts those to TIER1_PHRASES inflection-group regexes (matching the
existing `unpack(?:ing)?` style) and removes the now-redundant dict keys.
Deliberately conservative on false-positive risk: `underscor(?:es|ing|ed)`
excludes the bare noun "underscore" (the _ character), and adjectives with
legitimate technical senses (robust, comprehensive) are left untouched for
the LLM/prose path to judge by context. Full suite green; the Rust
technical-post FP guard still holds.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add social endorsement closer detection (#29)
LLMs commonly close LinkedIn/X share posts with a generic curatorial
sign-off teeing up a link — "This one is worth your time:", "Do yourself
a favor and read this", "Thank me later". The endorsement is
demonstrative-anchored and could sit under any link, which is the tell.
- New detector `type` social-cta-closer (engine 43 → 44 types), weighted
like formulaic-opener (8) so the length divisor doesn't wash it out on
a short post. Severity high (P1).
- New SKILL.md "Social endorsement closers" section (catalog 47 → 48),
plus P1-list and tolerance-matrix rows (strict on linkedin).
- Demonstrative anchors ("this one", "do yourself a favor", "won't want
to miss") keep it off plain human endorsements ("the book is worth
reading, but the middle drags"). Distinct from the bare "worth
[verb]ing" word-table entry and from infomercial mid-flow hooks.
- Tests: 3 fixtures (LinkedIn share post, bare-CTA variants, human-prose
negative). Mirrored into cursor-rules + synced plugin skill; counts
updated across README/CATEGORIES/package.json; version 3.8.0 → 3.9.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Harden social-cta-closer regexes against review findings
Two-agent review (code-reviewer + test-analyzer) found that 6 of the 10
closer patterns dropped their anchor and fired on ordinary prose, and
that the contraction branches missed curly apostrophes — so the detector
skipped the exact "you won't" form its own docs ship as the canonical
example (LinkedIn/Word/macOS auto-curl to U+2019).
Precision fixes:
- Apostrophe classes now admit U+2019 across every contraction branch
(won't / don't / can't / one's / you'll), matching the house style of
the existing smart-punct detector.
- "do yourself a favor and read THIS/IT" — gated to a demonstrative
object, so "...read the runbook" no longer matches.
- "miss this" / "bookmark this" — trailing-terminal lookahead, so the
closing-line shape fires but "miss this meeting" / "bookmark this page"
don't.
- "thank me later" / "save this for later" — sentence-initial lookbehind
(kept match text clean), so "she will thank me later" / "I usually save
this for later" don't.
- "trust me, ..." — dropped the bare "this one" branch that caught
"trust me, this one took all afternoon."
Tests: branch-coverage fixture (one positive per regex so a typo fails
loudly), a curly-apostrophe fixture, and a 7-case literal-prose negative
table built from the reviewers' false-positive probes. 66 checks green;
count + plugin-sync gates pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comprehensive doc review after the 3.4->3.7.2 churn. The pattern count
had rotted across files (README said 42, CLAUDE.md said 36, reality is
46 detection categories in SKILL.md).
- Fix the count to 46 and keep it in ONE user-facing place (the README
"46 pattern categories" bullet). README heading reframed to "Pattern
reference" (representative examples, not an exhaustive census), and the
table extended with the 7 missing categories (placeholders, citation
markup, UTM, gap-filling, hyphenated-pair, infomercial, low-TTR).
- De-duplicate the count: CLAUDE.md and detector/CATEGORIES.md now
reference SKILL.md + CI instead of restating their own numbers.
- Add scripts/check-pattern-count.sh + wire into the CI workflow: it
derives the count from SKILL.md's detection ### entries (excluding the
3 writer-side tests) and fails if the README number drifts. Drift is
now a red check, not silent rot.
- Fix stale detector/patterns.js version comment and the marketplace.json
description (was "detection-only mode").
No SKILL.md / version change — docs + CI only.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: make repo SSOT for the detection engine
Extract the 43-category detection engine + its dependency-free node test
harness from the (winding-down) private web-app repo into detector/, so this
open-source repo is now the canonical source of truth for both the rules
(SKILL.md) and their executable expression (detector/patterns.js).
- detector/patterns.js: zero-dep, build-step-free, runs in node>=18 + browser
- detector/patterns.test.js: 60 fixtures, no deps (node:assert)
- package.json + CI: `npm test` on push/PR touching detector/
- detector/CATEGORIES.md: anti-drift map of SKILL.md sections <-> detector types
- README: "Run the detector" section
The cert/attestation/x402/token layer stays behind in the private app; the
engine is a pure function with no knowledge of it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: reclassify smart-punct-signature as a partial skill map
Community PR #15 adds a curly-quotation-marks rule under SKILL.md's
Formatting section, so smart-punct-signature is no longer detector-only.
Move it from section B to a partial mapping in section A, noting that the
detector requires 4-signal co-occurrence while the skill rule treats curly
quotes as a weak corroborating signal — the two agree in spirit but differ
in mechanism.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: enforce CATEGORIES.md contract; docs: contributor guide + count clarity
- detector/categories.test.js: asserts every detector type is documented in
CATEGORIES.md and no stale type rows remain; wired into `npm test`.
- README + CATEGORIES.md: note that the 42 prose patterns, 43 detector types,
and ~50 SKILL.md sections are different counts on purpose — don't reconcile.
- CONTRIBUTING.md + PR template: encode the regex-vs-judgment decision, the
precision-over-recall bias, npm test, the clean-prose bar, and changelog/
version conventions — the things the first community PR surfaced by hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: require contributors to cite sources for empirical claims
Rules that assert how AI or humans write get fact-checked; some turn out
wrong (e.g. the curly-quote premise). Add a "Cite your sources" section to
CONTRIBUTING.md and a checklist item to the PR template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: require detector types to appear in a CATEGORIES.md table, not just prose
Tighten the anti-drift check so a type "documented" by a stray backtick
mention in prose no longer satisfies the gate — it has to land in an actual
mapping-table row, matching the test's own summary line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>