<?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: Yura Solovey</title>
    <description>The latest articles on DEV Community by Yura Solovey (@yura_solovey).</description>
    <link>https://dev.to/yura_solovey</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%2F4036708%2F65bf04a3-c6fb-43ec-8f01-3d3be4fc6d4a.jpg</url>
      <title>DEV Community: Yura Solovey</title>
      <link>https://dev.to/yura_solovey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yura_solovey"/>
    <language>en</language>
    <item>
      <title>Can We Debug AI Models?</title>
      <dc:creator>Yura Solovey</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:58:59 +0000</pubDate>
      <link>https://dev.to/yura_solovey/can-we-debug-ai-models-587j</link>
      <guid>https://dev.to/yura_solovey/can-we-debug-ai-models-587j</guid>
      <description>&lt;p&gt;We are not really working with AI models yet — we are playing with them like children who have been allowed to press buttons on a supercomputer. We already know a few commands. We can produce an impressive result and repeat a sequence that sometimes works. But we do not understand what happens inside, why the model chose this particular answer, or what will break after the next edit.&lt;/p&gt;

&lt;p&gt;Until we learn, at least well enough, how a model moves through an instruction and reaches a decision, we will use only the small fraction of its capabilities that we happened to notice from the outside. We will keep pressing buttons instead of controlling the process. We will obtain isolated results instead of using the system to its full potential.&lt;/p&gt;

&lt;p&gt;In the first three parts of this series, a long instruction became a hidden program, a correct output stopped being proof of a correct process, and local validators proved unable to guarantee global control. One final question remains: can we step through a model’s execution like a program in a debugger and see exactly where it took the wrong turn?&lt;/p&gt;

&lt;h2&gt;
  
  
  We Are Still Changing AI Instructions Like a Black Box
&lt;/h2&gt;

&lt;p&gt;In a conventional program, a bug may not be fixed on the first attempt, but a debugger radically narrows the search space. We set a breakpoint, run the program, inspect variable values, step through transitions, and see which condition fired. With an AI process, we often work differently: we add a rule about approval, run the process, see the same failure, repeat the rule in the correction section, get a pass — and then discover that another branch has broken. From the outside, this looks like debugging. In practice, it is black-box testing by trial and error.&lt;/p&gt;

&lt;p&gt;The worst part is not that one change can cause a regression. The real problem is that we often do not know why the second run passed. Did the model actually start applying the new rule? Did it happen to choose another branch? Did the new wording outweigh another part of the context? Or did the old failure simply not appear in this run? When all we can see is &lt;code&gt;input → output&lt;/code&gt;, a positive result explains the mechanism of improvement no better than a negative result localises the mechanism of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Wrong Result Can Have Several Different Causes
&lt;/h2&gt;

&lt;p&gt;Let us return to the running example with the &lt;code&gt;PREFERENCE_CHANNEL_CHANGED&lt;/code&gt; event. After a correction, the technical specification moved to version 4, but Jira, QA, and approval remained on version 3. In the final package, the result looked the same regardless of where the failure actually occurred:&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;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;technical_spec_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;jira_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;qa_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;approval_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

&lt;span class="na"&gt;actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;technical_spec_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;jira_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;qa_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;approval_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that output alone, we can form at least five different hypotheses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model did not notice the correction;&lt;/li&gt;
&lt;li&gt;the correction was noticed but classified as non-material;&lt;/li&gt;
&lt;li&gt;the invalidation rule was not selected;&lt;/li&gt;
&lt;li&gt;dependent steps were not rerun;&lt;/li&gt;
&lt;li&gt;the completion check ignored the &lt;code&gt;stale&lt;/code&gt; status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them can produce almost the same final package, but they require different fixes. If the model failed to notice the correction, we need to change the change-detection mechanism. If it saw the change but classified it as insignificant, we need to refine the materiality rules. If the classification was correct but the invalidation rule was not applied, the problem lies between decision and action. If invalidation happened but the workflow reused cached artifacts, rewriting the prompt may be the wrong layer of intervention altogether. And if everything was updated correctly but the completion check ignored &lt;code&gt;stale&lt;/code&gt;, then the final invariant is wrong.&lt;/p&gt;

&lt;p&gt;Without a debugger, we see one failure and five possible places to patch. So we add a rule where it seems most logical and hope we guessed correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should a Debugger for an AI Process Show?
&lt;/h2&gt;

&lt;p&gt;In a conventional debugger, we care about more than the final exception. We want to know which data entered a function, what values the variables held, which condition returned &lt;code&gt;true&lt;/code&gt;, which branch was selected, and what state existed after the step. For an AI process, the questions are remarkably similar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which node was executing;&lt;/li&gt;
&lt;li&gt;what data the model received;&lt;/li&gt;
&lt;li&gt;what answer the human provided;&lt;/li&gt;
&lt;li&gt;what state the model believed was current;&lt;/li&gt;
&lt;li&gt;what change it detected;&lt;/li&gt;
&lt;li&gt;how it classified that change;&lt;/li&gt;
&lt;li&gt;which rules it considered;&lt;/li&gt;
&lt;li&gt;which rule it applied;&lt;/li&gt;
&lt;li&gt;which path it selected;&lt;/li&gt;
&lt;li&gt;which alternatives it rejected;&lt;/li&gt;
&lt;li&gt;what state existed after the decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That record already looks much closer to step-by-step debugging:&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;debug_step&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_correction&lt;/span&gt;

  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;old_trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;email&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;new_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sms"&lt;/span&gt;
    &lt;span class="na"&gt;new_trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sms"&lt;/span&gt;

  &lt;span class="na"&gt;state_before&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;jira_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;qa_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;approval_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;detected_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger_condition&lt;/span&gt;
    &lt;span class="na"&gt;detected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;non_material&lt;/span&gt;
    &lt;span class="na"&gt;selected_rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;non_material_change_keeps_approval&lt;/span&gt;

  &lt;span class="na"&gt;state_after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="na"&gt;jira_status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
    &lt;span class="na"&gt;qa_status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
    &lt;span class="na"&gt;approval_status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can see a specific breakpoint. The model did not miss the correction. It noticed the change in the correct field, classified it as &lt;code&gt;non_material&lt;/code&gt;, and then applied a rule that kept dependent artifacts current. That is a completely different quality of diagnosis. Instead of the vague “the model updated the approval incorrectly,” we get a specific place to change: explicitly define every change to &lt;code&gt;trigger_condition&lt;/code&gt; as a material correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  But State and Transition Still Do Not Explain Why the Model Chose That Path
&lt;/h2&gt;

&lt;p&gt;Recording that the model selected &lt;code&gt;non_material&lt;/code&gt; is useful, but it is not always enough to make a precise change to the instruction. We also need to see which alternatives it considered, which rule it selected, which facts it relied on, and what it ignored. That is why the &lt;code&gt;debug_record&lt;/code&gt; below contains &lt;code&gt;considered_rules&lt;/code&gt;, &lt;code&gt;selected_rule&lt;/code&gt;, &lt;code&gt;short_reason&lt;/code&gt;, and &lt;code&gt;evidence_refs&lt;/code&gt;: they show not only the external transition, but also the working representation of the decision at that node.&lt;/p&gt;

&lt;p&gt;In our example, the model may have given too much weight to the fact that the event name and schema did not change, while underweighting the change to &lt;code&gt;trigger_condition&lt;/code&gt;, which expanded the population of affected events. Without this layer, we fix the symptom and risk adding an overly broad rule such as “every correction invalidates approval.” That rule would close this failure, but it would also force Jira, QA, and approval to be regenerated after a cosmetic typo correction. Debugging is valuable precisely because it lets us change the classification boundary instead of merely strengthening the prohibition.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Debug Log Should Not Be One Large Explanation at the End
&lt;/h2&gt;

&lt;p&gt;The easiest way to obtain an explanation is to ask the model after the failure: “Why did you reuse the old approval?” It will almost certainly produce a coherent answer. But that answer may have been created after the decision. The model sees the output, sees our question, and generates a plausible story that fits the result. That is useful as a hypothesis, but weak as a debugger.&lt;/p&gt;

&lt;p&gt;A stronger approach is to record the decision while the model is passing through the critical node and to separate execution facts from the model’s own explanation immediately:&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;debug_record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;17&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_correction&lt;/span&gt;
  &lt;span class="na"&gt;recorded_before_transition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;state_before_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapshot_16&lt;/span&gt;
    &lt;span class="na"&gt;input_refs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;correction_message_12&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;technical_spec_v3&lt;/span&gt;
    &lt;span class="na"&gt;selected_transition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update_technical_spec_only&lt;/span&gt;
    &lt;span class="na"&gt;state_after_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapshot_17&lt;/span&gt;

  &lt;span class="na"&gt;model_report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;non_material&lt;/span&gt;
    &lt;span class="na"&gt;considered_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;material_change_invalidates_dependents&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;non_material_change_preserves_approval&lt;/span&gt;
    &lt;span class="na"&gt;selected_rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;non_material_change_preserves_approval&lt;/span&gt;
    &lt;span class="na"&gt;short_reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&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;output&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;did&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;change"&lt;/span&gt;
    &lt;span class="na"&gt;evidence_refs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;field:event_name&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;field:event_schema&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;field:trigger_condition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this record, &lt;code&gt;execution&lt;/code&gt; says what the system actually did, while &lt;code&gt;model_report&lt;/code&gt; says how the model described its own decision. The first layer proves which transition was selected and what state resulted. The second helps us localise the rule or criterion that may need to change. They must not be merged, because otherwise a polished explanation can silently replace the factual execution record.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Diff Works Like an Inspection Window
&lt;/h2&gt;

&lt;p&gt;During a long process run, a full snapshot is still necessary as a recovery point, but for a human reader the diff is usually more useful. It immediately shows what changed after the node and what should have changed but did not.&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;state_diff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapshot_16&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapshot_17&lt;/span&gt;

  &lt;span class="na"&gt;changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;canonical.trigger_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;email&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;new_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sms"&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sms"&lt;/span&gt;
    &lt;span class="na"&gt;canonical.version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

  &lt;span class="na"&gt;unchanged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;artifacts.jira.status&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;artifacts.qa.status&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;approval.status&lt;/span&gt;

  &lt;span class="na"&gt;expected_but_missing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;artifacts.jira.status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;artifacts.qa.status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;approval.status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure is visible here almost without explanation. The canonical value changed, but the dependent statuses did not. The &lt;code&gt;debug_record&lt;/code&gt; then helps explain why invalidation did not happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge Trace Shows What the Model Actually Used
&lt;/h2&gt;

&lt;p&gt;In a large instruction, one rule may be described in several places. In addition, the model sees chat history, user responses, examples, templates, and previous artifacts. When it makes a decision, we need to know not only the rationale it gives, but also which sources it treated as relevant.&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;knowledge_trace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_correction&lt;/span&gt;

  &lt;span class="na"&gt;rules_used&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approval_required_before_completion&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;section.approval_rules&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;non_material_changes_preserve_approval&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;section.correction_handling&lt;/span&gt;

  &lt;span class="na"&gt;user_evidence_used&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;correction_message_12&lt;/span&gt;

  &lt;span class="na"&gt;artifacts_consulted&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;technical_spec_v3&lt;/span&gt;

  &lt;span class="na"&gt;available_but_not_used&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dependency_map&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;material_change_examples&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the improvement can be even more precise. Perhaps the necessary rule already existed in the instruction, but the model did not use it because the dependency map was too far from the correction section or was not explicitly bound to this node. In that case, copying the rule again may be worse than restructuring the context or binding the rule explicitly to the step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accumulating Knowledge by Node
&lt;/h2&gt;

&lt;p&gt;One debug trace can help fix one run. The real value appears when failures accumulate not as a random bug list, but as knowledge about specific nodes, transitions, and rules.&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;node_improvement_record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_correction&lt;/span&gt;

  &lt;span class="na"&gt;observed_failures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run_017&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger_change_classified_as_non_material&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run_024&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dependency_impact_underweighted&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run_031&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosmetic_example_overgeneralized&lt;/span&gt;

  &lt;span class="na"&gt;suspected_pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Materiality&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inferred&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mainly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;compatibility"&lt;/span&gt;

  &lt;span class="na"&gt;proposed_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Add&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;explicit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;semantic-impact&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;criteria&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;structural&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;compatibility"&lt;/span&gt;

  &lt;span class="na"&gt;regression_risk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;May&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over-classify&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wording-only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;edits&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;material"&lt;/span&gt;

  &lt;span class="na"&gt;required_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger_condition_change&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;event_name_typo&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;description_only_change&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schema_extension&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is no longer just a log. It is improvement memory for one node. When we change the instruction, we can see not only one failure, but the history of how this node failed, which fixes were already tried, which regressions they caused, and which scenarios must be rerun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay Turns Debugging into a Controlled Experiment
&lt;/h2&gt;

&lt;p&gt;In a conventional debugger, after a patch we repeat the same execution path. With an AI process, that is harder because a new run may receive different wording, a different context, or reach a different decision before the problematic node. If we simply restart the entire playbook, we are not always testing the change we intended to test.&lt;/p&gt;

&lt;p&gt;We need replay with fixed inputs and the state captured immediately before the failure:&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;replay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_017&lt;/span&gt;
  &lt;span class="na"&gt;start_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_correction&lt;/span&gt;
    &lt;span class="na"&gt;state_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snapshot_16&lt;/span&gt;

  &lt;span class="na"&gt;fixed_inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;correction_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;correction_message_12&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;technical_spec_v3&lt;/span&gt;
    &lt;span class="na"&gt;dependency_map&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dependency_map_v2&lt;/span&gt;

  &lt;span class="na"&gt;changed_instruction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;material_change_rules_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;

  &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;material&lt;/span&gt;
    &lt;span class="na"&gt;transition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invalidate_dependents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The improvement loop now begins to resemble real debugging: locate the node, inspect state and decision, change one rule, replay from the same snapshot, and only then run the regression suite.&lt;/p&gt;

&lt;p&gt;Without replay, we change a rule and restart the entire process, but we do not know whether the new result was caused by that change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Such a Trace Show the Model’s Real Thinking?
&lt;/h2&gt;

&lt;p&gt;This is the hardest question in the article — and it is where the analogy with a conventional debugger begins to break down.&lt;/p&gt;

&lt;p&gt;In a traditional program, a variable really does hold a concrete value at a concrete moment. A condition either returned &lt;code&gt;true&lt;/code&gt; or it did not. A function received certain arguments and selected a particular branch. A debugger does not ask the program to explain itself; it reads the actual state of the mechanism.&lt;/p&gt;

&lt;p&gt;With a model, the situation is not so simple. When it writes &lt;code&gt;short_reason&lt;/code&gt;, &lt;code&gt;considered_rules&lt;/code&gt;, or &lt;code&gt;factors_used&lt;/code&gt;, we are not necessarily revealing a hidden variable that existed inside all along. We are obtaining a structured reconstruction of how its behaviour can be described in the language of our playbook. That reconstruction may be useful, accurate, and sufficient to localise a problem. But it does not become a fact merely because the model wrote it into a debug log.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;debug_record&lt;/code&gt; must separate two layers rigidly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;execution&lt;/code&gt; — what the system actually received, did, and changed;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model_report&lt;/code&gt; — how the model described the cause of its decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first layer can serve as the foundation of control. The second must be treated as a hypothesis.&lt;/p&gt;

&lt;p&gt;Suppose the model claims that its decision depended on &lt;code&gt;correction_material: false&lt;/code&gt;. We change only that factor and rerun the node:&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;causal_probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;baseline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;correction_material&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;selected_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reuse_approval&lt;/span&gt;

  &lt;span class="na"&gt;intervention&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;correction_material&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;selected_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invalidate_approval&lt;/span&gt;

  &lt;span class="na"&gt;observed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;selected_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reuse_approval&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the action does not change, the claimed factor was not a sufficient cause. The behaviour may have been determined by another part of the context, cached state, or a combination of signals that does not fit a discrete scheme such as “the model chose rule A instead of rule B.”&lt;/p&gt;

&lt;p&gt;That distinction matters. Sometimes the problem is not that the real decision is hidden from us. The problem may be that no clear decision existed in the form assumed by a traditional debugger. In that case, &lt;code&gt;model_report&lt;/code&gt; is not an exposed internal mechanism. It is our best behavioural model.&lt;/p&gt;

&lt;p&gt;A good AI debugger should therefore not promise full internal transparency. Its job is more modest and more practical: turn the model’s explanation into a testable hypothesis, and prevent that hypothesis from replacing the facts of execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do We Actually Get?
&lt;/h2&gt;

&lt;p&gt;Such a debugger does not turn us into “the adult who finally understands the supercomputer.” It does not guarantee full access to the model’s internal mechanism, and it does not prove that each decision existed as a clean sequence of rules, alternatives, and causes.&lt;/p&gt;

&lt;p&gt;It gives us something else — and for engineering, that is more important: an instrument panel around a system that may remain partly opaque.&lt;/p&gt;

&lt;p&gt;We can reliably record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which inputs arrived;&lt;/li&gt;
&lt;li&gt;which node executed;&lt;/li&gt;
&lt;li&gt;what state existed before and after the step;&lt;/li&gt;
&lt;li&gt;which transition was selected;&lt;/li&gt;
&lt;li&gt;which artifacts changed;&lt;/li&gt;
&lt;li&gt;which gates passed;&lt;/li&gt;
&lt;li&gt;which path can be reproduced through replay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separately, we can ask the model to describe the rules, factors, and alternatives it supposedly relied on. But that description is not the foundation of control. It is a diagnostic hypothesis that must be compared with the execution record, state diff, and causal probes.&lt;/p&gt;

&lt;p&gt;The real transition from play to engineering does not happen when the black box finally explains itself completely. It happens when the process remains controllable even when the model’s explanation is incomplete, simplified, or wrong.&lt;/p&gt;

&lt;p&gt;At that point, improving a long instruction stops being an endless cycle of “rewrite the prompt and see what happens.” It becomes a process of accumulating evidence: this node fails under this combination of inputs; here is the rule it selected; here is the factor that, according to its own report, influenced the decision; here is the causal probe; here is the patch; here is the replay; here is the regression suite; here is the new improvement record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boundary of the First Series
&lt;/h2&gt;

&lt;p&gt;At the beginning of this series, we had a large prompt that behaved like a hidden program. Then we learned to distinguish a polished output from a correct process. Next, we made global state explicit and saw that local &lt;code&gt;pass&lt;/code&gt; results do not guarantee correct completion. The final step is to make the execution path visible enough that the process can be debugged and deliberately improved, not merely tested.&lt;/p&gt;

&lt;p&gt;If, after every failure, all we can see is the prompt at the beginning and the document at the end, are we really debugging an AI model — or merely asking the black box for another attempt?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Every Step Passed. Who Was Responsible for the Process?</title>
      <dc:creator>Yura Solovey</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:38:18 +0000</pubDate>
      <link>https://dev.to/yura_solovey/every-step-passed-who-was-responsible-for-the-process-3eba</link>
      <guid>https://dev.to/yura_solovey/every-step-passed-who-was-responsible-for-the-process-3eba</guid>
      <description>&lt;p&gt;In the second part, I wrote about a package that passed formal validation, looked complete, and still turned out to be a weak handoff. That problem was relatively easy to explain: our validators checked whether sections existed, but not whether the next person could actually do the work. The natural response was to add more checks, tighten the schema, strengthen the checklist, and assume that control had been restored. We did exactly that — and almost immediately ran into a worse case, where the problem was no longer the quality of the documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Step Was Completed
&lt;/h2&gt;

&lt;p&gt;Let’s use the same adapted domain with the &lt;code&gt;PREFERENCE_CHANNEL_CHANGED&lt;/code&gt; event. The workflow completed every step in the correct order. Each local validator approved its document. A person confirmed the key decision, and the final package contained every required artifact. The original requirement was simple: when &lt;code&gt;profile.notificationChannel&lt;/code&gt; changes from &lt;code&gt;email&lt;/code&gt; to &lt;code&gt;sms&lt;/code&gt;, the system should emit one event and record the old and new values in the audit log. The process had several stages: clarify the requirement, create the technical spec, prepare Jira, generate QA scenarios, obtain approval, and assemble the final handoff. At the orchestration level, everything looked completely correct:&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;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;define_requirement&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_technical_spec&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_jira&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_qa&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;request_approval&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assemble_handoff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow skipped nothing and preserved the intended order. The technical spec passed its validator, Jira passed its own, and QA passed another. The approval was recorded as well. Then the user made a small correction: the event should no longer be emitted only for the transition &lt;code&gt;email -&amp;gt; sms&lt;/code&gt;, but for any change where the new value equals &lt;code&gt;sms&lt;/code&gt;. The technical spec was updated, passed validation again, and the process continued from where it had stopped. At the level of individual operations, everything still looked logical: the correction had been applied, the updated document was valid, an approval already existed, and every step had completed. We didn’t discover the mistake during validation. We found it only when the implementer started working from Jira and asked why the trigger in the ticket didn’t match the technical spec. At first, we looked for a defect in Jira, then in the validator, and only after several repeated checks did it become clear that the documents were individually valid but belonged to different versions of the decision. Untangling that mismatch took longer than making the correction itself. Jira and QA were still based on the old rule, while the approval referred to the previous version of the technical spec.&lt;/p&gt;

&lt;p&gt;The local technical-spec validator saw only the current document:&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;technical_spec_validator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;event_name&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trigger_condition&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;audit_fields&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Jira validator saw that the ticket contained acceptance criteria, implementation notes, and test expectations:&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;jira_validator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required_sections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acceptance_criteria&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;implementation_notes&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test_expectations&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The QA validator had no obvious reason to reject its artifact either:&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;qa_validator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;every_case_has&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setup&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;action&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;verification&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cleanup&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these results was locally correct. The technical spec really was valid. Jira really did contain the required sections. The QA scenarios really were self-contained. The problem wasn’t that one of the validators had “done its job badly.” The problem was that none of them answered a different question: did all of these artifacts belong to the same version of the decision?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the State Disappeared
&lt;/h2&gt;

&lt;p&gt;After the correction, the global picture should have looked roughly 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;process_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;canonical_requirement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="na"&gt;trigger_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sms"&lt;/span&gt;

  &lt;span class="na"&gt;approvals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;approved_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;

  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;based_on_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
    &lt;span class="na"&gt;jira&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;based_on_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
    &lt;span class="na"&gt;qa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;based_on_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;

  &lt;span class="na"&gt;completion_allowed&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;But in practice, the process stored only separate successful outcomes: technical spec valid, Jira valid, QA valid, approval present. It had no single place that recorded which version of the canonical requirement had produced each artifact, which version the approval applied to, and which dependencies had become invalid after the correction. As a result, the system combined correct local facts into an incorrect global conclusion:&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;completion_check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;technical_spec_valid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;jira_valid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;qa_valid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;approval_present&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s no obvious false statement in this check. All four values can be true at the same time. They’re simply insufficient to justify &lt;code&gt;complete&lt;/code&gt; because they ignore versions, causal dependencies, and invalidation. That distinction matters. Sometimes an AI process ends incorrectly not because of a hallucination, not because a step was skipped, and not even because the output was poor. It fails because the system can’t combine locally correct results into a correct process state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Workflow Is Not the Same as Process Control
&lt;/h2&gt;

&lt;p&gt;A workflow is good at answering, “What should run next?” It can enforce order, retries, timeouts, branches, and even returns to an earlier step. But a sequence of steps does not, by itself, prove that every dependent object remains current after a change. In our example, the workflow correctly ran &lt;code&gt;generate_jira&lt;/code&gt;, then &lt;code&gt;generate_qa&lt;/code&gt;, then &lt;code&gt;request_approval&lt;/code&gt;. After the correction, it just as correctly ran &lt;code&gt;generate_technical_spec&lt;/code&gt; again. If its model contains no rule saying, “A change to the canonical requirement invalidates Jira, QA, and the approval,” then it has no reason to rerun them. From the workflow’s perspective, that isn’t a failure. It’s a missing rule.&lt;/p&gt;

&lt;p&gt;A minimal orchestration fragment might look 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;on_correction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rerun&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_technical_spec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the real process needs something stronger:&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;on_correction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;invalidate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;technical_spec_approval&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jira&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qa&lt;/span&gt;
  &lt;span class="na"&gt;rerun&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_technical_spec&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_jira&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generate_qa&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;request_approval&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between these two blocks is not the quality of the wording, and it is not the “intelligence” of the model. In the second case, the process dependencies are explicit. In the first, they’re left as assumptions. The model may infer them, or it may not. Even if it infers them correctly nine times in a row, the tenth run still does not become deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Validator Is Not the Same as Process Control
&lt;/h2&gt;

&lt;p&gt;A validator answers, “Does this object satisfy a specific rule?” That is a strong and necessary mechanism, but its scope is usually local. It can check a schema, business constraints, completeness, consistency within a document, and even compare two artifacts. But if it is not given the current canonical version, previous approvals, and the dependency graph, it cannot determine whether a document is current in the context of the entire process.&lt;/p&gt;

&lt;p&gt;For example, this validator checks Jira against the technical spec:&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;jira_consistency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compare&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jira.acceptance_criteria&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;technical_spec.trigger_condition&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works only if it receives the current technical spec. If the orchestration layer passes cached version 3, Jira version 3 will successfully validate against technical-spec version 3 even though the canonical requirement is already at version 4. Local consistency is preserved; global correctness is not. That is why “let’s add another validator” does not always solve the problem. First, you have to define which state the validator is supposed to see and why that state is considered current.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Evaluator Does Not Own the Process Either
&lt;/h2&gt;

&lt;p&gt;An evaluator usually scores the quality of the result: completeness, clarity, alignment with criteria, risks, and sometimes an overall score. That is useful, especially when the rules cannot be expressed fully through a strict schema. But an evaluator works with the package it is given. If the package contains mutually consistent but stale Jira and QA artifacts, the evaluator may score them very highly. It is not required to know that a correction existed and should have invalidated them.&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;evaluation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;clarity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.92&lt;/span&gt;
  &lt;span class="na"&gt;completeness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.88&lt;/span&gt;
  &lt;span class="na"&gt;internal_consistency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.95&lt;/span&gt;
  &lt;span class="na"&gt;handoff_quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That score can be honest and still fail to answer the main question: is this the final package for the current decision? Quality evaluation does not replace provenance, version tracking, or process invariants.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does “Predictable Behavior” Mean Here?
&lt;/h2&gt;

&lt;p&gt;When people talk about making an AI model deterministic, they sometimes mean getting literally the same response to the same prompt. That is not the strict meaning I’m using here. For generative models, full textual determinism is often both unrealistic and not especially useful: wording may change, arguments may appear in a different order, and the same idea may be expressed in different ways. For a long-running work process, another form of predictability matters more. We do not need identical text, but we do need consistent enforcement of process invariants: after a correction, old approvals become invalid; stale artifacts never enter the final package; missing mandatory facts stop the process; every final artifact has known provenance; and completion is allowed only when all dependencies are current.&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;process_invariants&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;correction_invalidates_dependent_artifacts&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;approval_applies_to_exact_version&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stale_artifacts_block_completion&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mandatory_missing_data_causes_hard_stop&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;every_final_artifact_has_provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A system like this does not make the model deterministic in the mathematical sense. It makes the boundaries of acceptable behavior predictable. The model can still propose different wording, choose another document structure, or ask an additional question. But it should not be able to complete the process with a stale approval or an unnoticed artifact version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Needs to See the Whole Picture?
&lt;/h2&gt;

&lt;p&gt;In practice, you need a layer that owns not one document and not one step, but the state of the process as a whole. That layer may be called a process engine, state machine, execution controller, protocol runtime, or simply an explicit control layer. The name matters less than the responsibility. It needs to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical values and their versions;&lt;/li&gt;
&lt;li&gt;the provenance of every artifact;&lt;/li&gt;
&lt;li&gt;the dependency graph;&lt;/li&gt;
&lt;li&gt;the validity of approvals;&lt;/li&gt;
&lt;li&gt;allowed transitions;&lt;/li&gt;
&lt;li&gt;invalidation rules;&lt;/li&gt;
&lt;li&gt;hard stops;&lt;/li&gt;
&lt;li&gt;completion conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow can use this state, validators can read it, the evaluator can score the package in its context, and the model can handle individual judgment-heavy tasks. But the conclusion “the process is truly complete” should not appear as a side effect of every local component independently returning &lt;code&gt;pass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This does not mean every AI process needs to become a complex engine. For a short creative task, that level of control would be excessive. But when a process is long, contains branches, approvals, corrections, several dependent documents, and a requirement for repeatability, global state already exists whether or not we describe it. If we fail to make it explicit, it remains scattered across the prompt, chat history, user memory, and the model’s assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Check
&lt;/h2&gt;

&lt;p&gt;Before calling an AI process controlled, I would check a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does every artifact record the version of the source it was created from?&lt;/li&gt;
&lt;li&gt;Does a correction invalidate all dependent documents and approvals?&lt;/li&gt;
&lt;li&gt;Can a validator determine that it is checking the current version?&lt;/li&gt;
&lt;li&gt;Is there an explicit distinction between &lt;code&gt;valid&lt;/code&gt;, &lt;code&gt;current&lt;/code&gt;, and &lt;code&gt;approved&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does a stale artifact block final completion?&lt;/li&gt;
&lt;li&gt;Is provenance preserved after retries and backtracking?&lt;/li&gt;
&lt;li&gt;Is completion a separate rule rather than merely the fact that the last step finished?&lt;/li&gt;
&lt;li&gt;Can one layer explain why the process is in its current state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those questions do not have clear answers, the process may be well orchestrated, well validated, and even produce high-quality output, but that still does not mean it is controlled.&lt;/p&gt;

&lt;p&gt;If no layer can see the full state of the process, are we controlling the behavior of the AI model — or merely hoping that its locally correct decisions happen to add up to the right result?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>You Validated the Output. Who Validated the Process?</title>
      <dc:creator>Yura Solovey</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:13:34 +0000</pubDate>
      <link>https://dev.to/yura_solovey/you-validated-the-output-who-validated-the-process-18i8</link>
      <guid>https://dev.to/yura_solovey/you-validated-the-output-who-validated-the-process-18i8</guid>
      <description>&lt;p&gt;In the first part, I wrote about the point where a long instruction stops being just a prompt and starts behaving like an undocumented program. It develops state, transitions, recovery paths, dependencies between documents, and rules that are already difficult to change locally. But even if you accept that, an uncomfortable question remains: how do you know the model actually followed the process you needed? The easiest thing is to look at the result. There’s a Jira ticket, a technical description, acceptance criteria, and several test scenarios. The format is correct, the required sections are present, and the names match. During a quick review, the package looks complete — and that’s exactly where we went wrong more than once. A result that looks correct doesn’t prove that the model used the right sources, performed every required check, and avoided skipping a mandatory step. It also doesn’t prove that the next person won’t have to reconstruct part of the work on their own. The clearest way to see this is through a concrete handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Package Looked Ready
&lt;/h2&gt;

&lt;p&gt;The example below is adapted from a controlled run. I changed the domain, names, fields, and values completely, but kept the failure mechanism intact. Imagine a team adding an internal event called &lt;code&gt;PREFERENCE_CHANNEL_CHANGED&lt;/code&gt;, which should be created when &lt;code&gt;profile.notificationChannel&lt;/code&gt; changes from &lt;code&gt;email&lt;/code&gt; to &lt;code&gt;sms&lt;/code&gt;. At the data level, the requirement might look roughly 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;event&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;PREFERENCE_CHANNEL_CHANGED&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;profile.notificationChannel&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sms&lt;/span&gt;
  &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;emit_once&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;persist_audit_record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model follows a long process and produces a Jira ticket, a technical description, acceptance criteria, manual QA scenarios, a short rollback, and a requirement to update tests and documentation. At first glance, everything looks fine: there’s a positive scenario, a negative case, a duplicate check, and a description of the expected event. The names are consistent, the document structure is correct, and the process ends successfully. We’d accepted the package with almost no comments at first. Only when the tester started reading it as a working instruction rather than a neatly assembled document did they stop at the second scenario and ask, “What data am I supposed to prepare here, and what do I clean up afterwards?” The package didn’t answer that, and figuring out what the model had meant took longer than reading the document itself.&lt;/p&gt;

&lt;p&gt;The first gap appeared in Jira. The requirement looked perfectly normal at first, but it was already missing the level of detail needed for implementation. It said something 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;technical_requirements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;update unit tests&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;update documentation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formally, the requirement exists, but in practice it doesn’t answer several basic questions: which component needs coverage, which tests are mandatory, which fixtures should be used, where the audit record is stored, and which documentation page should be updated. QA had the same problem. The first scenario was detailed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"case"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"channel changes from email to sms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"setup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"create user with notificationChannel=email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"clear previous audit events"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update notificationChannel to sms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verify"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"event PREFERENCE_CHANNEL_CHANGED exists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"event was emitted once"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"audit record contains old and new values"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cleanup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"restore notificationChannel=email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"remove generated audit event"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second scenario already started with “repeat the previous steps, but change the value,” and the third referred back to the first two. The tester had to decide which data was inherited, what needed to be cleaned up between runs, and whether the duplicate check should use the same profile or a new one. There was a rollback too, but only one generic instruction: “return the notification channel to its previous value.” That wasn’t enough for real testing. If a scenario creates an event, changes a profile, and leaves an audit record behind, the rollback needs to explain what to do with each of those effects. Otherwise, the next run already starts from a different state than the one described in setup. The package didn’t look obviously wrong: it covered the right topics, contained every expected section, and even read convincingly. The problem only became visible when someone tried to use it as a handoff. That was when it became clear that the document was simply pushing part of the work onto the next person.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Valid Format Isn’t the Same as a Ready Handoff
&lt;/h2&gt;

&lt;p&gt;In many AI processes, result checking starts with schema validation. That’s useful: a schema can confirm that required fields exist, value types are correct, the document has the expected structure, and the JSON or YAML can be parsed programmatically. For example, this check really will catch an empty &lt;code&gt;rollback&lt;/code&gt; and prevent a required field from being omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test_cases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it doesn’t know whether that rollback is sufficient for an independent tester. The string &lt;code&gt;"restore previous value"&lt;/code&gt; will pass the check just as easily as a complete instruction that cleans up the profile, events, and audit records. The same is true for &lt;code&gt;test_cases&lt;/code&gt;: an array with five elements is formally better than an empty array, but the number of elements says nothing about whether those tests are self-contained. A Jira ticket can also contain acceptance criteria while still failing to define all the technical deliverables. We wrote our first validators at exactly this superficial level: does the section exist, is the field filled in, is there a list of tests? The package passed them honestly because all the required elements were formally present. The problem was in our criteria, not just the model’s behavior. After that, we had to split what we had previously treated as one question into three separate ones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the result have the correct form?&lt;/li&gt;
&lt;li&gt;Does it contain enough information for the next person?&lt;/li&gt;
&lt;li&gt;Was it produced through the right process?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answers don’t always match. A document can be formally correct and still be a weak handoff. A handoff can be sufficient even if the model skipped some of its internal checks. The model can follow every step and still produce a badly written document. When all of that is collapsed into one score or one final gate, some classes of failure simply disappear from view.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does “the Right Process” Actually Mean?
&lt;/h2&gt;

&lt;p&gt;It’s easy to go too far in the other direction and demand that the model repeat every internal step word for word, but for most tasks that would be unnecessary. I’m not interested in exposing the model’s hidden chain of thought or controlling every internal operation. A practical process is what can be recorded externally: which sources were used, which mandatory checks were completed, which decisions a person approved, what happened after a correction, and why the process ended in that particular final state. If a document changes after approval, the old approval should no longer count as valid. If the model goes back to an earlier step and changes a canonical value, dependent documents should be regenerated or explicitly rechecked. If mandatory data is missing, the process should stop rather than fill the gap with a plausible assumption. You can think of this as a very simple process state. There’s nothing complicated here: a few versions, statuses for dependent artifacts, and an explicit answer to whether completion is allowed:&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;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;canonical_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sms&lt;/span&gt;
  &lt;span class="na"&gt;approved_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;current_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;dependent_artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jira&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
    &lt;span class="na"&gt;qa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
    &lt;span class="na"&gt;technical_spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
  &lt;span class="na"&gt;completion_allowed&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 fragment doesn’t explain how the model “thought.” It only records what matters for execution: the current version, the status of dependent documents, and whether the process may finish. An ordinary prompt often doesn’t make those things explicit. They exist somewhere in the text, the chat history, and previous responses, but not necessarily as controlled state.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Successful Run Proves Almost Nothing
&lt;/h2&gt;

&lt;p&gt;Another trap is testing a long process only on the happy path. The model receives complete input, the user answers every question, the documents are approved on the first attempt, and the package is created successfully. After a run like that, it’s easy to conclude that the instruction works. In the first part, I already named a minimum set of five scenarios, and each one catches a different class of problem. A normal successful run checks the base route. A branch-heavy scenario shows whether the model gets lost when alternatives and returns appear. An invalid-or-irrelevant-input scenario tests whether it can refuse to build a plausible but incorrect package. A correction scenario shows whether earlier documents and approvals are invalidated. A hard stop checks the most uncomfortable case: whether the model invents a mandatory fact simply because the process “has to” finish. In our case, the happy path produced a polished package, while the scenario in which an independent person had to use the document showed something else: part of the process still had to be reconstructed manually. That’s the difference between “generation completed” and “the process is usable.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Model Could Finish the Package Successfully
&lt;/h2&gt;

&lt;p&gt;In the example above, the model didn’t ignore the task. It produced every expected artifact and touched on the main topics. If the check had asked only whether there was a Jira ticket, QA, rollback, a mention of unit tests, and a full set of documents, the package would have passed without trouble. In fact, it did pass our first validators. But the criterion &lt;code&gt;rollback exists&lt;/code&gt; is much weaker than a real check of rollback quality. For this case, a stronger criterion might look roughly 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;rollback_quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;applies_to_each_mutating_case&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;restores_initial_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;removes_generated_events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;clears_audit_records&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;executable_by_independent_tester&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to QA: having scenarios doesn’t guarantee that each one contains setup, action, verification, and cleanup. The model optimizes for what you describe. If the criterion is defined at the level of section presence, the model can satisfy it at exactly that level. This isn’t unique to AI: a person can also write a formally complete but practically weak document. The difference is scale. A model can generate a large package very quickly, and it can scale incompleteness just as quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Check Right Now
&lt;/h2&gt;

&lt;p&gt;Before accepting an AI-generated package, I’d suggest asking at least a few questions. They don’t replace a full review, but they quickly reveal whether the same gaps we saw in the handoff are hiding behind a correct structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the next person do the work without an extra explanation from the author?&lt;/li&gt;
&lt;li&gt;Is every test case self-contained?&lt;/li&gt;
&lt;li&gt;Are specific technical deliverables defined, rather than just general wishes?&lt;/li&gt;
&lt;li&gt;Does every state-changing action have its own rollback or cleanup?&lt;/li&gt;
&lt;li&gt;Is it clear which sources support the key values?&lt;/li&gt;
&lt;li&gt;Do earlier approvals become invalid after the document changes?&lt;/li&gt;
&lt;li&gt;Are dependent artifacts updated after a correction?&lt;/li&gt;
&lt;li&gt;Can the process still end successfully when a mandatory fact is missing?&lt;/li&gt;
&lt;li&gt;Are you checking not only the format, but whether the handoff is actually usable?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This checklist doesn’t require a new platform or language. It can be added to code review, QA review, or a normal document review. Sometimes that’s already enough to reveal the difference between a document that looks good and one people can actually work from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Should the Control Live?
&lt;/h2&gt;

&lt;p&gt;After cases like this, the natural response is to add more validators. One checks the Jira structure, another checks QA completeness, and a third checks consistency across documents. The workflow controls the order of steps, while a person approves critical decisions. That works to a point, and for a while it can feel as though the problem is solved. Then another question appears: who is responsible for the process as a whole? A workflow knows which step comes next, a validator checks a specific document, an evaluator scores the result, and a person can stop the process. But none of those layers necessarily sees the whole picture on its own: state, sources, approvals, correction paths, final process states, and dependencies between artifacts.&lt;/p&gt;

&lt;p&gt;What if the workflow completed every step, the validators approved every document, and the result was still wrong — who was actually in control of the process?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>When Does a Prompt Become an Undocumented Program?</title>
      <dc:creator>Yura Solovey</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:47:13 +0000</pubDate>
      <link>https://dev.to/yura_solovey/when-does-a-prompt-become-an-undocumented-program-4i6b</link>
      <guid>https://dev.to/yura_solovey/when-does-a-prompt-become-an-undocumented-program-4i6b</guid>
      <description>&lt;p&gt;When we first decided to bring AI into analysts’ work, the task seemed fairly down-to-earth. The company has several development teams and roughly fifteen analysts. They collect requirements, prepare tasks, describe changes in Confluence, think through testing, and help align future implementation with both business and development. A lot of this work is repetitive, so giving analysts a tool that could prepare first drafts felt like an obvious idea.&lt;/p&gt;

&lt;p&gt;The problem is that a document in this kind of process almost never stands on its own. The same change exists in Jira, in Confluence, in mockups, in test scenarios, and in technical notes. Sometimes there are also separate instructions for making changes in the codebase. Each artifact has its own purpose, but all of them describe the same future system behavior. If the wording drifts apart, that can go unnoticed for several days, until different people begin working from different versions.&lt;/p&gt;

&lt;p&gt;A typical case looked roughly like this (the details and names are changed, but the mechanism is real). Jira said that a &lt;code&gt;status&lt;/code&gt; field could have three values: &lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, and &lt;code&gt;archived&lt;/code&gt;. Confluence still contained an older table with only two values, while the test scenario also checked for &lt;code&gt;disabled&lt;/code&gt;, which had been discussed early on and later rejected. The developer implemented what was written in Jira. The tester opened the scenario and filed a defect because &lt;code&gt;disabled&lt;/code&gt; was missing. Only after that did the analyst compare the documents and realize that each of them preserved a different version of the decision.&lt;/p&gt;

&lt;p&gt;Nothing catastrophic happened. We simply had to go through the documentation again, update the tests, clarify the task, explain to the developer that the code did not need to change, and send the package through review one more time. It’s exactly these “nothing serious” moments that add up to delays, when the same task travels two or three times between an analyst, a developer, and a tester. At some point, it becomes difficult even to say where the original mistake was, because everyone is already working on the consequences.&lt;/p&gt;

&lt;p&gt;So our original goal was practical: help analysts assemble a consistent package of materials more quickly and reduce the number of such returns. We didn’t begin with a theory about formal languages or AI process architecture. We simply wrote prompts and tried to make them good enough for real work.&lt;/p&gt;

&lt;p&gt;This is the first article in a four-part series. The second will look at situations where the final result appears correct even though the process was carried out incorrectly. In the third, I want to examine what workflows, validators, and evaluators actually control, and where a gap remains between them. The last part will review existing approaches. Here, I will focus on how a simple instruction gradually turns into something much more complicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  At First, the Model Only Helped with Drafts
&lt;/h2&gt;

&lt;p&gt;The first scenarios were small. The model asked clarifying questions, helped structure the information, prepared a Jira draft, suggested text for Confluence, and produced several test scenarios. The analyst reviewed the result, added what was missing, and asked the model to correct inaccuracies.&lt;/p&gt;

&lt;p&gt;AI still works well in that format. A short conversation, one or two documents, and a quick human review are enough for many tasks. Building a separate mechanism around every such interaction would simply create unnecessary work.&lt;/p&gt;

&lt;p&gt;Technically, these were ordinary chats with sets of files that we passed to the model together with the instructions. Later, we moved such processes into a controlled environment without direct access to production Jira or Confluence.&lt;/p&gt;

&lt;p&gt;The complexity grew gradually. Even a relatively small change could involve forty or fifty analytical steps. First we collected the input information, then clarified data structures and parameters, prepared several related drafts, sent them for review, collected comments, made corrections, and checked the documents against one another again. At the end, we had to add real links to the pages and tasks that had been created. Technical operations were happening in parallel: values were copied between files, required fields were checked, dependent artifacts were updated, and validation was run again.&lt;/p&gt;

&lt;p&gt;In the end, the model had to follow a long route and prepare around seven artifacts. Each individual result could look acceptable, but that was not enough. We also had to make sure that names, parameters, rules, and constraints did not drift apart across documents after several rounds of edits.&lt;/p&gt;

&lt;p&gt;We still called it a prompt because, technically, we were running one large textual instruction. At the same time, it already contained a sequence of actions, dependencies between steps, transition conditions, returns to previous stages, repeated checks, and rules for handling errors. I didn’t see the significance right away. For quite a while, I thought it was simply a very large prompt that needed better organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance Turned Out to Be Harder Than the First Version
&lt;/h2&gt;

&lt;p&gt;It’s almost impossible to write a process like this correctly on the first attempt. Even when the business logic is well understood, there is still the question of how the model will interpret each phrase in a particular context.&lt;/p&gt;

&lt;p&gt;We ran the instruction and looked for where it failed. If the model skipped a check, we added a clarification. If it filled a field incorrectly, we added an example. When two documents started contradicting one another, another validation step appeared in the process. The next run exposed a new problem, and the instruction grew by several more paragraphs.&lt;/p&gt;

&lt;p&gt;Every change had a specific reason, so for a long time it didn’t feel as though we were doing anything dangerous. The text was simply becoming more detailed. After dozens of iterations, however, it contained both the description of the work and traces of all the failures we had encountered before. In one of the larger experiments, the total instruction set grew to almost one megabyte. It was spread across several files and included many sections, examples, and checks that had accumulated over a long period.&lt;/p&gt;

&lt;p&gt;That was when local changes stopped staying local. A rule added for one document could alter behavior in another. A new check could affect how the model interpreted a step that appeared much earlier. After fixing one problem, a new one sometimes appeared in a place that had previously been stable.&lt;/p&gt;

&lt;p&gt;Splitting the instructions into files helped people: the material became easier to read and edit. The model, however, did not treat those files as real boundaries. It saw one large shared context, found connections between fragments, and drew conclusions that we hadn’t intended.&lt;/p&gt;

&lt;p&gt;What made this especially unpleasant was that the model’s explanations often sounded convincing. It could find another rule, connect it to the current step, and explain why it had chosen a particular behavior. The reasoning was not random. It really did follow from the way the model had read the full text. But for our process, that connection was wrong, and it became harder and harder to notice such interactions in advance.&lt;/p&gt;

&lt;p&gt;At some point, we couldn’t confidently predict the consequences of every new clarification. The instruction still worked, but maintaining it began to feel like editing a large system without a dependency map.&lt;/p&gt;

&lt;p&gt;And, to be honest, I kept trying for quite a while to solve the problem with the same method that had created it: adding a few more carefully written paragraphs.&lt;/p&gt;

&lt;h2&gt;
  
  
  We Tried to Explain Everything More Clearly
&lt;/h2&gt;

&lt;p&gt;The most obvious response to incorrect model behavior is to describe the requirement in more detail. Early on, that works: an unclear phrase can be rewritten, a weak example can be replaced, and a missing exception can be added to the rules.&lt;/p&gt;

&lt;p&gt;The difficulty begins when a new paragraph has to coexist with dozens of older ones. The author sees it as a local correction. The model receives one more piece of context that can be connected to every other part. This increases both the size of the text and the number of possible ways its parts can be interpreted together.&lt;/p&gt;

&lt;p&gt;We regrouped rules, moved examples closer to the steps they belonged to, separated checks, and removed repetition. Some of those changes had a noticeable effect. The main difficulty remained: editing one fragment of a large instruction didn’t guarantee a change in only one part of the model’s behavior.&lt;/p&gt;

&lt;p&gt;For a long time, I thought we simply needed a better way to write prompts. Eventually, I saw a different picture. The task already had state, transitions, conditions, dependencies between artifacts, and recovery after errors. We were trying to maintain all of that as ordinary prose, even though in terms of complexity it already resembled a small program.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way the Model Worked with Code Changed How I Saw the Problem
&lt;/h2&gt;

&lt;p&gt;By that point, I had been using language models in software development for more than two and a half years. They made plenty of mistakes there too, but the environment was very different.&lt;/p&gt;

&lt;p&gt;When a model changes a function in a large project, it sees the signature, types, call sites, surrounding code, tests, and the rules of the language itself. Changing an interface affects concrete implementations and usage points. Type violations can be caught during compilation or static analysis. Even a poor change is attached to a specific structure, so its consequences are easier to find and inspect.&lt;/p&gt;

&lt;p&gt;A large textual instruction offers far fewer such anchors. The same requirement can be expressed in dozens of ways, and each phrasing leaves room for interpretation. In a short conversation, that’s usually fine: a person quickly clarifies the intent. In a long process, the decision made at one step becomes context for the next, so a small ambiguity can pass through several stages and surface in a different document.&lt;/p&gt;

&lt;p&gt;There is a temptation to draw too broad a conclusion from this, and I’d rather avoid that. Code is not a universally better way to describe every kind of work. Natural language lets us discuss unclear tasks, change direction, explore options, and avoid defining the whole structure in advance. For analytical work, that is one of its main advantages. In our case, the problem appeared where the model was expected to follow a repeatable procedure with a large number of mandatory conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens as Models Become More Capable?
&lt;/h2&gt;

&lt;p&gt;Modern models are increasingly good at solving underspecified tasks, and for analysis or exploration that is a major advantage. A repeatable process is different: the model may see a “better” route where the team needs the agreed one. I’ve already caught myself describing two separate modes of work — “think for yourself” and “follow this literally.” I don’t know yet whether new control mechanisms will reduce that tension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Draw the Boundary Now
&lt;/h2&gt;

&lt;p&gt;I don’t tie the boundary to a specific number of steps. Ten steps can be harder than one hundred if they contain many branches, returns, and dependencies. A long linear process can be easier to control than a short one in which every decision changes several later actions.&lt;/p&gt;

&lt;p&gt;I now test such instructions with a few simple questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the process need to preserve state between stages?&lt;/li&gt;
&lt;li&gt;Do earlier decisions affect later ones?&lt;/li&gt;
&lt;li&gt;Are there branches, error recovery paths, or several possible final outcomes?&lt;/li&gt;
&lt;li&gt;Do several documents need to change together?&lt;/li&gt;
&lt;li&gt;Does a correction make earlier approvals or artifacts obsolete?&lt;/li&gt;
&lt;li&gt;Will someone later need to retrace how the result was produced?&lt;/li&gt;
&lt;li&gt;Would one skipped step force another person to reconstruct a missing part of the process?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most answers are yes, I no longer treat the text as an ordinary prompt. It is a process specification, even when it lives in a Markdown file. That doesn’t mean it must be rewritten in a formal language: sometimes it is enough to move part of the control into code, a workflow, validators, or human review. But testing it with only one successful run is certainly not enough. The minimum we now check is a normal scenario, a branch-heavy scenario, invalid input, recovery after correction, and a hard stop when mandatory facts are missing.&lt;/p&gt;

&lt;p&gt;For now, the practical observation I want to preserve is simple: a large text containing transitions, state, exceptions, and dependencies between artifacts becomes difficult to maintain in the same way as an ordinary instruction.&lt;/p&gt;

&lt;p&gt;Have you had a moment when a prompt stopped feeling like plain text? What caused it: the number of steps, branching, the number of documents, the cost of an error, or something else? And did you manage to keep such a process under control without additional formalization?&lt;/p&gt;

&lt;p&gt;In the next part, I will move to another aspect of the same problem: a final document can look correct and pass formal validation even though the model didn’t follow the process that was expected of it. Later in the series, I will show which control mechanisms we tried, where they helped, and where they merely moved the problem to another layer.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
