Skip to content
John Hodge

← Blog

slopscore 0.9.0 adds a dimension for manufactured sincerity

A probe document dense with I have to be honest, honest framing, and truth be told used to score 16.9 out of 100 on slopscore, labeled “low”, with zero evidence spans. All fifteen slop dimensions read 0.0. The document also earned positive credit on human_writing_signals, so the sincerity filler was pushing the score down.

Version 0.9.0 adds a sixteenth dimension, performative_candor, and the same probe now scores 56.0, “elevated”, with 12 spans. A more realistic 243-word passage of the same kind scores 68.6.

pip install -U slopscore-lint

slopscore is a prose linter for AI-slop writing patterns. It reads a file and returns a 0 to 100 score, a per-dimension breakdown, and the exact phrases that triggered each finding. It scores patterns in text and says nothing about who wrote it. Version 0.9.0 is MIT licensed, on PyPI as slopscore-lint, and runs on Python 3.11 and up.

What the old scorer missed

The gap was structural. slopscore already had a dimension for fake insight, insight_signaling, which flags phrases that announce a realization instead of containing one. It already had one for fake evidence, weasel_attribution, covering impersonal attribution and unearned certainty. It had nothing for fake vulnerability: a writer performing the act of being forthright.

The miss compounded. human_signals.py was crediting the word “told” inside truth be told as a human writing signal, so a candor span was scored as evidence of a person rather than against the document. In 0.9.0 that credit is withdrawn, and a span is no longer penalized and rewarded at the same time.

The pattern shows up in assistant output as filler that softens a critique by framing it as a confession, or that lowers a reader’s resistance with confessional styling. The tell is the frame and the density. “Honestly” in speech is ordinary English.

The rules

Ten rules run by default. Three more sit behind --broad, the opt-in tier for higher-false-positive patterns.

RuleSeverityFires on
CANDOR_CONFESSIONAL_FRAMEmediumI have to be honest, let me be candid, if I'm being honest
CANDOR_DISCLOSURE_MARKERmediumtruth be told, full disclosure:, cards on the table
CANDOR_HONEST_ABSTRACT_NOUNmediumhonest framing, honest limits, candid appraisal
CANDOR_HONEST_COMMON_NOUNlowthe honest answer, an honest assessment
CANDOR_ADVERB_PARENTHETICALlowHonestly, and Frankly, as a clause opener
CANDOR_TO_BE_HONESTlowto be honest, in all honesty
CANDOR_SINCERITY_COLLOCATIONlowgenuinely interesting, truly remarkable
CANDOR_NOT_LIGHTLYmediumI don't say this lightly, I'm not going to pretend
CANDOR_ADMISSIONlowI have to admit, used as a transition
CANDOR_PUSH_BACKlowI'm pushing back on that

The canonical case is CANDOR_HONEST_ABSTRACT_NOUN. Writing honest limits where “limitations” would do adds a claim about the writer’s character to a sentence that was about the software. The head noun is the gate: the rule matches honest framing and honest reckoning but not honest work, and conversation is deliberately absent from the list, because “an honest conversation about the budget” is ordinary workplace prose.

Every example above sits in a code span in this file. slopscore’s Markdown reader skips code spans and fenced blocks, so a post about the rules does not trip them.

Why it is a separate dimension

The obvious move was to add these as more insight_signaling rules. That fails, because the genre multipliers point in opposite directions.

Profileperformative_candorinsight_signaling
social0.61.15
marketing1.20.90
blog1.11.15
essay1.11.20
technical0.80.60
academic1.00.60

Conversational “honestly” is native human speech in a social post, so candor is softened there while insight signaling is boosted. Marketing inverts the pair: manufactured sincerity is marketing’s house style, while “first principles” and “the crux” are comparatively harmless in a landing page. A shared dimension could express neither. The 243-word passage above scores 68.6 under the blog profile and 45.9 under social, which is the multiplier doing its job.

The rules that have to stay quiet

The negative cases took more work than the positive ones. tests/test_performative_candor.py asserts that nothing fires on fourteen ordinary sentences, including “He is an honest man who did honest work for thirty years”, “It was an honest mistake; the clerk fixed it the same day”, “The company disclosed the full disclosure schedule under Regulation FD”, “Real talk shows dominated daytime television in 1994”, and the email sign-offs “Sincerely,” and “Yours truly,”. The rules compile under MULTILINE, so an unanchored Sincerely, pattern would have fired on the last line of every email in a corpus.

Two of those fourteen are the ones that matter most:

Honestly speaking, my first winter in Helsinki was very hard.

Frankly speaking, the training was too fast for me.

Both are calques common in non-native English. Neither fires, because CANDOR_ADVERB_PARENTHETICAL requires a comma directly after the adverb. A code comment calls this the rule set’s most important fairness property, and benchmark.jsonl carries non_native rows asserting it rather than leaving the guarantee to unit tests.

Two core rules do fire on the protected slices: CANDOR_TO_BE_HONEST on 1 of 15 non-native rows (0.07) and CANDOR_ADVERB_PARENTHETICAL on 1 of 17 simple-English rows (0.06). Both sit under the project’s 0.10 and 0.15 acceptance bars, and document-level false-positive rate stays at 0.00 on both slices. Those rates are published in MODEL_CARD.md rather than tuned away.

The dimension is also deliberately dull. It saturates at 4.0 weighted hits per 100 words instead of the 3.0 the other packs use, because at 3.0 a single low-severity hit in a 60-word document scores 0.56 and crosses the corroboration gate’s threshold. At 4.0 it scores 0.42 and stays under. Bare sincerity adverbs are --broad-only, the same call already made for bare quantifiers and intensifiers. Running the new code against the old dataset leaves the rule scorer’s catch rate at a 1% false-positive threshold unchanged at 0.800, so the new pack did not cost precision elsewhere.

Where this still fails

performative_candor joins the set of weak dimensions, which means its contribution is multiplied by 0.3 when nothing else in the document is elevated. The consequence is documented in the model card: a 119-word passage that fires 13 candor rules scores 5.6. The dimension is the only thing elevated, so it gets damped, and human_writing_signals saturates at 1.0 on the passage’s dates and figures.

That is the intended behavior, and it is worth understanding before you rely on the score. A concrete, specific, first-person passage carrying heavy candor filler reads as plausible human memoir. The benchmark carries no positive fixture of that shape on purpose, because labeling it slop would teach the evaluation set to punish specific writing. The findings still appear as evidence spans. Only the composite score stays low.

Two more limits. The fairness command builds its scorer with default settings, so it never measures the --broad tier, and CANDOR_BROAD_BARE_CANDOR fires on ESL rows by construction. That unmeasured risk is why the tier is off by default. And these rules carry the source tag AI-tell (2024-2026), meaning reported observation. The lexical markers cite published work (Juzek and Ward, 2024 and Kobak et al., 2025); the candor pack does not, and the file says so.

Upgrading

SCHEMA_VERSION is now 0.9.0 and the JSON report gains a performative_candor entry under dimensions. For pre-commit, bump rev to v0.9.0; the GitHub Action reference jman4162/slopscore@v0 floats forward on its own.

The upgrade note that matters: ten new rules run by default, so scores move on content you have not touched, and a pipeline gated with --fail-on can start failing. Record a baseline first.

slopscore-lint baseline ./content --recursive -o .slopscore-baseline.json
slopscore-lint scan ./content --recursive --baseline-file .slopscore-baseline.json --fail-on-new

0.9.0 also fixes a display bug from 0.8.0: insight_signaling was missing from the label map, so it rendered in neither the Markdown nor the console dimension table while still contributing to the score. The published evaluation tables were refreshed from 128 to 141 examples in the same release.

What this looks like in my own projects

I run slopscore daily. Every draft for this site goes through it before it ships, and it runs as a pre-commit hook and a CI gate at --fail-on high across my repos, with a recorded baseline so existing files do not block a commit. The rules in this release came out of that use: the candor pack exists because I kept reading assistant output that scored clean and read wrong.

This post scores 3.0 out of 100 under the blog profile, with no evidence spans. The only two dimensions above zero are genericity at 0.24 and human_writing_signals, which saturates at 1.0 on the post’s dates and figures. The rule names and example phrases above sit in code spans, which the Markdown reader skips, so that number covers the prose. And the saturated human-writing signal is doing most of the work, which is the same mechanism that lets the 119-word passage above score 5.6.

The wider argument for publishing a linter’s failure modes alongside its numbers is in how to measure a grader; the 5.6 passage above is exactly the kind of case a report card has to name. The original design writeup, covering the rule system, the benchmark, and why authorship detection is the wrong question, is in the slopscore introduction. For how I think about grouping this sort of failure before trying to fix it, see failure taxonomies.

Try it

Scan text in your browser at slopscore.john-hodge.com. The repo, the docs, the changelog, and the PyPI package have the rest.

slopscore is beta software and intentionally conservative. The benchmark and fairness numbers are the project’s own evaluation on small labeled sets, so read them as a sense of scale rather than a guarantee.

slopscore is an independent project I build on my own time. The views are my own and do not represent any current or former employer.

Frequently asked questions

What is performative candor in writing?

Sincerity used as a rhetorical move rather than as information. Three shapes cover most of it: a point framed as a difficult confession, a sincerity adjective bolted to an abstract noun, and manufactured reluctance. The words themselves are ordinary English. The tell is the frame and the density.

Does slopscore detect whether text was written by AI?

No, and it must never be used that way. It scores writing patterns in text, not authorship, and says nothing about who wrote a document. Authorship detectors are unreliable and biased against non-native English speakers, which is why slopscore is not one.

Will upgrading to 0.9.0 change the scores of my existing content?

Yes. Ten new rules run by default, so documents that scored clean under 0.8.1 can produce new findings. If you gate CI with --fail-on, record a baseline with slopscore-lint baseline and switch to --fail-on-new before you upgrade, so the first run does not turn into a cleanup project.

Why is performative_candor a separate dimension instead of more insight_signaling rules?

The genre multipliers point in opposite directions. On the social profile, insight_signaling is boosted to 1.15 while performative_candor is softened to 0.6, because conversational 'honestly' is native human speech. On marketing the pair inverts: 1.2 for candor against 0.9 for insight signaling. One dimension cannot carry both weights.

Does the new dimension over-flag non-native English?

The calques 'Honestly speaking' and 'Frankly speaking' do not fire, because the adverb rule requires a comma directly after the adverb. On the protected evaluation slices, one rule fires on 1 of 15 non-native rows and another on 1 of 17 simple-English rows, both under the project's acceptance bars, with document-level false-positive rate at 0.00. Bare sincerity adverbs are opt-in through --broad for the same reason.

More in Side projects