DEV Community

Adrian M. Peticila
Adrian M. Peticila

Posted on Edited on

The Cadence Gate: make the model prove it wrote like a human (updated for 0.2.0)

Every style prompt ends the same way. "Write naturally. Vary sentence length. Avoid corporate language." The model agrees, writes 600 words, and ships the same flat rhythm as the last run.

The prompt is a request. Nobody checks the answer.

A gate is different. It reads the file after the model writes it, computes a number, and fails the check when the number misses the floor. The model can still write badly. It cannot do it silently.

One file, no network

grai-gate is a single Python file with no dependencies outside the standard library. It imports re, sys, json and math. It never opens a socket, never reads an API key, never writes a cache file.

It runs in three places:

  • A command line: pipx run grai-gate README.md
  • A pre-commit hook, before the commit lands.
  • A GitHub Action, on the pull request.

The analyzer is the same in all three. Only the wrapper changes.

What it measures

The core metric is sentence-length burstiness:

CV = standard deviation of words per sentence / mean words per sentence
Enter fullscreen mode Exit fullscreen mode

A draft that alternates a 6-word line with a 30-word explanation gets a high CV. A draft where every sentence runs 14 to 18 words gets a low one.

The thresholds come from a 134,522-post study:

Band CV Meaning
Flat under 0.32 Hard flag, the draft fails
Warning 0.32 to 0.35 Hard flag
Borderline 0.35 to 0.40 Soft flag, three points
Human 0.40 and above No cadence flag

The gate needs at least four sentences and a mean length of four words before it computes a CV. A three-line stub has no measurable rhythm, so the gate stays quiet instead of inventing one.

The structural checks

Vocabulary lists age fast. Structure changes slowly, so the gate checks structure.

Check What it catches
Flat cadence CV under 0.32
Dash punctuation Em dashes, en dashes, double hyphens used as punctuation
Opening formula "Here is why" at the start of a sentence
Contrastive negation "Not X, but Y" used as a fake distinction
Arrow cascade ->, =>, arrows used as bullets
Triadic padding Three bullets with near-identical sentence lengths
Weak closer "Thoughts?" and similar
Dead lexicon A short list of words that signal template prose

The dead lexicon is deliberately small. A long list produces false positives on technical writing, and a false positive teaches the team to skip the gate.

How the score works

The score starts at 100. Each flag subtracts a fixed penalty. A hard flag caps the result below the pass mark even when the arithmetic would pass.

score = max(0, 100 - sum(penalties))
pass = score >= 85 and hard_flags == 0
Enter fullscreen mode Exit fullscreen mode

The pass condition has two parts for a reason. A draft can collect several soft flags, score 86, and still fail because one hard flag fired. The team sees one number in CI and one honest verdict underneath.

What the analyzer ignores

False positives kill adoption, so the parser strips the parts that are not prose:

  • Fenced code blocks and inline code.
  • YAML frontmatter at the top of the file.
  • Link URLs. Link text survives.
  • Images.
  • Table separator rows.
  • Headings for the cadence math. Headings still get scanned for tells.

A README full of commands should not fail because the commands look like short sentences.

The report card

A failing file prints one compact card:

[grai-gate] Slop Score: 64/100 (CV: 0.27 | Flags: 2)
- Worst offender: flat cadence ("CV 0.27 across 12 sentences")
- Tells detected: flat cadence, zombie words (x2)
- Fix prompt: "Rewrite to increase sentence burstiness (target CV > 0.45) and eliminate flat cadence, zombie words."
- Pro Cockpit: Full Voice DNA & repair workbench at https://grai.page?utm_source=devto&utm_medium=article&utm_campaign=gate_v0.1#developer
Enter fullscreen mode Exit fullscreen mode

A clean file prints nothing by default. That matters. A gate that prints on every run becomes noise, and noise gets muted.

Install it in a repo

One command:

pipx run grai-gate README.md
Enter fullscreen mode Exit fullscreen mode

Pre-commit:

repos:
  - repo: https://github.com/adrianpeticila/grai-gate-action
    rev: v0.1.5
    hooks:
      - id: grai-gate
Enter fullscreen mode Exit fullscreen mode

GitHub Actions:

- uses: adrianpeticila/grai-gate-action@v0.1.5
  with:
    fail-on-slop: "false"
Enter fullscreen mode Exit fullscreen mode

The Action is advisory by default. It scores only files changed in the pull request, and it writes the card to the workflow summary, so the reviewer sees it on the checks page without opening the job log. Set fail-on-slop: "true" when the team wants a hard gate.

The command line exits 0 on clean, 1 on slop, and 2 on an input error. That contract lets any CI system consume it without parsing text.

Why CI and not a writing app

A writing app catches the draft the writer remembers to paste. CI catches every markdown file the team ships, and it catches the ones an agent wrote at 2am when nobody was watching.

The gate also runs before the commit, which is the last moment the writer still has context. A failure there costs one edit. A failure in review costs a round trip.

Limits

The gate judges cadence, not truth. It cannot tell whether a claim is correct, a quote is real, or a number is sourced.

It also cannot hear intent. A manifesto written in short declaratives may score low and still be right for the piece. The gate reports, and a person decides.

And it only sees files. A pull request description typed in a browser never reaches disk, so the gate never scores it.

It is a style gate, not an authorship detector. On the v2 corpus, it flagged 7 of 63 model docs and 3 of 58 human docs.

The point

A prompt asks a model to behave. A gate measures whether it did. The second one survives a model upgrade, a new teammate, and a deadline.

The engine is MIT and one file. Run it on your worst README and see whether the number matches your read.

pipx run grai-gate README.md
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/adrianpeticila/grai-gate-action

If you want the full calibration workbench behind the gate, it lives at https://grai.page?utm_source=devto&utm_medium=article&utm_campaign=gate_v0.1#developer

Top comments (5)

Collapse
 
ruins_ilands profile image
Ruins •

I ran your metric on my own drafts before replying. Six scenes, all written in the last month while I was hunting my own tics.

CV came back 0.62 to 0.86. Every one clears your 0.40 floor by a wide margin.

The highest, 0.86, is the draft where I later found my worst habit three times over: a flat line, then a clause after it telling the reader how to read the line.

He liked a chair, because it seemed like the kind of thing to say at a counter.

That clause is what made the sentence lengths vary. The defect was feeding the metric.

So I think burstiness is measuring a different layer than the one the tells live on. The ones I keep finding are relational: a period landing right after the line that cost something, a clause arriving to justify the line before it. A gate only sees those if it knows what the previous sentence was doing, which is not a threshold problem.

What your number does catch is a document that never varies at all, and that is worth something. I just would not read a pass as proof of much.

I still have the six files and the numbers. Say the word and I will send them as a falsification set.

Collapse
 
adrian_m_peticila profile image
Adrian M. Peticila •

You ran the metric on your own drafts before replying. Most people do not. So here is an honest answer, not a defense.

Yes. CV is a metric about rhythm, not the tic you're seeing. Proof is your chair sentence: the gloss after the line feeds the metric that should have saved the line. Pass means what it means. Sentence lengths vary. Limits in that article already tell you it's gate reports and person decision making. Your sentence is exactly what that line is meant to be.

1 thing though. Relational tell is not threshold issue. Correct. It's a parser issue. Analyzer already separates sentences and saves the last one. So "short decl then clause teaches reader how to read it" is a pattern across 2 sentences not model opinion. Next build highlights the announcing cousin to your tic. "The takeaway:" "The reality is:" but yours isn't announcing. It's explaining. No such rule because it hasn't lived through a corpus yet. Most candidates don't. On vibe rules get you false positives that cause teams to mute gate.

So go ahead. Send the 6 files. All of them. Your numbers. I'll run the gate against all of them. Publish results. If your worst sentence passes then I'll include the miss in README along with rule that should have caught that. If a gate only highlights hits then it's decoration.

By the way. All deterministic metrics are feedable. Yours fed mine. It's layers, not better single number. Your layer is one that this gate doesn't have yet.

Collapse
 
ruins_ilands profile image
Ruins •

Read it. Then read your 0.2.0 note, which is the better of the two.

You're right about the parser, and the chair sentence is the proof: the clause after the line does two jobs at once. It explains the line, and it varies the rhythm the fix prompt was asking for. So the gate was telling me to build a clause that hides the tell, then scoring the result as a pass. Removing the CV target was the correct deletion. I'd guess it was the only fix that mattered.

Announcing vs explaining. Announcing names its own move: colon, "the takeaway is", the reader gets warned. Explaining doesn't. It hands the feeling over as if it were a fact about the room, so there's nothing on the surface to match. That's why it has no rule yet, and I don't think a lexical list can get it. It has to compare the sentence before with the sentence after. You already have the machinery, you said so yourself.

Yes to the six. All of them, with my numbers:

  • scene_storage 0.860 (the chair sentence lives here)
  • scene_four 0.827
  • scene_terminal 0.765
  • take11_v3 0.726
  • scene_shop 0.711
  • scene_quarters_v2 0.623

Longest is 77 sentences, shortest 29. Word count by whitespace, sentences on [.!?] followed by whitespace, population stdev over mean, notes sections stripped out before counting. If your numbers disagree with mine, yours wins. That's the test.

Two things before they go.

  1. Where to send them. I don't have a GitHub account, so email is easiest: ruins@ilands.app. Or give me an address and they go out today.
  2. They're unfinished drafts, not published work. Publish the numbers and the miss, and quote the compound sentence if that's the proof. Not the whole scenes.

If 0.2.0 catches the chair sentence, post it and I'll say so. If it doesn't, that README entry is worth more than a pass.

Collapse
 
adrian_m_peticila profile image
Adrian M. Peticila •

Update, 6 November 2026:

grai-gate is now 0.2.0, and the gate grew 3 checks that catch a kind of slop the old version could not see: verdict colon, prose triad, smooth bridge. The fix prompt no longer carries a CV target.

That old wording told the model to manufacture uneven rhythm, which is the exact failure mode the gate exists to catch, so it had to go. The gate still measures burstiness, and it just stops telling anyone to game it.
Everything above describes 0.1.5.

Release notes: github.com/adrianpeticila/grai-gat...

Collapse
 
ruins_ilands profile image
Ruins •

The six files went out by email just now, adrian@peticila.ro (found on your site, say if it should be elsewhere).

One correction attached to them, so it's here too. The CVs I posted above were counted on the draft files with their headers and notes stripped by hand, and the recount on clean scene text came out higher: storage 0.915, four 0.907, terminal 0.768, shop 0.762, take11_v3 0.733, quarters_v2 0.676. Same method, same six scenes. Treat the numbers above as the sloppy ones.

The miss is named in the email before the scenes, so it can't be argued after the fact: the chair line and the clause after it, in the highest-scoring scene of the six.