<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Adrian M. Peticila]]></title><description><![CDATA[Notes on writing, marketing, and building practical AI tools. Expect firsthand experiments, useful systems, and the occasional hard-earned lesson.]]></description><link>https://adrianmpeticila.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:02:53 GMT</lastBuildDate><atom:link href="https://adrianmpeticila.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Cadence Gate: make the model prove it wrote like a human]]></title><description><![CDATA[A deterministic linter for Markdown that flags flat sentence rhythm and common AI-writing patterns before publication. Runs locally or in CI.]]></description><link>https://adrianmpeticila.hashnode.dev/the-cadence-gate-make-the-model-prove-it-wrote-like-a-human</link><guid isPermaLink="true">https://adrianmpeticila.hashnode.dev/the-cadence-gate-make-the-model-prove-it-wrote-like-a-human</guid><category><![CDATA[Python]]></category><category><![CDATA[AI]]></category><category><![CDATA[writing]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Adrian M. Peticila]]></dc:creator><pubDate>Thu, 05 Nov 2026 12:51:18 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>The prompt is a request. Nobody checks the answer.</p>
<p>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.</p>
<h2>One file, no network</h2>
<p><code>grai-gate</code> is a single Python file with no dependencies outside the standard library. It imports <code>re</code>, <code>sys</code>, <code>json</code> and <code>math</code>. It never opens a socket, never reads an API key, never writes a cache file.</p>
<p>It runs in three places:</p>
<ul>
<li>A command line: <code>pipx run grai-gate README.md</code></li>
<li>A pre-commit hook, before the commit lands.</li>
<li>A GitHub Action, on the pull request.</li>
</ul>
<p>The analyzer is the same in all three. Only the wrapper changes.</p>
<h2>What it measures</h2>
<p>The core metric is sentence-length burstiness:</p>
<pre><code class="language-plaintext">CV = standard deviation of words per sentence / mean words per sentence
</code></pre>
<p>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.</p>
<p>The thresholds come from a 134,522-post study:</p>
<table>
<thead>
<tr>
<th>Band</th>
<th align="right">CV</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>Flat</td>
<td align="right">under 0.32</td>
<td>Hard flag, the draft fails</td>
</tr>
<tr>
<td>Warning</td>
<td align="right">0.32 to 0.35</td>
<td>Hard flag</td>
</tr>
<tr>
<td>Borderline</td>
<td align="right">0.35 to 0.40</td>
<td>Soft flag, three points</td>
</tr>
<tr>
<td>Human</td>
<td align="right">0.40 and above</td>
<td>No cadence flag</td>
</tr>
</tbody></table>
<p>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.</p>
<h2>The structural checks</h2>
<p>Vocabulary lists age fast. Structure changes slowly, so the gate checks structure.</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>What it catches</th>
</tr>
</thead>
<tbody><tr>
<td>Flat cadence</td>
<td>CV under 0.32</td>
</tr>
<tr>
<td>Dash punctuation</td>
<td>Em dashes, en dashes, double hyphens used as punctuation</td>
</tr>
<tr>
<td>Opening formula</td>
<td>"Here is why" at the start of a sentence</td>
</tr>
<tr>
<td>Contrastive negation</td>
<td>"Not X, but Y" used as a fake distinction</td>
</tr>
<tr>
<td>Arrow cascade</td>
<td><code>-&gt;</code>, <code>=&gt;</code>, arrows used as bullets</td>
</tr>
<tr>
<td>Triadic padding</td>
<td>Three bullets with near-identical sentence lengths</td>
</tr>
<tr>
<td>Weak closer</td>
<td>"Thoughts?" and similar</td>
</tr>
<tr>
<td>Dead lexicon</td>
<td>A short list of words that signal template prose</td>
</tr>
</tbody></table>
<p>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.</p>
<h2>How the score works</h2>
<p>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.</p>
<pre><code class="language-plaintext">score = max(0, 100 - sum(penalties))
pass = score &gt;= 85 and hard_flags == 0
</code></pre>
<p>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.</p>
<h2>What the analyzer ignores</h2>
<p>False positives kill adoption, so the parser strips the parts that are not prose:</p>
<ul>
<li>Fenced code blocks and inline code.</li>
<li>YAML frontmatter at the top of the file.</li>
<li>Link URLs. Link text survives.</li>
<li>Images.</li>
<li>Table separator rows.</li>
<li>Headings for the cadence math. Headings still get scanned for tells.</li>
</ul>
<p>A README full of commands should not fail because the commands look like short sentences.</p>
<h2>The report card</h2>
<p>A failing file prints one compact card:</p>
<pre><code class="language-plaintext">[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 &gt; 0.45) and eliminate flat cadence, zombie words."
- Pro Cockpit: Full Voice DNA &amp; repair workbench at https://grai.page?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=gate_v0.1#developer
</code></pre>
<p>A clean file prints nothing by default. That matters. A gate that prints on every run becomes noise, and noise gets muted.</p>
<h2>Install it in a repo</h2>
<p>One command:</p>
<pre><code class="language-bash">pipx run grai-gate README.md
</code></pre>
<p>Pre-commit:</p>
<pre><code class="language-yaml">repos:
  - repo: https://github.com/adrianpeticila/grai-gate-action
    rev: v0.1.5
    hooks:
      - id: grai-gate
</code></pre>
<p>GitHub Actions:</p>
<pre><code class="language-yaml">- uses: adrianpeticila/grai-gate-action@v0.1.5
  with:
    fail-on-slop: "false"
</code></pre>
<p>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 <code>fail-on-slop: "true"</code> when the team wants a hard gate.</p>
<p>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.</p>
<h2>Why CI and not a writing app</h2>
<p>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.</p>
<p>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.</p>
<h2>Limits</h2>
<p>The gate judges cadence, not truth. It cannot tell whether a claim is correct, a quote is real, or a number is sourced.</p>
<p>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.</p>
<p>And it only sees files. A pull request description typed in a browser never reaches disk, so the gate never scores it.<br /><br />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.</p>
<h2>The point</h2>
<p>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.</p>
<p>The engine is MIT and one file. Run it on your worst README and see whether the number matches your read.</p>
<pre><code class="language-bash">pipx run grai-gate README.md
</code></pre>
<p>Repo: <a href="https://github.com/adrianpeticila/grai-gate-action">https://github.com/adrianpeticila/grai-gate-action</a></p>
<p>If you want the full calibration workbench behind the gate, it lives at <a href="https://grai.page?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=gate_v0.1#developer">https://grai.page?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=gate_v0.1#developer</a></p>
]]></content:encoded></item></channel></rss>