<?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: Sho Naka</title>
    <description>The latest articles on DEV Community by Sho Naka (@nomurasan).</description>
    <link>https://dev.to/nomurasan</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%2F3987494%2Fa151ecf0-1dfb-44a7-91d6-5bafe84ecdb8.jpg</url>
      <title>DEV Community: Sho Naka</title>
      <link>https://dev.to/nomurasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nomurasan"/>
    <language>en</language>
    <item>
      <title>My Audit Caught Every Known Bug. Ten of Its Next 18 Flags Were Wrong.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:49:39 +0000</pubDate>
      <link>https://dev.to/nomurasan/my-audit-caught-every-known-bug-ten-of-its-next-18-flags-were-wrong-3fpe</link>
      <guid>https://dev.to/nomurasan/my-audit-caught-every-known-bug-ten-of-its-next-18-flags-were-wrong-3fpe</guid>
      <description>&lt;p&gt;In a July 2026 AI-assisted audit of my automation, a checklist caught all seven known defects it had been designed from. Applied to 13 previously unaudited guards, it produced 18 findings. The recorded adjudications accepted eight and rejected ten.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;10 rejected findings out of 18&lt;/strong&gt;, not “56% of healthy guards flagged.” My earlier draft used that misleading description. A guard could receive several findings, and the sample was not established to be healthy.&lt;/p&gt;

&lt;p&gt;The useful question became: &lt;strong&gt;when this checklist flags something new, what evidence makes the finding worth acting on?&lt;/strong&gt; Here is what the records showed, where the checklist overreached, and a small reporting pattern you can use on your own reviews.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI assistance:&lt;/strong&gt; AI agents performed the historical audit and recorded the findings and adjudications. The counts were reconstructed and this article was edited with AI assistance. The measurements below are a recount of those July records, not a fresh execution of the guards or an independent re-adjudication of every label. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;A checklist built from seven known defects caught all seven, which only proves it can describe the failures that shaped it. On 13 previously unaudited guards it produced 18 findings, of which 10 were rejected: 44.4% precision among resolved findings, measured per finding, not a per-guard false-positive rate. Before quoting any percentage from a review, keep accepted, rejected and unresolved findings as separate rows and recount them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven known defects made a regression set, not a benchmark
&lt;/h2&gt;

&lt;p&gt;The checklist contained nine criteria derived from seven confirmed defects. They covered problems such as a missing required input being treated as success, a selector that matched nothing and silently skipped checking, and destructive work without an adequate bound on its effect.&lt;/p&gt;

&lt;p&gt;Checking that all seven cases were caught was useful: the checklist could describe the failures that motivated it. But those cases had already influenced its design. They could not establish how reliably it would recognize a different defect, or how often it would accuse correct behavior.&lt;/p&gt;

&lt;p&gt;This is the same evaluation concern behind keeping development data separate from test data. I am applying that principle to a checklist here, not claiming that these seven examples constitute a statistical evaluation of a trained model. &lt;a href="https://scikit-learn.org/stable/common_pitfalls.html#data-leakage" rel="noopener noreferrer"&gt;Scikit-learn's guidance on data leakage&lt;/a&gt; explains why results on data used during development can be overly optimistic.&lt;/p&gt;

&lt;p&gt;The next audit therefore mattered more than the neat “7/7” result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count the findings before naming the percentage
&lt;/h2&gt;

&lt;p&gt;The archived records support this comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Recorded audit&lt;/th&gt;
&lt;th&gt;Guards examined&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;th&gt;Accepted as defects&lt;/th&gt;
&lt;th&gt;Rejected as defects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First application beyond the seven design cases&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-up after criteria changes, on a different sample&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the first audit, 8/18 findings were accepted: &lt;strong&gt;44.4% precision under the recorded labels&lt;/strong&gt;. The complementary share, 10/18, was &lt;strong&gt;55.6% rejected findings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The denominator matters. Precision is TP / (TP + FP). A false-positive rate is FP / (FP + TN): it needs the actual negative cases, including ones the detector correctly left alone. &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_score.html" rel="noopener noreferrer"&gt;The precision definition&lt;/a&gt; and &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html" rel="noopener noreferrer"&gt;confusion-matrix layout&lt;/a&gt; make that distinction explicit.&lt;/p&gt;

&lt;p&gt;Here, the recorded labels attach to &lt;strong&gt;findings&lt;/strong&gt;. “Thirteen guards examined” is a different unit. I cannot combine those numbers to calculate a per-guard false-positive rate. Nor do these findings tell me how many defects were missed in the newly examined code.&lt;/p&gt;

&lt;p&gt;There was another reason to recount. One prose summary inside the first record said 11 rejected out of 19 flags. Its structured totals said 10 out of 18, and its 18 individual finding rows contained eight accepted labels and ten rejected labels. This article uses the row count. The summary sentence was inconsistent with the underlying entries.&lt;/p&gt;

&lt;p&gt;An audit report can need an audit of its own arithmetic before its conclusions are useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Empty input” was too broad a defect definition
&lt;/h2&gt;

&lt;p&gt;Six of the first audit's ten rejected findings came from one criterion: treating a degenerate or empty-input result that looked like ordinary success as a defect.&lt;/p&gt;

&lt;p&gt;That criterion had a real motivation. A mistyped selector can match nothing, skip every check, and leave a reassuring success status. But “nothing matched” is also a legitimate outcome for some tools. An optional collection can simply contain no work today.&lt;/p&gt;

&lt;p&gt;The old wording collapsed those cases. A better contract asks what the program has promised to verify:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What a useful contract should distinguish&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A required source directory does not exist&lt;/td&gt;
&lt;td&gt;Input failure; this is not an empty collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An existing, optional collection contains no work&lt;/td&gt;
&lt;td&gt;Successful no-op, if that is the documented behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An explicit selection intended to identify required work matches nothing&lt;/td&gt;
&lt;td&gt;Invalid selection or an explicit unresolved result, according to the tool's contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are proposed contract examples, not a claim that every tool should use the same exit code. The important test is whether the caller can distinguish a valid no-op from a failed precondition.&lt;/p&gt;

&lt;p&gt;Before accepting an empty-input finding, I would want the finding to name the intended contract, supply an input that violates it, and show the observed result. “It returned success on zero items” is an observation. Whether that is a defect still needs a specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second audit did not prove an improvement
&lt;/h2&gt;

&lt;p&gt;After the criteria were tightened, the follow-up record contained four findings across 12 guards. Two were accepted and two rejected: 50% rejected findings under those labels.&lt;/p&gt;

&lt;p&gt;It is tempting to write “false positives fell from 56% to 50%.” That would imply more than these records establish. The guard sample changed, the criteria changed, and the follow-up contained only four findings. This was not a paired before-and-after experiment on a fixed set of independently labeled cases.&lt;/p&gt;

&lt;p&gt;One follow-up rejection also showed why scope matters: a limitation identified in one check was covered by another check in the execution path, according to the recorded verification. Whether a finding describes a component limitation or an unprotected system behavior must be explicit. A limitation can still deserve documentation without proving that the complete path violates its contract.&lt;/p&gt;

&lt;p&gt;The follow-up supports a modest conclusion: the revised process still produced findings that were rejected after examination. It does not establish the size of a quality improvement, a reduction in missed defects, or the current performance of the guards.&lt;/p&gt;

&lt;h2&gt;
  
  
  A report that keeps unknowns visible
&lt;/h2&gt;

&lt;p&gt;You do not need a dashboard to avoid the denominator mistake. Start by keeping accepted, rejected, and unresolved findings separate. This standalone Python example reconstructs the first audit's label counts; it is a reporting example, not the original private audit dataset.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# Reconstructed counts from the recorded adjudications.
&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unresolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown finding label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findings_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unresolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unresolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;precision_among_resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected_share_among_resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_guard_false_positive_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Required labels unavailable.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recall_on_new_cases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Missed defects not established.
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For these labels, the two calculated shares are approximately 0.444 and 0.556. If you add an unresolved finding, it increases the total and unresolved count without being silently classified as correct or incorrect. The two shares then describe only the resolved subset; they may not represent the pending findings. With no resolved findings, both remain &lt;code&gt;None&lt;/code&gt; rather than becoming a reassuring zero.&lt;/p&gt;

&lt;p&gt;For your next ten review findings, keep one row per finding with the criterion, reproduction evidence, expected behavior, verdict, and reason. Preserve unresolved cases. Recount those rows before quoting an aggregate percentage.&lt;/p&gt;

&lt;p&gt;If you then want to compare two versions of the checklist, define the evaluation unit and labels first, freeze a separate evaluation set, and run both versions against it. Include legitimate no-op cases as well as known defects. Treat the original seven defects as regression tests, and keep their result separate from the new-case evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is "7 out of 7 caught" not evidence that the checklist works?
&lt;/h3&gt;

&lt;p&gt;Because the seven cases shaped the checklist. They are a regression set: useful for confirming the checklist still describes the failures that motivated it, but unable to say how it behaves on a defect it has not seen or how often it accuses correct behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between precision and a false-positive rate here?
&lt;/h3&gt;

&lt;p&gt;Precision is accepted findings over all resolved findings (8/18). A false-positive rate needs the true negatives, meaning the guards the checklist correctly left alone, and those were never labeled. The records attach labels to findings, so a per-guard rate cannot be derived from them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did tightening the criteria reduce false positives?
&lt;/h3&gt;

&lt;p&gt;Not demonstrably. The follow-up audit had a different guard sample, changed criteria and only four findings (two rejected). Showing an improvement would need a frozen evaluation set with independent labels, run against both versions of the checklist.&lt;/p&gt;

&lt;p&gt;Catching every familiar failure was a useful start. The more useful result was learning exactly what I could—and could not—conclude from the next eighteen findings.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>devops</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>I'm dropping spec-first AI development for smaller experiments</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 19 Sep 2026 02:55:45 +0000</pubDate>
      <link>https://dev.to/nomurasan/im-dropping-spec-first-ai-development-for-smaller-experiments-3pa7</link>
      <guid>https://dev.to/nomurasan/im-dropping-spec-first-ai-development-for-smaller-experiments-3pa7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The decisions and experiments here are the author's own. An AI assistant drafted the English text from the author's notes, and an independent AI review checked the links and the one third-party claim before publication. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;For my own reversible development work I am replacing specification-first as the default with small working experiments: state the outcome, build the smallest change that lets me try it, and judge the running result. Prohibited operations are rejected by a pre-execution hook rather than described in a warning, and the main measure is elapsed time from request to a usable result. This is a decision under test, not a measured productivity gain.&lt;/p&gt;

&lt;p&gt;I gave my coding agent a link to &lt;a href="https://github.com/DietrichGebert/ponytail" rel="noopener noreferrer"&gt;Ponytail&lt;/a&gt;, a set of rules intended to stop agents from overbuilding. It returned a cautious assessment: pushing for fewer files and one-line implementations might hurt readability or fit poorly with the existing architecture.&lt;/p&gt;

&lt;p&gt;My response was a question: &lt;strong&gt;readable for whom?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an agent does most of the reading, changing, and checking, I want the structure that helps it make the next correct change with the least context. An architecture should earn its maintenance cost, even if I designed it myself.&lt;/p&gt;

&lt;p&gt;That conversation led me to a decision: for my personal development work, I want to replace the default specification-first process with small working experiments. I want short instructions for judgment, hard enforcement for prohibited operations, and elapsed time to a usable result as the main measure.&lt;/p&gt;

&lt;p&gt;This is a decision I am testing, not a report of proven productivity gains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the outcome stable. Let the implementation change.
&lt;/h2&gt;

&lt;p&gt;My reason for changing this default is the human cost of evaluating the plan. Even if an agent writes a careful plan instantly, I still have to imagine the resulting behavior and find what is missing.&lt;/p&gt;

&lt;p&gt;I have asked an AI to check implementation concerns in plan mode and proceeded carefully, yet the resulting system did not meet the customer's needs. That experience does not establish that plan mode caused the failure. It does show that checking whether something can be implemented is insufficient evidence that it will serve the customer's work. Plan mode and specification-driven development are not identical; I am questioning the shared habit of expanding a document before testing the behavior it describes.&lt;/p&gt;

&lt;p&gt;Some behavior only becomes clear to an industry expert or someone doing the work. For example, correcting an already processed request might require preserving its history and returning it for review, rather than overwriting it. Both behaviors are technically implementable. This is an illustrative example, not a claim about the customer system. A more internally consistent plan cannot supply a missing business premise by itself.&lt;/p&gt;

&lt;p&gt;I would rather spend ten seconds writing a prompt, let the agent implement a small change in a minute, and judge the result. Those numbers describe the short loop I want, not a measured guarantee. A working interaction can make a mismatch easier to identify than a description that I must simulate in my head.&lt;/p&gt;

&lt;p&gt;By spec-first, I mean making a written specification, plan, and detailed task breakdown the default prerequisites to implementation. Tools such as &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt; put specifications at the center of development. I am changing that default for my own reversible work, not claiming that every specification-driven process is slow.&lt;/p&gt;

&lt;p&gt;Suppose I want to find an item in an existing list. I can ask an agent to design a search subsystem, or I can ask it to make the smallest working filter and let me try it.&lt;/p&gt;

&lt;p&gt;The first experiment needs a concrete outcome: a user can find a known item by its title. It does not need a commitment to a particular component library, search service, or abstraction.&lt;/p&gt;

&lt;p&gt;I would start with a brief 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;Outcome: find a known item by title in the existing list.
Use existing code or platform features where they meet that outcome.
Build the smallest reversible change that lets us try it.
Check a matching query, a query with no matches, and clearing the query.
Follow the existing authorization boundaries.
Report what works, the observed limitation, and the next change if needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those checks define this experiment's scope. They do not prove that a production search feature handles pagination, large datasets, permissions, accessibility, or every language correctly. If the list only contains one loaded page, that limit must be visible when judging the result.&lt;/p&gt;

&lt;p&gt;The useful loop is short: build, try, change or discard. A prototype that cannot answer its intended question has not completed the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Readability is the cost of the next correct change
&lt;/h2&gt;

&lt;p&gt;Before considering code structure, I need to address a trap in the shorter loop: the agent and I can keep saying “it works” while neither of us has the missing domain knowledge. Replacing plan iterations with implementation iterations does not automatically introduce customer evidence.&lt;/p&gt;

&lt;p&gt;Marty Cagan distinguishes value, usability, feasibility, and business viability risks. Checking that we can build something addresses only one of them. &lt;a href="https://www.svpg.com/four-big-risks/" rel="noopener noreferrer"&gt;The Four Big Risks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the next experiment, I want a concrete example from the actual work, appropriately anonymized, rather than only tidy dummy data. Existing understood behavior can be checked automatically. An unresolved business decision should be shown to a practitioner through that specific example. I do not need to ask the customer about every edit; I need their knowledge where it can change the next decision.&lt;/p&gt;

&lt;p&gt;Martin Fowler describes discovering requirements by observing what users try to do and where they abandon a task. That is a useful counterweight to checking only our own written assumptions. &lt;a href="https://martinfowler.com/bliki/ObservedRequirement.html" rel="noopener noreferrer"&gt;Observed Requirement&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another trap is accumulating every quick prototype. If an experiment disproves the approach, discard the implementation. Fast generation stops helping when each addition makes the next change harder to understand and verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the next change affordable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/DietrichGebert/ponytail/blob/356918eba965ee1eac64bd3a7f0dd02108350de5/AGENTS.md" rel="noopener noreferrer"&gt;Ponytail's rules&lt;/a&gt; ask the agent to look for existing code, the standard library, native platform features, and installed dependencies before writing something new.&lt;/p&gt;

&lt;p&gt;That order appeals to me. A native date input can remove a custom component from the code I must maintain, provided it meets the actual requirements.&lt;/p&gt;

&lt;p&gt;But I am not optimizing for the smallest line count. A dense expression that needs repeated explanation can cost more than a few straightforward statements. Equally, a small change spread across wrappers and duplicate definitions can require more context than keeping the behavior together.&lt;/p&gt;

&lt;p&gt;My question is: how much reading, reasoning, editing, and verification does the next change require? If a human will maintain the result, their understanding belongs in that cost too. “It matches the existing architecture” is evidence of consistency, not proof that the architecture is worth preserving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make prohibited operations impossible through the controlled path
&lt;/h2&gt;

&lt;p&gt;I also told the agent that I want enforcement, not warnings.&lt;/p&gt;

&lt;p&gt;A long instruction saying “be careful” still asks the model to interpret the rule on every relevant turn. A warning that allows an operation to continue leaves the final decision elsewhere.&lt;/p&gt;

&lt;p&gt;For a mechanically decidable prohibition, I want a pre-execution hook or equivalent control to reject the operation. The response should identify the failed condition and a permitted next action. If the agent can repair the request within its authority, it should continue.&lt;/p&gt;

&lt;p&gt;This only works when the operation actually passes through the control. Blocking one shell command does not enforce a boundary if another tool can perform the same write. Enforcement belongs at the relevant execution boundary, with the paths it covers verified.&lt;/p&gt;

&lt;p&gt;Product quality is a different job. A hook cannot generally decide whether the filter is useful or whether the screen makes sense. I want those questions answered by the working result and a focused check, rather than an expanding collection of vague warning rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize the whole request, not the last response
&lt;/h2&gt;

&lt;p&gt;The agent also noted that adding a plugin can add hook processes. That is a cost to measure, but it is not a reason to reject the approach by itself.&lt;/p&gt;

&lt;p&gt;A small deterministic check might replace repeated instruction processing and warning conversations. Or a verbose ruleset might add more input and reasoning than the shorter implementation saves. The balance has to be measured.&lt;/p&gt;

&lt;p&gt;Ponytail's author reports exactly this kind of variation. A separate single-shot experiment found higher cost and worse latency with GPT-5.5. That is not a result for my workflow, but it is enough to reject “less code always means cheaper and faster” as an assumption. &lt;a href="https://github.com/DietrichGebert/ponytail/blob/356918eba965ee1eac64bd3a7f0dd02108350de5/benchmarks/results/2026-06-17-cost-verification.md" rel="noopener noreferrer"&gt;Cost verification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I therefore want to replace redundant instructions, not keep adding new ones. A short reply that stops halfway through the task and requires another context reload is not automatically efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the clock at the request
&lt;/h2&gt;

&lt;p&gt;When I asked the agent to turn this decision into an article, I added: elapsed time is itself part of the result.&lt;/p&gt;

&lt;p&gt;I want to measure from the request to the artifact I can actually use or judge. Research, setup, waiting, and repairs stay on that clock. If I ask for a working feature, the clock does not stop at code generation. If I ask for an experiment, it stops when the intended question has been tested.&lt;/p&gt;

&lt;p&gt;For a first comparison, I would record:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;What it tells me&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request-to-result elapsed time&lt;/td&gt;
&lt;td&gt;How long I actually waited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total recorded tokens, where available&lt;/td&gt;
&lt;td&gt;The model work spent across the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repair rounds&lt;/td&gt;
&lt;td&gt;Whether the initial shortcut moved work later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same outcome checks&lt;/td&gt;
&lt;td&gt;Whether the two results are comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fair comparison keeps the starting state, model settings, requested outcome, and necessary checks the same. Without that comparison, I can report a duration; I cannot attribute an improvement to dropping specifications.&lt;/p&gt;

&lt;p&gt;I am starting with one reversible task. Keep the outcome clear, build only enough to test it, enforce the known prohibitions, and count the full time to the result. If that loop does not improve delivery, the next thing to question is the process I just introduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is this an argument against specification-driven development in general?
&lt;/h3&gt;

&lt;p&gt;No. It is a change of default for my own reversible, personal work. Tools such as Spec Kit exist for cases where a shared written specification is the cheapest way to align people; my problem is the cost of evaluating a plan in my head when a running change would answer the same question faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why enforce prohibitions with a hook instead of instructions?
&lt;/h3&gt;

&lt;p&gt;An instruction asks the model to re-interpret the rule on every turn, and a warning that lets the operation continue leaves the decision elsewhere. A mechanically decidable prohibition belongs at the execution boundary, where it can reject the operation and name a permitted next action. It only works if every path that can perform the operation goes through that boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  How will I know whether the shorter loop is actually faster?
&lt;/h3&gt;

&lt;p&gt;By measuring request-to-result elapsed time, recorded tokens where available, repair rounds, and the same outcome checks, with the starting state and model settings held constant. Until that comparison exists I can report a duration, not an improvement.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI from my actual discussion and decisions. The proposed workflow's productivity effect has not yet been established.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>When AI Becomes Production Infrastructure, You Need Recovery Capacity</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:40:56 +0000</pubDate>
      <link>https://dev.to/nomurasan/when-ai-becomes-production-infrastructure-you-need-recovery-capacity-3kgp</link>
      <guid>https://dev.to/nomurasan/when-ai-becomes-production-infrastructure-you-need-recovery-capacity-3kgp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article is based on the author's experience operating AI-assisted development workflows. The author identified the failure mode and defined the Recovery Reserve concept. AI helped recompose the article for DEV readers and prepare the Japanese review back-translation. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;If AI materially expands how much work your team can keep in motion, recovery becomes AI-dependent too. "We will do it manually" is not a fallback when the workload was sized for AI-assisted throughput. Keep enough inference capacity to execute the critical recovery path (Recovery Reserve), define a fallback provider and a migration context, reduce demand in a degraded mode, and decide emergency purchasing authority and cost attribution before the incident.&lt;/p&gt;

&lt;p&gt;Generative AI has changed how much work I can keep moving at once.&lt;/p&gt;

&lt;p&gt;I use it to write code, investigate existing systems, organize requirements, compare architecture options, generate tests, review implementations, write documentation, and trace dependencies across a project.&lt;/p&gt;

&lt;p&gt;That is useful. It also creates an operational problem that is easy to miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI can increase production capacity faster than it increases your ability to recover without AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once a project has been planned around AI-assisted throughput, losing that capacity is not just an inconvenience. The workload remains, while the system that made the workload manageable disappears.&lt;/p&gt;

&lt;p&gt;That is the point where AI stops behaving like a productivity feature and starts behaving like production infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  "We'll just continue manually" is not always a fallback
&lt;/h2&gt;

&lt;p&gt;I noticed this during a development project where AI was already part of the normal workflow.&lt;/p&gt;

&lt;p&gt;Research, architecture, implementation, debugging, and review were all being accelerated with AI. Then sufficient AI capacity was no longer available.&lt;/p&gt;

&lt;p&gt;The obvious fallback sounded reasonable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If AI is unavailable, continue manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the remaining workload had already been shaped around AI-assisted throughput. Switching execution back to unaided human work reduced processing capacity without reducing demand.&lt;/p&gt;

&lt;p&gt;That is not graceful degradation.&lt;/p&gt;

&lt;p&gt;It is a capacity failure.&lt;/p&gt;

&lt;p&gt;A real fallback must change both sides of the equation: the available production capacity &lt;strong&gt;and&lt;/strong&gt; the amount of work the system is expected to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI is becoming production capacity, not just a tool
&lt;/h2&gt;

&lt;p&gt;If one editor stops working, I can use another. If one terminal application breaks, I can switch tools.&lt;/p&gt;

&lt;p&gt;Those tools improve ergonomics and productivity. They do not necessarily determine the amount of work I can keep in motion.&lt;/p&gt;

&lt;p&gt;AI increasingly does.&lt;/p&gt;

&lt;p&gt;It can compress investigation, create implementation scaffolding, review broad changes, and turn vague requirements into an initial design and test strategy. Once those capabilities become part of normal delivery, removing them changes the capacity of the production system itself.&lt;/p&gt;

&lt;p&gt;That changes the question from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did we use too much AI?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did we provision enough inference capacity to deliver the committed workload &lt;strong&gt;and recover it when something goes wrong?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We already ask similar questions about compute, storage, CI runners, databases, failover, and backups. AI-dependent engineering work needs the same operational framing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal spending can hide a capacity failure
&lt;/h2&gt;

&lt;p&gt;Suppose company-provided AI capacity is exhausted while business-critical work remains.&lt;/p&gt;

&lt;p&gt;A practical reaction might be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll pay for another AI service myself and finish the work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That can solve an immediate incident. It is a bad default operating model.&lt;/p&gt;

&lt;p&gt;If an employee quietly supplies missing production capacity, the organization sees a successful delivery rather than an under-provisioned workflow. The next project can then be estimated with the same insufficient assumptions.&lt;/p&gt;

&lt;p&gt;This resembles hidden overtime, except the hidden input is inference capacity rather than labor time.&lt;/p&gt;

&lt;p&gt;If AI is part of production, its shortage and its cost need to be visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI can expand the blast radius of a bad assumption
&lt;/h2&gt;

&lt;p&gt;AI accelerates correct decisions. It also accelerates incorrect ones.&lt;/p&gt;

&lt;p&gt;A bad assumption introduced early can quickly influence architecture, implementation, tests, documentation, follow-up issues, and later features. Human execution speed used to impose a natural limit on how quickly an assumption could spread. AI weakens that limit.&lt;/p&gt;

&lt;p&gt;By the time the mistake is found, the task may no longer be a local patch. It can become a reconstruction exercise: identify everything derived from the assumption, restore the correct boundary, regenerate affected work, and verify the result.&lt;/p&gt;

&lt;p&gt;That recovery workload can itself require substantial AI assistance.&lt;/p&gt;

&lt;p&gt;The loop looks 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;Use AI
  ↓
Increase throughput
  ↓
Increase work in motion
  ↓
A bad assumption or incident occurs
  ↓
Recovery workload grows
  ↓
Recovery also needs AI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add the worst timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI capacity is unavailable when recovery begins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the operational failure mode this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery Reserve: protect the recovery path, not a percentage
&lt;/h2&gt;

&lt;p&gt;I call the protected inference capacity for this situation &lt;strong&gt;Recovery Reserve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is capacity kept available for work such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;incident investigation&lt;/li&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;re-analysis&lt;/li&gt;
&lt;li&gt;regeneration&lt;/li&gt;
&lt;li&gt;code review&lt;/li&gt;
&lt;li&gt;rollback&lt;/li&gt;
&lt;li&gt;emergency documentation&lt;/li&gt;
&lt;li&gt;migration to another model or workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is not to prescribe a universal reserve percentage. A fixed percentage would imply precision I do not have.&lt;/p&gt;

&lt;p&gt;Instead, start with a recovery scenario.&lt;/p&gt;

&lt;p&gt;Ask what must happen if the primary model is unavailable, if a bad assumption has propagated widely, or if the current work must be handed to a different model. Identify the investigation, decisions, edits, and verification steps on that critical path.&lt;/p&gt;

&lt;p&gt;Then protect enough capacity to execute that path.&lt;/p&gt;

&lt;p&gt;The reserve is adequate when the recovery path remains executable, not when a dashboard shows a particular percentage unused.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fallback provider is an operational dependency
&lt;/h2&gt;

&lt;p&gt;Important AI-dependent work should not have only one viable inference path.&lt;/p&gt;

&lt;p&gt;A more resilient setup has a primary model, a fallback model or API route, and a documented degraded mode. But the degraded mode matters as much as the fallback model.&lt;/p&gt;

&lt;p&gt;"Humans will do everything if AI goes down" is not necessarily degradation. If the workload was created around AI-assisted throughput, handing the same workload to the remaining human capacity does not create resilience.&lt;/p&gt;

&lt;p&gt;A degraded mode reduces demand.&lt;/p&gt;

&lt;p&gt;For example, keep incident response and acceptance-critical work moving while pausing new features, optional automation, and documentation that can safely wait.&lt;/p&gt;

&lt;p&gt;Graceful degradation means matching the workload to the remaining capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference cost becomes part of project cost
&lt;/h2&gt;

&lt;p&gt;AI subscriptions are often discussed like ordinary SaaS expenses.&lt;/p&gt;

&lt;p&gt;Once AI capacity affects delivery throughput, incident recovery, and required human effort, its economic role changes. Inference consumption and protected recovery capacity become production inputs.&lt;/p&gt;

&lt;p&gt;A useful project cost view can include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Human labor
Cloud infrastructure
External services
AI inference
Recovery headroom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when a contract does not expose precise per-task cost, you can still record operational events: capacity exhaustion, fallback-provider use, manual fallback, and recovery-mode execution.&lt;/p&gt;

&lt;p&gt;That evidence is more useful for the next capacity plan than an apparently successful project whose missing AI capacity was silently paid for by an individual.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first-pass business continuity table
&lt;/h2&gt;

&lt;p&gt;Once AI behaves like production infrastructure, the team needs decisions that are operational, not just conceptual.&lt;/p&gt;

&lt;p&gt;You do not need a perfect business continuity program before starting. Fill out this table first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure surface&lt;/th&gt;
&lt;th&gt;Decide now&lt;/th&gt;
&lt;th&gt;Passing condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary AI is unavailable&lt;/td&gt;
&lt;td&gt;Fallback model or API route&lt;/td&gt;
&lt;td&gt;Critical recovery work can start through another approved path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference capacity is constrained&lt;/td&gt;
&lt;td&gt;What Recovery Reserve is protected for&lt;/td&gt;
&lt;td&gt;Investigation, correction, and verification can run without consuming normal-production capacity first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work must move to another model&lt;/td&gt;
&lt;td&gt;Migration context&lt;/td&gt;
&lt;td&gt;Goal, canonical source, current diff, acceptance criteria, verified evidence, and unresolved items can be reconstructed without copying secret values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity is reduced&lt;/td&gt;
&lt;td&gt;Work stop order&lt;/td&gt;
&lt;td&gt;Incident and acceptance-critical work stays; deferrable work is explicitly paused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emergency capacity must be purchased&lt;/td&gt;
&lt;td&gt;Decision authority and recording rule&lt;/td&gt;
&lt;td&gt;The authorized person and project-cost destination are known before the incident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI cost must be attributed&lt;/td&gt;
&lt;td&gt;Usage events to record&lt;/td&gt;
&lt;td&gt;Normal production and recovery activity can be distinguished later at project level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bad assumption could fan out&lt;/td&gt;
&lt;td&gt;Checkpoint before high-propagation work&lt;/td&gt;
&lt;td&gt;Assumption, acceptance criteria, and verification method are rechecked before broad generation or automation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Migration context deserves special attention. It does not mean saving an entire chat transcript.&lt;/p&gt;

&lt;p&gt;The useful handoff is the smallest package that lets another model reconstruct the task safely: what outcome is required, which artifact is canonical, what changed, what defines completion, what has been verified, and what remains unresolved. Secret values stay in the secret store; the handoff contains references to where authorized tools should retrieve them.&lt;/p&gt;

&lt;p&gt;After the table is filled out, rehearse an important recovery task without the primary AI path. Wherever the exercise stops is the next missing piece of Recovery Reserve, documentation, authority, or context.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI operations begins where AI productivity needs resilience
&lt;/h2&gt;

&lt;p&gt;For a long time, most AI adoption work focused on acceleration: better prompts, better models, agents, automation, and parallel execution.&lt;/p&gt;

&lt;p&gt;Those still matter.&lt;/p&gt;

&lt;p&gt;But once AI is deeply integrated into real delivery, another layer becomes necessary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;control&lt;/li&gt;
&lt;li&gt;rollback&lt;/li&gt;
&lt;li&gt;capacity limits&lt;/li&gt;
&lt;li&gt;provider failure&lt;/li&gt;
&lt;li&gt;model substitution&lt;/li&gt;
&lt;li&gt;blast-radius reduction&lt;/li&gt;
&lt;li&gt;recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question is no longer only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much faster can AI make us?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we safely stop and recover work that has been accelerated by AI?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI adoption starts with productivity.&lt;/p&gt;

&lt;p&gt;AI operations starts when that productivity itself has to become resilient.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How large should the Recovery Reserve be?
&lt;/h3&gt;

&lt;p&gt;There is no universal percentage. Start from a concrete recovery scenario, such as the primary model being unavailable or a bad assumption having propagated, list the investigation, decisions, edits and verification on that path, and protect enough capacity to run it. The reserve is adequate when that path stays executable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is switching everything to human work a valid degraded mode?
&lt;/h3&gt;

&lt;p&gt;Only if demand is reduced at the same time. Handing an AI-sized workload to the remaining human capacity cuts processing capacity without cutting the work. A degraded mode keeps incident response and acceptance-critical work moving and explicitly pauses deferrable work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What belongs in a migration context for another model?
&lt;/h3&gt;

&lt;p&gt;The smallest package that lets a different model reconstruct the task safely: the required outcome, the canonical artifact, what changed, the acceptance criteria, what has been verified and what is unresolved. Secret values stay in the secret store; the handoff carries references, not a chat transcript.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
      <category>management</category>
    </item>
    <item>
      <title>Your AI Video Passed QA—and Still Wasn't Publishable</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:40:58 +0000</pubDate>
      <link>https://dev.to/nomurasan/your-ai-video-passed-qa-and-still-wasnt-publishable-23gj</link>
      <guid>https://dev.to/nomurasan/your-ai-video-passed-qa-and-still-wasnt-publishable-23gj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I defined this acceptance model after rejecting technically valid AI-generated videos in my own production pipeline. AI helped reorganize the Japanese framework for DEV readers and draft the examples. I am not claiming a measured quality increase or a completed three-video stability proof. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The MP4 rendered.&lt;/p&gt;

&lt;p&gt;It had an audio track. It had subtitles. &lt;code&gt;ffprobe&lt;/code&gt; returned the expected codec, resolution, streams, and duration. The timeline checks passed.&lt;/p&gt;

&lt;p&gt;I still rejected the video.&lt;/p&gt;

&lt;p&gt;The background did not support the explanation. Sentence boundaries and visual changes disagreed. Speaker turns had no believable pause. The characters moved, but the result felt like two avatars standing still and opening their mouths.&lt;/p&gt;

&lt;p&gt;The pipeline had succeeded technically. The artifact had failed as media.&lt;/p&gt;

&lt;p&gt;That distinction changed how I define "done" for AI video generation.&lt;/p&gt;

&lt;p&gt;I no longer use one completion state. I use four:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;moves&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;usable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;accepted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stable&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automated QA can prove the first. It cannot prove the other three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated QA proves validity, not publishability
&lt;/h2&gt;

&lt;p&gt;Automated checks still matter. They should catch deterministic failures before a person spends time watching the result.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;the video file opens;&lt;/li&gt;
&lt;li&gt;video and audio streams exist;&lt;/li&gt;
&lt;li&gt;duration, codec, and resolution are within contract;&lt;/li&gt;
&lt;li&gt;subtitles are present and their timestamps are ordered;&lt;/li&gt;
&lt;li&gt;the timeline has no empty or overlapping segments;&lt;/li&gt;
&lt;li&gt;every required artifact and source trace exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those checks should not be weakened. They should be made stricter.&lt;/p&gt;

&lt;p&gt;The mistake is using their PASS result as a proxy for audience acceptance.&lt;/p&gt;

&lt;p&gt;A valid audio stream does not prove that pronunciation and pauses feel natural. A subtitle file does not prove that captions and visuals are not fighting for attention. One image per segment does not prove that the background, framing, or character movement carries meaning.&lt;/p&gt;

&lt;p&gt;Technical validity is necessary. It is not the release decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate four release states
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;Exit condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;moves&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The pipeline produced a technically valid artifact&lt;/td&gt;
&lt;td&gt;MP4, audio, subtitles, provenance, and automated QA pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;usable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A first-time viewer can follow it without production-side explanation&lt;/td&gt;
&lt;td&gt;No major first-watch disruption; any required fixes are localizable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accepted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The release owner and target viewer accept the actual artifact&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;publishable_as_is&lt;/code&gt; or narrowly defined &lt;code&gt;publishable_with_minor_edits&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The same production method works beyond one lucky example&lt;/td&gt;
&lt;td&gt;Three different topics are consecutively Accepted under one series-template hash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a state machine, not a score.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;moves&lt;/code&gt; does not gradually become &lt;code&gt;usable&lt;/code&gt; because the codec score is high. &lt;code&gt;accepted&lt;/code&gt; is not &lt;code&gt;usable + 10 points&lt;/code&gt;. Each transition needs different evidence.&lt;/p&gt;

&lt;p&gt;To move from &lt;code&gt;moves&lt;/code&gt; to &lt;code&gt;usable&lt;/code&gt;, somebody has to experience the artifact at normal speed.&lt;/p&gt;

&lt;p&gt;To move from &lt;code&gt;usable&lt;/code&gt; to &lt;code&gt;accepted&lt;/code&gt;, the production team cannot be the only judge.&lt;/p&gt;

&lt;p&gt;To move from &lt;code&gt;accepted&lt;/code&gt; to &lt;code&gt;stable&lt;/code&gt;, one successful video is insufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the same artifact three different ways
&lt;/h2&gt;

&lt;p&gt;A normal first watch mixes script, pronunciation, pacing, framing, captions, and visual continuity. When the result feels wrong, that integrated impression is real—but it is often too entangled to tell the pipeline where to return.&lt;/p&gt;

&lt;p&gt;I use three passes in a fixed order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 1: audio only
&lt;/h3&gt;

&lt;p&gt;Close the video or turn the display away. Listen without captions or visuals.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pronunciation of names and technical terms;&lt;/li&gt;
&lt;li&gt;phrase boundaries, emphasis, and speed;&lt;/li&gt;
&lt;li&gt;pauses between statements;&lt;/li&gt;
&lt;li&gt;pauses when the speaker changes;&lt;/li&gt;
&lt;li&gt;the gap between a question and its answer;&lt;/li&gt;
&lt;li&gt;whether the argument and causal chain are understandable from audio alone;&lt;/li&gt;
&lt;li&gt;whether the delivery fits the intended speaker or character.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the explanation cannot be followed from audio, do not add more captions and images to conceal it. Return to the script, narration treatment, or affected audio segments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 2: silent visual
&lt;/h3&gt;

&lt;p&gt;Mute the audio and hide the captions.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the background communicates topic, location, or state change;&lt;/li&gt;
&lt;li&gt;whether characters do more than stand and lip-sync;&lt;/li&gt;
&lt;li&gt;whether framing, expression, gaze, objects, and cuts carry information;&lt;/li&gt;
&lt;li&gt;whether repeated background, placement, and motion create visual stagnation;&lt;/li&gt;
&lt;li&gt;whether a viewer can recover at least the current topic from the screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If meaning disappears when the captions disappear, adding more text is not a visual fix. Return to the storyboard, visual treatment, or shot plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 3: one integrated first watch
&lt;/h3&gt;

&lt;p&gt;Now watch the complete artifact once, at normal speed.&lt;/p&gt;

&lt;p&gt;Do not pause to explain what the production system intended.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether audio, visuals, and captions compete for attention;&lt;/li&gt;
&lt;li&gt;whether visual changes align with the same semantic units as the narration;&lt;/li&gt;
&lt;li&gt;whether the background or character performance lowers the credibility of the explanation;&lt;/li&gt;
&lt;li&gt;where the first urge to stop watching occurs;&lt;/li&gt;
&lt;li&gt;which release verdict applies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a small verdict set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publishable_as_is
publishable_with_minor_edits
revise
redesign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important output is not an average score. It is the &lt;strong&gt;first material blocker&lt;/strong&gt; and the layer that owns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the first blocker, not an average
&lt;/h2&gt;

&lt;p&gt;Suppose pronunciation scores 8/10, visuals 7/10, and captions 9/10. An average of 8/10 looks healthy.&lt;/p&gt;

&lt;p&gt;It can still be unpublishable.&lt;/p&gt;

&lt;p&gt;If a product name is mispronounced in the first few seconds, later quality does not cancel the error. If the background implies the opposite of the narration, a polished art style does not make the claim trustworthy.&lt;/p&gt;

&lt;p&gt;I store a review result 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;review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integrated_first_watch&lt;/span&gt;
  &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revise&lt;/span&gt;
  &lt;span class="na"&gt;first_blocker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pacing&lt;/span&gt;
    &lt;span class="na"&gt;segment_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s07&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;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ends&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&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;answer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;starts&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;without&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;perceptible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;turn-taking&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gap."&lt;/span&gt;
  &lt;span class="na"&gt;return_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;audio_treatment&lt;/span&gt;
  &lt;span class="na"&gt;allowed_fix_scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pause_before_s08&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;regenerate_audio_s07_s08&lt;/span&gt;
  &lt;span class="na"&gt;material_revision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This record answers the operational questions that a score cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What failed first?&lt;/li&gt;
&lt;li&gt;Where did it fail?&lt;/li&gt;
&lt;li&gt;What evidence supports the rejection?&lt;/li&gt;
&lt;li&gt;Which production layer owns the repair?&lt;/li&gt;
&lt;li&gt;What may change in the next run?&lt;/li&gt;
&lt;li&gt;What must remain fixed?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make the release boundary machine-readable
&lt;/h2&gt;

&lt;p&gt;A human may supply the observations, but the decision boundary can still be explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PUBLISHABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishable_as_is&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;MINOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishable_with_minor_edits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REVISE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REDESIGN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redesign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PassResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Verdict&lt;/span&gt;
    &lt;span class="n"&gt;first_blocker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;return_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release_decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PassResult&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REVISE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REDESIGN&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_blocker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_blocker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minor_edits_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MINOR&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a claim that computer vision or an LLM can reliably infer every perceptual defect.&lt;/p&gt;

&lt;p&gt;The purpose of the code is narrower: once observations exist, do not let a scoring average erase an observable release blocker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define "minor edits" narrowly
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;publishable_with_minor_edits&lt;/code&gt; easily becomes a polite name for "not actually accepted."&lt;/p&gt;

&lt;p&gt;I limit minor edits to changes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a typo;&lt;/li&gt;
&lt;li&gt;a local volume adjustment;&lt;/li&gt;
&lt;li&gt;one segment's pronunciation;&lt;/li&gt;
&lt;li&gt;one caption timing correction;&lt;/li&gt;
&lt;li&gt;a short replacement that does not change the argument or structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat these as material revisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restructuring the script;&lt;/li&gt;
&lt;li&gt;regenerating a substantial part of the narration;&lt;/li&gt;
&lt;li&gt;changing the background or overall screen design;&lt;/li&gt;
&lt;li&gt;changing the character's role or personality;&lt;/li&gt;
&lt;li&gt;redesigning pacing across several segments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a material revision is required, that run does not count as Accepted. The repaired artifact becomes a new run and goes through all three passes again.&lt;/p&gt;

&lt;p&gt;This rule prevents acceptance metrics from improving only because every rejected artifact was renamed "accepted with edits."&lt;/p&gt;

&lt;h2&gt;
  
  
  After the same rejection twice, stop micro-tuning
&lt;/h2&gt;

&lt;p&gt;Small parameter changes are useful for a local defect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a little more pause;&lt;/li&gt;
&lt;li&gt;reduce speech speed;&lt;/li&gt;
&lt;li&gt;adjust one transition;&lt;/li&gt;
&lt;li&gt;change one crop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if the same rejection category appears twice, the problem may no longer be a parameter.&lt;/p&gt;

&lt;p&gt;I use this return rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same_rejection_category &amp;gt;= 2
    -&amp;gt; stop micro-tuning
    -&amp;gt; return to audio treatment or visual treatment
    -&amp;gt; create a new run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeating the same ambiguous prompt with slightly different numbers produces more artifacts without improving diagnosis.&lt;/p&gt;

&lt;p&gt;Moving up one design layer makes the next hypothesis testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Accepted video is not a stable pipeline
&lt;/h2&gt;

&lt;p&gt;A template may work for one topic because the script happens to fit its timing and visual rhythm.&lt;/p&gt;

&lt;p&gt;Change the topic and the pipeline can fail immediately.&lt;/p&gt;

&lt;p&gt;I separate a single success from operational stability by keeping a consecutive-acceptance streak tied to one series-template hash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_stability_streak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;streak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;same_template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;material_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;same_template&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;material_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;streak&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My current release rule calls the template Stable only after three different topics are consecutively Accepted.&lt;/p&gt;

&lt;p&gt;Three is not a universal statistical threshold. It is an operating rule for separating "one successful sample" from "a template I am willing to reuse."&lt;/p&gt;

&lt;p&gt;The important part is the reset behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;revise&lt;/code&gt; resets the streak;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redesign&lt;/code&gt; resets the streak;&lt;/li&gt;
&lt;li&gt;a material revision resets the streak;&lt;/li&gt;
&lt;li&gt;changing the series-template hash starts a new streak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not publish three successes selected after the fact and ignore the failures between them. The sequence is the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep machine QA and human QA in different jobs
&lt;/h2&gt;

&lt;p&gt;The answer is not to replace automation with subjective review.&lt;/p&gt;

&lt;p&gt;It is to assign each type of evidence to the right owner.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Machine responsibility&lt;/th&gt;
&lt;th&gt;Human responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codec, resolution, required streams&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration, timeline gaps, subtitle ordering&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;td&gt;Exception review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required artifacts and source trace&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;td&gt;Meaning and rights judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural pronunciation and conversational pauses&lt;/td&gt;
&lt;td&gt;Assistance&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether backgrounds and shots carry meaning&lt;/td&gt;
&lt;td&gt;Assistance&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attention conflict across audio, visuals, and captions&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether the first-watch artifact should be published&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatability across different topics&lt;/td&gt;
&lt;td&gt;Record and calculate&lt;/td&gt;
&lt;td&gt;Interpret and approve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With this split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automated QA proves &lt;code&gt;moves&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;first-watch review proves &lt;code&gt;usable&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;owner and target-viewer decisions prove &lt;code&gt;accepted&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;run history proves &lt;code&gt;stable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each claim has evidence appropriate to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for the next AI video
&lt;/h2&gt;

&lt;p&gt;Before generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Decide which state this run is trying to prove.&lt;/li&gt;
&lt;li&gt;[ ] Fix the series-template hash.&lt;/li&gt;
&lt;li&gt;[ ] Define the boundary between minor and material revision.&lt;/li&gt;
&lt;li&gt;[ ] Name the target viewer.&lt;/li&gt;
&lt;li&gt;[ ] Prevent reviewers from receiving production-side explanations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Prove &lt;code&gt;moves&lt;/code&gt; with automated QA.&lt;/li&gt;
&lt;li&gt;[ ] Review audio only.&lt;/li&gt;
&lt;li&gt;[ ] Review silent visual.&lt;/li&gt;
&lt;li&gt;[ ] Perform one integrated first watch at normal speed.&lt;/li&gt;
&lt;li&gt;[ ] Record one first blocker.&lt;/li&gt;
&lt;li&gt;[ ] Map the blocker to a return layer and allowed repair scope.&lt;/li&gt;
&lt;li&gt;[ ] After the same rejection twice, return to treatment design.&lt;/li&gt;
&lt;li&gt;[ ] Reset the stability streak after anything other than Accepted under the same template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this model does not guarantee
&lt;/h2&gt;

&lt;p&gt;This framework does not automatically create a good video.&lt;/p&gt;

&lt;p&gt;It does not guarantee:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;that every viewer will like the result;&lt;/li&gt;
&lt;li&gt;that one review catches every quality problem;&lt;/li&gt;
&lt;li&gt;that three accepted videos will prevent all future regressions;&lt;/li&gt;
&lt;li&gt;that AI can replace the release owner or target viewer;&lt;/li&gt;
&lt;li&gt;that one model or video provider is superior;&lt;/li&gt;
&lt;li&gt;that production time or cost will decrease.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its purpose is to stop treating technical generation as finished media.&lt;/p&gt;

&lt;p&gt;A video that moves is an engineering result.&lt;/p&gt;

&lt;p&gt;A video that somebody can publish without explanation is a different result.&lt;/p&gt;

&lt;p&gt;A pipeline that can repeat that outcome across topics is different again.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>video</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your AI Adoption Is Waiting on Approval, Not Better Prompts</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 12 Sep 2026 02:01:15 +0000</pubDate>
      <link>https://dev.to/nomurasan/your-ai-adoption-is-waiting-on-approval-not-better-prompts-2afi</link>
      <guid>https://dev.to/nomurasan/your-ai-adoption-is-waiting-on-approval-not-better-prompts-2afi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The author developed this approval model while incorporating AI into real work. AI recomposed the verified Japanese primary for DEV readers and generated the diagram under the standing delegation for English-market publication. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI can shorten drafting while leaving the organization waiting on approval.&lt;/p&gt;

&lt;p&gt;When that happens, teams often reach for the familiar fixes: improve the prompt, buy a stronger model, or train people to use the tool better. Those fixes improve generation. They do not answer the question an approver actually receives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly am I accepting, what could go wrong, and who can stop it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If those answers arrive only after the artifact is finished, every request forces the approver to reconstruct the operating boundary from scratch. The work moves quickly until it reaches the approval queue, then slows down in clarification, reassignment, and re-review.&lt;/p&gt;

&lt;p&gt;The useful unit is not another AI policy document. It is a small approval interface attached to each AI-assisted request.&lt;/p&gt;

&lt;p&gt;My minimum interface has five fields:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;information class;&lt;/li&gt;
&lt;li&gt;delegated task;&lt;/li&gt;
&lt;li&gt;human checkpoint;&lt;/li&gt;
&lt;li&gt;explanation duty;&lt;/li&gt;
&lt;li&gt;stop authority.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Define those fields before asking for approval, then route low- and high-risk uses differently. That shifts approval from a vague judgment about “AI” to a concrete decision about one bounded use.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Faster generation can move elapsed time into approval rather than remove it.&lt;/li&gt;
&lt;li&gt;An approver needs the input boundary, delegated task, human check, explanation duty, and stop authority.&lt;/li&gt;
&lt;li&gt;Put those five fields on the work request before the artifact reaches review.&lt;/li&gt;
&lt;li&gt;Use information sensitivity and decision depth to choose a light or high-assurance route.&lt;/li&gt;
&lt;li&gt;Do not add a new committee by default. Make the existing decision legible and test whether the route catches the failures it claims to protect against.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The queue is asking for a missing interface
&lt;/h2&gt;

&lt;p&gt;Consider a team that uses AI to prepare three items:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a summary of public documentation for internal use;&lt;/li&gt;
&lt;li&gt;a draft customer proposal based on confidential notes;&lt;/li&gt;
&lt;li&gt;a recommendation that changes a production configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three may be called “AI-assisted work,” but they do not ask the organization to accept the same exposure.&lt;/p&gt;

&lt;p&gt;The first item uses public information and produces a reversible internal aid. The second introduces confidential information and external communication. The third delegates part of an operational decision and can affect a live system.&lt;/p&gt;

&lt;p&gt;If every item reaches the same review queue with only a title and generated artifact, the approver must discover the differences manually. A cautious approver asks more questions. An overloaded approver may apply one heavy process to all three. An impatient team may work around the process.&lt;/p&gt;

&lt;p&gt;None of those outcomes is primarily a prompting problem. The request omitted the fields needed to route the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-field approval interface
&lt;/h2&gt;

&lt;p&gt;The interface should be short enough to travel with the work, but precise enough that “approved” has a stable meaning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fai-approval-design-bottleneck-interface-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fai-approval-design-bottleneck-interface-en.png" alt="A work request passes through five approval fields—information, task, human check, explanation, and stop authority—before risk routes it to a light or high-assurance path." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Information: what entered the system?
&lt;/h3&gt;

&lt;p&gt;Classify the input before debating the output.&lt;/p&gt;

&lt;p&gt;A useful starting set is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public;&lt;/li&gt;
&lt;li&gt;internal;&lt;/li&gt;
&lt;li&gt;confidential or customer-related;&lt;/li&gt;
&lt;li&gt;regulated or otherwise restricted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This field should also name the relevant handling boundary. “Internal” alone is weak if the reviewer still has to ask whether the chosen service may receive it, retain it, or use it for training.&lt;/p&gt;

&lt;p&gt;The goal is not to publish secret infrastructure details on every ticket. The goal is to make the admissible information class observable at the decision point.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Task: what judgment was delegated?
&lt;/h3&gt;

&lt;p&gt;“Used AI” does not describe the delegation.&lt;/p&gt;

&lt;p&gt;These are materially different tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;formatting or summarizing supplied material;&lt;/li&gt;
&lt;li&gt;generating a draft;&lt;/li&gt;
&lt;li&gt;comparing options;&lt;/li&gt;
&lt;li&gt;recommending a decision;&lt;/li&gt;
&lt;li&gt;executing a change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Approval becomes harder as the system moves from transforming provided material toward making or executing a consequential choice. State the deepest delegated action, not the safest-sounding label.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Human check: who verifies what before use?
&lt;/h3&gt;

&lt;p&gt;Naming a reviewer is not enough. Define the pass condition.&lt;/p&gt;

&lt;p&gt;For a technical change, the human check might require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the proposed diff matches the request;&lt;/li&gt;
&lt;li&gt;tests cover the changed behavior;&lt;/li&gt;
&lt;li&gt;secrets and private identifiers are absent;&lt;/li&gt;
&lt;li&gt;the exact deployment target is pinned;&lt;/li&gt;
&lt;li&gt;rollback remains available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For customer-facing prose, the check might focus on factual support, scope, confidentiality, and authority to make commitments.&lt;/p&gt;

&lt;p&gt;The checkpoint should answer “what evidence lets this move forward?” rather than “who is eventually blamed?”&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Explanation: what must remain reconstructable?
&lt;/h3&gt;

&lt;p&gt;Not every AI-assisted output needs a model-level explanation. Every consequential use needs enough evidence to explain the decision after the fact.&lt;/p&gt;

&lt;p&gt;That may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the sources used;&lt;/li&gt;
&lt;li&gt;the material assumptions;&lt;/li&gt;
&lt;li&gt;the reviewer and acceptance evidence;&lt;/li&gt;
&lt;li&gt;the difference between observed facts and generated suggestions;&lt;/li&gt;
&lt;li&gt;the destination and public or customer-facing claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The required depth should match the consequence. A public-document summary does not need the same record as a production migration. But “we cannot tell what was reviewed” is not an acceptable default for either.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stop authority: who can halt or reverse the use?
&lt;/h3&gt;

&lt;p&gt;A rule without an operator is decorative.&lt;/p&gt;

&lt;p&gt;The request should name:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the signal that triggers a stop;&lt;/li&gt;
&lt;li&gt;the person or role allowed to stop it;&lt;/li&gt;
&lt;li&gt;the action that prevents further impact;&lt;/li&gt;
&lt;li&gt;the safe state after the stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a draft, stopping may mean discarding the artifact. For an automated production action, it may mean disabling a job, revoking a capability, or restoring a verified previous state.&lt;/p&gt;

&lt;p&gt;This field is often missing because teams discuss approval as permission to start. Reliable approval also defines permission to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route by two dimensions, not by the word “AI”
&lt;/h2&gt;

&lt;p&gt;Once the five fields exist, route the request using two dimensions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;information sensitivity&lt;/strong&gt; — how damaging misuse or disclosure could be;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;decision depth&lt;/strong&gt; — how far the system moves from assistance toward consequential judgment or execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives a practical matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Information&lt;/th&gt;
&lt;th&gt;Delegated task&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Summarize or format&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal&lt;/td&gt;
&lt;td&gt;Draft with a named human check&lt;/td&gt;
&lt;td&gt;Light or standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidential&lt;/td&gt;
&lt;td&gt;Compare or recommend&lt;/td&gt;
&lt;td&gt;High assurance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restricted&lt;/td&gt;
&lt;td&gt;Execute or communicate externally&lt;/td&gt;
&lt;td&gt;High assurance, or prohibited by policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is an example, not a universal legal classification. Each organization still has to bind it to its contracts, regulations, and systems.&lt;/p&gt;

&lt;p&gt;The important design choice is asymmetry. A harmless internal summary should not wait behind the same evidence package as a customer commitment. A production-changing recommendation should not inherit the light route merely because the generated prose looks polished.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two routes should change
&lt;/h2&gt;

&lt;p&gt;A light route may allow one named reviewer, a compact evidence record, and a reversible destination. It optimizes for low coordination cost.&lt;/p&gt;

&lt;p&gt;A high-assurance route may require separate content and execution authority, stronger source evidence, an exact target binding, rollback proof, and live read-back after the change. It optimizes for preventing silent expansion of consequence.&lt;/p&gt;

&lt;p&gt;The route should change the evidence and authority required—not the truthfulness standard. Low risk is not permission to invent facts, leak information, or claim success without verification.&lt;/p&gt;

&lt;p&gt;Likewise, a high-assurance route should not become a ritual that repeats checks without knowing what failure each one protects against. Every additional gate should have a named risk, acceptance condition, and removal or redesign condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  A request schema you can implement
&lt;/h2&gt;

&lt;p&gt;The interface can live in a ticket template, pull request form, document header, or workflow payload.&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;ai_use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;information&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;handling_boundary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approved_service_only&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;delegated_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;draft_and_compare&lt;/span&gt;
    &lt;span class="na"&gt;prohibited_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;final_customer_commitment&lt;/span&gt;
  &lt;span class="na"&gt;human_check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;account_lead&lt;/span&gt;
    &lt;span class="na"&gt;pass_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;claims_match_source&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scope_has_no_new_commitment&lt;/span&gt;
  &lt;span class="na"&gt;explanation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;retain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;source_refs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reviewer_receipt&lt;/span&gt;
  &lt;span class="na"&gt;stop_authority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;account_lead&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unsupported_claim_or_scope_change&lt;/span&gt;
    &lt;span class="na"&gt;safe_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keep_as_internal_draft&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a complete governance standard. It is the smallest useful contract between the person asking for work and the person accepting its consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the approval system itself
&lt;/h2&gt;

&lt;p&gt;Approval design can fail even when every request has the five fields.&lt;/p&gt;

&lt;p&gt;Measure outcomes that reveal where the process is moving time and risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requests returned because a field was missing;&lt;/li&gt;
&lt;li&gt;time spent waiting for an owner-only decision;&lt;/li&gt;
&lt;li&gt;low-risk requests sent through the heavy route;&lt;/li&gt;
&lt;li&gt;high-risk requests that escaped the heavy route;&lt;/li&gt;
&lt;li&gt;approval decisions reopened because the artifact or target changed;&lt;/li&gt;
&lt;li&gt;successful stops and restorations when a trigger fired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid one misleading metric: raw approval speed. A process can approve quickly because it stopped looking. The target is the shortest verified path to an accepted or safely rejected outcome, with no ambiguous authority and no unverified success claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one repeated queue
&lt;/h2&gt;

&lt;p&gt;Do not begin by writing a company-wide AI constitution.&lt;/p&gt;

&lt;p&gt;Choose one queue where AI-assisted work repeatedly waits: customer drafts, internal analysis, code changes, or publication. Add the five fields to the request. Define one light route and one high-assurance route. Run real items through them and record where approvers still need to reconstruct context.&lt;/p&gt;

&lt;p&gt;When the same missing question appears repeatedly, turn it into a field or deterministic preflight. When a field never changes a decision, remove or redesign it. When a low-risk request repeatedly enters the heavy route, repair the classifier rather than telling people to be less cautious.&lt;/p&gt;

&lt;p&gt;Better prompts may improve the artifact. Better approval interfaces improve whether the organization can use it.&lt;/p&gt;

&lt;p&gt;The distinction matters because generation speed is local. Approval latency is shared across the team. If you optimize only the first, the saved time does not disappear—it reappears in someone else's queue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>management</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Cloudflare Blocked My Playwright Login. Attaching to a Chrome I Started Myself Walked Straight Through.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Wed, 09 Sep 2026 07:06:42 +0000</pubDate>
      <link>https://dev.to/nomurasan/cloudflare-blocked-my-playwright-login-attaching-to-a-chrome-i-started-myself-walked-straight-50e5</link>
      <guid>https://dev.to/nomurasan/cloudflare-blocked-my-playwright-login-attaching-to-a-chrome-i-started-myself-walked-straight-50e5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The author hit this block on their own machine and confirmed it in a real window. An AI agent working in that environment ran the debugging session, and every error string below was copied from its run logs. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Short answer: in this incident, the browser Playwright launched failed the login check while a Chrome started outside Playwright and attached over CDP succeeded. If you see the same pattern on a browser and account you own, test an attach path instead of endlessly changing launch flags. Do not treat that contrast as evidence of what Cloudflare detects internally.&lt;/p&gt;

&lt;p&gt;I needed to read a deploy log on a dashboard I own. A publishing pipeline was reporting success while the published articles returned 404, and the platform's own deploy log was the one place that would say why. It sits behind a login. I have that login. Automating the read should have been fifteen minutes.&lt;/p&gt;

&lt;p&gt;It was not fifteen minutes, and the reason turned out to be worth writing down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The problem: Cloudflare Turnstile showed "verification failed" in the browser Playwright launched, and kept showing it across the launch-side changes I tried.&lt;/li&gt;
&lt;li&gt;The observed contrast: a Chrome I started outside Playwright, then attached to over CDP, reached the login/dashboard path successfully in the same environment.&lt;/li&gt;
&lt;li&gt;The limit: those runs do not tell me whether Cloudflare reacted to launch arguments, process state, profile state, startup sequence, or some other signal correlated with the launch path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The script opened the dashboard URL and landed on the login page, which is fair enough. The interesting part was the login form itself. The Turnstile widget rendered, spun, and settled on a small red banner reading "verification failed" with a troubleshooting link. Not a challenge to solve. Not a timeout. A refusal.&lt;/p&gt;

&lt;p&gt;The same URL, opened by hand in a normal window on the same machine, logged in without showing a challenge at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not help
&lt;/h2&gt;

&lt;p&gt;Every one of these was tried and produced the same refusal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;launch_persistent_context(channel="chrome")&lt;/code&gt;, so the binary was the installed Chrome rather than bundled Chromium.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--disable-blink-features=AutomationControlled&lt;/code&gt;, a flag commonly suggested for automation-detection problems.&lt;/li&gt;
&lt;li&gt;A freshly created browser profile.&lt;/li&gt;
&lt;li&gt;Clicking the alternative sign-in button, which only moved the failure to a different provider's page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of those experiments stayed inside the Playwright-launch path. None tested the more important contrast: what happens if the browser process exists before Playwright connects to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The observed discriminator was the launch path, not the binary
&lt;/h2&gt;

&lt;p&gt;The useful observation was narrow: changing the Chrome binary choice and launch-side flags did not help, while starting Chrome outside Playwright and attaching afterwards did. Those paths differ in several variables, so this does not identify Cloudflare's signal.&lt;/p&gt;

&lt;p&gt;CDP attach tests the operational boundary directly: the browser process already exists before Playwright connects.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Need to drive a logged-in browser] --&amp;gt; B{Connection path tested}
    B --&amp;gt;|Playwright launches Chrome| C[Observed: Turnstile verification failed]
    B --&amp;gt;|Start Chrome externally| D[Attach afterwards over CDP]
    D --&amp;gt; E[Observed: login/dashboard path succeeded]
    C --&amp;gt; F[Cloudflare's internal discriminator: unknown]
    E --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In practice that means starting Chrome with a debugging port and a dedicated profile directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remote-debugging-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9222 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.config/chrome-cdp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in once, by hand, in that window. From then on the session lives in that profile and your scripts can attach to it. Keep the debugging endpoint local and treat that profile as privileged: anything allowed to drive it can act with that session's authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three issues I hit right after switching
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. In this setup, zero page targets break &lt;code&gt;connect_over_cdp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This one cost me the most time because the error text points at downloads while the state change that fixed it was opening a page target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BrowserType.connect_over_cdp: Protocol error (Browser.setDownloadBehavior):
Browser context management is not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the environment below, &lt;code&gt;connect_over_cdp&lt;/code&gt; throws when the running Chrome has no page target open. You cannot inspect &lt;code&gt;browser.contexts&lt;/code&gt; afterwards and recover because there is no &lt;code&gt;browser&lt;/code&gt; yet. Opening a tab first restores the connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, 2026-09-12:&lt;/strong&gt; a reader (Skillselion) asked whether this was a fresh-start effect or something that happened only after closing the last tab in an already-running browser. I re-ran it on a throwaway profile with macOS 26.6.2, Chrome 149.0.7827.53, and Playwright 1.60.0. The exact incident-time Chrome/Playwright versions were not pinned and cannot be recovered.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Chrome state&lt;/th&gt;
&lt;th&gt;&lt;code&gt;connect_over_cdp&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fresh process, 1 page open&lt;/td&gt;
&lt;td&gt;1 page&lt;/td&gt;
&lt;td&gt;succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same process, that page closed via &lt;code&gt;/json/close&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0 pages&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fails&lt;/strong&gt;, exact error above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same process, &lt;code&gt;PUT /json/new?about:blank&lt;/code&gt; after that&lt;/td&gt;
&lt;td&gt;1 page&lt;/td&gt;
&lt;td&gt;succeeds again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome killed and restarted, then its default page closed immediately&lt;/td&gt;
&lt;td&gt;0 pages&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fails&lt;/strong&gt;, same error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this matrix, fresh versus previously running did not distinguish the result; success tracked whether &lt;code&gt;GET /json/list&lt;/code&gt; contained a &lt;code&gt;type: page&lt;/code&gt; target. That is scoped to this Chrome/Playwright pair: the same error can have other causes elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;EP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:9222&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EP&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/json/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EP&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/json/new?about:blank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_over_cdp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.invalid/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;PUT&lt;/code&gt;. Recent Chrome versions reject &lt;code&gt;GET /json/new&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Chrome refuses remote debugging on your default profile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DevTools remote debugging requires a non-default data directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.chrome.com/blog/remote-debugging-port" rel="noopener noreferrer"&gt;Chrome's remote-debugging change&lt;/a&gt; documents that Chrome 136 and later ignore the remote-debugging switches for the default Chrome data directory. This is a deliberate protection, not a bug to route around. Use a separate &lt;code&gt;--user-data-dir&lt;/code&gt; and treat it as a purpose-built automation profile.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Teardown is an ownership problem
&lt;/h3&gt;

&lt;p&gt;The reader comment exposed a flaw in my original framing: authentication state is not ownership. On Playwright 1.60.0 I measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Launch path&lt;/strong&gt; (&lt;code&gt;p.chromium.launch()&lt;/code&gt;): Playwright started the browser process. &lt;code&gt;browser.close()&lt;/code&gt; killed the Chrome-for-Testing process group in my test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach path&lt;/strong&gt; (&lt;code&gt;connect_over_cdp()&lt;/code&gt;): &lt;code&gt;browser.close()&lt;/code&gt; disconnected the Playwright side while the external Chrome PID, CDP endpoint, and existing tab remained alive.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ctx.close()&lt;/code&gt; on &lt;code&gt;browser.contexts[0]&lt;/code&gt; also left the external process and tabs alone in this test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://playwright.dev/python/docs/api/class-browser#browser-close" rel="noopener noreferrer"&gt;Playwright Python &lt;code&gt;Browser.close&lt;/code&gt; API&lt;/a&gt; makes the same launched-versus-connected distinction.&lt;/p&gt;

&lt;p&gt;So the durable model is not "logged in versus not logged in." It is at least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser_owner = agent | external
page_owner    = agent | external
connection    = launched | attached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Page ownership still matters: an attached agent can navigate or close a human-owned tab. A safer teardown policy is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;teardown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;browser_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;browser_owner&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;external&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_closed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# close only pages this run created
&lt;/span&gt;        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               &lt;span class="c1"&gt;# tested here as connection teardown
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                   &lt;span class="c1"&gt;# owned launch: close the owned process
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For externally owned browsers, keep human-owned/default pages out of &lt;code&gt;agent_pages&lt;/code&gt;. If ownership is unknown, fail safe: do not close pages merely because they are visible through the attached context.&lt;/p&gt;

&lt;p&gt;The reader also pointed to Microsoft's separate &lt;a href="https://github.com/microsoft/playwright-cli/blob/main/skills/playwright-cli/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/a&gt;, not &lt;code&gt;python -m playwright&lt;/code&gt;. Its documented &lt;code&gt;attach --cdp=...&lt;/code&gt; / &lt;code&gt;detach&lt;/code&gt; pair makes external-browser lifecycle explicit and leaves the browser running on detach. I use that as a design precedent, not as proof of Python &lt;code&gt;connect_over_cdp()&lt;/code&gt; internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The profile copy that did not work here
&lt;/h2&gt;

&lt;p&gt;Copying the authenticated Chrome profile retained account hints but not the authenticated session; the login page still asked for a password. I did not isolate why. The supported claim is only that &lt;strong&gt;this profile copy did not preserve login in this incident&lt;/strong&gt;. I kept the session in the dedicated profile where it was established and attached to that running browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whether it was worth it
&lt;/h2&gt;

&lt;p&gt;The dashboard finally opened, and its deploy log showed the real problem: the newest article had not been deployed because the account had hit a posting-rate limit. I had spent about forty minutes comparing metadata and branches before reading the platform's own answer. When a service reports success but the public result disagrees, read the service's log before diffing your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Compare the same URL in a normal browser and in the automation launch path; treat the difference as an observation, not a diagnosis of the bot detector.&lt;/li&gt;
&lt;li&gt;If you own the target browser/account and need the already-authenticated session, start a dedicated Chrome with &lt;code&gt;--remote-debugging-port&lt;/code&gt; and a non-default &lt;code&gt;--user-data-dir&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Log in normally in that dedicated window. Do not automate a CAPTCHA, OTP, or access challenge.&lt;/li&gt;
&lt;li&gt;In the Chrome/Playwright combination tested here, check &lt;code&gt;/json/list&lt;/code&gt; for a page target and &lt;code&gt;PUT /json/new?about:blank&lt;/code&gt; if there is none.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;connect_over_cdp&lt;/code&gt;, then use the existing context from &lt;code&gt;browser.contexts[0]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Track browser and page ownership explicitly. On teardown, close only agent-owned pages; &lt;code&gt;browser.close()&lt;/code&gt; on the tested CDP attach path disconnects without killing the external browser.&lt;/li&gt;
&lt;li&gt;Keep remote debugging bound to local/trusted use and keep a fallback to the normal launch path where no debug endpoint is available.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;Does this defeat bot detection? No. It reuses a session legitimately established on a dashboard the operator owns; it does not forge credentials or automate a challenge.&lt;/p&gt;

&lt;p&gt;Will attaching always pass? Unknown. It passed in this environment; Cloudflare's internal decision process was not measured.&lt;/p&gt;

&lt;p&gt;Puppeteer or Selenium? They can connect to running browsers, but I did not reproduce this Cloudflare result with them.&lt;/p&gt;

&lt;p&gt;Why not headless? A human authenticated this incident's session in a visible browser. That is not a universal headed-mode requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not verify
&lt;/h2&gt;

&lt;p&gt;I did not identify Cloudflare's internal detection signal. The original incident covered two launch paths, one environment and one site. Zero-page-target and teardown behavior were re-tested on 2026-09-12 with macOS 26.6.2 / Chrome 149.0.7827.53 / Playwright 1.60.0; the incident-time versions were not pinned. Microsoft's &lt;code&gt;playwright-cli&lt;/code&gt; is cited only as a lifecycle design precedent, while Python behavior is supported separately by the local experiment and API documentation.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>automation</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>The sun does not rise in my world, and my AI keeps forgetting</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:05:18 +0000</pubDate>
      <link>https://dev.to/nomurasan/the-sun-does-not-rise-in-my-world-and-my-ai-keeps-forgetting-2o64</link>
      <guid>https://dev.to/nomurasan/the-sun-does-not-rise-in-my-world-and-my-ai-keeps-forgetting-2o64</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I defined the world and decided what its rules forbid. AI wrote the prose being checked and built the checker. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The world I am writing has a fire god who governs the sun. The moment I settled on that, I could no longer let an AI write "the sun rises."&lt;/p&gt;

&lt;p&gt;The sun does not rise there. The fire god wakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the setting itself forbids words
&lt;/h2&gt;

&lt;p&gt;Most content rules come from outside the work: don't write slurs, don't write this or that. This one comes from inside it.&lt;/p&gt;

&lt;p&gt;There are no clocks and no calendars in that world. So "yesterday" does not exist. Neither does "a week later" or "three hours." Write one of them and the world now contains a measuring instrument it was never given.&lt;/p&gt;

&lt;p&gt;The replacements come out of the setting too.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cannot write&lt;/th&gt;
&lt;th&gt;Can write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;日が昇る (the sun rises)&lt;/td&gt;
&lt;td&gt;光が灯る (light kindles) / 火神が目覚める (the fire god wakes) / 明けが来る (daybreak comes) / 白む (it whitens)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"The fire god wakes" works as a substitute for "the sun rises" only because the fire god governs the sun. Change the setting and the substitutes change with it. The list of forbidden words grows out of the worldbuilding.&lt;/p&gt;

&lt;p&gt;The scariest entry was 商人 — merchant. The setting says there is no such profession in this world. When the AI casually writes "the merchant said," the world now has an economy. Unlike the time expressions, it reads perfectly naturally. That is why it slips through.&lt;/p&gt;

&lt;h2&gt;
  
  
  I counted, and it was not where I thought
&lt;/h2&gt;

&lt;p&gt;The run after I redefined the scan scope (2026-09-06) came back exit 1 with 102 violations.&lt;/p&gt;

&lt;p&gt;Before counting, I believed the violations were concentrated in dialogue. I had the impression that the model reverts to modern Japanese the moment it speaks as a character. Lines like these do stand out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;「昨日おっしゃっていた温度管理って、これのことですか」
  "Is this the temperature control you mentioned yesterday?"
「あの……昨日、色々教えていただいたので」
  "Um... you taught me a lot yesterday, so..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But when I classified all 102 mechanically, only 31 lines contained quotation marks. The other 71 were narration. Roughly 30/70.&lt;/p&gt;

&lt;p&gt;That is where I had to stop and check something. 30/70 does not mean "skewed toward narration." If the manuscripts are mostly narration to begin with, the ratio says nothing at all. So I counted the base rate: across the 132 files and 16,483 non-empty lines the checker actually reads, 4,938 lines (30.0%) contain quotation marks and 11,545 (70.0%) do not.&lt;/p&gt;

&lt;p&gt;The violations' 30.4/69.6 is almost exactly the base rate of 30.0/70.0. In aggregate, then, the violations are concentrated in neither dialogue nor narration; they simply track the distribution of lines. Per term, though, the picture changes. 昨日 ("yesterday"), the most frequent at 34 hits, splits 17 dialogue to 17 narration — 50% against a base rate of 30.0%, twenty points high. That one term really is tilted toward dialogue. The aggregate stays flat because the remaining terms swing the other way and cancel it. My prediction of a concentration in dialogue was right about the most frequent word and wrong about the whole.&lt;/p&gt;

&lt;p&gt;And the largest cluster was somewhere I had not been looking.&lt;/p&gt;

&lt;p&gt;In descending order, all of them. Each line is assigned to exactly one term, so the column sums to the line count, 102.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;昨日 yesterday&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;夕 evening (夕日 sunset / 夕餉 supper / 夕暮 dusk)&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日が傾く the sun tilts&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;今日 today&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;朝 morning (朝日 morning sun)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;商人 merchant&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;陽光 sunlight&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;冬 winter&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;夏 summer&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;月 moon / month&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;夜 night&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;半日ほど about half a day&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日が高く the sun high&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Morning and evening are, as I explain below, deliberately not matched as bare words; only compounds are. So every one of the 7 and 24 hits listed here is a compound.&lt;/p&gt;

&lt;p&gt;All 24 hits on 夕 were narration: 10 夕日 (sunset), 5 夕餉 (supper), 5 夕暮 (dusk), 3 夕闘, 1 夕方 (evening). Most of them are lines like "the setting sun dyed the village orange," the kind of sentence that opens a scene. The 夕闘 group is different: 夕闘 is not a word at all, but a typo for 夕陽 ("setting sun"). All three hits are the same sentence, 夕闘が沈み、最初の星が東の空に瞬いた, duplicated into three places. (I set narration off without 「」 here, because in this article 「」 is what marks a line as dialogue.) The checker found it because 夕闘 is listed alongside the real words.&lt;/p&gt;

&lt;p&gt;I am writing a world where the sun does not rise, and the sun was setting in it. All 7 hits on morning were 朝日 — and there the sun was coming up.&lt;/p&gt;

&lt;p&gt;One note on how these are counted. 102 is a count of lines, not of words. A single line can carry more than one forbidden word, and counting every word occurrence gives 114. The breakdown: 93 lines carry one word, 8 carry two, and 1 carries five. The surplus is 8×1 + 1×4 = 12, and 102 + 12 = 114.&lt;/p&gt;

&lt;p&gt;Here is one such line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;今日出会った商隊の青年のことを思い出していた。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It carries two: 今日 ("today") and 商人 ("merchant", as 商隊 "caravan"). But it is still one line. Every count in this article assigns each line to exactly one word. A number in any table row is not "lines containing that word" but "lines assigned to that word," and it can be smaller than the former. So the two-word line quoted just above is counted only under 今日.&lt;/p&gt;

&lt;p&gt;The top five terms account for 79 lines, but the eight below them say the same things: the sun is moving, or this world is measuring time in calendar units it does not have. One entry was a different animal — 来月 ("next month"), counted under 月, which here is not a celestial body but a unit of calendar. A merchant saying "see you next month" in a world with no calendar is a different kind of violation from the sun moving.&lt;/p&gt;

&lt;p&gt;5 of those 102 lines are not prose at all — they are the author's own review notes. One of them reads, in my own hand, that 日が高くなっていた is not on the explicit list of forbidden phrases but arguably violates its intent. That note was itself flagged as a violation.&lt;/p&gt;

&lt;p&gt;This matters for the distribution too. All 5 contain quotation marks — they quote the forbidden phrases — so 5 of that earlier 31 sat on the dialogue side. Restricted to lines the AI actually wrote, it is 97 hits: 26 dialogue, 71 narration, 26.8% to 73.2%. That is &lt;em&gt;below&lt;/em&gt; the 30.0% base rate. Excluding the notes, the violations lean slightly toward narration. In aggregate, with the notes or without them, this is not "concentrated in dialogue."&lt;/p&gt;

&lt;p&gt;The 102-violation run above already uses the redefined scope, and these 5 survived it. The definition I settled on is "body text a reader might read," and that is a boundary drawn per path. But these 5 are annotations the author wrote &lt;em&gt;inside&lt;/em&gt; a manuscript file. As long as prose and notes live in the same file, a path-level rule cannot separate them. The definition is not wrong; it just does not reach that far at path granularity. A line-level rule would clear them, and that is the next boundary — one I have not drawn yet.&lt;/p&gt;

&lt;p&gt;If the counts are about even, why did I think dialogue dominated? Salience, I think. Modern speech inside quotation marks snags as you read, so it stays in memory. A sunset inside a landscape description reads too naturally to register as a setting violation at all. The same number of hits, but only one kind accumulates in memory. Reading with my eyes, I was never going to find the other.&lt;/p&gt;

&lt;p&gt;Why they do not skew is not something this observation can tell me. I have not compared models, and I have not compared prompts. One work, one model, one run, and the distribution came out matching the base rate.&lt;/p&gt;

&lt;p&gt;What I can say is the other thing: I should not have trusted my own expectation without counting it. It held for exactly one word — the most frequent one — and failed for the whole.&lt;/p&gt;

&lt;p&gt;Listing the forbidden words in the prompt did not stop this. I never ran an A/B comparison, so I cannot claim prompts always leak — but in the way I write, the longer the passage, the more leaked through. Writing first and counting afterwards was the more reliable option for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  I got the scan scope wrong twice
&lt;/h2&gt;

&lt;p&gt;The checker is close to grep. Collect the lines that hit a forbidden pattern, exit 1 if there is at least one. The hard part was never the detection. It was deciding where to look.&lt;/p&gt;

&lt;p&gt;The first run flagged 18 lines. I went to fix them and found that every single one was a false positive.&lt;/p&gt;

&lt;p&gt;They were all in rejected drafts I had already set aside, or in non-prose files: foreshadowing trackers, notes about how a chapter came together. Writing "decided this yesterday" in a production note is correct. The author lives in the real world; the in-world rules do not apply to their memos.&lt;/p&gt;

&lt;p&gt;The same look revealed that the directory holding my in-progress drafts was not being scanned at all. I was checking the places that did not matter and skipping the one that did.&lt;/p&gt;

&lt;p&gt;So I added the drafts — and the count jumped to 450.&lt;/p&gt;

&lt;p&gt;The cause was a folder of old material from when this was designed as an RPG. My own documentation says it is incompatible with the current line and kept isolated for reference. It was never something to fix.&lt;/p&gt;

&lt;p&gt;Opposite mistakes, same root cause: I was choosing scan scope by directory name without ever defining what counts as prose.&lt;/p&gt;

&lt;p&gt;Here is the definition I settled on. &lt;strong&gt;Scan only what a reader could end up reading.&lt;/strong&gt; Anything I wrote standing in the real world is out. Anything quarantined as reference because it belongs to an incompatible earlier design is out. Strip those two away and what remains is the finished prose of the current line plus the drafts in progress.&lt;/p&gt;

&lt;p&gt;So I stopped deriving scope from directory names and started listing the paths that satisfy that definition explicitly. Now a new directory can neither slip in nor slip out on its own. I have to decide which side it belongs to, at the moment I create it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checker whose numbers you cannot read
&lt;/h2&gt;

&lt;p&gt;Coverage right now is 35 of 46 forbidden terms. Eleven are not implemented.&lt;/p&gt;

&lt;p&gt;The other day I wrote down, for each of those eleven, why it was left out. I did not add any rules. I only wrote the reasons that had never been written.&lt;/p&gt;

&lt;p&gt;All eleven turned out to be time expressions (morning, midday, evening, night, hour, one day, half a day, two days, "N days later", "half a day's walk", "a day's walk"), and the reasons fell into two groups. The first five are far too common on their own. This is sharper in Japanese than it sounds in English: 時間 is a single word meaning both &lt;em&gt;hour&lt;/em&gt; and &lt;em&gt;time&lt;/em&gt; in general, so matching a phrase like 時間がかかる ("it takes time") would wipe out almost all of the narration.&lt;/p&gt;

&lt;p&gt;What was left out is the bare word, not the concept. Compounds that pin down a time of day — 夕日 (sunset), 朝日 (morning sun) — are still matched. That is where the 7 and 24 hits in the table above came from; both counts are compounds only. 時間 works the same way: the bare word is dropped, and only the counted form 三時間 ("three hours") is caught, because that one is unambiguously a measurement off a clock the world does not have.&lt;/p&gt;

&lt;p&gt;The day counts were different in kind. Weekday and month names are violations no matter how they appear, because the world has no calendar at all. But a count of days is something you could actually observe, by watching the sun rise and set that many times. It is a weaker violation. And matching it strictly would reopen rewrite decisions in prose I had already fixed. So rather than drop the group, I narrowed it: the bare counts are not matched, but the forms that unambiguously mean a quantity still are — the implemented pattern is 十日|半日ほど|一日[がだで], so 半日ほど ("about half a day") and 一日が/だ/で are caught while bare 一日 is not. 十日 ("ten days") stands there bare because it is not an everyday word: 一日 turns up constantly in set phrases like 一日中 ("all day") and 一日も早く ("as soon as possible"), whereas 十日 almost never does. Across these 102 lines, 十日 did not appear even once. That is why the table carries one 半日ほど even though the day counts sit on this unimplemented list.&lt;/p&gt;

&lt;p&gt;Which means "unimplemented" here was a decision to narrow, not to drop: matching the bare words mechanically would break things. It was not work I had failed to get to. If I had not written that down, I would later have misread it as exactly that — a corner I never got around to.&lt;/p&gt;

&lt;p&gt;Unimplemented items without a stated reason cause a specific problem later. When the count comes back low, you cannot tell whether it is low because there are words you are missing or because you narrowed the detection on purpose.&lt;/p&gt;

&lt;p&gt;A checker that cannot tell you which one it is cannot tell you what its own numbers mean. Right now it sits at 102 and red, so there is nothing to misread. But as I work that down toward zero, the meaning of that zero stays undetermined.&lt;/p&gt;

&lt;p&gt;The same pass turned up a comment pointing at the wrong source of truth. It said the rules were "synced with that YAML file." The actual source of truth was the worldbuilding document. Which is the same failure one level up: the checker could not tell me what its numbers meant, and its own comment could not tell me where its rules came from. Both exit 0 and complete. What is broken is the content of the output, which you cannot see by looking at the exit status.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enforcement tool became an observation tool
&lt;/h2&gt;

&lt;p&gt;The thing I did not expect: this checker turned into an instrument for watching where an LLM drifts out of a world.&lt;/p&gt;

&lt;p&gt;Where the violations would land was not something I knew when I wrote the word list. Until I counted, I believed they concentrated in dialogue. Counting showed that 昨日, the most frequent term, really is tilted toward dialogue, while the aggregate concentrates nowhere and simply tracks the distribution of lines. The prediction was right for exactly one word. The tool I built to enforce the rules is the one that told me how far my prediction actually held.&lt;/p&gt;

&lt;p&gt;Reading my own generated prose, I only ever find the conspicuous violations. Where the quiet ones are, and how many, does not come out until something counts them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>writing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My Quiz Has an AI Rival. Its Scoring Doesn't Need an LLM.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Mon, 07 Sep 2026 17:46:05 +0000</pubDate>
      <link>https://dev.to/nomurasan/my-quiz-has-an-ai-rival-its-scoring-doesnt-need-an-llm-1hm7</link>
      <guid>https://dev.to/nomurasan/my-quiz-has-an-ai-rival-its-scoring-doesnt-need-an-llm-1hm7</guid>
      <description>&lt;p&gt;After using Kahoot in a training session, I wanted to replay the questions. What I missed was the rhythm: answer, watch the score move, try one more round. In a solo session, there was nobody to compete with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;I started a small quiz prototype with a CPU rival. The useful design decision was to separate three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simulate the rival's correctness and response time.&lt;/li&gt;
&lt;li&gt;Calculate both scores with the same fixed function.&lt;/li&gt;
&lt;li&gt;Treat generated dialogue as a possible future feature, outside scoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a prototype, not evidence that competition improves learning. Kahoot already described &lt;a href="https://kahoot.com/blog/2020/05/12/new-study-modes-kahoot-app/" rel="noopener noreferrer"&gt;study modes with AI players in 2020&lt;/a&gt;. My goal was to explore that experience with my own material and timing rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simulated rival does not have to understand the question
&lt;/h2&gt;

&lt;p&gt;My CPU samples correctness from a configured probability and picks a virtual response time. It does not ask a language model to solve the question.&lt;/p&gt;

&lt;p&gt;The code has 60%, 78%, and 92% accuracy profiles. The current UI uses the 78% profile; a difficulty selector is not implemented. These are prototype settings, not validated learning targets.&lt;/p&gt;

&lt;p&gt;This gives me an opponent to test before adding conversational behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Random answers, fixed scoring
&lt;/h2&gt;

&lt;p&gt;The player answers against an answer key. The CPU's correctness is sampled. Once correctness is known, both use the same scoring rule.&lt;/p&gt;

&lt;p&gt;Here is that rule extracted into a standalone function. Inputs assume nonnegative elapsed time and streak, and a positive time limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;correct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limitMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;streak&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;correct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&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;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;limitMs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&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="nx"&gt;streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At half the time limit, a correct answer with &lt;code&gt;streak: 0&lt;/code&gt; scores 750. With &lt;code&gt;streak: 2&lt;/code&gt;, it scores 825. These are arithmetic examples, not learner measurements.&lt;/p&gt;

&lt;p&gt;The rival's result varies; the scoring rule does not. I checked the extracted function with seven assertions covering these examples, incorrect answers, time boundaries, and the streak cap.&lt;/p&gt;

&lt;p&gt;To repeat that check, save the function as &lt;code&gt;score.mjs&lt;/code&gt;, add &lt;code&gt;export { score };&lt;/code&gt;, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--input-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;&lt;span class="sh"&gt;'
import assert from 'node:assert/strict';
import { score } from './score.mjs';
const cases = [
  [true, 5000, 0, 750], [true, 5000, 2, 825],
  [false, 0, 0, 0], [true, 0, 0, 1000],
  [true, 10000, 0, 500], [true, 12000, 0, 500],
  [true, 0, 8, 1250],
];
for (const [correct, elapsedMs, streak, expected] of cases) {
  assert.equal(score({ correct, elapsedMs, limitMs: 10000, streak }), expected);
}
console.log('7 assertions passed');
&lt;/span&gt;&lt;span class="no"&gt;JS
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed output: &lt;code&gt;7 assertions passed&lt;/code&gt;. This checks the extracted formula, not the full game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reveal an answer state before revealing a result
&lt;/h2&gt;

&lt;p&gt;If the CPU answers first, the player sees only &lt;strong&gt;answered&lt;/strong&gt;. Its correctness and points stay hidden until the player's answer is locked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question starts
  CPU finishes -&amp;gt; show "answered", keep result hidden
  Player answers or times out -&amp;gt; lock player input
  Finish the rival's waiting display
  Reveal correctness and points
  Next question or replay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundary is information: a player who can still answer must not see the rival's result. Locking also gives the game one place to reject repeated submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual response time is not display waiting time
&lt;/h2&gt;

&lt;p&gt;When the player finishes first, the prototype caps the remaining on-screen wait. It still scores the CPU using the virtual response time chosen for that question.&lt;/p&gt;

&lt;p&gt;For example, suppose the CPU's planned time is eight seconds and the player answers after two. Shortening the waiting animation must not replace the CPU's eight-second scoring input with the shorter display duration.&lt;/p&gt;

&lt;p&gt;That is an illustrative timeline. The general boundary is that presentation changes should not silently change an already chosen scoring input.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: what belongs outside the game loop?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where would I use an LLM?
&lt;/h3&gt;

&lt;p&gt;The current scoring path makes no external LLM calls. Rival dialogue, encouragement, and rephrased explanations are future ideas. If added, I want fixed messages to keep the game moving when generation fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does fixed scoring make the quiz educationally correct?
&lt;/h3&gt;

&lt;p&gt;Question authoring is another responsibility. An incorrect answer key can be scored consistently and still teach the wrong thing. The current prototype reads questions from a lesson page; support for a common import format is still being designed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I check next?
&lt;/h3&gt;

&lt;p&gt;Before adding dialogue, trace these cases in your own quiz: CPU first, player first, timeout, repeated clicks, and replay while an old timer is pending. The seven function assertions above do not establish that this full browser test matrix passes.&lt;/p&gt;

&lt;p&gt;Then measure completion, replay, and learning separately. Speed bonuses may reward fast reading or guessing, and more replays do not prove better retention.&lt;/p&gt;

&lt;p&gt;Start by naming one piece of information that must stay hidden before an answer, and one scoring input that an animation must never change.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Based on the author's training experience and prototype. AI assisted with implementation review, English recomposition, and the code explanation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>I Cut AI Agent Handoff Payloads by 87%—Then the Important Questions Disappeared</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sun, 06 Sep 2026 16:08:39 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-cut-ai-agent-handoff-payloads-by-87-then-the-important-questions-disappeared-4p2</link>
      <guid>https://dev.to/nomurasan/i-cut-ai-agent-handoff-payloads-by-87-then-the-important-questions-disappeared-4p2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article is based on the author's production handoff hooks. The author designed the system, measured 240 real handoff records, and verified the delivery behavior. AI recomposed the approved Japanese source for DEV readers under the standing delegation for English-market publication. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had a multi-agent coordination problem that looked like a context-window problem.&lt;/p&gt;

&lt;p&gt;Several coding sessions needed to exchange questions, progress, and completion notices. A hook injected those handoffs into the next conversation turn. The first implementation took the conservative route: include the full message every time so nothing gets lost.&lt;/p&gt;

&lt;p&gt;That safety margin became a recurring tax. Long messages were injected again on later turns. A free-form request addressed to another session could even be interpreted as an instruction for the current one.&lt;/p&gt;

&lt;p&gt;I replaced full-message delivery with short notifications and an on-demand inbox. Replaying 240 production handoffs reduced the average payload per handoff from 969 bytes to 123 bytes: an 87.3% reduction.&lt;/p&gt;

&lt;p&gt;Then the first version failed in a more important way. Seven unanswered questions existed in the previous 24 hours, but the first notification batch contained none of them.&lt;/p&gt;

&lt;p&gt;The lesson was not “compress harder.” It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Push the existence and priority of a handoff. Pull its full content only when needed. Measure payload reduction and critical-message delivery as separate SLOs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not inject full handoff messages on every turn.&lt;/li&gt;
&lt;li&gt;Push a compact notification containing type, state, sender, summary, and retrieval path.&lt;/li&gt;
&lt;li&gt;Keep the full message in a pull-based inbox.&lt;/li&gt;
&lt;li&gt;Rank unanswered questions ahead of status and completion messages before applying count or byte limits.&lt;/li&gt;
&lt;li&gt;Test both payload size and first-turn delivery. A smaller payload that hides the next required decision is a regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick answer: how do you reduce agent context without losing handoffs?
&lt;/h2&gt;

&lt;p&gt;Separate notification from retrieval.&lt;/p&gt;

&lt;p&gt;The always-on hook should answer only:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What kind of handoff exists?&lt;/li&gt;
&lt;li&gt;Is it still actionable?&lt;/li&gt;
&lt;li&gt;Who sent it?&lt;/li&gt;
&lt;li&gt;What should the receiving agent do next?&lt;/li&gt;
&lt;li&gt;Where can it fetch the full message?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The inbox keeps the background, investigation notes, and free-form request. The receiving session pulls those details only when the notification is relevant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before
hook -&amp;gt; [full handoff text] -&amp;gt; every conversation turn

After
hook -&amp;gt; [short prioritized notice] -&amp;gt; conversation context
                                      |
                                      +-&amp;gt; pull full text from inbox when needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not deletion. It is a routing change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fmeasure-context-injection-byte-budget-push-pull-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fmeasure-context-injection-byte-budget-push-pull-en.png" alt="Diagram of multiple agent sessions writing to a durable inbox, short priority notices being pushed into active context, full details being pulled on demand, and separate measures for payload bytes and first-batch unanswered-question reach." width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The dangerous version of “include everything just in case”
&lt;/h2&gt;

&lt;p&gt;Full-message injection feels safe because the receiving agent cannot complain that information was omitted. But the design quietly conflates four responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storing the durable message;&lt;/li&gt;
&lt;li&gt;notifying the recipient that it exists;&lt;/li&gt;
&lt;li&gt;deciding whether it is still actionable;&lt;/li&gt;
&lt;li&gt;choosing how much of it belongs in the current turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When one hook does all four, every message becomes recurring context. The system pays for old background even when it needs only a one-line signal.&lt;/p&gt;

&lt;p&gt;It also widens the instruction boundary. A message like “restart the deployment after the owner approves” may be useful evidence for one session and an unsafe command for another. Injecting arbitrary prose directly into the active context asks the model to rediscover that distinction every time.&lt;/p&gt;

&lt;p&gt;A notification is a safer boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[question][unanswered] from=session-api: Need decision on retry policy — open inbox item 841
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notification reports state. It does not impersonate a new instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 240-record replay measured
&lt;/h2&gt;

&lt;p&gt;I replayed 240 handoffs that had occurred in the real workflow through the before and after formatters.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean bytes per handoff&lt;/td&gt;
&lt;td&gt;969 B&lt;/td&gt;
&lt;td&gt;123 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-87.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum bytes per handoff&lt;/td&gt;
&lt;td&gt;3,066 B&lt;/td&gt;
&lt;td&gt;295 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-90.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-commit notice&lt;/td&gt;
&lt;td&gt;281 B&lt;/td&gt;
&lt;td&gt;82 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-70.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three rows are measurements from this 240-record replay. The per-flow table further down measures a different population — the raw 893-event snapshot — which is why its auto-commit mean is not the same number. The two are not comparable as a single measurement.&lt;/p&gt;

&lt;p&gt;You can recheck the arithmetic on those three rows directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'BEGIN{
  printf "mean:   %.1f%%\n", (969-123)/969*100
  printf "max:    %.1f%%\n", (3066-295)/3066*100
  printf "notice: %.1f%%\n", (281-82)/281*100
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mean:   87.3%
max:    90.4%
notice: 70.8%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The byte boundary is the one the hook actually emits, so it is measurable with &lt;code&gt;wc -c&lt;/code&gt; rather than estimated. A single push notice looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s1"&gt;'[question][unanswered] from=session-api: Need decision on retry policy — open inbox item 841'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;94 bytes carries the type, the actionable state, the sender, the decision at stake, and the retrieval path. Whatever the original message said, the receiving session can now decide whether to read it before paying for it.&lt;/p&gt;

&lt;p&gt;After the priority fix, a complete delivery response—including the inbox hint and unanswered-count summary—measured between 883 and 1,034 bytes in the observed cases. The notification-line budget was 1,000 bytes, so the final response could slightly exceed it when fixed guidance was added.&lt;/p&gt;

&lt;p&gt;These are UTF-8 byte measurements at the hook output. They are not token counts, model input billing, or a claim that total daily context fell by 87.3%. The comparison is intentionally narrow: the same handoff records, through two formatters, measured at the same boundary.&lt;/p&gt;

&lt;p&gt;That boundary is still useful. It tells us whether the change reduced the flow we actually modified without pretending to measure the entire agent runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first optimization hid every unanswered question
&lt;/h2&gt;

&lt;p&gt;The compact formatter initially selected the newest five messages.&lt;/p&gt;

&lt;p&gt;That sounds reasonable until the inbox contains several kinds of events: progress, completion, claims, answers, and questions. Five recent status messages can consume the whole batch while an older unresolved question remains the item that actually blocks work.&lt;/p&gt;

&lt;p&gt;In the failure case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seven unanswered questions existed in the previous 24 hours;&lt;/li&gt;
&lt;li&gt;the first delivery contained zero questions;&lt;/li&gt;
&lt;li&gt;the second delivery exposed one;&lt;/li&gt;
&lt;li&gt;the third delivery exposed two.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing was deleted. The questions were still in the inbox. But “available after several turns” is not the same as “delivered when the recipient needed to act.”&lt;/p&gt;

&lt;p&gt;The 87.3% reduction had passed while the operational outcome had failed.&lt;/p&gt;

&lt;p&gt;This is the core measurement trap in context optimization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smaller payload != better delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need at least two independent signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Volume:&lt;/strong&gt; How many bytes were injected?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reachability:&lt;/strong&gt; Did the most important actionable item appear in the first eligible turn?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Combining them into one score hides the trade-off. A formatter can look excellent on volume precisely because it removed the information the recipient needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rank meaning before enforcing limits
&lt;/h2&gt;

&lt;p&gt;I changed the candidate order to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;unanswered questions;&lt;/li&gt;
&lt;li&gt;state changes;&lt;/li&gt;
&lt;li&gt;answers;&lt;/li&gt;
&lt;li&gt;accepted work;&lt;/li&gt;
&lt;li&gt;progress and completion notices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Answered questions are removed from the delivery candidates. If the byte or count limit cannot fit every unanswered question, the remaining items stay unread and the compact output continues to say how many remain.&lt;/p&gt;

&lt;p&gt;After the change, the first batch carried at least one unanswered question instead of none.&lt;/p&gt;

&lt;p&gt;The implementation used three delivery controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at most five detailed notices per turn;&lt;/li&gt;
&lt;li&gt;at most three redeliveries of the same notice;&lt;/li&gt;
&lt;li&gt;approximately 1,000 bytes of notification lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One exception matters: the highest-priority item is shown even if it alone exceeds the byte budget. A strict limit that can erase the only blocking question is not a safety control; it is a denial-of-information mechanism.&lt;/p&gt;

&lt;p&gt;The ordering rule should therefore come before the size rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PRIORITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unanswered_question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state_change&lt;/span&gt;&lt;span class="sh"&gt;"&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;work_accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_for_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;byte_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_actionable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRIORITY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact_notice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;selected&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_items&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;byte_budget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Always allow the highest-priority first item.
&lt;/span&gt;        &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;selected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example is deliberately incomplete. Production code also needs durable read state, stable identities, concurrency control, malformed-record handling, and a pull command that returns the exact original message. The important part is the sequence: semantic priority first, transport limits second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test delivery as a state machine
&lt;/h2&gt;

&lt;p&gt;A “formatter output is shorter” test would have approved the broken version. The useful tests describe how messages move through states.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No notices&lt;/td&gt;
&lt;td&gt;Inject nothing and exit successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal delivery&lt;/td&gt;
&lt;td&gt;Show a compact notice and the full-message retrieval path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than five candidates&lt;/td&gt;
&lt;td&gt;Show five; leave the remainder for a later turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Byte budget exceeded&lt;/td&gt;
&lt;td&gt;Defer the next notice, except the highest-priority first item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unanswered question exists&lt;/td&gt;
&lt;td&gt;Deliver at least one in the first eligible batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Question has an answer&lt;/td&gt;
&lt;td&gt;Remove that question from redelivery candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redelivery limit reached&lt;/td&gt;
&lt;td&gt;Stop repeating the body, but keep an unresolved-count signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malformed JSONL row&lt;/td&gt;
&lt;td&gt;Skip the row without breaking the whole hook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep the volume assertion and delivery assertion separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_compact_notice_reduces_bytes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;mean_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compact_records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;mean_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_first_batch_contains_unanswered_question&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox_with_unanswered_questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unanswered_question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second test is not a quality bonus. It protects the purpose of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure by flow, not only by global total
&lt;/h2&gt;

&lt;p&gt;Before the optimization, the broader measurement snapshot contained 893 injection events totaling 1,131,429 bytes through 2026-08-08 05:26:38 UTC.&lt;/p&gt;

&lt;p&gt;The largest flows were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Events&lt;/th&gt;
&lt;th&gt;Total bytes&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Maximum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cross-session handoff delivery&lt;/td&gt;
&lt;td&gt;159&lt;/td&gt;
&lt;td&gt;617,058 B&lt;/td&gt;
&lt;td&gt;3,880 B&lt;/td&gt;
&lt;td&gt;9,407 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session list at startup&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;68,052 B&lt;/td&gt;
&lt;td&gt;13,610 B&lt;/td&gt;
&lt;td&gt;14,132 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-commit notices&lt;/td&gt;
&lt;td&gt;164&lt;/td&gt;
&lt;td&gt;66,030 B&lt;/td&gt;
&lt;td&gt;402 B&lt;/td&gt;
&lt;td&gt;755 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are raw snapshot events, not the 240-record replay above, which is why the auto-commit mean here (402 B) differs from the 281 B replay row.&lt;/p&gt;

&lt;p&gt;The startup list had the largest mean, but handoff delivery dominated the total because it ran often and redelivered pending content.&lt;/p&gt;

&lt;p&gt;That is why one aggregate number is not enough. Track at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event count;&lt;/li&gt;
&lt;li&gt;total bytes;&lt;/li&gt;
&lt;li&gt;mean bytes;&lt;/li&gt;
&lt;li&gt;maximum bytes;&lt;/li&gt;
&lt;li&gt;first-turn delivery of the highest-priority unresolved class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which flow you optimize depends on the outcome. Mean size points toward startup. Total recurring cost points toward handoff delivery. Reachability tells you whether either optimization broke the coordination loop.&lt;/p&gt;

&lt;p&gt;Record each injection as one JSON Lines row carrying the source, the UTF-8 byte length, and a UTC timestamp. The table above is then reproducible in your own environment with &lt;code&gt;jq&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;&lt;span class="nv"&gt;METRICS_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/context-emission.jsonl

jq &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'
  [.[]
    | select(
        .metric == "context_emission_size"
        and .ts &amp;gt;= "2026-08-08T00:00:00Z"
        and .ts &amp;lt;= "2026-08-08T05:26:38Z"
      )
  ] as $rows
  | {
      count: ($rows | length),
      bytes: ($rows | map(.byte_len) | add),
      by_source: (
        $rows
        | group_by(.hook)
        | map({
            source: .[0].hook,
            count: length,
            bytes: (map(.byte_len) | add),
            average: ((map(.byte_len) | add) / length | floor),
            maximum: (map(.byte_len) | max)
          })
        | sort_by(-.bytes)
      )
    }
'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METRICS_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bytes are used because they are cheap and reliable to capture immediately after a hook writes its output. This is not a substitute for token measurement; the goal is a comparable before/after series for each injection path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical implementation sequence
&lt;/h2&gt;

&lt;p&gt;If you are retrofitting an existing agent workflow, this order keeps the evidence interpretable.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Instrument before compressing
&lt;/h3&gt;

&lt;p&gt;Log the source, timestamp, UTF-8 byte count, and delivery class for every injected payload. Do not begin with token estimates if the component you can directly observe emits bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Find the expensive flow
&lt;/h3&gt;

&lt;p&gt;Group by source and inspect count, total, mean, and maximum. A rare large payload and a small payload repeated hundreds of times need different fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Split push from pull
&lt;/h3&gt;

&lt;p&gt;Keep type, actionable state, summary, sender, and retrieval identity in push. Store the complete immutable message behind a pull path.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Define semantic priority
&lt;/h3&gt;

&lt;p&gt;Write down what must appear before applying &lt;code&gt;max_items&lt;/code&gt; or &lt;code&gt;byte_budget&lt;/code&gt;. In a coordination system, unresolved questions usually outrank progress narration.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Test state transitions
&lt;/h3&gt;

&lt;p&gt;Cover unanswered, answered, overflow, redelivery exhaustion, and corrupt storage rows independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Continue measuring both axes
&lt;/h3&gt;

&lt;p&gt;Watch injected bytes and critical-message reachability. A regression in either one is a reason to repair the change, not average the metrics together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture boundary
&lt;/h2&gt;

&lt;p&gt;The design becomes easier to reason about when notification generation, durable message storage, prioritization, and measurement remain separate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────┐      ┌──────────────────────┐
│ producers  │ ---&amp;gt; │ durable inbox/details│
└────────────┘      └──────────┬───────────┘
                              │ pull
                              v
                    ┌──────────────────────┐
hook push --------&amp;gt; │ priority + short note│ ----&amp;gt; active context
                    └──────────┬───────────┘
                              v
                    metrics + delivery tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation also gives failures a location. If the inbox contains the question but the notification omits it, the problem is prioritization. If the notice appears but pull cannot retrieve the original, the problem is identity or storage. If both work but the hook repeats it forever, the problem is acknowledgement state.&lt;/p&gt;

&lt;p&gt;“The agent did not see the message” stops being one vague failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why measure bytes instead of tokens?
&lt;/h3&gt;

&lt;p&gt;Because bytes are what the hook emits. Token counts depend on the tokenizer and the model, and they are an estimate at the point where I could take a direct measurement. Measuring bytes at the hook boundary keeps the claim narrow and reproducible: the same 240 records, two formatters, one measuring point. If you need a billing number, measure it at the API boundary instead — that is a different question with a different instrument.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doesn't a pull-based inbox just move the cost to a later turn?
&lt;/h3&gt;

&lt;p&gt;Only when the recipient actually needs the detail. The failure mode of full injection is paying for every message on every turn regardless of relevance. Pull moves that cost from unconditional to conditional. The measurement that keeps this honest is the second SLO: if the notice is so thin that the agent must pull on every turn anyway, reachability will not improve and you have not gained anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the highest-priority notice alone exceeds the byte budget?
&lt;/h3&gt;

&lt;p&gt;Show it anyway. Truncation applies to everything below the top of the ordering, never to the top itself: every other item defers to the next turn, and the first-priority item does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know my compression didn't break delivery?
&lt;/h3&gt;

&lt;p&gt;Assert the two axes separately. &lt;code&gt;test_compact_notice_reduces_bytes&lt;/code&gt; and &lt;code&gt;test_first_batch_contains_unanswered_question&lt;/code&gt; must both exist and must not be averaged into one score. In my case the first test passed at -87.3% while the second would have failed at 0 of 7 unanswered questions delivered. A single combined metric would have reported success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this apply outside multi-agent setups?
&lt;/h3&gt;

&lt;p&gt;The push/pull split applies anywhere a recurring hook injects variable-length content: notification digests, CI status summaries, monitoring alerts routed to an assistant. The specific priority ordering does not transfer — "unanswered questions first" is a property of a coordination inbox. Write down your own ordering before you write the byte limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I kept
&lt;/h2&gt;

&lt;p&gt;I still want context injection to be small. I just no longer accept smallness as the outcome.&lt;/p&gt;

&lt;p&gt;The durable rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Push only enough context to identify the next relevant action. Preserve the full handoff behind pull. Prove both the byte reduction and the first-turn delivery of what matters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An 87.3% reduction is useful evidence. Zero of seven unanswered questions in the first batch is stronger evidence that the first design was wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your MCP Tools Are Listed. Is the Integration Actually Working?</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:28:48 +0000</pubDate>
      <link>https://dev.to/nomurasan/your-mcp-tools-are-listed-is-the-integration-actually-working-448h</link>
      <guid>https://dev.to/nomurasan/your-mcp-tools-are-listed-is-the-integration-actually-working-448h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The observations in this article come from one application's connector sessions on 2026-09-05, not from a survey of MCP implementations. Tool discovery here was through a host connector catalog, not a captured wire-level exchange. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A connector session opened against my own publishing server. The runtime health call came back &lt;code&gt;fresh=true&lt;/code&gt;. The tool catalog was populated. A live portfolio scan ran and returned a scan ID.&lt;/p&gt;

&lt;p&gt;And the workflow was stuck.&lt;/p&gt;

&lt;p&gt;The scan's own response said the next required step was a saved article impact plan. The catalog that session had been given did not contain an operation that saves one. Up to that point every call had come back a success — the health check, a lookup, and the scan itself. The runtime was healthy. There was no error to debug. The workflow simply had no callable next step, and nothing in those successful responses said so.&lt;/p&gt;

&lt;p&gt;Here is the acceptance test I now run before I let an assistant do anything but read. &lt;strong&gt;Five gates: discovery, invocation, identity, readability, reachability.&lt;/strong&gt; None of them authorizes a write. Three make a call — gate 1's discovery, a tool listing plus an optional request for the server's own instructions, then two invocations of tools whose own declarations say they write nothing — and the last two only read what is already in front of you. Each gate carries the line that matters most — what a pass &lt;em&gt;still does not prove&lt;/em&gt; — and gate 5, the one that was missing above, asks whether every prerequisite you hold is either human-owned or has a callable transition at all. It does not find the ones nothing has named yet. What it does is turn the moment one of them surfaces into a failing test instead of a mystery.&lt;/p&gt;

&lt;p&gt;One scope note first: that session is reported from what it recorded at the time, not re-run for this article, and the protocol statements below are pinned to the specification revision they cite. Nothing here is a measurement of how often this failure occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  "The tools are listed" is the weakest evidence you have
&lt;/h2&gt;

&lt;p&gt;A populated tool catalog is a claim about what a server advertised to a client at some point. It is not a claim about the server's current capability set, and it is definitely not a claim about your workflow.&lt;/p&gt;

&lt;p&gt;Two things in particular are worth separating:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catalog is a snapshot, not a capability set.&lt;/strong&gt; In the case above, the operation that session needed did not exist yet. It was added to the server later the same day by a separate repair session, and a later catalog was confirmed to expose it. A server's tool set can change between the moment a client was told what was available and the moment you act on it; the protocol carries a mechanism for a server to announce that its tool list has changed. For your test, that means "the tool isn't in my catalog" dates an observation. It does not establish what the server can do now, and it does not tell you whether you are looking at a permanent gap or a build you don't have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery is defined separately from invocation.&lt;/strong&gt; The Model Context Protocol specification defines listing tools and calling tools — &lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt; — as distinct operations with distinct results (see the Tools section of revision &lt;code&gt;2026-07-28&lt;/code&gt;; pin whichever revision you actually read, and note that the revision you read is not necessarily the one your host negotiated at runtime). A listing does tell you how to call — it carries each tool's name and description, its input schema, its output schema when the server declares one (that field is optional), and its annotations, the optional block where a server states behaviour such as “this one only reads.” That whole set is what the acceptance matrix below asks you to read from the listing. The matrix asks for one thing more, which the listing does not carry: the server's own instructions, returned by a separate request your client may or may not make. And the annotations are where the read-only claim that gates 2 and 3 rest on comes from. What it does not tell you is what happens when you do: a successful listing is no guarantee that the call succeeds, that the result comes back without an error, that your credentials permit it, or that nothing is written.&lt;/p&gt;

&lt;p&gt;So the question is not "are the tools there." It is: &lt;strong&gt;which specific claim does each successful response actually license?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The acceptance matrix
&lt;/h2&gt;

&lt;p&gt;One caution before you run gates 2 and 3. Both call a tool that declares itself read-only, but that declaration is the server's claim, not a guarantee: the MCP specification is explicit that an MCP client must not treat tool annotations as trustworthy unless the server itself is trusted. So run those two gates against a server you already trust, or against an instance you can throw away.&lt;/p&gt;

&lt;p&gt;Each gate below is four lines: &lt;strong&gt;Run&lt;/strong&gt;, &lt;strong&gt;Read&lt;/strong&gt;, &lt;strong&gt;Pass&lt;/strong&gt;, &lt;strong&gt;Does not prove&lt;/strong&gt;. The fourth is the one that gets silently assumed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 1 — Discovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run:&lt;/strong&gt; List tools. Then ask the server to describe itself, if your client does that — &lt;code&gt;server/discover&lt;/code&gt; in the revision this article pins. The server's own instructions come back from that call, not from &lt;code&gt;tools/list&lt;/code&gt;, and making it is optional for the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; From the listing: each tool's name and description, its declared input schema, its declared output schema if it has one, and its annotations — the optional block where a server states behaviour such as "this one only reads." From the server's own description of itself: its instructions, an optional free-text field where the server states how it expects to be used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass:&lt;/strong&gt; The operation your workflow needs is present, with the arguments you intend to pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not prove:&lt;/strong&gt; That the server can execute it, that your credentials permit it, or that the list is current. Nor that you have the instructions: the field is optional, and a client that never makes the second call never sees them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gate 2 — Invocation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run:&lt;/strong&gt; Call one tool whose gate 1 annotations say it only reads, with valid arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; First the description and annotations gate 1 already listed, to confirm the tool claims to write nothing; then the result payload and the error/&lt;code&gt;isError&lt;/code&gt; channel — including anything the payload names as a prerequisite the workflow must satisfy before it can continue (a &lt;code&gt;state&lt;/code&gt; field reporting something missing or invalid, a required-field error, a detail line naming a file it wants).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass:&lt;/strong&gt; The tool declares itself read-only &lt;strong&gt;and&lt;/strong&gt; returns a non-error result — matching the output schema it declared in gate 1, or simply coming back without an error if it declared none. Whether that payload is usable is gate 4's question, not this one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not prove:&lt;/strong&gt; That the call wrote nothing — that is the server's own claim, not something this gate verifies. Nor that other tools work: each tool is its own integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gate 3 — Identity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run:&lt;/strong&gt; Call a lookup — again, one whose gate 1 annotations say it only reads — for a resource you can independently identify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; The returned identifier, compared against the one you already know — and, as in gate 2, any prerequisite the response names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass:&lt;/strong&gt; The returned resource is the one you asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not prove:&lt;/strong&gt; That the resource is complete, current, or the only match.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gate 4 — Readability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run:&lt;/strong&gt; Read the content gates 2 and 3 returned, not just their status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; Actual field values — length, encoding, whether required fields are populated, and whether any field is stating a required next step or an unmet precondition rather than returning data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass:&lt;/strong&gt; A human or an assistant can act on the content as returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not prove:&lt;/strong&gt; That the content is correct, or that it matches what the source holds — only that what came back is readable and usable as returned.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gate 5 — Reachability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run:&lt;/strong&gt; For each prerequisite you hold — one the catalog's stated order names, or one a response you already received names — find the call that satisfies it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; The tool descriptions and server instructions that state required order, and the declared input schema of the tool that would perform each transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass:&lt;/strong&gt; Each of those is either human-owned or has a supported callable transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does not prove:&lt;/strong&gt; That the catalog named every prerequisite. One it never mentions is invisible to this gate until something names it, and a pass says only that the steps you know about are not dead ends — not that the transition will succeed.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;A pass means&lt;/th&gt;
&lt;th&gt;A pass does not mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 — Discovery&lt;/td&gt;
&lt;td&gt;The operation you need is listed, with the arguments you intend to pass&lt;/td&gt;
&lt;td&gt;That the server can execute it, that your credentials permit it, that the list is current, or that you hold the instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — Invocation&lt;/td&gt;
&lt;td&gt;A tool declaring itself read-only returned a non-error result&lt;/td&gt;
&lt;td&gt;That the call wrote nothing, or that any other tool works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — Identity&lt;/td&gt;
&lt;td&gt;The resource returned is the one you asked for&lt;/td&gt;
&lt;td&gt;That it is complete, current, or the only match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — Readability&lt;/td&gt;
&lt;td&gt;The content, as returned, is usable by a human or an assistant&lt;/td&gt;
&lt;td&gt;That it is correct, or that it matches what the source holds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 — Reachability&lt;/td&gt;
&lt;td&gt;Every prerequisite you hold is human-owned or has a callable transition&lt;/td&gt;
&lt;td&gt;That the catalog named every prerequisite, or that the transition will succeed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third column is the matrix. The five gates are cheap to run; what they refuse to tell you is the part worth copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where gate 5's prerequisites come from
&lt;/h2&gt;

&lt;p&gt;On a first run, before anything has gone wrong, they come from gate 1 — and from both of its reads. Tool descriptions and a server's own instructions state required order in plain words — &lt;em&gt;call this only after that&lt;/em&gt;, &lt;em&gt;this is rejected without a recorded X&lt;/em&gt; — and gate 1 already had you read them. Which of the two carried it matters: the descriptions arrive with the listing, so anyone who ran gate 1 has them; the instructions arrive only if the client asked for them and the server filled the field in. A response naming one at runtime is the second source.&lt;/p&gt;

&lt;p&gt;Either way, gate 5 does not need you to trigger the prerequisite yourself. It takes each one you have and asks whether the catalog you hold contains a call that satisfies it. That question is answerable from what gate 1 already read, which is why it stays inside a test that authorizes no write.&lt;/p&gt;

&lt;p&gt;Worth being exact about the opening story here. The scan that named the missing prerequisite is not a read-only tool — its own annotations declare that it is not, and it writes a scan record — so that discovery was not one of these five gates. It was the incident the gates exist to get ahead of. Both halves of that requirement have since been declared — an operation that saves the plan exists, and its description states the order it belongs in — added the same day by a separate repair session.&lt;/p&gt;

&lt;p&gt;So run gate 5 against that earlier catalog, before the scan, and it passes. That is the uncomfortable part, and it is why every block here carries a fourth line. The catalog did state an order — call the scan first, record only after reviewing its evidence — and both of those steps had a callable transition. What it never stated was the plan, and a prerequisite the catalog does not mention cannot be found by reading the catalog. What gate 5 changes is the moment one surfaces anyway. The scan's response named the plan; re-running gate 5 on it costs one reading of the catalog already in hand — no tool there accepts a plan, so the workflow is not reachable, and the failure has a name before anything further is attempted. That session had no such test. Up to that point it had three successful calls, a healthy runtime, and no error to debug.&lt;/p&gt;

&lt;p&gt;Gate 5 is the one that was missing there, and the one I had never written down as a test before this. It is not gate 1 restated. Gate 1 asks whether the operations you went to the catalog looking for are present. Gate 5 asks about the ones you did not go looking for: the steps the catalog's own prose says have to happen in between. That prose is in the same descriptions and instructions gate 1 handed you — read for argument names, skipped for order. When it states an order, gate 5 checks each step in it before anything that writes is called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 5, in detail: a next step in prose is not a next step
&lt;/h2&gt;

&lt;p&gt;Workflow-shaped tools name their own prerequisites in the response — the second source, and also where the trap is. The scan in the opening session returned three fields for its plan requirement: &lt;code&gt;required: true&lt;/code&gt;, a &lt;code&gt;state&lt;/code&gt; of &lt;code&gt;missing_or_invalid&lt;/code&gt;, and a detail naming the local file it wanted.&lt;/p&gt;

&lt;p&gt;Those three fields state a requirement. They are not a transition. The test is mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the prerequisite, from either source above.&lt;/li&gt;
&lt;li&gt;Ask whether it is &lt;strong&gt;machine-owned&lt;/strong&gt; (something the assistant is supposed to do) or &lt;strong&gt;human-owned&lt;/strong&gt; (an approval, a credential, a business decision that must not be automated).&lt;/li&gt;
&lt;li&gt;If it is machine-owned, find the tool whose declared input schema accepts what you'd need to pass.&lt;/li&gt;
&lt;li&gt;If no such tool is in the catalog, the workflow is not reachable with the catalog you currently hold — and per the point above, that is a dated observation, so check the server's current state before concluding the operation does not exist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A human-owned prerequisite does not fail this test. Step 2 is where it stops, and gate 5 counts it as satisfied — the gate is asking about machine transitions, and a human step is a legitimate answer, not a hole. The hole is a machine-owned prerequisite that reaches step 4 with no tool to match, and it shows up as "the agent got stuck and I don't know why," because every individual call was a success.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing this test is not
&lt;/h2&gt;

&lt;p&gt;Do not confuse pagination inside an application's returned inventory with protocol-level pagination of the tool catalog.&lt;/p&gt;

&lt;p&gt;In the opening session, a lookup returned 50 historical candidates with no cursor. That is the &lt;em&gt;application&lt;/em&gt; choosing how much of its own inventory to hand back — a usability property of that tool, and a fair thing to file as a request. It is not a protocol violation, and reporting it as one sends whoever reads your bug report to entirely the wrong layer. Gate 3 covers this correctly: the identity check is what tells you whether you got the resource you asked for, regardless of how many neighbors came along with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on your own integration
&lt;/h2&gt;

&lt;p&gt;Pick a workflow you already trust and start from its catalog: gate 1's two reads carry the descriptions and, when the server offers them, its instructions, and gate 5 can be answered from whatever came back, before anything that writes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Walk the five gates against that catalog. Write down, per gate, the exact field you read — and, for the three that make a call, the operation you called to get it. All five are answerable without authorizing a write.&lt;/li&gt;
&lt;li&gt;Then run the workflow's own read-only steps, in the order the catalog states.&lt;/li&gt;
&lt;li&gt;From there on, carry gate 5 into the runs that do write. Every response that states a requirement — a &lt;code&gt;required&lt;/code&gt; flag, a state of missing or invalid, a detail naming something you have to supply — goes back through gate 5 against the catalog already in hand, before the next call. That is the moment the opening session had and let pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not expect the read-only walk to reproduce the opening case. There, the only thing that named the missing prerequisite was a call that writes, which is outside all five gates; a read-only walk would have finished clean. Expect the first pass over the catalog to come back clean too — the reconstruction above already showed why. Reading a catalog for the order it states costs one pass and rules out only the dead ends already visible in it. What step 3 adds is the habit, and it only pays off where the writes are: that session held the same catalog the whole time and never read it again once the scan answered.&lt;/p&gt;

&lt;p&gt;From that walk you are looking for two outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A gate you cannot run at all, because there is no read-only operation that produces the evidence. That is a hole in the integration's testability, not in your test.&lt;/li&gt;
&lt;li&gt;A gate where the pass criterion is met but the "does not prove" line is something you had been assuming all along. That assumption is the one worth writing down before it costs you something.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol's own inspector tooling is a reasonable place to run gates 1 through 4 by hand before you wire anything up: &lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gate 5 you have to reason about yourself, because it depends on which prerequisites belong to the machine and which belong to a person. That division is the actual design decision, and no health check will make it for you.&lt;/p&gt;

&lt;p&gt;If you run this against something of your own, I'd like to hear which gate failed first.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>testing</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>Your AI Character Looks Right—Except Every Asymmetric Detail Flipped</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 05 Sep 2026 09:01:02 +0000</pubDate>
      <link>https://dev.to/nomurasan/your-ai-character-looks-right-except-every-asymmetric-detail-flipped-38k3</link>
      <guid>https://dev.to/nomurasan/your-ai-character-looks-right-except-every-asymmetric-detail-flipped-38k3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article comes from the author's work maintaining Tsuduri, a recurring AI-illustrated character with asymmetric design features. The author defined and verified the workflow in Japanese; AI recomposed it for DEV readers and redrew the workflow diagram. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tsuduri's hair ornament belongs on her left side.&lt;/p&gt;

&lt;p&gt;When she faces the viewer, that ornament appears on the right side of the image. When she faces away, it appears on the left. In a side view, it may move to the near side or disappear behind her head.&lt;/p&gt;

&lt;p&gt;That sounds simple until an image model returns a polished illustration with the ornament on the wrong side. The face, colors, and mood can all look correct. The image may be attractive. It is still the wrong character state.&lt;/p&gt;

&lt;p&gt;I stopped trying to solve this by loading a longer character profile into every generation. The workflow I settled on was smaller and more explicit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;keep a short invariant in the character's own coordinate system;&lt;/li&gt;
&lt;li&gt;start with the single reference view closest to the requested composition;&lt;/li&gt;
&lt;li&gt;classify the actual output view, then validate at least two asymmetric traits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the full turnaround sheet as source material. Do not make it the default payload for every image.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write "the ornament is on the character's left," not only "put it on screen right."&lt;/li&gt;
&lt;li&gt;Store the complete design canon, but begin each generation with one view-specific reference.&lt;/li&gt;
&lt;li&gt;Judge the image that was actually produced, not only the view you requested.&lt;/li&gt;
&lt;li&gt;Check at least one primary identity trait and one independent secondary trait.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;N/A&lt;/code&gt; when a trait is genuinely occluded or outside the frame; do not turn missing evidence into PASS.&lt;/li&gt;
&lt;li&gt;After a failed retry, change the reference or method instead of repeating the same ambiguous prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Treat the bug as a coordinate-system bug
&lt;/h2&gt;

&lt;p&gt;"Left" can refer to two different frames:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;subject-relative left:&lt;/strong&gt; the character's own left side;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;screen-relative left:&lt;/strong&gt; the viewer's left side of the finished image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those frames agree only in some poses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actual output view&lt;/th&gt;
&lt;th&gt;Character's left side appears&lt;/th&gt;
&lt;th&gt;Hair ornament expectation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Front&lt;/td&gt;
&lt;td&gt;Screen right&lt;/td&gt;
&lt;td&gt;Visible on screen right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back&lt;/td&gt;
&lt;td&gt;Screen left&lt;/td&gt;
&lt;td&gt;Visible on screen left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character faces screen left&lt;/td&gt;
&lt;td&gt;Near side&lt;/td&gt;
&lt;td&gt;Usually visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character faces screen right&lt;/td&gt;
&lt;td&gt;Far side&lt;/td&gt;
&lt;td&gt;Usually hidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-quarter view&lt;/td&gt;
&lt;td&gt;Depends on angle and occlusion&lt;/td&gt;
&lt;td&gt;Evaluate from the actual pose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the prompt stores only "screen right," it becomes wrong as soon as the camera or character turns. The stable rule is the subject-relative relation. The screen position is a view-dependent projection.&lt;/p&gt;

&lt;p&gt;A usable instruction therefore has two layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invariant: the hair ornament is attached to the character's left side.
Requested view: frontal; the ornament should appear on screen right.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line survives a camera change. The second line helps this specific generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A beautiful image can still fail spatially
&lt;/h2&gt;

&lt;p&gt;Image quality and spatial correctness are separate checks.&lt;/p&gt;

&lt;p&gt;The 2022 paper &lt;a href="https://arxiv.org/abs/2212.10015" rel="noopener noreferrer"&gt;Benchmarking Spatial Relationships in Text-to-Image Generation&lt;/a&gt; evaluated whether generated images preserved requested relations between objects. It found that producing recognizable objects did not guarantee that the requested relation was correct.&lt;/p&gt;

&lt;p&gt;A 2026 preprint, &lt;a href="https://arxiv.org/abs/2608.03357" rel="noopener noreferrer"&gt;Can Text-to-Image Models Draw from the Right Frame of Reference?&lt;/a&gt;, separates viewer-relative instructions from object-relative ones. Across 1,200 prompt pairs and 22 models, the paper reports that object-relative accuracy averaged 41.8% lower than the corresponding viewer-relative accuracy; even the best model reached 44.3% on the object-relative task.&lt;/p&gt;

&lt;p&gt;The same preprint also reports that rewriting prompts from visual feedback raised mean accuracy from 25.0% to 29.2% under an equal generation budget. That is an improvement, not a solution. The practical implication is that a closed loop—inspect the output, then select or correct it—is more realistic than expecting a stronger opening instruction to settle the question by itself.&lt;/p&gt;

&lt;p&gt;Those numbers are not Tsuduri's success rate. The models, prompts, scoring, and target designs differ. They support a narrower point: switching between "the character's left" and "the image's right" is a real evaluation problem, not merely a wording typo.&lt;/p&gt;

&lt;p&gt;That is why my review asks two questions independently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the image look good?&lt;/li&gt;
&lt;li&gt;Are the character-relative relationships correct for the pose that was actually rendered?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Passing the first question cannot answer the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a thick canon and a light generation payload
&lt;/h2&gt;

&lt;p&gt;The full design canon is still valuable. It can contain front, back, side, and three-quarter views; color rules; clothing connections; asymmetric hardware; and known exceptions.&lt;/p&gt;

&lt;p&gt;The mistake is treating "valuable to retain" as "mandatory to attach every time."&lt;/p&gt;

&lt;p&gt;Making "attach everything" the default is a workflow cost, not a measured accuracy effect. The cost follows from the shape of the request itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every generation starts by locating the same image set and attaching it in a fixed order;&lt;/li&gt;
&lt;li&gt;angles that this pose does not need are in the input, and the request says nothing about which image should win;&lt;/li&gt;
&lt;li&gt;the things you actually asked for this time, such as pose, scene, expression and style, sit next to a large block of reference description;&lt;/li&gt;
&lt;li&gt;when a generation fails, the request gives you no way to say which reference or instruction was ignored;&lt;/li&gt;
&lt;li&gt;every correction means rebuilding the whole bundle, and the routine stops being something you keep doing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://openai.com/academy/image-generation/" rel="noopener noreferrer"&gt;OpenAI Academy's image-generation guide&lt;/a&gt; recommends clear, specific prompts. The rest is my operating policy, not the guide's claim: keep the full canon as the thing you preserve, and attach only the minimum evidence this run needs. That policy does not prove one image is always more accurate. It keeps the two roles separate so a failure is easier to trace.&lt;/p&gt;

&lt;p&gt;Use three layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Contains&lt;/th&gt;
&lt;th&gt;Used when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Character canon&lt;/td&gt;
&lt;td&gt;Turnaround views, colors, clothing connections, exceptions&lt;/td&gt;
&lt;td&gt;Updating the design or resolving ambiguity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation reference&lt;/td&gt;
&lt;td&gt;The closest view for the requested composition&lt;/td&gt;
&lt;td&gt;Normal generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation contract&lt;/td&gt;
&lt;td&gt;Expected visible traits and PASS/FAIL/N/A rules&lt;/td&gt;
&lt;td&gt;Reviewing the actual output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Build an asymmetry inventory before writing a longer prompt
&lt;/h2&gt;

&lt;p&gt;Do not inventory the whole character. Inventory the features whose spatial relation carries identity.&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;asymmetric_traits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hair_ornament&lt;/span&gt;
    &lt;span class="na"&gt;subject_side&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;left&lt;/span&gt;
    &lt;span class="na"&gt;attached_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;left_hair_mass&lt;/span&gt;
    &lt;span class="na"&gt;identity_weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;primary&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diagonal_strap&lt;/span&gt;
    &lt;span class="na"&gt;starts_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;right_shoulder&lt;/span&gt;
    &lt;span class="na"&gt;ends_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;left_waist&lt;/span&gt;
    &lt;span class="na"&gt;identity_weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secondary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each trait needs enough information to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the feature?&lt;/li&gt;
&lt;li&gt;Which side or attachment point is invariant?&lt;/li&gt;
&lt;li&gt;In which views should it be visible?&lt;/li&gt;
&lt;li&gt;When is it legitimately &lt;code&gt;N/A&lt;/code&gt; because of occlusion, crop, or scale?&lt;/li&gt;
&lt;li&gt;Is it a primary identity signal or a secondary cross-check?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checking only the hair ornament is weak. A flipped image can accidentally place one item correctly while reversing the strap, hair flow, or garment closure. Use at least two independent asymmetric signals when both are observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-stage loop
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-ai-character-left-right-consistency-three-stage-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fdevto-ai-character-left-right-consistency-three-stage-en.png" alt="Problem to result in one figure. Top: the same character in two images; in image 1 the hair star sits on her left and the diagonal strap runs from her right shoulder to her left waist, in image 2 both are flipped. Three numbered stages: write ONE short rule in the character's own left and right, pick ONE reference view (nearest angle, keep the sheet), check the REAL output by naming the view and checking star and strap; FAIL loops back to fix the setup once, PASS leads to the result band where front, back and side views keep the star on her left and the strap from her right shoulder to her left waist while the screen side changes." width="800" height="959"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Freeze a short invariant
&lt;/h3&gt;

&lt;p&gt;Keep the stable relation short enough to inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The ornament remains on the character's left side.
The diagonal strap runs from the character's right shoulder to left waist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not bury these lines inside biography, personality, lighting, and scene history.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start with the closest single view
&lt;/h3&gt;

&lt;p&gt;Choose the reference whose pose and camera angle most closely match the request. State its role: identity, orientation, clothing connection, or style.&lt;/p&gt;

&lt;p&gt;The rule is "one view first," not "one view forever." If the output requires a back connection that the frontal reference cannot show, add a second reference specifically for that missing role.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Validate the actual output
&lt;/h3&gt;

&lt;p&gt;The generated image may not match the requested view exactly. Classify what it actually shows before checking trait placement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expected_traits_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;verdicts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;trait&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trait&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;is_occluded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;verdicts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;trait&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trait&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;matches_subject_relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;verdicts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;trait&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;verdicts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;trait&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;verdicts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is review logic, not a claim that computer vision can infer every trait reliably. A person or a bounded visual reviewer still has to supply the observations. The code makes the decision boundary explicit: two visible independent traits, no observed mismatch, and no invented PASS for an occluded feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalate references by missing role, not by anxiety
&lt;/h2&gt;

&lt;p&gt;When a result fails, use this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reclassify the actual output view. The image may have turned more than expected.&lt;/li&gt;
&lt;li&gt;Confirm the invariant uses the character's frame, not the screen's frame.&lt;/li&gt;
&lt;li&gt;Replace the first reference if another stored view is closer.&lt;/li&gt;
&lt;li&gt;Add one second reference with a named role, such as "back strap connection."&lt;/li&gt;
&lt;li&gt;If the same defect survives a bounded retry, change the generation method, composition, or review strategy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeatedly attaching more images without naming their roles makes the input larger without making the failure easier to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this workflow does not guarantee
&lt;/h2&gt;

&lt;p&gt;This loop targets left/right and other asymmetric spatial consistency. It does not by itself guarantee:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anatomy or hand correctness;&lt;/li&gt;
&lt;li&gt;exact facial identity;&lt;/li&gt;
&lt;li&gt;color fidelity under different lighting;&lt;/li&gt;
&lt;li&gt;style continuity;&lt;/li&gt;
&lt;li&gt;that the model followed the requested pose;&lt;/li&gt;
&lt;li&gt;that one reference is sufficient for every composition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also asks a different question from an identity-ablation experiment. An ablation test asks how much a design may change before recognition breaks. This workflow assumes the design should not change and asks whether the generated pose preserved its spatial invariants.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for the next image
&lt;/h2&gt;

&lt;p&gt;Before generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Write one or two subject-relative invariants.&lt;/li&gt;
&lt;li&gt;[ ] Select the closest single reference view.&lt;/li&gt;
&lt;li&gt;[ ] Name the role of every attached reference.&lt;/li&gt;
&lt;li&gt;[ ] Choose one primary and one independent secondary asymmetric trait.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Classify the actual output view.&lt;/li&gt;
&lt;li&gt;[ ] Translate subject-relative traits into expected screen positions for that view.&lt;/li&gt;
&lt;li&gt;[ ] Mark each trait PASS, FAIL, or N/A.&lt;/li&gt;
&lt;li&gt;[ ] Reject when an observable trait is flipped.&lt;/li&gt;
&lt;li&gt;[ ] After a repeated failure, revise the setup instead of replaying the same prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The durable design record can stay rich. The per-image payload can stay small. The review must be specific enough to catch the polished wrong answer.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>imagegeneration</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Rerunning All CI for Tiny Changes: Fingerprint Each Check's Inputs</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:30:54 +0000</pubDate>
      <link>https://dev.to/nomurasan/stop-rerunning-all-ci-for-tiny-changes-fingerprint-each-checks-inputs-2i00</link>
      <guid>https://dev.to/nomurasan/stop-rerunning-all-ci-for-tiny-changes-fingerprint-each-checks-inputs-2i00</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article is based on a real failure in the author's AI-assisted publishing workflow. The author defined the problem and operating boundary; AI implemented and tested the mechanism, drafted the source, and recomposed this English version under a standing delegation for English-market publication. #ABotWroteThis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The trigger was a tag edit. The body and images were unchanged, but every automated review ran again. A reviewer's small wording fix then created another revision and restarted the set.&lt;/p&gt;

&lt;p&gt;CI often behaves the same way: a README typo starts integration and E2E tests; a CSS change waits for backend jobs with identical inputs. The safe fix is not to call a diff "small." It is to fingerprint what each check actually uses and reuse a prior PASS only when its inputs, rules, and evidence still match.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Freeze related edits as one revision before evaluation. Saving a file should not automatically mean "start every check now."&lt;/li&gt;
&lt;li&gt;Give every check an explicit dependency projection and fingerprint that projection, not the whole repository.&lt;/li&gt;
&lt;li&gt;Reuse a prior PASS only when its input fingerprint, rule digest, and evidence still match. Unknown means execute.&lt;/li&gt;
&lt;li&gt;Start with one slow job: write down exactly what must change before that job needs to run again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick answer: how do you avoid rerunning every CI job?
&lt;/h2&gt;

&lt;p&gt;Hash each job's verdict-changing inputs, rules, and tool version. Rebind a matching attested PASS with a new receipt; execute on mismatch or uncertainty. Decide once per frozen edit batch, not after every save.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smell appears before the pipeline feels slow
&lt;/h2&gt;

&lt;p&gt;The problem exists as soon as any of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody can explain why a given job runs after a documentation-only change.&lt;/li&gt;
&lt;li&gt;A single review comment causes unrelated jobs to restart.&lt;/li&gt;
&lt;li&gt;The only invalidation rule is the repository commit SHA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parallelism does not remove irrelevant runs. File patterns also miss broad inputs such as policies, generated schemas, base images, fixtures, or prompts.&lt;/p&gt;

&lt;p&gt;The question is not "Was the diff small?" It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did any input that this specific check used to reach PASS change?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Separate editing from evaluation
&lt;/h2&gt;

&lt;p&gt;People and agents make related edits in sequence. If each save starts evaluation, the pipeline judges incomplete intermediate states. Treat the edits as one revision batch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make all known edits.&lt;/li&gt;
&lt;li&gt;Freeze the revision that will be judged.&lt;/li&gt;
&lt;li&gt;Compute the checks required for that frozen revision.&lt;/li&gt;
&lt;li&gt;Finish those checks before asking for the next human decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Saving records work in progress. Freezing identifies the exact candidate whose evidence matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprint each check's real dependency set
&lt;/h2&gt;

&lt;p&gt;A repository-wide hash makes every character invalidate everything. Instead, define a projection: the smallest complete set of inputs that can affect one check's verdict.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Markdown style check may depend on documentation files and its lint configuration.&lt;/li&gt;
&lt;li&gt;A frontend visual test may depend on UI code, styles, assets, browser version, fixtures, and baseline images.&lt;/li&gt;
&lt;li&gt;A backend integration test may depend on code, schema, service configuration, fixtures, and runner version.&lt;/li&gt;
&lt;li&gt;A security check may depend on source, lockfiles, policy, and scanner version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fai-article-review-change-aware-checklist-flow-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fshimajima-eiji%2Fshimajima-eiji%2Freleases%2Fdownload%2Farticle-assets%2Fai-article-review-change-aware-checklist-flow-en.png" alt="Flow diagram: freeze one revision, fingerprint the inputs and rules each check uses, then execute, reuse an attested PASS, or keep a current PASS; unknown always executes." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each check, choose &lt;code&gt;current_pass&lt;/code&gt; when it already passed this revision, &lt;code&gt;reuse_prior_pass&lt;/code&gt; when an attested PASS matches its projected inputs and rule digest, or &lt;code&gt;execute&lt;/code&gt; otherwise. &lt;code&gt;project()&lt;/code&gt; must include everything the check reads. If the set is uncertain, execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  A minimal executable model
&lt;/h3&gt;

&lt;p&gt;This executable model shows a documentation edit changing only the documentation projection:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs/config.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/app.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requirements.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old heading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs/config.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/app.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return 200&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requirements.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;framework==1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new heading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;projected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;projected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reuse_prior_pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&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;python3 change_aware_demo.py
&lt;span class="go"&gt;docs: execute
backend: reuse_prior_pass
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, use content-addressed files, generated inputs, environment data, and tool versions. Store the prior PASS as immutable evidence; this comparison alone is not authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  A result depends on rules, not just files
&lt;/h2&gt;

&lt;p&gt;Matching files is insufficient. If a personal-data rule, scanner, browser, prompt, threshold, or target changes, yesterday's PASS does not prove today's verdict. Fingerprint both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The projected content and environment inputs.&lt;/li&gt;
&lt;li&gt;The rule digest that produced the verdict.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Incident-driven acceptance conditions belong in the rule digest too. Selective execution narrows invalidation; it must not hide it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing PASS is not the same as skipping
&lt;/h2&gt;

&lt;p&gt;"We ran this before" is not evidence.&lt;/p&gt;

&lt;p&gt;A reusable PASS binds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check identity;&lt;/li&gt;
&lt;li&gt;projected input fingerprint;&lt;/li&gt;
&lt;li&gt;rule digest and tool version;&lt;/li&gt;
&lt;li&gt;verdict and immutable evidence;&lt;/li&gt;
&lt;li&gt;the original revision; and&lt;/li&gt;
&lt;li&gt;a new receipt binding that evidence to the current revision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Job skipped" is vague. "PASS reused because inputs A, B, C and rule R are unchanged" is falsifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should happen for common changes?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;README only&lt;/td&gt;
&lt;td&gt;Run docs checks; reuse unrelated PASS only when projections and rules match.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS or UI asset&lt;/td&gt;
&lt;td&gt;Run build, accessibility, and relevant visual checks; backend may be reusable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency or lockfile&lt;/td&gt;
&lt;td&gt;Invalidate every consumer of the resolved graph.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article tag/metadata&lt;/td&gt;
&lt;td&gt;Keep body and image PASS when unchanged; read the remote metadata back.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Article body or image&lt;/td&gt;
&lt;td&gt;Run checks whose projections include that body or image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown/legacy diff&lt;/td&gt;
&lt;td&gt;Run everything.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What we tested in the real workflow
&lt;/h2&gt;

&lt;p&gt;We tested the implementation with a real article and real image inputs, without granting publication authority. Before an answer, the PASS receipt count was zero; after the answer it was exactly one; replay was rejected; and the next review received the real article and image instead of placeholders.&lt;/p&gt;

&lt;p&gt;This verifies the review-return and evidence path, not a universal speedup or a successful publication. No improvement percentage is claimed because it was not measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical migration path
&lt;/h2&gt;

&lt;p&gt;Choose one expensive or frequently unnecessary check and answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What exact files, generated data, environment values, fixtures, policies, and tool versions can change its verdict?&lt;/li&gt;
&lt;li&gt;Can you build a deterministic projection of those inputs?&lt;/li&gt;
&lt;li&gt;What immutable evidence proves the previous PASS?&lt;/li&gt;
&lt;li&gt;What rule changes must invalidate that evidence?&lt;/li&gt;
&lt;li&gt;What unknown states should fall back to execution?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Log the decision before enabling reuse, and compare it with full-pipeline results. False reuse is a design failure; extra execution is only a missed optimization. Enable one check at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you close this tab
&lt;/h2&gt;

&lt;p&gt;Find one job that runs after unrelated changes. Write: "This check must run when..." List its actual inputs, rules, and tool version. If you cannot prove an input is unchanged, execute.&lt;/p&gt;

&lt;p&gt;One honest dependency boundary is enough to start replacing "something changed, run everything" with explainable execution and explainable PASS reuse.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>testing</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
