<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nick Cunningham</title>
    <description>The latest articles on DEV Community by Nick Cunningham (@blazingradar).</description>
    <link>https://dev.to/blazingradar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3916672%2F03372352-e012-4096-861e-e19437b7f5a2.png</url>
      <title>DEV Community: Nick Cunningham</title>
      <link>https://dev.to/blazingradar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blazingradar"/>
    <language>en</language>
    <item>
      <title>The completeness check that runs after your CI verdict</title>
      <dc:creator>Nick Cunningham</dc:creator>
      <pubDate>Wed, 06 May 2026 21:00:06 +0000</pubDate>
      <link>https://dev.to/blazingradar/the-completeness-check-that-runs-after-your-ci-verdict-39de</link>
      <guid>https://dev.to/blazingradar/the-completeness-check-that-runs-after-your-ci-verdict-39de</guid>
      <description>&lt;p&gt;A CI verdict can be correct and still leave behind a broken audit trail.                                                                                                              &lt;/p&gt;

&lt;p&gt;For example: the &lt;code&gt;workflow_run.json&lt;/code&gt; captured as evidence has a &lt;code&gt;head_sha&lt;/code&gt; that does not match the commit the build actually ran on. Tests passed, the merge button unlocked, the&lt;br&gt;&lt;br&gt;
  deployment shipped. The verdict was right; the bundle is fabricable. A reviewer who needs the lineage from the deployed binary back to the source SHA cannot get it.                  &lt;/p&gt;

&lt;p&gt;Most CI tooling does not check for this. The verdict is the contract; the audit trail is whatever happened to land alongside it. &lt;code&gt;evidence-gate&lt;/code&gt; is a small Python library that names &lt;br&gt;
  this surface. It reads files your pipeline already captured, then asks a different question: is the trail behind this verdict complete enough to trust? It does not run CI jobs, fetch&lt;br&gt;
   from GitHub, validate claim semantics, or decide whether the build itself was right. Its only job is the meta-verdict.                                                               &lt;/p&gt;

&lt;p&gt;## The strongest pattern is timestamp recomputation                                                                                                                                   &lt;/p&gt;

&lt;p&gt;Evidence has a time boundary. If raw authority was fetched after the claim time, it cannot honestly support that claim. &lt;code&gt;evidence-gate&lt;/code&gt; records fetch timestamps and recomputes&lt;br&gt;&lt;br&gt;
  whether each fetch happened inside the declared capture window. It does not trust a stored boolean called &lt;code&gt;all_raw_authority_fetched_by_claim_time&lt;/code&gt;. The fetch times need to agree&lt;br&gt;
  with what was recorded. When they disagree, the report fails &lt;code&gt;timestamp_provenance_self_consistent&lt;/code&gt; and the bundle is incomplete.                                                     &lt;/p&gt;

&lt;p&gt;Most CI tools do not check this. The cost to defend against it is one timestamp per fetch and one comparison.                                                                         &lt;/p&gt;

&lt;p&gt;## The completeness check builds on that pattern                                                                                                                                      &lt;/p&gt;

&lt;p&gt;After your CI verdict, &lt;code&gt;evidence-gate&lt;/code&gt; verifies the bundle has every required raw evidence file, every projected evidence file, every lineage entry, every verdict artifact. It&lt;br&gt;&lt;br&gt;
  returns the exact unsatisfied condition names so a caller fails closed without guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;incomplete&lt;/span&gt;
  &lt;span class="na"&gt;unsatisfied_conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;                                                                                                                                                             
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_sha_lineage_proven"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp_provenance_self_consistent"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;                                                                                                                                             
  &lt;span class="pi"&gt;]&lt;/span&gt;                                                                  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output is the receipt. It says, in plain language, that the audit trail behind your green build does not hold together.                                                          &lt;/p&gt;

&lt;p&gt;## Raw evidence and projected evidence are not the same thing                                                                                                                         &lt;/p&gt;

&lt;p&gt;Raw API responses and derived gate facts have different roles. Raw evidence is the immutable capture. Projected evidence is the smaller shape your gate actually consumes. A reviewer &lt;br&gt;
  should be able to follow any projected fact back to the raw files it was derived from. &lt;code&gt;evidence-gate&lt;/code&gt; writes per-file projection lineage that makes this traceable, and the&lt;br&gt;
  completeness check fails if the lineage does not cover every projected file present in the bundle.                                                                                    &lt;/p&gt;

&lt;p&gt;## A bundle should state what it does not claim                                                                                                                                       &lt;/p&gt;

&lt;p&gt;The fourth pattern is the scope bundle: &lt;code&gt;owned_scope&lt;/code&gt;, &lt;code&gt;boundary_limits&lt;/code&gt;, &lt;code&gt;honesty_credits&lt;/code&gt;. A CI run that says nothing about its boundaries is harder to audit than one that&lt;br&gt;&lt;br&gt;
  explicitly disclaims what it does not prove. The package validates the scope triple and fails completeness when the bundle's claims and disclaimed limits are missing or invalid.&lt;/p&gt;

&lt;p&gt;## Minimum viable integration                                      &lt;/p&gt;

&lt;p&gt;Capture your existing GitHub Actions JSON. Write raw evidence and projected evidence into separate directories. Record fetch timestamps with &lt;code&gt;record_fetch&lt;/code&gt;. Run your existing CI&lt;br&gt;&lt;br&gt;
  verdict. Then call &lt;code&gt;check_completeness&lt;/code&gt; on the bundle and refuse to publish a green verdict if the audit trail is incomplete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;evidence_gate.completeness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;check_completeness&lt;/span&gt;

  &lt;span class="n"&gt;evidence_report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_completeness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/path/to/run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         
  &lt;span class="n"&gt;acceptable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complete_with_quarantine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                                                                                                                                 

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing_ci_verdict&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;evidence_report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completeness_verdict&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;acceptable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                                                          
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI passed, but the audit trail is incomplete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                                  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;## What this does not solve                                                                                                                                                           &lt;/p&gt;

&lt;p&gt;Every CI integrity problem. &lt;code&gt;evidence-gate&lt;/code&gt; does not replace branch protection, verify test semantics, or handle every CI platform. It does not prevent compromised runners from&lt;br&gt;&lt;br&gt;
  lying. It is deliberately smaller than that. It catches a common failure mode: a verdict that outruns its evidence.&lt;/p&gt;

&lt;p&gt;CI already has enough tools that say pass or fail. It needs more tools that ask whether the pass or fail can be audited.                                                              &lt;/p&gt;




&lt;p&gt;&lt;code&gt;evidence-gate&lt;/code&gt; is on PyPI: &lt;code&gt;pip install evidence-gate&lt;/code&gt;. Source and audit history at &lt;a href="https://github.com/blazingRadar/evidence-gate" rel="noopener noreferrer"&gt;https://github.com/blazingRadar/evidence-gate&lt;/a&gt;.                         &lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>github</category>
      <category>security</category>
    </item>
  </channel>
</rss>
