<?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: Yusuke Shiki</title>
    <description>The latest articles on DEV Community by Yusuke Shiki (@shikiyusuke).</description>
    <link>https://dev.to/shikiyusuke</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078263%2F82f5374c-930c-4704-8462-b1fb7ba36019.png</url>
      <title>DEV Community: Yusuke Shiki</title>
      <link>https://dev.to/shikiyusuke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shikiyusuke"/>
    <language>en</language>
    <item>
      <title>Before Asking an AI Coding Agent to Fix a Bug, Verify the Bug Exists</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:25:58 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/before-asking-an-ai-coding-agent-to-fix-a-bug-verify-the-bug-exists-5and</link>
      <guid>https://dev.to/shikiyusuke/before-asking-an-ai-coding-agent-to-fix-a-bug-verify-the-bug-exists-5and</guid>
      <description>&lt;p&gt;Before asking an AI coding agent to “fix this bug,” there is one question worth asking first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the bug actually exist?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An issue says it does. A review points it out. Someone reports seeing it in another environment.&lt;/p&gt;

&lt;p&gt;Any of those is a good reason to investigate. None of them, by itself, proves that the problem currently exists exactly as described.&lt;/p&gt;

&lt;p&gt;It may already be fixed. It may only happen in a particular version or environment. The reported behavior may even be expected.&lt;/p&gt;

&lt;p&gt;As I have started relying more on AI coding agents, I have become more deliberate about checking this step.&lt;/p&gt;

&lt;h2&gt;
  
  
  A wrong premise can still produce a perfectly reasonable implementation
&lt;/h2&gt;

&lt;p&gt;Suppose you give an agent this task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Problem X is happening. Find the cause and fix it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent can inspect the code, reason under the assumption that X exists, and produce a plausible fix.&lt;/p&gt;

&lt;p&gt;That is not necessarily an AI capability problem.&lt;/p&gt;

&lt;p&gt;If the input says “X is happening,” solving the problem under that premise is a reasonable thing to do.&lt;/p&gt;

&lt;p&gt;The part I want to question happens one step earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bug report
 ↓
Fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bug report
 ↓
Does the reported problem actually occur?
 ↓
Fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before implementation, verify the premise the implementation depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  I had a review finding that changed after I measured it
&lt;/h2&gt;

&lt;p&gt;This happened publicly while I was working on &lt;a href="https://github.com/shiki-yusuke/spec-lane/pull/28" rel="noopener noreferrer"&gt;PR #28&lt;/a&gt; in my OSS project, &lt;code&gt;spec-lane&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The PR added a gate that can execute an external verification command. During review, a strong claim came up around timeout behavior:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even with SIGKILL, the CLI timeout itself may hang.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I had accepted that statement as the premise, the next task would have been straightforward: implement a fix for a hanging timeout.&lt;/p&gt;

&lt;p&gt;Instead, I measured it first.&lt;/p&gt;

&lt;p&gt;Under the three conditions I tested, I could not reproduce a call exceeding the configured deadline.&lt;/p&gt;

&lt;p&gt;That did not mean there was no problem.&lt;/p&gt;

&lt;p&gt;I found a different boundary.&lt;/p&gt;

&lt;p&gt;Even after the direct child process exits, a descendant process that still holds the inherited stdout/stderr pipes can affect how long &lt;code&gt;spawnSync&lt;/code&gt; waits.&lt;/p&gt;

&lt;p&gt;In one measurement, the direct child exited immediately while a grandchild remained alive for four seconds. The call took 3112 ms.&lt;/p&gt;

&lt;p&gt;But when the grandchild remained alive for ten seconds, a two-second timeout returned in 2035 ms, and a 0.5-second timeout returned in 501 ms.&lt;/p&gt;

&lt;p&gt;In the conditions I measured, the configured deadline still held.&lt;/p&gt;

&lt;p&gt;So these were two different statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original strong claim:
SIGKILL can allow the timeout itself to hang past its deadline.

What I could actually observe:
Descendants can influence waiting and timeout reporting within the deadline.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They sound similar, but they lead to different implementation work.&lt;/p&gt;

&lt;p&gt;I did not add a new “hang fix.”&lt;/p&gt;

&lt;p&gt;Instead, I changed the specification and tests to describe the boundary I could actually measure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measuring the premise changed the problem I was about to implement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One important qualification: this incident was not automatically stopped by &lt;code&gt;spec-lane&lt;/code&gt;'s &lt;code&gt;premise_evidence&lt;/code&gt; gate. It was a manual review-and-measurement decision made while developing the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put “verify” between “reported” and “fix”
&lt;/h2&gt;

&lt;p&gt;I now think about the flow like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issue / review finding
          ↓
Verify the premise
          ↓
      Reproduced?
       ↙       ↘
     No         Yes
     ↓           ↓
Stop or        Spec
re-scope         ↓
              Implement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the issue reproduces, proceed with the specification and implementation.&lt;/p&gt;

&lt;p&gt;If it does not, do not force yourself—or an agent—to find a fix for the problem you expected to see.&lt;/p&gt;

&lt;p&gt;Sometimes the right answer is to stop.&lt;/p&gt;

&lt;p&gt;Sometimes, as in PR #28, the investigation reveals a different, measurable problem. In that case, change the scope to the problem that actually exists.&lt;/p&gt;

&lt;p&gt;This extra step matters more as implementation becomes cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  spec-lane can record that decision before implementation
&lt;/h2&gt;

&lt;p&gt;I wanted this decision to exist in the development workflow rather than only in someone's memory.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;spec-lane&lt;/code&gt; 0.9.0, an intent can contain an optional &lt;code&gt;premise_evidence&lt;/code&gt; record.&lt;/p&gt;

&lt;p&gt;For example, when you decide that a change requires premise verification but fail to reproduce the reported problem:&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;premise_evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;live&lt;/span&gt;
  &lt;span class="na"&gt;reproduced&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reported&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;were&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;executed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;against&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;but&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reported&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;behavior&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;was&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;observed."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;required: true&lt;/code&gt; records the decision that premise verification applies to this change.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reproduced: false&lt;/code&gt; records that the premise was not confirmed.&lt;/p&gt;

&lt;p&gt;With that current record, &lt;code&gt;spec-lane&lt;/code&gt; refuses the &lt;code&gt;1_intent -&amp;gt; 2_spec&lt;/code&gt; transition. A failed &lt;code&gt;lane advance&lt;/code&gt; does not modify &lt;code&gt;lane-state.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The purpose is not to have a CLI magically detect whether an AI is wrong.&lt;/p&gt;

&lt;p&gt;The purpose is simpler.&lt;/p&gt;

&lt;p&gt;Once the workflow has explicitly recorded “we could not confirm this problem,” do not silently continue into specification and implementation as if it had been confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recorded evidence is not the same thing as truth
&lt;/h2&gt;

&lt;p&gt;There is an important limit to this mechanism.&lt;/p&gt;

&lt;p&gt;Suppose the file says:&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;reproduced&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI can verify that &lt;code&gt;true&lt;/code&gt; was recorded.&lt;/p&gt;

&lt;p&gt;It cannot verify that someone honestly reproduced the bug.&lt;/p&gt;

&lt;p&gt;The same applies to:&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;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI can verify the field and its value. It cannot know whether somebody actually inspected a live system.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spec-lane&lt;/code&gt; also enforces a minimum length for the evidence text. That is a structural threshold, not a quality score.&lt;/p&gt;

&lt;p&gt;A longer evidence string is not automatically better evidence.&lt;/p&gt;

&lt;p&gt;The boundary I use is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CLI proves recorded shape and transition behavior, not real-world truth.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system can mechanically inspect the record and decide whether a transition is allowed.&lt;/p&gt;

&lt;p&gt;The truth of the real-world observation remains outside that guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Missing premise evidence does not automatically block a lane
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;premise_evidence&lt;/code&gt; itself is optional.&lt;/p&gt;

&lt;p&gt;If it is absent, the current CLI emits a warning rather than a hard error. A warning alone does not prevent the transition to the next phase.&lt;/p&gt;

&lt;p&gt;That is deliberate.&lt;/p&gt;

&lt;p&gt;The CLI cannot determine whether every change actually requires premise verification.&lt;/p&gt;

&lt;p&gt;A bug already observed directly by a human is different from an unverified report. A feature addition may not have a “does this bug exist?” premise at all.&lt;/p&gt;

&lt;p&gt;Making the CLI decide applicability on its own would introduce another form of false certainty.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;spec-lane&lt;/code&gt; starts enforcing after a human or agent has decided that premise verification applies and recorded the result.&lt;/p&gt;

&lt;p&gt;It is a mechanical backstop for an explicit decision, not an oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster implementation makes the question before implementation more important
&lt;/h2&gt;

&lt;p&gt;AI coding agents have made the implementation step dramatically faster.&lt;/p&gt;

&lt;p&gt;That makes the decision immediately before implementation more consequential.&lt;/p&gt;

&lt;p&gt;What are we changing?&lt;/p&gt;

&lt;p&gt;Why are we changing it?&lt;/p&gt;

&lt;p&gt;Does the problem this change depends on actually occur?&lt;/p&gt;

&lt;p&gt;If that premise is wrong, faster implementation only lets us move in the wrong direction faster.&lt;/p&gt;

&lt;p&gt;So before handing an issue or review finding to an agent and saying “fix it,” I now want one more step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this problem actually exist?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it reproduces, proceed.&lt;/p&gt;

&lt;p&gt;If it does not, stop.&lt;/p&gt;

&lt;p&gt;If the investigation reveals a different problem, change the scope to what you can actually observe.&lt;/p&gt;

&lt;p&gt;That is the role premise evidence plays in &lt;code&gt;spec-lane&lt;/code&gt;: preserve that decision before implementation and provide a mechanical backstop against ignoring an explicitly failed premise.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/shiki-yusuke/spec-lane" rel="noopener noreferrer"&gt;shiki-yusuke/spec-lane&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Japanese version: &lt;a href="https://zenn.dev/yusuke_shiki/articles/2c3bb57fe6f801" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you use AI coding agents, where in your workflow do you verify that the problem they are about to fix is real?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Real Deployment Exposed Two Contract Defects Before I Froze the Contract</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Wed, 26 Aug 2026 10:14:59 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/a-real-deployment-exposed-two-contract-defects-before-i-froze-the-contract-2pao</link>
      <guid>https://dev.to/shikiyusuke/a-real-deployment-exposed-two-contract-defects-before-i-froze-the-contract-2pao</guid>
      <description>&lt;p&gt;A green CI run does not prove that the artifact built from reviewed source matches what production later exposes.&lt;/p&gt;

&lt;p&gt;The build job may have produced one directory while the deploy job selected another. A file may have been added after review. The live manifest and representative file bytes may match the digests sealed at build time—or they may not.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/shiki-yusuke/release-evidence" rel="noopener noreferrer"&gt;release-evidence&lt;/a&gt; and its &lt;a href="https://github.com/shiki-yusuke/ai-agent-skills-playbook/tree/main/contracts/release-evidence/v0" rel="noopener noreferrer"&gt;v0 contract&lt;/a&gt; to connect those claims. It records source and build provenance, seals artifact digests, appends deployment events, and checks a bounded part of the live production target.&lt;/p&gt;

&lt;p&gt;But I did not freeze the first reviewed version of the contract.&lt;/p&gt;

&lt;p&gt;Deployment contracts are unusually good at lying while every synthetic fixture stays green. A&lt;br&gt;
schema can be internally consistent and still describe a topology that does not exist. Two digest&lt;br&gt;
fields can both look reasonable until an adapter has to decide which one to compute first.&lt;/p&gt;

&lt;p&gt;Keeping the contract in draft until a real adapter exercised it exposed two defects.&lt;/p&gt;
&lt;h2&gt;
  
  
  The goal was not another “build succeeded” record
&lt;/h2&gt;

&lt;p&gt;The first target was a static dashboard deployed through GitHub Pages.&lt;/p&gt;

&lt;p&gt;The evidence chain needed to support a narrower statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reviewed source
  -&amp;gt; one built artifact
  -&amp;gt; that artifact deployed to production
  -&amp;gt; live manifest read back
     + representative file bytes spot-checked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The draft contract used a sealed release bundle and an append-only event ledger. The bundle held&lt;br&gt;
the source commit and tree, toolchain evidence, and artifact digests. The ledger appended events&lt;br&gt;
such as &lt;code&gt;prepared&lt;/code&gt;, &lt;code&gt;deployed&lt;/code&gt;, and &lt;code&gt;verified&lt;/code&gt;; current state was derived by folding those events,&lt;br&gt;
not by editing a mutable status row.&lt;/p&gt;

&lt;p&gt;There were schemas, semantic checks, and accepted and rejected fixtures. The draft passed review.&lt;br&gt;
The pull request still said that v0 would remain unfrozen until a real adapter had exercised it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defect 1: the contract expected a preview that did not exist
&lt;/h2&gt;

&lt;p&gt;The first transition graph expected a preview environment before production.&lt;/p&gt;

&lt;p&gt;This scheduled GitHub Pages deployment has no real preview tier. The workflow builds the static&lt;br&gt;
dashboard and deploys it directly to the production Pages site.&lt;/p&gt;

&lt;p&gt;I could have labeled a CI workspace “preview” and made the ledger satisfy the graph. That would&lt;br&gt;
have been worse than an explicit gap: an evidence system would be changing the description of&lt;br&gt;
reality to pass its own validator.&lt;/p&gt;

&lt;p&gt;The contract changed instead.&lt;/p&gt;

&lt;p&gt;A direct &lt;code&gt;prepared -&amp;gt; deployed|production&lt;/code&gt; transition is now valid only when it carries&lt;br&gt;
&lt;code&gt;preview_skipped: true&lt;/code&gt; and a closed reason code. The same flag is rejected on the normal preview&lt;br&gt;
path. The absence of a preview is an explicit property of the target, not a silently missing event.&lt;/p&gt;

&lt;p&gt;GitHub Pages was not defective. The contract's assumption that every target had a preview was.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defect 2: the manifest and bundle could not be finalized
&lt;/h2&gt;

&lt;p&gt;The static site exposes a &lt;code&gt;release-manifest.json&lt;/code&gt; containing a digest for each deployed path. A&lt;br&gt;
read-back can fetch the manifest and compare a live file with the value it records.&lt;/p&gt;

&lt;p&gt;The first design also made the release bundle carry the byte digest of that manifest, while the&lt;br&gt;
manifest carried the digest of the bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle
  contains sha256(release-manifest.json bytes)

release-manifest.json
  contains sha256(bundle)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundle required the finished manifest. The manifest required the finished bundle. Neither&lt;br&gt;
value could be computed first.&lt;/p&gt;

&lt;p&gt;The cycle looks obvious when written as two arrows. It was much less obvious while reviewing the&lt;br&gt;
two record shapes separately. It surfaced when the real adapter had to generate files in an actual&lt;br&gt;
order.&lt;/p&gt;

&lt;p&gt;The deployed manifest now contains only its schema version and content map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-evidence/v0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"index.html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not repeat the enclosing bundle digest. During production read-back, the adapter&lt;br&gt;
canonicalizes the fetched &lt;code&gt;content&lt;/code&gt;, computes its SHA-256 digest, compares that value with the&lt;br&gt;
static-site artifact in the sealed bundle, and spot-checks live file bytes against the manifest.&lt;/p&gt;

&lt;p&gt;Removing the mutual reference made the evidence flow one-directional.&lt;/p&gt;
&lt;h2&gt;
  
  
  I fixed both defects, then exercised production
&lt;/h2&gt;

&lt;p&gt;On August 22, 2026, the corrected adapter ran in the real workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shiki-yusuke/agent-metrics-harvester/actions/runs/32572501427" rel="noopener noreferrer"&gt;Workflow run 32572501427&lt;/a&gt;&lt;br&gt;
built the dashboard, deployed it to GitHub Pages, fetched the live manifest, compared its content&lt;br&gt;
map, spot-checked a representative live file, and persisted the evidence ledger.&lt;/p&gt;

&lt;p&gt;The ledger contains this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepared
  -&amp;gt; deployed | production | preview_skipped=true
  -&amp;gt; verified | production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recomputing the digest from the live manifest's canonicalized content produced the same value as&lt;br&gt;
the artifact sealed in the bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sha256:35e4b7ee5a17ca64f0851e54d83385f2c478b4d04e3210a2e08639fecd3b6d0c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after that receipt existed did the contract replace its synthetic dashboard fixture with the&lt;br&gt;
real deployed bundle and freeze &lt;code&gt;release-evidence/v0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The order mattered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;review the draft
  -&amp;gt; let a real adapter break it
  -&amp;gt; repair the contract
  -&amp;gt; verify a production read-back
  -&amp;gt; freeze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Freezing first would have forced the adapter either to invent a preview or immediately violate the&lt;br&gt;
contract I had just declared stable.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second success verified the repeat-run link
&lt;/h2&gt;

&lt;p&gt;After the first production exercise succeeded, I found another issue in the adapter. A later&lt;br&gt;
release should derive &lt;code&gt;previous_release_id&lt;/code&gt; from the last verified production event. The first&lt;br&gt;
implementation still needed that repeat-run chain logic.&lt;/p&gt;

&lt;p&gt;I keep this separate from the two contract defects. It did not change the transition model or the&lt;br&gt;
digest definition. It was an adapter defect revealed by asking what the second release must do.&lt;/p&gt;

&lt;p&gt;After that fix reached main, scheduled&lt;br&gt;
&lt;a href="https://github.com/shiki-yusuke/agent-metrics-harvester/actions/runs/32615972524" rel="noopener noreferrer"&gt;run 32615972524&lt;/a&gt;&lt;br&gt;
created a second production release from commit &lt;code&gt;6f0b5c8&lt;/code&gt;, which includes&lt;br&gt;
&lt;a href="https://github.com/shiki-yusuke/agent-metrics-harvester/pull/7" rel="noopener noreferrer"&gt;PR #7&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The new bundle records the link explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release_id:
  agent-metrics-dashboard@32615972524-1

previous_release_id:
  agent-metrics-dashboard@32572501427-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first release was the preceding verified production release; its sealed bundle digest was&lt;br&gt;
&lt;code&gt;sha256:63392d1a635e68f670f07dcb717ad7e1437f8282be82a1dd8c0ab2c6a664c9e1&lt;/code&gt;.&lt;br&gt;
The second release recorded &lt;code&gt;prepared&lt;/code&gt; at &lt;code&gt;2026-08-23T03:41:48.031Z&lt;/code&gt;, &lt;code&gt;deployed | production&lt;/code&gt;&lt;br&gt;
at &lt;code&gt;2026-08-23T03:42:20.467Z&lt;/code&gt;, and &lt;code&gt;verified | production&lt;/code&gt; at&lt;br&gt;
&lt;code&gt;2026-08-23T03:42:20.659Z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its ledger persisted the same three-event sequence under one sealed bundle digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepared
  -&amp;gt; deployed | production | preview_skipped=true
  -&amp;gt; verified | production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sealed bundle digest is&lt;br&gt;
&lt;code&gt;sha256:7dabd0a36be4308cb1c70387d1097dcb37c7fde3be3aee991f8287371bc661df&lt;/code&gt;.&lt;br&gt;
Recomputing the static-site digest from the live manifest content produced&lt;br&gt;
&lt;code&gt;sha256:ac69214db9909b1f1d97bd1f806214034172e0dac780e1c1470434f158b70d49&lt;/code&gt;,&lt;br&gt;
and the live &lt;code&gt;index.html&lt;/code&gt; bytes matched the manifest entry&lt;br&gt;
&lt;code&gt;sha256:ce99db76efd55bace6b50bd9ba88caa76e1dbef841c76e87dcb07b7d88380c9e&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Auditing the persisted collection against the frozen v0 contract reported two bundles, six&lt;br&gt;
events, and no problems. The repeat-run link is now observed in production. The order still&lt;br&gt;
matters: the first success did not prove that future linkage until a second release exercised it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two later runs preserved the verified chain
&lt;/h2&gt;

&lt;p&gt;Two subsequent scheduled runs created production releases from the same PR #7 merge commit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Release ID&lt;/th&gt;
&lt;th&gt;Previous verified release&lt;/th&gt;
&lt;th&gt;Sealed bundle digest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/shiki-yusuke/agent-metrics-harvester/actions/runs/32687508604" rel="noopener noreferrer"&gt;32687508604&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-metrics-dashboard@32687508604-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-metrics-dashboard@32615972524-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sha256:fd2ee5b9ef6d531b1c377257a60ced576a949964be186fd9b1c6337278b2a8a4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/shiki-yusuke/agent-metrics-harvester/actions/runs/32806015116" rel="noopener noreferrer"&gt;32806015116&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-metrics-dashboard@32806015116-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-metrics-dashboard@32687508604-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sha256:94d9f3ee15e2238eb84dcae6dbe26bb4f69afd434ce8a1491be636e24d40b8e0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first follow-up chain recorded &lt;code&gt;prepared&lt;/code&gt; at &lt;code&gt;2026-08-24T03:45:20.910Z&lt;/code&gt;, &lt;code&gt;deployed |&lt;br&gt;
production&lt;/code&gt; at &lt;code&gt;2026-08-24T03:45:54.808Z&lt;/code&gt;, and &lt;code&gt;verified | production&lt;/code&gt; at&lt;br&gt;
&lt;code&gt;2026-08-24T03:45:55.135Z&lt;/code&gt;. The second recorded the same sequence at&lt;br&gt;
&lt;code&gt;2026-08-25T03:40:22.482Z&lt;/code&gt;, &lt;code&gt;2026-08-25T03:40:53.785Z&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;2026-08-25T03:40:53.929Z&lt;/code&gt;. Within each chain, all three events carry the same sealed bundle&lt;br&gt;
digest shown in the table. The Actions logs also record &lt;code&gt;read-back verified&lt;/code&gt; against the&lt;br&gt;
corresponding static-site artifact digest.&lt;/p&gt;

&lt;p&gt;These runs add evidence that the &lt;code&gt;previous_release_id&lt;/code&gt; chain and read-back continued to operate.&lt;br&gt;
They do not prove that every future deployment will succeed, that every deployment is safe, or&lt;br&gt;
that an external user adopted the tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Matching digests do not prove everything
&lt;/h2&gt;

&lt;p&gt;The integrity level of &lt;code&gt;release-evidence/v0&lt;/code&gt; is &lt;code&gt;digest_only&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no signature, SLSA, or SBOM claim. GitHub Actions credentials, pinned actions, the tool&lt;br&gt;
checkout, and the production host remain inside the trust boundary.&lt;/p&gt;

&lt;p&gt;A successful read-back shows that the live manifest's content map matched the static-site&lt;br&gt;
artifact digest sealed in the bundle, and that a representative live file matched its manifest&lt;br&gt;
entry at verification time.&lt;/p&gt;

&lt;p&gt;It does not show that every production file was fetched and compared byte for byte, or that the&lt;br&gt;
host cannot change afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content integrity
!= semantic correctness

digest match
!= complete supply-chain security

successful read-back
!= future correctness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The v0 ledger assumes one writer. The first adapter is specific to a GitHub Pages/static-site&lt;br&gt;
deployment. It is not a universal release database or an automatic promotion system.&lt;/p&gt;

&lt;p&gt;Stating those limits does not weaken the receipt. It prevents a content-digest proof from silently&lt;br&gt;
turning into a broader supply-chain claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make reality answer before the contract becomes permanent
&lt;/h2&gt;

&lt;p&gt;Schema review and synthetic fixtures were necessary. They remain valuable regression guards after&lt;br&gt;
the fixes.&lt;/p&gt;

&lt;p&gt;They were not enough to justify freezing a deployment contract.&lt;/p&gt;

&lt;p&gt;Real targets have topology that diagrams omit. Artifacts have generation order that isolated&lt;br&gt;
schemas hide. Production read-back crosses boundaries that a local build never touches.&lt;/p&gt;

&lt;p&gt;The point was not to delay stability. It was to keep the contract breakable until the last cheap&lt;br&gt;
moment to discover that it described something impossible.&lt;/p&gt;

&lt;p&gt;The real adapter found a preview environment that did not exist and a digest cycle that could not&lt;br&gt;
be computed. Both were contract problems, not cosmetic implementation details.&lt;/p&gt;

&lt;p&gt;Before freezing a deployment-evidence schema, I now want one answer from the production path:&lt;br&gt;
can an honest adapter actually satisfy this contract without inventing state?&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;release-evidence/v0&lt;/code&gt;, asking that question before the freeze is what found the two defects.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI-assistance disclosure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used ChatGPT and Codex to help structure, edit, and translate this article. I reviewed the release IDs, digests, timestamps, implementation details, and claim boundaries against the recorded evidence before publication.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Could Measure Claude and Codex Usage. I Still Couldn't Honestly Assign It to a Task.</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Sat, 22 Aug 2026 21:20:35 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/i-could-measure-claude-and-codex-usage-i-still-couldnt-honestly-assign-it-to-a-task-2ghj</link>
      <guid>https://dev.to/shikiyusuke/i-could-measure-claude-and-codex-usage-i-still-couldnt-honestly-assign-it-to-a-task-2ghj</guid>
      <description>&lt;p&gt;Once you use Claude Code or Codex for real work, a total usage number stops being enough. You want to know which change consumed it.&lt;/p&gt;

&lt;p&gt;I did not build &lt;code&gt;agent-cost&lt;/code&gt; because I had missed the existing token and cost trackers. I knew about multi-agent reporting CLIs, local dashboards, and OpenTelemetry-style observability stacks. I had even built a similar view in Notion before.&lt;/p&gt;

&lt;p&gt;The problem appeared when I tried to use that kind of reporting in an operational workflow. I needed agent logs to stay on the machine. I wanted a small runtime dependency surface, custom metrics I could audit, and a machine-readable result that another tool could consume. Most importantly, I needed session measurement and task attribution to remain two different claims.&lt;/p&gt;

&lt;p&gt;I did not need another universal dashboard. I needed a boundary underneath the dashboard that could answer: is this number supported well enough to enter task accounting?&lt;/p&gt;

&lt;h2&gt;
  
  
  A measurement layer below the UI
&lt;/h2&gt;

&lt;p&gt;Different tools optimize for different jobs. A broad CLI such as &lt;code&gt;ccusage&lt;/code&gt; is useful when coverage across agents matters. Local interfaces such as &lt;code&gt;token-tracker&lt;/code&gt; or &lt;code&gt;AgentMeter&lt;/code&gt; are a better fit for visual exploration of projects, sessions, subagents, and tools. An OpenTelemetry stack is the natural choice for fleet-level metrics, logs, and traces.&lt;/p&gt;

&lt;p&gt;Those are not inferior versions of &lt;code&gt;agent-cost&lt;/code&gt;. They serve different use cases and trust models.&lt;/p&gt;

&lt;p&gt;The layer I wanted looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local observations
  -&amp;gt; auditable normalized facts
  -&amp;gt; explicit pricing status
  -&amp;gt; caller-selected sessions
  -&amp;gt; task-attribution policy
  -&amp;gt; optional dashboard / Notion / spec-lane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent-cost&lt;/code&gt; reads logs that Claude Code and Codex CLI have already written locally. It normalizes each usage event into a fact with a model, token kind, timestamp, and count. At runtime it makes no network calls and declares no Python runtime dependencies. Its price catalog has a version and SHA-256 digest, both carried into machine-readable output.&lt;/p&gt;

&lt;p&gt;That “zero-network” claim is deliberately limited to runtime behavior. Installing from PyPI still means trusting a registry, installer, build backend, Python runtime, and operating system. The tool also needs access to the source logs. The design narrows runtime data egress and dependency surface; it does not make the supply chain disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  A session is observable. A task is another claim.
&lt;/h2&gt;

&lt;p&gt;There is an attractive shortcut when building task-level cost reports:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure usage in a time window.&lt;/li&gt;
&lt;li&gt;Find the issues or branches active during that window.&lt;/li&gt;
&lt;li&gt;Apportion the total by working time or commit count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This always produces numbers that add up. But the agreement comes from the allocation rule, not from an observation.&lt;/p&gt;

&lt;p&gt;One session can cover several tasks. One task can span several sessions. A branch can stay unchanged while the operator investigates a different issue or reviews someone else's work. Elapsed time does not describe the computational weight of prompts and tool calls.&lt;/p&gt;

&lt;p&gt;The invariant I wanted was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Session usage is observable. Session-to-task attribution is a separate claim.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;agent-cost measure&lt;/code&gt; accepts only session IDs selected by its caller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-cost measure &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &amp;lt;session-a&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &amp;lt;session-b&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not infer a task from a branch, pull request, or timestamp. A workflow that already owns the task-to-session binding passes the corresponding session set.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;spec-lane&lt;/code&gt; adapter invokes &lt;code&gt;agent-cost&lt;/code&gt; as a subprocess and checks the JSON, the &lt;code&gt;measure/v1&lt;/code&gt; protocol version, the schema, and forbidden personal dimensions. &lt;code&gt;agent-cost&lt;/code&gt; does not learn what the task is. The caller that knows the task selects the sessions.&lt;/p&gt;

&lt;p&gt;If a session crosses tasks and there is no defensible way to split it, I would rather leave that usage unattributed than manufacture a precise-looking allocation. Unknown is pending evidence, not zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same rule applies to prices
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agent-cost&lt;/code&gt; carries uncertainty instead of smoothing it away.&lt;/p&gt;

&lt;p&gt;An unknown model is &lt;code&gt;unpriced&lt;/code&gt;. A Claude cache write without a TTL breakdown is priced at the cheaper five-minute rate and labeled &lt;code&gt;lower_bound&lt;/code&gt;. Codex logs do not expose cache-write tokens, so the tool does not invent a zero-valued cache-write row. Malformed events, unreadable files, and decreasing cumulative counters remain visible in &lt;code&gt;data_quality&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;“Fail closed” does not mean every imperfect input crashes the command. It means unsupported pricing or attribution does not quietly become a confirmed value downstream.&lt;/p&gt;

&lt;p&gt;On August 23, 2026, I reran the published &lt;code&gt;coding-agent-cost 0.1.0&lt;/code&gt; package in temporary &lt;code&gt;uvx&lt;/code&gt; directories. Its doctor command found the local sources and loaded catalog version &lt;code&gt;2026-07-29&lt;/code&gt;. The explicit unknown-model path still rejected a made-up model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uvx &lt;span class="nt"&gt;--refresh&lt;/span&gt; &lt;span class="nt"&gt;--from&lt;/span&gt; coding-agent-cost &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    agent-cost rates show --model model-not-in-catalog
[unpriced] no rate entry for 'model-not-in-catalog'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A numeric zero next to an unpriced row is not a claim that the usage was free. Consumers must inspect &lt;code&gt;pricing_status&lt;/code&gt; and &lt;code&gt;unpriced_tokens&lt;/code&gt;, then choose a policy: exclude the value from a headline, stop the workflow, or supply a verified catalog.&lt;/p&gt;

&lt;p&gt;The output field is &lt;code&gt;estimated_cost_usd&lt;/code&gt;, not a bill. Allowances, contracts, credits, and batch usage are not fully recoverable from local logs. The number is a list-price estimate attached to observed tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the smaller boundary buys
&lt;/h2&gt;

&lt;p&gt;There are intentional limits. &lt;code&gt;agent-cost&lt;/code&gt; alone will not label a session as belonging to an issue. Local execution does not remove installation-time supply-chain risk or the need to trust local log access.&lt;/p&gt;

&lt;p&gt;In exchange, each layer has a narrower claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local logs support session usage facts.&lt;/li&gt;
&lt;li&gt;A versioned catalog supports an estimated price.&lt;/li&gt;
&lt;li&gt;Unsupported prices remain &lt;code&gt;unpriced&lt;/code&gt; or &lt;code&gt;lower_bound&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The caller owns task binding as separate evidence.&lt;/li&gt;
&lt;li&gt;Ambiguous usage is not silently apportioned just to complete a total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not an argument against dashboards. Use a dashboard when visual exploration is the job. Use OpenTelemetry when fleet observability is the job. Use a small accounting primitive when you need a subprocess contract and want measurement to remain separate from attribution policy.&lt;/p&gt;

&lt;p&gt;Keeping an unknown visible is not a failure to measure. It is how the next layer avoids false confidence.&lt;/p&gt;

&lt;p&gt;Start with the 60-second path in &lt;a href="https://github.com/shiki-yusuke/agent-cost" rel="noopener noreferrer"&gt;&lt;code&gt;agent-cost&lt;/code&gt;&lt;/a&gt;. If you also need a workflow to own task attribution, see &lt;a href="https://github.com/shiki-yusuke/spec-lane" rel="noopener noreferrer"&gt;&lt;code&gt;spec-lane&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codex</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Writing evidence-linked docs exposed two missing regression tests</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Fri, 14 Aug 2026 23:15:18 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/writing-evidence-linked-docs-exposed-two-missing-regression-tests-3524</link>
      <guid>https://dev.to/shikiyusuke/writing-evidence-linked-docs-exposed-two-missing-regression-tests-3524</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a crosspost of the &lt;a href="https://github.com/shiki-yusuke/evidence-docs/blob/main/docs/articles/writing-evidence-linked-docs-exposed-two-missing-regression-tests.md" rel="noopener noreferrer"&gt;canonical version on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Problem
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://github.com/shiki-yusuke/agent-cost" rel="noopener noreferrer"&gt;&lt;code&gt;agent-cost&lt;/code&gt;&lt;/a&gt;, a small open-source CLI that reads local Claude Code / Codex CLI usage logs and estimates token cost. I wanted documentation for it more trustworthy than a hand-written README: not prose claiming "the reader handles the TTL cache-write breakdown correctly," but claims that each point at the exact test or source line backing them, checked against real git history so the pointer can't silently go stale.&lt;/p&gt;

&lt;p&gt;To do that I used a second tool I've been building, &lt;a href="https://github.com/shiki-yusuke/evidence-docs" rel="noopener noreferrer"&gt;&lt;code&gt;evidence-docs&lt;/code&gt;&lt;/a&gt;, to build a &lt;strong&gt;claim corpus&lt;/strong&gt; against &lt;code&gt;agent-cost&lt;/code&gt;: 17 individual &lt;code&gt;observation&lt;/code&gt;s grouped into 5 &lt;code&gt;topic&lt;/code&gt;s (pricing-catalog validation rules, the cache-write lower-bound behavior, unpriced-fact handling, decimal arithmetic for money, and the &lt;code&gt;measure&lt;/code&gt; command's v1 contract). Each observation is one statement — a behavior, an invariant, a decision record — with a &lt;code&gt;claim_kind&lt;/code&gt;, an &lt;code&gt;epistemic_status&lt;/code&gt;, and one or more &lt;code&gt;provenance&lt;/code&gt; entries naming the exact file, test, or spec section it's backed by, plus a &lt;code&gt;content_digest&lt;/code&gt; of that source at a specific commit.&lt;/p&gt;

&lt;p&gt;Building that corpus is what surfaced this post's actual finding. To write an honest &lt;code&gt;provenance&lt;/code&gt; entry for a behavior claim, I had to point at the test that exercises it — reading the test, not just the source, for every claim. Doing that for the cache-write TTL breakdown logic and the pricing-status aggregation logic turned up two places where the claim I wanted to make ("the code does X in this case") was true by reading the code, but had &lt;strong&gt;no test asserting it&lt;/strong&gt;. Both went into &lt;code&gt;docs/claims/gaps.yaml&lt;/code&gt; as &lt;code&gt;GAP-01&lt;/code&gt; and &lt;code&gt;GAP-02&lt;/code&gt;, and both became small, scoped pull requests that added regression tests with zero implementation changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why it mattered
&lt;/h2&gt;

&lt;p&gt;Neither gap was a live bug — that's the whole point of this post. &lt;code&gt;agent-cost&lt;/code&gt;'s existing test suite already covered the two extremes of each piece of logic; what was missing was the &lt;em&gt;middle&lt;/em&gt; case, the one that's easy to reason yourself past while staring at the code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GAP-01&lt;/strong&gt; (&lt;code&gt;agent_cost/readers/claude.py&lt;/code&gt;, &lt;code&gt;parse_session_facts&lt;/code&gt;): when a Claude Code usage event's &lt;code&gt;cache_creation&lt;/code&gt; field is a dict, the reader computes &lt;code&gt;leftover = cache_creation_input_tokens - (ephemeral_5m + ephemeral_1h)&lt;/code&gt; and emits it as a &lt;code&gt;cache_write_unknown&lt;/code&gt; fact if positive. Existing tests covered a breakdown that exactly accounts for the total, and no breakdown at all — but not a breakdown dict that's &lt;em&gt;present and partial&lt;/em&gt;, where the two counters sum to less than the total. That's a real, reachable path with no assertion on what it produces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAP-02&lt;/strong&gt; (&lt;code&gt;agent_cost/aggregate.py&lt;/code&gt;, &lt;code&gt;_STATUS_RANK&lt;/code&gt; / &lt;code&gt;build_rows&lt;/code&gt;): a row's &lt;code&gt;pricing_status&lt;/code&gt; should be the worst status among its facts, ordered &lt;code&gt;unpriced&lt;/code&gt; (0) &amp;lt; &lt;code&gt;lower_bound&lt;/code&gt; (1) &amp;lt; &lt;code&gt;priced&lt;/code&gt; (2). The existing test only mixed an &lt;code&gt;unpriced&lt;/code&gt; fact with a &lt;code&gt;priced&lt;/code&gt; fact. No test built a row from a &lt;code&gt;lower_bound&lt;/code&gt; fact (e.g. a &lt;code&gt;cache_write_unknown&lt;/code&gt; token, priced as an explicit floor) mixed with a plain &lt;code&gt;priced&lt;/code&gt; fact to confirm the row lands on &lt;code&gt;lower_bound&lt;/code&gt;, not &lt;code&gt;priced&lt;/code&gt; — the half of the ordering that isn't "obviously" covered by the unpriced case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If either had silently broken in a later refactor — reordering &lt;code&gt;_STATUS_RANK&lt;/code&gt;'s values, or changing the leftover math — nothing in CI would have caught it. The cost-estimation output would have quietly started under- or over-reporting cost floors, without a single red test.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why the obvious fix was insufficient
&lt;/h2&gt;

&lt;p&gt;The obvious response to "I want good docs for this repo" is: write a good README, or write good docstrings, and be careful. I already had both — &lt;code&gt;agent-cost&lt;/code&gt;'s README has a "What this measures, and what it doesn't" section, and &lt;code&gt;price_fact()&lt;/code&gt; has a docstring explaining the lower-bound design decision. Careful prose is necessary but not sufficient, for two reasons that showed up directly here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prose doesn't force you to check the thing it describes against a test.&lt;/strong&gt; I could (and did, informally) describe the TTL-breakdown behavior accurately in a docstring without ever asking "is there a test where the breakdown dict is present but incomplete?" Writing prose doesn't create that question; writing a &lt;code&gt;provenance&lt;/code&gt; entry that must name a specific test does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A doc that "looks read" and a doc that's actually checked are indistinguishable from the outside.&lt;/strong&gt; Nothing in a normal README tells a reader which sentences were verified by running something versus which were a best guess — every claim carries the same visual weight.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the fix isn't "write better docs" in the abstract — it's structural: every behavior claim needs a machine-checkable pointer to what backs it, narrow enough that "no test covers this case" becomes visible while writing it, not something discovered later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Invariant
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;evidence-docs&lt;/code&gt; enforces this as a schema-level invariant on every &lt;code&gt;observation&lt;/code&gt;, not a style guideline. &lt;strong&gt;&lt;code&gt;epistemic_status&lt;/code&gt;&lt;/strong&gt; must be one of a fixed vocabulary (from &lt;code&gt;execution_verified&lt;/code&gt; down to &lt;code&gt;model_inference&lt;/code&gt; / &lt;code&gt;single_source_observation&lt;/code&gt; / &lt;code&gt;recorded_decision&lt;/code&gt;) — you have to say how strongly the claim was checked, not just assert it. &lt;strong&gt;&lt;code&gt;provenance&lt;/code&gt;&lt;/strong&gt; is a list of entries, each with a &lt;code&gt;source_kind&lt;/code&gt; (&lt;code&gt;source&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;spec&lt;/code&gt;, or &lt;code&gt;review_memory&lt;/code&gt;), a repo-relative &lt;code&gt;uri&lt;/code&gt;, a &lt;code&gt;selector&lt;/code&gt; (which test/function/section), a &lt;code&gt;content_digest&lt;/code&gt;, and the &lt;code&gt;repo_commit&lt;/code&gt; the digest was taken against.&lt;/p&gt;

&lt;p&gt;Neither field is optional, and both are validated structurally: unknown &lt;code&gt;source_kind&lt;/code&gt; values are rejected outright, and &lt;code&gt;content_digest&lt;/code&gt; is checked against the actual git blob at the declared commit, not the working tree. Two of &lt;code&gt;agent-cost&lt;/code&gt;'s own observations (OBS-004, the cache-write-TTL claim, and OBS-010, the pricing-status-ranking claim) exist because writing their &lt;code&gt;provenance&lt;/code&gt; forced me to go find "the test that proves this," and in both cases the honest answer was "there isn't one for this specific sub-case" — which is what &lt;code&gt;gaps.yaml&lt;/code&gt; records.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Design (evidence-docs, minimally)
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;evidence-docs&lt;/code&gt; corpus is a directory (conventionally &lt;code&gt;docs/claims/&lt;/code&gt;) with: &lt;code&gt;id-registry.yaml&lt;/code&gt; (every &lt;code&gt;topic_id&lt;/code&gt;/&lt;code&gt;observation_id&lt;/code&gt; used anywhere must be pre-registered here, closing typo/duplicate ID bugs at generate time); &lt;code&gt;topics/*.yaml&lt;/code&gt; and &lt;code&gt;observations/*.yaml&lt;/code&gt; (one file per topic by convention, not enforced); and &lt;code&gt;gaps.yaml&lt;/code&gt;, a ledger of gaps found (&lt;code&gt;gap_found: true&lt;/code&gt;) &lt;em&gt;or&lt;/em&gt; explicitly searched for and not found (&lt;code&gt;gap_found: false&lt;/code&gt;), each tagged with a &lt;code&gt;taxonomy&lt;/code&gt; — &lt;code&gt;test_missing&lt;/code&gt; for both GAP-01 and GAP-02 here, &lt;code&gt;independent_re_search_no_drift&lt;/code&gt; for a third entry, GAP-03, where I re-checked three README pricing claims against the current &lt;code&gt;rates.json&lt;/code&gt; and found no drift, recorded as a negative result rather than omitted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;evidence-docs validate docs/claims --repo-commit &amp;lt;sha&amp;gt;&lt;/code&gt; runs full structural + provenance verification with no output written (CI-friendly); &lt;code&gt;evidence-docs generate&lt;/code&gt; does the same and then deterministically writes a human-readable &lt;code&gt;site/index.md&lt;/code&gt; and an AI-facing &lt;code&gt;bundle/*.jsonl&lt;/code&gt; + &lt;code&gt;manifest.json&lt;/code&gt;. &lt;code&gt;evidence-docs context docs/claims --query '{"seeds": {"paths": [...]}, "token_budget": ...}'&lt;/code&gt; selects a relevant subset of claims from the bundle for a given set of source paths, for feeding into an LLM context window without shipping the whole corpus. &lt;code&gt;--generated-at&lt;/code&gt; and &lt;code&gt;--repo-commit&lt;/code&gt; are always explicit CLI arguments, never derived from &lt;code&gt;datetime.now()&lt;/code&gt; or &lt;code&gt;git rev-parse&lt;/code&gt; at run time, so the same corpus and arguments always produce byte-identical output.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Fail behavior
&lt;/h2&gt;

&lt;p&gt;The parts of &lt;code&gt;evidence-docs&lt;/code&gt; that made this finding possible are the parts designed to fail loudly on drift or forgery, not pass silently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Digest verification targets the declared commit's git blob&lt;/strong&gt; (&lt;code&gt;git show &amp;lt;repo_commit&amp;gt;:&amp;lt;uri&amp;gt;&lt;/code&gt;, hashed with sha256), not the current worktree file. This closes a real bypass: checking only the worktree's current hash would still let someone edit the file, recompute the digest, and leave &lt;code&gt;repo_commit&lt;/code&gt; on the old SHA. A worktree/declared-commit mismatch is a warning (repos move on after a snapshot); a mismatch against the &lt;em&gt;declared commit's own&lt;/em&gt; blob is always fatal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown &lt;code&gt;source_kind&lt;/code&gt; is a hard rejection, not a skip.&lt;/strong&gt; An earlier design silently skipped unrecognized kinds — exactly what a forged or typo'd entry would want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--repo-commit&lt;/code&gt; must equal every &lt;code&gt;valid_at_commit&lt;/code&gt; and &lt;code&gt;provenance[].repo_commit&lt;/code&gt; corpus-wide&lt;/strong&gt;, with no exceptions, since a corpus is a snapshot of one commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two independent path-traversal guards&lt;/strong&gt; sit in front of the digest check, since &lt;code&gt;git show&lt;/code&gt; never touches the filesystem and needs its own check separate from worktree path resolution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this checks whether a &lt;em&gt;statement&lt;/em&gt; is true (see Boundary, below) — but it does mean a fake or lazy &lt;code&gt;provenance&lt;/code&gt; entry gets caught at &lt;code&gt;validate&lt;/code&gt; time rather than trusted forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verification: gap to merged test, with receipts
&lt;/h2&gt;

&lt;p&gt;Both gaps followed the same path: recorded in &lt;code&gt;gaps.yaml&lt;/code&gt;, then closed by a small PR that added a test and changed no implementation code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GAP-01 → &lt;a href="https://github.com/shiki-yusuke/agent-cost/pull/2" rel="noopener noreferrer"&gt;&lt;code&gt;shiki-yusuke/agent-cost#2&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; ("test: cover the partial TTL-breakdown leftover branch in the Claude reader"), merged 2026-08-14. Adds &lt;code&gt;test_cache_creation_partial_ttl_breakdown_leftover_is_unknown&lt;/code&gt; to &lt;code&gt;tests/test_reader_claude.py&lt;/code&gt;: a &lt;code&gt;cache_creation&lt;/code&gt; dict with &lt;code&gt;ephemeral_5m_input_tokens=150&lt;/code&gt; and &lt;code&gt;ephemeral_1h_input_tokens=100&lt;/code&gt; against a &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; total of 300, asserting the reader emits &lt;code&gt;cache_write_5m=150&lt;/code&gt;, &lt;code&gt;cache_write_1h=100&lt;/code&gt;, &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;cache_write_unknown=50&lt;/code&gt; for the 50-token leftover. No implementation changes — the leftover math already behaved this way; the PR's own description says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAP-02 → &lt;a href="https://github.com/shiki-yusuke/agent-cost/pull/3" rel="noopener noreferrer"&gt;&lt;code&gt;shiki-yusuke/agent-cost#3&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; ("test: pin the worst-status row aggregation order for pricing_status"), merged 2026-08-14. Adds four tests to &lt;code&gt;tests/test_aggregate.py&lt;/code&gt;: &lt;code&gt;test_build_rows_all_priced_facts_mark_row_priced&lt;/code&gt; (baseline), &lt;code&gt;test_build_rows_mixed_priced_and_lower_bound_marks_row_lower_bound&lt;/code&gt; (the previously-untested half — a &lt;code&gt;lower_bound&lt;/code&gt; fact plus a &lt;code&gt;priced&lt;/code&gt; fact should mark the row &lt;code&gt;lower_bound&lt;/code&gt;), &lt;code&gt;test_build_rows_mixed_lower_bound_and_unpriced_marks_row_unpriced&lt;/code&gt;, and &lt;code&gt;test_build_rows_worst_status_ranking_is_order_independent&lt;/code&gt; (same three facts constructed in three different orders, same resulting status). Again, no implementation changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both PRs are public and merged; the diffs and CI runs are the actual evidence for this post, not my summary of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Boundary — what this does not show
&lt;/h2&gt;

&lt;p&gt;To be precise about scope, since it's easy to over-read a post like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is not a bug report.&lt;/strong&gt; Both gaps were missing regression tests for code that already behaved correctly — nothing shipped broken, nothing was fixed except test coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;evidence-docs&lt;/code&gt; does not check whether a claim is true.&lt;/strong&gt; Its own schema doc says so plainly: "the prose claim itself is never checked against the code... validation checks structure and provenance shape, not truth." What forced these two gaps into the open was &lt;em&gt;my own&lt;/em&gt; process of trying to write an honest provenance pointer for each claim, not an automated truth-checker. A careless author could still write &lt;code&gt;execution_verified&lt;/code&gt; next to a claim backed only by reading the source; the schema's &lt;code&gt;negation_check&lt;/code&gt; field helps catch that but is optional and unenforced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gaps.yaml&lt;/code&gt; completeness is not verified either.&lt;/strong&gt; A corpus with zero recorded gaps is syntactically valid; whether the author actually looked for contradictions isn't something the tool can check. GAP-03 in this corpus (a re-check of three README pricing claims against &lt;code&gt;rates.json&lt;/code&gt; that found no drift) exists because I chose to record a negative result, not because anything required it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is an n=1 experience report on one small repo, not an evaluation.&lt;/strong&gt; No general effect size, no hit rate across repos, no claim that repeating this will surface N more gaps. It found two missing regression tests in one corpus-authoring pass over one repo I maintain. Trying the same process elsewhere might find zero, or something entirely different — the claim here is about what happened and how to reproduce the &lt;em&gt;process&lt;/em&gt;, not what you should expect to get out of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Try it yourself
&lt;/h2&gt;

&lt;p&gt;The minimal path that reproduces the "write a claim, discover you can't honestly back it" moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;evidence-docs

evidence-docs init docs/claims
&lt;span class="c"&gt;# scaffolds topics/, observations/, id-registry.yaml, gaps.yaml, README.md&lt;/span&gt;

&lt;span class="c"&gt;# ... author one observation for a behavior you believe is true, with a&lt;/span&gt;
&lt;span class="c"&gt;#     provenance entry naming the exact test that proves it. If you can't&lt;/span&gt;
&lt;span class="c"&gt;#     name one, that's the gap. ...&lt;/span&gt;

evidence-docs validate docs/claims &lt;span class="nt"&gt;--repo-commit&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; will reject an unregistered ID, an unknown &lt;code&gt;source_kind&lt;/code&gt;, or a digest that doesn't match the git blob at the commit you passed — all useful on their own — but the actual gap-finding step is upstream of the tool: it happens while you're trying to fill in the &lt;code&gt;provenance&lt;/code&gt; field honestly, before you ever run &lt;code&gt;validate&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shiki-yusuke/agent-cost" rel="noopener noreferrer"&gt;&lt;code&gt;shiki-yusuke/agent-cost&lt;/code&gt;&lt;/a&gt; — the repo the corpus was written against&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shiki-yusuke/evidence-docs" rel="noopener noreferrer"&gt;&lt;code&gt;shiki-yusuke/evidence-docs&lt;/code&gt;&lt;/a&gt; — the claim-corpus tool&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shiki-yusuke/agent-cost/pull/2" rel="noopener noreferrer"&gt;&lt;code&gt;agent-cost#2&lt;/code&gt;&lt;/a&gt; — GAP-01 fix (merged)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/shiki-yusuke/agent-cost/pull/3" rel="noopener noreferrer"&gt;&lt;code&gt;agent-cost#3&lt;/code&gt;&lt;/a&gt; — GAP-02 fix (merged)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/claims/gaps.yaml&lt;/code&gt; in &lt;code&gt;agent-cost&lt;/code&gt; — the gap ledger (GAP-01/02/03)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/schema.md&lt;/code&gt; in &lt;code&gt;evidence-docs&lt;/code&gt; — the trust-boundary write-up referenced throughout&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>testing</category>
      <category>documentation</category>
    </item>
  </channel>
</rss>
