<?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>The Cost Wasn't $0: My AI Coding Measurement Timed Out at 30 Seconds</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:04:38 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/the-cost-wasnt-0-my-ai-coding-measurement-timed-out-at-30-seconds-198e</link>
      <guid>https://dev.to/shikiyusuke/the-cost-wasnt-0-my-ai-coding-measurement-timed-out-at-30-seconds-198e</guid>
      <description>&lt;p&gt;I was recording usage and cost for AI coding sessions when I noticed something odd: one session had no cost entry at all.&lt;/p&gt;

&lt;p&gt;My first assumption was that the measurement tool had failed. But when I ran &lt;code&gt;agent-cost&lt;/code&gt; directly against the same Claude Code session, it completed successfully in 36.454 seconds. Through the &lt;code&gt;spec-lane&lt;/code&gt; adapter, however, the same measurement failed after 30.019 seconds.&lt;/p&gt;

&lt;p&gt;The reason was simple: the caller had a 30-second timeout.&lt;/p&gt;

&lt;p&gt;What mattered more than the timeout itself was what happened afterward. The failed measurement did not produce a &lt;code&gt;$0&lt;/code&gt; record. It produced no measurement record at all.&lt;/p&gt;

&lt;p&gt;That distinction became the main lesson from this incident:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Measurement failed != $0&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Failing to measure something is not the same as measuring it as zero.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same session succeeded directly and failed through the adapter
&lt;/h2&gt;

&lt;p&gt;I first reran the session from Issue #42 directly with &lt;code&gt;agent-cost&lt;/code&gt;:&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;--format&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit: 0
wall: 36.454s
JSON parse: success
protocol: measure/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The measurement took longer than 30 seconds, but it completed normally.&lt;/p&gt;

&lt;p&gt;I then passed the same session through an &lt;code&gt;AgentCostTelemetryAdapter&lt;/code&gt; built from the pre-fix Git revision &lt;code&gt;cca0338...&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30.019s
TelemetryImportFailed
Command failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time it stopped around the 30-second mark.&lt;/p&gt;

&lt;p&gt;For comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct &lt;code&gt;agent-cost measure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;36.454s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-fix adapter&lt;/td&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;30.019s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-fix adapter&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;35.410s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pre-fix adapter had this default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the underlying measurement could finish in roughly 36 seconds, while the caller terminated it after 30.&lt;/p&gt;

&lt;p&gt;There was another problem: the error message was only &lt;code&gt;Command failed&lt;/code&gt;. It did not say that a timeout had occurred or that the process had been stopped after 30 seconds.&lt;/p&gt;

&lt;p&gt;From that message alone, it was difficult to distinguish a failure inside &lt;code&gt;agent-cost&lt;/code&gt; from a timeout imposed by its caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  No record is not a zero-dollar record
&lt;/h2&gt;

&lt;p&gt;I wanted to know what a failed measurement left behind.&lt;/p&gt;

&lt;p&gt;For this part, I used a separate controlled reproduction with a deliberately short timeout. This was not the same real session used in the timing comparison above.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;lane calibrate&lt;/code&gt; failed during measurement, the result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CalibrationObservation: 0
cost_ledger: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It did &lt;strong&gt;not&lt;/strong&gt; write a record such as:&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;"cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;There was simply no record.&lt;/p&gt;

&lt;p&gt;The source code matched that behavior. If &lt;code&gt;adapter.measure()&lt;/code&gt; fails, the command returns before generating the observation and before writing the cost ledger.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;emit-metrics&lt;/code&gt; against the empty ledger produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;built 0 record(s), coverage.status=no_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;no_data&lt;/code&gt; does not mean "the measured cost was zero." It means there is no measured record available.&lt;/p&gt;

&lt;p&gt;That matters once the numbers are aggregated. If ten failed measurements in a hundred tasks are silently converted to &lt;code&gt;$0&lt;/code&gt;, the resulting average can look lower than the evidence actually supports.&lt;/p&gt;

&lt;p&gt;Zero is a value. Missing measurement evidence is a different state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was more than changing 30 seconds to 180
&lt;/h2&gt;

&lt;p&gt;PR #43 changed the default timeout from 30 seconds to 180 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;DEFAULT_AGENT_COST_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also added a CLI override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--agent-cost-timeout-ms &amp;lt;n&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The option is available for four commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;calibrate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emit-metrics&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usage-import&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The accepted range is from 1ms through &lt;code&gt;3_600_000ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The timeout error was also made explicit. For example, a controlled 200ms timeout now reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timed out after 200 ms (killed with SIGTERM)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes it much easier to tell the difference between a measurement command failing on its own and the caller terminating it because of its deadline.&lt;/p&gt;

&lt;p&gt;There is still a boundary here: 180 seconds is not a strict wall-clock guarantee. The implementation sends &lt;code&gt;SIGTERM&lt;/code&gt; when the timeout is reached. A child process that does not terminate immediately after that signal can continue beyond the nominal timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the fix, a 38-second run persisted the measurement
&lt;/h2&gt;

&lt;p&gt;Increasing the timeout is useful only if the measurement can actually finish and its evidence is saved.&lt;/p&gt;

&lt;p&gt;I reran another historical session and lane snapshot using the v0.10.1 CLI with the default 180-second timeout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit: 0
wall: 38.01s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time the process did more than return successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CalibrationObservation: 1
cost_ledger: scope=lane
emit-metrics: coverage.status=complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The observation was created, the cost ledger was written, and metrics coverage reached &lt;code&gt;complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is also a historical PR record reporting a successful run of roughly 39 seconds. I treat that separately: the raw log containing the exact historical wall time is no longer available, so that reported value is not the same evidence as the independently reproduced 38.01-second run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Completing a measurement does not prove the number is correct
&lt;/h2&gt;

&lt;p&gt;Another boundary became important while checking the fix.&lt;/p&gt;

&lt;p&gt;What I verified here is that the measurement process can complete, return a protocol payload, and persist its result.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; prove all of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;that the estimated dollar amount is correct&lt;/li&gt;
&lt;li&gt;that the pricing catalog is correct&lt;/li&gt;
&lt;li&gt;that a session has been attributed to the correct task&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In fact, the historical record and the new reproduction produced different token counts and estimated costs. They also used different &lt;code&gt;agent-cost&lt;/code&gt; versions.&lt;/p&gt;

&lt;p&gt;I did not investigate the cause of that difference as part of this timeout work.&lt;/p&gt;

&lt;p&gt;Measurement completion, cost accuracy, and task attribution are separate claims. A successful run only establishes the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;exit 0&lt;/code&gt; can still mean measurement incomplete
&lt;/h2&gt;

&lt;p&gt;There was one more edge case worth keeping.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;calibrate&lt;/code&gt; exits with code 2 when measurement fails. &lt;code&gt;usage-import&lt;/code&gt;, however, has a different contract.&lt;/p&gt;

&lt;p&gt;In some measurement-failure cases it records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;measurement-incomplete
matched: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and writes no ledger entry, while still returning &lt;code&gt;exit 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So this is not a safe equivalence either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit 0 == measurement succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit code, measurement state, and persisted evidence need to be interpreted separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't collapse unknown into zero
&lt;/h2&gt;

&lt;p&gt;The immediate bug was a 30-second timeout. The more useful lesson for me was about state representation.&lt;/p&gt;

&lt;p&gt;For measurement systems, these are not interchangeable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0
unknown
failed
not measured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collapsing all of them into zero makes aggregation easier. It also destroys information about what actually happened.&lt;/p&gt;

&lt;p&gt;This applies beyond AI coding cost: test results, evaluation scores, usage metrics, and observability pipelines can all run into the same problem.&lt;/p&gt;

&lt;p&gt;The rule I kept from Issue #42 is still the simplest description:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Measurement failed != $0&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As more of an engineering workflow becomes automated, preserving the difference between "the value was zero" and "we never obtained a value" becomes more important, not less.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/shiki-yusuke/spec-lane/issues/42" rel="noopener noreferrer"&gt;Issue #42 — agent-cost timeout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/shiki-yusuke/spec-lane/pull/43" rel="noopener noreferrer"&gt;PR #43 — default 180s / timeout override / diagnostic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/shiki-yusuke/spec-lane/releases/tag/v0.10.1" rel="noopener noreferrer"&gt;spec-lane v0.10.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A note on the version reported in the original issue
&lt;/h3&gt;

&lt;p&gt;Issue #42 records the historical runtime as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lane 0.9.1, cca0338 on main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Git, however, &lt;code&gt;cca0338...&lt;/code&gt; is the peeled commit for the &lt;code&gt;v0.10.0&lt;/code&gt; tag, and &lt;code&gt;packages/cli/package.json&lt;/code&gt; at that revision reports version &lt;code&gt;0.10.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The 30.019-second reproduction in this article was built from the &lt;code&gt;cca0338...&lt;/code&gt; source revision.&lt;/p&gt;

&lt;p&gt;I have not established why the historical executable reported &lt;code&gt;0.9.1&lt;/code&gt;, so I do not claim that the original incident occurred on the v0.10.0 CLI.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A YAML Comment Truncated an AI Coding Success Criterion — and the Gate Still Passed</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Tue, 29 Sep 2026 01:37:56 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/a-yaml-comment-truncated-an-ai-coding-success-criterion-and-the-gate-still-passed-15jg</link>
      <guid>https://dev.to/shikiyusuke/a-yaml-comment-truncated-an-ai-coding-success-criterion-and-the-gate-still-passed-15jg</guid>
      <description>&lt;p&gt;When I hand implementation work to a coding agent, I want the definition of “done” to exist before the implementation does.&lt;/p&gt;

&lt;p&gt;That usually means writing explicit success criteria, keeping them in a machine-readable form, and checking them again at a verification gate.&lt;/p&gt;

&lt;p&gt;I do this in an open-source tool I maintain called &lt;code&gt;spec-lane&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While dogfooding it, I ran into a small YAML detail with a much larger implication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;part of a success criterion was interpreted as a YAML comment before it ever reached the verification gate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The more interesting part was that the gate still passed.&lt;/p&gt;

&lt;p&gt;The bug was not in the string comparison itself. The information had already disappeared from the value being compared.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started with one line of YAML
&lt;/h2&gt;

&lt;p&gt;The minimal case reported in Issue #45 looked like this:&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;success&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ledger has exactly one PhaseGate row&lt;/span&gt; &lt;span class="c1"&gt;# include the negative case too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent described in the issue included the part after the &lt;code&gt;#&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But this is an unquoted YAML plain scalar.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;yaml@2.9.0&lt;/code&gt;, it is parsed roughly as:&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;"success"&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="s2"&gt;"ledger has exactly one PhaseGate row"&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;So the two representations are different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source:

ledger has exactly one PhaseGate row # include the negative case too


parsed value:

ledger has exactly one PhaseGate row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original file has not been modified. The text after &lt;code&gt;#&lt;/code&gt; is still physically present in the YAML file.&lt;/p&gt;

&lt;p&gt;But it is a comment, so it is not part of the JavaScript value returned by &lt;code&gt;yaml.parse()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That created a boundary I had not been checking closely enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what a human sees in the source
        ↓
      YAML parse
        ↓
what downstream code receives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The verification gate still passed
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;spec-lane&lt;/code&gt;, the success criteria and the verification matrix are separate inputs.&lt;/p&gt;

&lt;p&gt;The success criterion lives in &lt;code&gt;intent.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The corresponding verification record lives in &lt;code&gt;verification.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the reproduction, the matrix contained the already-truncated form:&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;success_criteria_matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;criterion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ledger has exactly one PhaseGate row&lt;/span&gt;
    &lt;span class="na"&gt;covered_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test/ledger.test.ts::records PhaseGate&lt;/span&gt;
    &lt;span class="na"&gt;negation_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no PhaseGate row fails&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important caveat: those &lt;code&gt;evidence&lt;/code&gt; and &lt;code&gt;negation_test&lt;/code&gt; values are declarations in the reproduction fixture.&lt;/p&gt;

&lt;p&gt;For this reproduction, I did not create and execute the referenced ledger test and then prove that its behavior matched those strings.&lt;/p&gt;

&lt;p&gt;The purpose here was to reproduce the gate behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reproducing the behavior before the fix
&lt;/h3&gt;

&lt;p&gt;I built the CLI from the revision immediately before PR #48's fix and ran the reproduction in a temporary directory.&lt;/p&gt;

&lt;p&gt;With the unquoted success criterion and the shortened matrix entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ lane validate ...

intent.yaml is valid (phase=3_implement).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ lane advance ... --phase 4_verify

Advanced ...: 3_implement -&amp;gt; 4_verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit code: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verification phase was entered.&lt;/p&gt;

&lt;p&gt;At that point, the two strings reaching the gate were effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success:

ledger has exactly one PhaseGate row


matrix criterion:

ledger has exactly one PhaseGate row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They matched.&lt;/p&gt;

&lt;p&gt;So the gate passed.&lt;/p&gt;

&lt;p&gt;The important distinction is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the verification gate did not remove the text after &lt;code&gt;#&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That had already happened at the earlier boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YAML source
    ↓
parsed JS value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate was comparing the values it had been given, and those values were equal.&lt;/p&gt;

&lt;p&gt;The relevant code paths are in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages/cli/src/intent-store.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/gate.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full reproduction and investigation are documented in Issue #45:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Quoting the same criterion changed the result
&lt;/h2&gt;

&lt;p&gt;I then changed only the success criterion so the full text became an explicit YAML string:&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;success&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;ledger&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exactly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PhaseGate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;include&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;negative&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;case&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;too"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the parsed value retains everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success:

ledger has exactly one PhaseGate row # include the negative case too


matrix criterion:

ledger has exactly one PhaseGate row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the same pre-fix CLI, both &lt;code&gt;validate&lt;/code&gt; and &lt;code&gt;advance&lt;/code&gt; now failed with exit code &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The gate reported that there was no matrix row corresponding to the full success criterion.&lt;/p&gt;

&lt;p&gt;The phase remained &lt;code&gt;3_implement&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the control case looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Success criterion&lt;/th&gt;
&lt;th&gt;Matrix&lt;/th&gt;
&lt;th&gt;validate&lt;/th&gt;
&lt;th&gt;advance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain scalar; text after &lt;code&gt;#&lt;/code&gt; is a comment&lt;/td&gt;
&lt;td&gt;Short value&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quoted; full value is preserved&lt;/td&gt;
&lt;td&gt;Short value&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At least in this fixture, the result did not come from the gate failing to run.&lt;/p&gt;

&lt;p&gt;The value reaching the gate changed.&lt;/p&gt;

&lt;p&gt;That changed the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema-valid does not mean source-equivalent
&lt;/h2&gt;

&lt;p&gt;The schema for this part of the success criteria is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shortened result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ledger has exactly one PhaseGate row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is still a perfectly valid string.&lt;/p&gt;

&lt;p&gt;So after parsing, these two source forms can result in the same value:&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ledger has exactly one PhaseGate row&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ledger has exactly one PhaseGate row&lt;/span&gt; &lt;span class="c1"&gt;# include the negative case too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a validator only receives the parsed value, it can no longer tell whether:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the author originally wrote the shorter criterion, or&lt;/li&gt;
&lt;li&gt;additional source text became a YAML comment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That distinction led me to separate several different claims that are easy to blur together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the value has the correct shape
≠
the original source expression was preserved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there are further boundaries after that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the string was preserved
≠
the string correctly represents human intent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;an evidence field contains a test name
≠
that test was actually executed and proved the claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A schema validator can validate the structure it receives.&lt;/p&gt;

&lt;p&gt;It cannot automatically recover information that disappeared before that boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The v0.11.0 fix stops earlier
&lt;/h2&gt;

&lt;p&gt;PR #48 did not change the success-criteria comparison itself.&lt;/p&gt;

&lt;p&gt;Instead, the fix moved the check to the &lt;code&gt;intent.yaml&lt;/code&gt; reading boundary.&lt;/p&gt;

&lt;p&gt;The reason is straightforward: once only the parsed value remains, there is not enough information to distinguish the two source forms.&lt;/p&gt;

&lt;p&gt;The implementation now roughly does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Walk the YAML AST and identify unquoted &lt;code&gt;PLAIN&lt;/code&gt; scalars.&lt;/li&gt;
&lt;li&gt;Use the scalar's source range to inspect the original text.&lt;/li&gt;
&lt;li&gt;Check whether spaces or tabs immediately after the scalar are followed by &lt;code&gt;#&lt;/code&gt; on the same line.&lt;/li&gt;
&lt;li&gt;If such a candidate value appears in the parsed success criteria, reject the input before it reaches the gate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shiki-yusuke/spec-lane/blob/64f2ca42c5929be0413e5f4f240897dffe56878b/packages/cli/src/intent-store.ts#L108-L164" rel="noopener noreferrer"&gt;https://github.com/shiki-yusuke/spec-lane/blob/64f2ca42c5929be0413e5f4f240897dffe56878b/packages/cli/src/intent-store.ts#L108-L164&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The check deliberately does not rely only on the AST node's &lt;code&gt;.comment&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;One reason is that certain anchor forms can associate a comment with a different node. The implementation therefore combines AST type and range information with the original source text.&lt;/p&gt;

&lt;p&gt;With the v0.11.0 source, the same reproduction now behaves like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Success criterion&lt;/th&gt;
&lt;th&gt;Matrix&lt;/th&gt;
&lt;th&gt;validate&lt;/th&gt;
&lt;th&gt;advance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain scalar + inline comment&lt;/td&gt;
&lt;td&gt;Short value&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quoted full value&lt;/td&gt;
&lt;td&gt;Full value&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the rejected case, the phase remains &lt;code&gt;3_implement&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a literal &lt;code&gt;#&lt;/code&gt; belongs in the success criterion, it can be made explicit by quoting the string:&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;success&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;ledger&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exactly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PhaseGate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;include&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;negative&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;case&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;too"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix shipped in &lt;code&gt;spec-lane&lt;/code&gt; v0.11.0:&lt;/p&gt;

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

&lt;p&gt;PR #48 contains the implementation and regression tests:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  This still does not mean the tool understands human intent
&lt;/h2&gt;

&lt;p&gt;The fix is intentionally narrower than that.&lt;/p&gt;

&lt;p&gt;It also has a known over-detection case.&lt;/p&gt;

&lt;p&gt;Consider:&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;intent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;business_goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared text&lt;/span&gt; &lt;span class="c1"&gt;# unrelated comment&lt;/span&gt;
  &lt;span class="na"&gt;success&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;shared&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing was truncated from the quoted success criterion.&lt;/p&gt;

&lt;p&gt;But the current check can still reject this document because another commented plain scalar has the same parsed value as the success criterion.&lt;/p&gt;

&lt;p&gt;The implementation does not prove that the two occurrences share the same semantic origin.&lt;/p&gt;

&lt;p&gt;That limitation is preserved in a regression test rather than hidden.&lt;/p&gt;

&lt;p&gt;So I would not describe the fix as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;spec-lane now understands whether the author's full intent has been preserved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;The design is closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if the source contains a value that could have been silently shortened before it reaches a success criterion, stop instead of guessing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a fail-closed choice.&lt;/p&gt;

&lt;p&gt;It trades some false positives for avoiding this known silent-truncation path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I now pay more attention to is the boundary
&lt;/h2&gt;

&lt;p&gt;This incident changed how I think about verification in AI-assisted development.&lt;/p&gt;

&lt;p&gt;It is tempting to think about the pipeline as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specification
    ↓
implementation
    ↓
verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in practice there are more boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Human expression
what someone is trying to achieve
        ↓
Spec source
what was actually written
        ↓
Parsed representation
what the tool interpreted
        ↓
Verification
what the gate compared
        ↓
Execution evidence
what actually ran or was observed
        ↓
Acceptance
why the result was accepted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure in this post was mostly at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spec source
↓
Parsed representation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything after that can behave consistently and still verify less than a human reader thought had been written.&lt;/p&gt;

&lt;p&gt;The same distinction appears elsewhere.&lt;/p&gt;

&lt;p&gt;A test name written into a verification matrix is not the same thing as evidence that the test actually ran.&lt;/p&gt;

&lt;p&gt;A command exiting successfully is not necessarily the same thing as the intended property being proved.&lt;/p&gt;

&lt;p&gt;And a schema-valid specification is not necessarily the same thing as preserving every meaningful part of its source representation.&lt;/p&gt;

&lt;p&gt;Machine-readable specifications are useful.&lt;/p&gt;

&lt;p&gt;But once a workflow starts treating “the gate passed” as evidence of completion, I also want to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exactly reached the gate, where did that value come from, and what transformations happened before it got there?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this case, one small YAML line was enough to expose that boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Issue #45 — reproduction and original report
&lt;a href="https://github.com/shiki-yusuke/spec-lane/issues/45" rel="noopener noreferrer"&gt;https://github.com/shiki-yusuke/spec-lane/issues/45&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #48 — fix and regression tests
&lt;a href="https://github.com/shiki-yusuke/spec-lane/pull/48" rel="noopener noreferrer"&gt;https://github.com/shiki-yusuke/spec-lane/pull/48&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;spec-lane v0.11.0
&lt;a href="https://github.com/shiki-yusuke/spec-lane/releases/tag/v0.11.0" rel="noopener noreferrer"&gt;https://github.com/shiki-yusuke/spec-lane/releases/tag/v0.11.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;AI-assisted disclosure:&lt;/strong&gt; I used AI tools to help draft, translate, and edit this article. The reproduction, source inspection, and factual claims were checked against the linked issue, pull request, release, and the evidence collected during the investigation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>programming</category>
      <category>agents</category>
    </item>
    <item>
      <title>Tracking Codex Astra costs took more than updating a price table</title>
      <dc:creator>Yusuke Shiki</dc:creator>
      <pubDate>Sun, 06 Sep 2026 01:10:55 +0000</pubDate>
      <link>https://dev.to/shikiyusuke/tracking-codex-astra-costs-took-more-than-updating-a-price-table-3p3i</link>
      <guid>https://dev.to/shikiyusuke/tracking-codex-astra-costs-took-more-than-updating-a-price-table-3p3i</guid>
      <description>&lt;p&gt;I updated &lt;a href="https://github.com/shiki-yusuke/agent-cost" rel="noopener noreferrer"&gt;agent-cost&lt;/a&gt;, a CLI I maintain, to version 0.1.1 because I wanted to measure Codex's GPT-6 Astra usage too. It reads local Claude Code and Codex CLI logs and reports token usage and estimated costs, not actual billed amounts.&lt;/p&gt;

&lt;p&gt;Fast mode was where I ran into trouble.&lt;/p&gt;

&lt;p&gt;After adding Astra to the price table, I tested it with synthetic logs. Usage marked as Fast was still being priced at the Standard rate. The multiplier was in the catalog, but the log reader wasn't recognizing the setting.&lt;/p&gt;

&lt;p&gt;The fix involved both detecting the mode and deciding what to do when the logs didn't give me enough information.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the synthetic tests showed
&lt;/h2&gt;

&lt;p&gt;I used synthetic data with two million uncached input tokens, two million cached input tokens, and two million output tokens: six million tokens in total.&lt;/p&gt;

&lt;p&gt;Here are the results for Standard, Fast, and unknown settings, using the rate catalog bundled with 0.1.1. This is a test fixture, not real usage or a list of current official prices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting identified from the log&lt;/th&gt;
&lt;th&gt;Estimated USD&lt;/th&gt;
&lt;th&gt;Estimated credits&lt;/th&gt;
&lt;th&gt;Unpriced tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;td&gt;3,050&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;305&lt;/td&gt;
&lt;td&gt;7,625&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;0*&lt;/td&gt;
&lt;td&gt;0*&lt;/td&gt;
&lt;td&gt;6,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The zero in the unknown row means none of that usage could be priced. **It does not mean the usage was free.&lt;/em&gt;*&lt;/p&gt;

&lt;p&gt;The same token counts produce an estimate of $122 at Standard rates and $305 at Fast rates. During development, the Fast case was incorrectly returning the Standard estimate.&lt;/p&gt;

&lt;p&gt;The already-published 0.1.0 release didn't have Astra rates at all, so it treated Astra usage as unpriced. This was a problem found while implementing Astra support, not a report that 0.1.0 had shown users the wrong Fast amount.&lt;/p&gt;

&lt;p&gt;The changes are in &lt;a href="https://github.com/shiki-yusuke/agent-cost/pull/8" rel="noopener noreferrer"&gt;PR #8&lt;/a&gt;. The &lt;a href="https://github.com/shiki-yusuke/agent-cost/blob/48c2731c3336401470d9d9016a98f437390abeef/tests/fixtures/synthetic-codex-astra.jsonl" rel="noopener noreferrer"&gt;synthetic fixture&lt;/a&gt; and &lt;a href="https://github.com/shiki-yusuke/agent-cost/blob/48c2731c3336401470d9d9016a98f437390abeef/tests/test_astra_pricing.py" rel="noopener noreferrer"&gt;tests&lt;/a&gt; are public too. The table above summarizes test results; it isn't a copy of the CLI's output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does "not known to be Fast" mean Standard?
&lt;/h2&gt;

&lt;p&gt;I checked the public source for Codex 0.153.4.&lt;/p&gt;

&lt;p&gt;For this Astra support, agent-cost reads &lt;code&gt;service_tier&lt;/code&gt; as well as the model name. It checks that the settings belong to the same session and match the target turn and model. An explicit &lt;code&gt;default&lt;/code&gt; selects Standard; &lt;code&gt;priority&lt;/code&gt; selects Fast.&lt;/p&gt;

&lt;p&gt;But a setting can be missing. It can also conflict with the information recorded for a turn. In those cases, the mode isn't clear.&lt;/p&gt;

&lt;p&gt;Treating everything that isn't recognized as Fast as Standard would still produce a number. It would also assign Standard prices to usage whose settings the reader simply couldn't establish.&lt;/p&gt;

&lt;p&gt;Instead, &lt;strong&gt;usage stays &lt;code&gt;unpriced&lt;/code&gt; when the reader can't determine which rate applies&lt;/strong&gt;. Knowing Astra's rates doesn't establish whether a particular piece of usage was Standard or Fast.&lt;/p&gt;

&lt;p&gt;This is based on the request settings recorded in the log. It doesn't confirm the processing mode actually used by the server or what was billed. The source references and attribution rules are in the &lt;a href="https://github.com/shiki-yusuke/agent-cost/blob/48c2731c3336401470d9d9016a98f437390abeef/docs/astra-pricing.md#standardfast-attribution-request-based-estimate" rel="noopener noreferrer"&gt;implementation notes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the tokens even when the price is unknown
&lt;/h3&gt;

&lt;p&gt;Dropping an unpriced row would hide the fact that the usage happened. Keeping only a zero amount would make it look free.&lt;/p&gt;

&lt;p&gt;So the output carries &lt;code&gt;pricing_status&lt;/code&gt;, which describes the pricing state, and &lt;code&gt;unpriced_tokens&lt;/code&gt;, which counts tokens that couldn't be priced, separately from the amount.&lt;/p&gt;

&lt;p&gt;Checking those fields alongside the total shows whether any usage was left out of the calculation. The unknown row above doesn't just say $0: it preserves all six million tokens as unpriced.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;priced&lt;/code&gt; doesn't mean a confirmed charge either. It means the usage could be calculated using the selected rate catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which usage gets the new setting when Fast is switched on?
&lt;/h2&gt;

&lt;p&gt;Settings can also change during a session.&lt;/p&gt;

&lt;p&gt;Suppose a session starts in Standard, uses Fast for the next turn, then switches back to Standard. Pricing the entire session with its final setting would treat the Fast portion as Standard too.&lt;/p&gt;

&lt;p&gt;When settings change between completed turns, the reader applies the new settings only to subsequent usage. The log contains cumulative token counts, so it takes the increase since the previous count and prices that increase using the turn's settings.&lt;/p&gt;

&lt;p&gt;An update while a turn is still running leaves an ambiguous interval: the reader can't safely assign that usage to the old or new settings. Those tokens stay &lt;code&gt;unpriced&lt;/code&gt; as well.&lt;/p&gt;

&lt;p&gt;This behavior was added in 0.1.1 for logs containing Astra. It doesn't fully reconstruct missing history, concurrent updates, or alternative storage formats. The supported scope is limited to the &lt;a href="https://github.com/shiki-yusuke/agent-cost/blob/48c2731c3336401470d9d9016a98f437390abeef/agent_cost/readers/codex.py" rel="noopener noreferrer"&gt;reader implementation&lt;/a&gt; and the cases checked with synthetic data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it locally
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;uvx&lt;/code&gt; available, you can run the published 0.1.1 package with the commands below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Astra coverage in this version:&lt;/strong&gt; the model ID must be exactly &lt;code&gt;gpt-6-astra&lt;/code&gt;, and usage must be at or after &lt;strong&gt;September 5, 2026, 17:18:23 UTC&lt;/strong&gt; (September 6, 02:18:23 JST). Earlier usage remains &lt;code&gt;unpriced&lt;/code&gt;, even when Standard or Fast can be identified.&lt;/p&gt;

&lt;p&gt;This is a catalog boundary based on when the rates were checked, not an official launch or tariff start time. This release also does not update every model's rates: known discrepancies in the GPT-5.6 family remain.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--from&lt;/span&gt; coding-agent-cost&lt;span class="o"&gt;==&lt;/span&gt;0.1.1 agent-cost doctor
uvx &lt;span class="nt"&gt;--from&lt;/span&gt; coding-agent-cost&lt;span class="o"&gt;==&lt;/span&gt;0.1.1 agent-cost report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; checks things such as the configured input locations. &lt;code&gt;report&lt;/code&gt; actually reads the local logs and aggregates usage. Running &lt;code&gt;doctor&lt;/code&gt; alone doesn't verify that every log is valid.&lt;/p&gt;

&lt;p&gt;The CLI doesn't send the logs anywhere. The &lt;code&gt;uvx&lt;/code&gt; package runner may use the network to download the package. To see output before reading your own logs, use the README's &lt;a href="https://github.com/shiki-yusuke/agent-cost#synthetic-output-example" rel="noopener noreferrer"&gt;synthetic output example&lt;/a&gt;. That example remains pinned to 0.1.0 as a record of an earlier verification.&lt;/p&gt;

&lt;p&gt;For use from another program, there is JSON output and the &lt;code&gt;measure/v1&lt;/code&gt; interface. The output includes the rate catalog's version and SHA-256, so a result can be traced back to the catalog used to calculate it. The &lt;a href="https://github.com/shiki-yusuke/agent-cost" rel="noopener noreferrer"&gt;README&lt;/a&gt; covers installation requirements and the timezone-data caveat on Windows.&lt;/p&gt;

&lt;p&gt;The additional Astra checks used synthetic data. I haven't validated compatibility against real Astra logs or measured a cost reduction from using the tool. API-specific tariffs and actual charges under individual contracts are outside this calculation. The &lt;a href="https://github.com/shiki-yusuke/agent-cost/releases/tag/v0.1.1" rel="noopener noreferrer"&gt;0.1.1 release notes&lt;/a&gt; record the scope and verification results at publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I landed
&lt;/h2&gt;

&lt;p&gt;An unknown price isn't a particularly satisfying result when you ran the tool to find out the cost.&lt;/p&gt;

&lt;p&gt;But assigning a Standard rate when the settings couldn't be read would hide that uncertainty from the person using the result. For this Astra update, I kept the tokens even when they couldn't be priced, so the output shows how much usage the calculation actually covers.&lt;/p&gt;




&lt;p&gt;This is the English version of my &lt;a href="https://zenn.dev/yusuke_shiki/articles/d7209850cb7a03" rel="noopener noreferrer"&gt;article on Zenn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>codex</category>
    </item>
    <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>
