<?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: Daniel Romitelli</title>
    <description>The latest articles on DEV Community by Daniel Romitelli (@romiteld).</description>
    <link>https://dev.to/romiteld</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%2F2564609%2F45e9921e-df6d-47a9-a7b5-344290cb30a0.jpg</url>
      <title>DEV Community: Daniel Romitelli</title>
      <link>https://dev.to/romiteld</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/romiteld"/>
    <language>en</language>
    <item>
      <title>A Perfect Fit Can Recover the Wrong Physics</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:42:51 +0000</pubDate>
      <link>https://dev.to/romiteld/a-perfect-fit-can-recover-the-wrong-physics-372g</link>
      <guid>https://dev.to/romiteld/a-perfect-fit-can-recover-the-wrong-physics-372g</guid>
      <description>&lt;p&gt;AutoLensAI moves photographed vehicles into virtual studios for dealership listings. I need to change the surroundings without changing the car: its equipment, its cabin, even the damage in its paint. A reflection across a headrest puts those requirements in the same pixels.&lt;/p&gt;

&lt;p&gt;On October 4, I had to change what a glass estimate was called. The code could fit a factor relating two photographs. It had not established that the factor measured how much light the glass transmitted. Illumination and camera processing could affect it too.&lt;/p&gt;

&lt;p&gt;Calling the value transmission would have made downstream code treat an uncalibrated estimate as a measured property of the pane. I kept the appearance estimate and made the missing evidence explicit.&lt;/p&gt;

&lt;p&gt;That distinction is easy to dismiss while the reconstructed image looks right. It becomes harder to dismiss when two explanations fit every observed sample, yet one predicts the wrong result as soon as the lighting changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number the photographs actually identify
&lt;/h2&gt;

&lt;p&gt;Rendering starts with geometry, materials, and light, then predicts an image. Inverse rendering works backward from images to those causes. My glass problem belongs to the second direction, but a good reconstruction alone does not tell me how much of the explanation I have recovered.&lt;/p&gt;

&lt;p&gt;Consider one intensity channel with perfect alignment, a linear camera response, no reflection, no clipping, and no noise. Let a reference sample be $x_i$, the observed sample be $y_i$, transmission be $\tau$, and the illumination ratio between conditions be $g$:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
y_i = \tau g x_i.&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;The observations determine the product $\tau g$. They do not, by themselves, separate its factors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Explanation&lt;/th&gt;
&lt;th&gt;Transmission $\tau$&lt;/th&gt;
&lt;th&gt;Illumination ratio $g$&lt;/th&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synthetic ground truth&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternative&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both rows produce the same image. Assuming unchanged illumination picks the second row and makes the transmission estimate 50% too high.&lt;/p&gt;

&lt;p&gt;These numbers are chosen for a reproducible counterexample, not taken from a vehicle. The ambiguity is exact: replacing $(\tau,g)$ with $(c\tau,g/c)$ leaves the image unchanged for any positive $c$ that keeps both parameters admissible.&lt;/p&gt;

&lt;p&gt;An optimizer can return one answer from that family. A prior can favor one answer, and regularization can make the choice stable. None of those choices adds an observation. I need to know which part of the result came from the photograph and which part came from an assumption.&lt;/p&gt;

&lt;p&gt;Removing reflection from this example makes the point more specific, not less relevant. Transmission and illumination are already inseparable here. Adding an unknown reflection field does not automatically supply the missing information.&lt;/p&gt;
&lt;h2&gt;
  
  
  Held-out pixels can have the same blind spot
&lt;/h2&gt;

&lt;p&gt;The script at the end generates 64 positive reference samples. It fits the product on 32 and reserves the other 32 for evaluation. Both explanations reproduce all the reserved samples with zero error in the recorded run.&lt;/p&gt;

&lt;p&gt;Nothing leaked from the held-out set. The split did its job. It tested reconstruction under a condition that both explanations could reproduce.&lt;/p&gt;

&lt;p&gt;The distinction I care about appears under a different test. Set a new illumination ratio independently to $g_{\mathrm{new}}=1$, and do not allow another fit. The true transmission predicts $0.4x$. The alternative predicts $0.6x$. Their agreement on the source has become a 50% relative prediction error.&lt;/p&gt;

&lt;p&gt;Allowing the second explanation to adjust its illumination again would hide the mistake. The changed condition has to remain fixed by evidence outside the fit.&lt;/p&gt;

&lt;p&gt;More pixels are useful when estimating the product in noise. They do not separate the factors in this model. The accompanying tests check 8, 64, and 4,096 samples and find the same missing degree of freedom. More images have a similar qualification: if every image brings another unconstrained illumination multiplier, the absolute scale can remain ambiguous.&lt;/p&gt;

&lt;p&gt;This is why I separate source replay from a test under changed lighting. Source replay asks whether the explanation can reproduce what it was given. The second test asks whether it predicts something it was not allowed to adjust itself to match.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture of the public experiment
&lt;/h3&gt;


&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  truth[Known synthetic parameters] --&amp;gt; source[Source observations]
  source --&amp;gt; fit[Fit on training samples]
  fit --&amp;gt; alternatives[Image-equivalent explanations]
  alternatives --&amp;gt; replay[Reconstruct held-out samples]
  source --&amp;gt; replay
  replay --&amp;gt; sourceError[Source reconstruction error]
  alternatives --&amp;gt; changed[Predict under known new lighting]
  lighting[Independent lighting condition] --&amp;gt; changed
  truth --&amp;gt; target[Separate target ground truth]
  lighting --&amp;gt; target
  changed --&amp;gt; compare[Compare changed-condition predictions]
  target --&amp;gt; compare
  compare --&amp;gt; parameterError[Error hidden by source agreement]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The target comes from the known synthetic scene, not the candidate estimate. Using the candidate's own parameters to build its reference would make the comparison circular. The diagram shows the evaluation; the private recovery method is outside its scope.&lt;/p&gt;
&lt;h2&gt;
  
  
  What would let me separate them?
&lt;/h2&gt;

&lt;p&gt;The Jacobian makes the missing information visible. It describes how the predicted samples change as each parameter changes:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
J=\begin{bmatrix}g\mathbf{x}&amp;amp;\tau\mathbf{x}\end{bmatrix}.&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;The columns are proportional. There are two parameter columns but only one independent direction of change, so the matrix has rank one. An increase in transmission can be canceled by a decrease in illumination.&lt;/p&gt;

&lt;p&gt;Now add a separate measurement of $g$. Its derivative with respect to $(\tau,g)$ is $[0,1]$. Appending that observation raises the rank to two, provided the reference has nonzero energy and $g$ is nonzero. In this model, that is enough to separate the factors.&lt;/p&gt;

&lt;p&gt;An illumination value computed from the same unresolved image ratio adds no independent information. Neither does another region that carries the same unknown optical factor merely because I give it a different label.&lt;/p&gt;

&lt;p&gt;Uncertainty matters after the rank problem is resolved. Suppose the product is exactly $0.6$, but the new measurement bounds illumination between $1.425$ and $1.575$. Then&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
\tau\in\left[\frac{0.6}{1.575},\frac{0.6}{1.425}\right]&lt;br&gt;
\approx[0.381,0.421].&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;That interval uses deliberately chosen bounds. It shows what happens when the independent reading is imperfect. On a real capture, uncertainty in the fitted product, camera response, alignment, and image model would also have to be accounted for.&lt;/p&gt;

&lt;p&gt;Camera calibration cannot be skipped by calling the working pixels linear. Inverting an sRGB transfer function removes that transfer function; it does not undo every operation that produced a processed photograph. In &lt;a href="https://www.pauldebevec.com/Research/HDR/abstract.html" rel="noopener noreferrer"&gt;Debevec and Malik's radiance-recovery method&lt;/a&gt;, known exposure relationships are used to recover the imaging response up to scale. That response is part of the recovery, not a name assigned to the decoded array.&lt;/p&gt;

&lt;p&gt;The October 4 audit had not established the corresponding calibration for my capture path. Its illumination diagnostics also lacked uncertainty bounds. Those are reasons to retain an appearance estimate, even when the structural test and image fit look encouraging. They are not reasons to discard the work, but they change what I can use its output to claim.&lt;/p&gt;
&lt;h2&gt;
  
  
  608 images, five matched openings
&lt;/h2&gt;

&lt;p&gt;I had already encountered a different way for a good image score to conceal a bad result.&lt;/p&gt;

&lt;p&gt;In August, I evaluated a trained alpha refiner on 608 images from 38 vehicles. Alpha matting determines how much of the source to retain at each pixel. The refiner's job was to improve vehicle boundaries, and its boundary score did improve over the upstream semantic mask.&lt;/p&gt;

&lt;p&gt;Then I counted the enclosed background openings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Region IoU&lt;/th&gt;
&lt;th&gt;Boundary IoU&lt;/th&gt;
&lt;th&gt;Matched openings&lt;/th&gt;
&lt;th&gt;Extra openings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semantic mask&lt;/td&gt;
&lt;td&gt;0.9904&lt;/td&gt;
&lt;td&gt;0.4233&lt;/td&gt;
&lt;td&gt;1 of 1,193&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback refinement&lt;/td&gt;
&lt;td&gt;0.9815&lt;/td&gt;
&lt;td&gt;0.2596&lt;/td&gt;
&lt;td&gt;28 of 1,193&lt;/td&gt;
&lt;td&gt;2,492&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trained refinement&lt;/td&gt;
&lt;td&gt;0.9908&lt;/td&gt;
&lt;td&gt;0.4762&lt;/td&gt;
&lt;td&gt;5 of 1,193&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;From the August evaluation record. These images were used for checkpoint selection; they were not an untouched test set.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;IoU measures overlap with the reference region. Body pixels dominate it. A filled wheel opening occupies little area beside an entire door or hood, but it still carries the old background into the composite. Boundary IoU looks more closely at contours. The opening comparison checks whether enclosed background components have survived at all.&lt;/p&gt;

&lt;p&gt;The trained model had the best boundary score and still missed 1,188 reference openings. The fallback matched more openings but created 2,492 extras. The extra count prevented me from treating that apparent improvement as useful recovery.&lt;/p&gt;

&lt;p&gt;This failure had a different cause from the glass ambiguity. The refiner could modify only its allowed boundary region. A confidently filled opening outside that region was fixed with respect to the learned weights. Training that same stage longer could not give it control of pixels its output definition excluded.&lt;/p&gt;

&lt;p&gt;In the glass example, different parameters explain the same observations. Here, the desired correction can sit outside the model's permitted outputs. The first problem needs evidence or assumptions that separate the explanations. The second needs a prediction target and output region capable of representing the correction, plus tests for the new false openings that freedom can introduce.&lt;/p&gt;

&lt;p&gt;I retained the checkpoint for its boundary improvement. Its clean studio images and binary glass labels did not establish performance on dealer lots or recovery of light through glass. The opening counts were enough to stop the broader acceptance claim without denying what the model had learned.&lt;/p&gt;
&lt;h2&gt;
  
  
  I rejected the lowest error
&lt;/h2&gt;

&lt;p&gt;On September 3, the relighting work presented another trap. I had no photograph of the actual truck under the destination lighting. Judging a correction only by how little it disturbed that truck could favor a method that barely changed anything.&lt;/p&gt;

&lt;p&gt;I used a synthetic calculation with a known destination response to compare the alternatives. The approximate geometry used for lighting could leave its own pattern in the result. Suppressing that pattern was useful until the suppression also weakened the broad shading I was trying to add.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Target RMS log error&lt;/th&gt;
&lt;th&gt;Unwanted-pattern correlation&lt;/th&gt;
&lt;th&gt;Protected-detail damage&lt;/th&gt;
&lt;th&gt;Intended-shading correlation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;0.3869&lt;/td&gt;
&lt;td&gt;0.9994&lt;/td&gt;
&lt;td&gt;0.0594&lt;/td&gt;
&lt;td&gt;0.9472&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;0.3242&lt;/td&gt;
&lt;td&gt;0.0276&lt;/td&gt;
&lt;td&gt;0.0042&lt;/td&gt;
&lt;td&gt;0.9557&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;0.3257&lt;/td&gt;
&lt;td&gt;0.0134&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;0.9530&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;0.3335&lt;/td&gt;
&lt;td&gt;0.0109&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;0.8955&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Recorded September 3 synthetic comparison. Candidate letters replace the private settings. Damage is a fraction of protected detail; the two correlations compare the unwanted pattern and the intended shading.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A had the lowest error against the target in log brightness, but it damaged protected structure. C removed more of the unwanted pattern than B, but it also weakened the intended shading. I selected B. A single ranking by either target error or artifact suppression would have selected something else.&lt;/p&gt;

&lt;p&gt;The damage test needed checking too. Brightening a badge is not the same as changing its lettering. In the retained controls, smooth illumination changes registered zero structural damage. Replacing “F-150” with “F-250” registered 0.0629. That gave the rejection a specific meaning: the test was responding to an identity change, rather than any difference in RGB.&lt;/p&gt;

&lt;p&gt;The calculation isolated the operator's behavior. It left out indirect illumination, render noise, real geometry gaps, and material boundaries. I can use it to compare those candidates, not quote its error as an expected result on a dealership photograph.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run the optical check
&lt;/h2&gt;

&lt;p&gt;The transmission example is intentionally small enough to inspect. It is standard multiplicative ambiguity, not a claim to a new theorem. The broader problem has a long history: &lt;a href="https://vision.ucsd.edu/publications/1999/bas-relief-ambiguity" rel="noopener noreferrer"&gt;Belhumeur, Kriegman, and Yuille's bas-relief result&lt;/a&gt; shows how compensating changes in shape, reflectance, and light can preserve images under different assumptions from this glass example. Modern methods such as &lt;a href="https://arxiv.org/abs/2106.01970" rel="noopener noreferrer"&gt;NeRFactor&lt;/a&gt; make their representations and priors explicit when recovering shape, reflectance, and illumination.&lt;/p&gt;

&lt;p&gt;Here the useful result is that even a perfect held-out image fit cannot tell the two rows apart. A known change of lighting can.&lt;/p&gt;

&lt;p&gt;Save the following as &lt;code&gt;optical_identifiability.py&lt;/code&gt;. Install NumPy with &lt;code&gt;python -m pip install numpy&lt;/code&gt;, then run &lt;code&gt;python optical_identifiability.py&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndim&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected matching nonempty 1D arrays&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples must be finite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference must have finite positive energy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;energy&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fitted product must be finite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;


&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;true_tau&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;true_light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
&lt;span class="n"&gt;observed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true_tau&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;true_light&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="n"&gt;training&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;heldout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;training&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;training&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;training&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fitted_light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tau&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fitted_light&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;heldout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rmse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;heldout&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;rmse&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-14&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tau=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tau&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, light=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fitted_light&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, RMSE=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rmse&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The new light is fixed independently; it cannot compensate for wrong tau.
&lt;/span&gt;&lt;span class="n"&gt;new_light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true_tau&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;new_light&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;heldout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;new_light&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;heldout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;relative_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrong&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relative_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Changed-light relative error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;relative_error&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Check whether the observations distinguish the two parameters.
&lt;/span&gt;&lt;span class="n"&gt;jacobian&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column_stack&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;true_light&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;true_tau&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;augmented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;jacobian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matrix_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jacobian&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matrix_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;augmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;recovered_tau&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;true_light&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recovered_tau&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;true_tau&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;light_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;light_high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.425&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.575&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;light_low&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;light_high&lt;/span&gt;
&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;light_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;light_low&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;true_tau&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ranks:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matrix_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jacobian&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matrix_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;augmented&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recovered tau with known light: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recovered_tau&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tau bound with uncertain light: [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tau=0.4, light=1.5, RMSE=0
tau=0.6, light=1.0, RMSE=0
Changed-light relative error: 50.0%
Ranks: 1 2
Recovered tau with known light: 0.400
Tau bound with uncertain light: [0.381, 0.421]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RMSE is the root mean square difference between the held-out prediction and observation. The assertions allow floating-point roundoff. The companion ten-test suite also checks invalid inputs, unchanged input arrays, rank at different sample counts, and propagation of the illumination bounds. The example and all ten tests passed with Python 3.13.5 and NumPy 2.3.5.&lt;/p&gt;

&lt;p&gt;The private model, capture procedure, training recipe, and recovery settings are not needed to reproduce this calculation. The August and September tables report retained experiments; the code above is the separate synthetic example run for this article.&lt;/p&gt;

&lt;p&gt;For the actual glass estimate, the next empirical test needs an independently known change of condition and an uncertainty budget for the measurements. I need the estimate to survive that change of light, with no second fit to rescue it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover: generated editorial artwork, not an image from the experiments.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>inverserendering</category>
      <category>opticalidentifiability</category>
      <category>computervision</category>
      <category>experimentaldesign</category>
    </item>
    <item>
      <title>Part 2: The Window Is Not the Vehicle</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 14 Oct 2026 06:16:22 +0000</pubDate>
      <link>https://dev.to/romiteld/part-2-the-window-is-not-the-vehicle-3iao</link>
      <guid>https://dev.to/romiteld/part-2-the-window-is-not-the-vehicle-3iao</guid>
      <description>&lt;p&gt;AutoLensAI composites photographs of real vehicles into virtual studios for dealership listings. The setting changes. The vehicle's bodywork, equipment, cabin, and visible condition have to survive that change.&lt;/p&gt;

&lt;p&gt;The windows make this harder than the outline suggests.&lt;/p&gt;

&lt;p&gt;A side window can show a headrest, a reflection of a building behind the photographer, and another car visible through the far side of the cabin. Move the vehicle into a studio and pieces of the parking lot can come with it. Remove too much and the headrest goes too.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/blog/negative-space-is-a-label"&gt;Negative Space Is a Label&lt;/a&gt;, I wrote about teaching a matting model to reject shadows and floor reflections beside a vehicle. A dark patch touching a tire can still be background. The work in this follow-up concerns side glass, where the unwanted surroundings can overlap the vehicle detail I need to preserve.&lt;/p&gt;

&lt;p&gt;Getting the outer edge right leaves that problem unresolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem moves inside the outline
&lt;/h2&gt;

&lt;p&gt;A vehicle matte tells the compositor how much of the source image to retain at each location. Around the car, that helps separate a bumper from the lot behind it or preserve the fine edge of a mirror.&lt;/p&gt;

&lt;p&gt;Inside a window, retaining the photographed color can retain several things at once.&lt;/p&gt;

&lt;p&gt;The glass itself belongs to the vehicle. So do the seats, headrests, pillars, and other cabin surfaces visible behind it. A building reflected across those surfaces belongs to the surroundings. So does the parked car seen through an unobstructed view out the opposite window.&lt;/p&gt;

&lt;p&gt;Those contributions don't necessarily occupy separate pixels. A reflection can cross a headrest. Calling that pixel “vehicle” keeps the reflection with it. Calling it “background” can remove part of the headrest.&lt;/p&gt;

&lt;p&gt;Treating the whole window as opaque foreground preserves the old scene. Treating it as an empty opening can put the new backdrop where upholstery should be. Adjusting one transparency value across the pane cannot distinguish the seat from the view beyond it.&lt;/p&gt;

&lt;p&gt;A glass mask can trace the window perfectly while the correction inside it damages the cabin. The mask locates the work. It does not establish which contributions can be separated.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A test passed with the correction removed
&lt;/h2&gt;

&lt;p&gt;In a local glass-authoring experiment recorded on September 8, 2026, the correction was replaced with a function that returned the original image. A check was supposed to reject that result because no glass treatment had occurred.&lt;/p&gt;

&lt;p&gt;The check passed anyway.&lt;/p&gt;

&lt;p&gt;The operation assembling the image introduced tiny floating-point differences even when its inputs were identical. The check detected those differences and treated them as evidence that the correction had done something.&lt;/p&gt;

&lt;p&gt;A regression test with synthetic floating-point data exposed the size of the error: &lt;strong&gt;31 of 988 numeric elements differed&lt;/strong&gt;, with a maximum absolute difference of &lt;strong&gt;1.1920929e-07&lt;/strong&gt;. Those are individual numeric values, not 31 altered vehicle pixels and not a measurement of reflection-removal accuracy.&lt;/p&gt;

&lt;p&gt;The control had failed for a reason that would be almost impossible to establish by looking at the image. It was accepting arithmetic noise as intervention.&lt;/p&gt;

&lt;p&gt;After the fix, unchanged inputs stayed bit-identical. The recorded local caller rejected the source-copy control and wrote no after image. The focused test run finished with 22 tests passing. That record establishes the local test behavior on that date, not coverage of the production system.&lt;/p&gt;

&lt;p&gt;This exposed two different obligations. Protected pixels must remain unchanged, but a difference elsewhere does not establish useful work. A test that asks only whether two images differ cannot tell a repaired reflection from rounding error or damaged upholstery.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A cleaner window is not necessarily a more accurate one
&lt;/h2&gt;

&lt;p&gt;For a dealership image, I care about whether the result still represents the vehicle being sold. A window that looks cleaner can fail that requirement.&lt;/p&gt;

&lt;p&gt;An erased headrest might disappear into a dark cabin without drawing attention. An opening that has been made too transparent might show an attractive studio background where the car should block it.&lt;/p&gt;

&lt;p&gt;A brighter interior might look better while giving the wrong impression of the tint. Darkness alone is no reason to erase a seat, just as brightness alone is no reason to erase a reflection crossing one.&lt;/p&gt;

&lt;p&gt;Moving an image toward a cleaner reference does not, by itself, measure the glass's physical properties. It does not prove how much light the pane transmitted, or that the removed color came entirely from reflection.&lt;/p&gt;

&lt;p&gt;The same applies to detail hidden by a reflection. An inferred seat can be visually convincing without matching the seat in that vehicle. Where the capture does not establish the underlying detail, generating a plausible replacement changes the nature of the photograph.&lt;/p&gt;

&lt;p&gt;I am using this system to present an existing car. Preserving uncertainty is preferable to presenting an inferred feature as something the camera recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The glass step has no permission to repair the rest of the car
&lt;/h2&gt;

&lt;p&gt;In Part 1, the boundary refiner could work only within an uncertain strip at the edge of the car. The glass step follows the same restriction: it can modify a limited region rather than the whole photograph.&lt;/p&gt;

&lt;p&gt;At the glass-processing step, pixels outside the permitted region are copied from the input unchanged. That includes the painted door, window trim, pillars, and other protected areas. The restriction applies to this operation, rather than making a claim that every pixel in a resized or encoded final image is numerically identical to the capture.&lt;/p&gt;

&lt;p&gt;The architecture separates proposing a change, limiting where it can be written, and deciding whether the result is acceptable. The correction method remains inside one box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  source[Source photograph] --&amp;gt; proposal[Proposed glass correction]
  source --&amp;gt; copy[Original protected pixels]
  proposal --&amp;gt; write[Restricted write]
  copy --&amp;gt; write
  region[Permitted edit region] --&amp;gt; write
  write --&amp;gt; check[Protected pixel check]
  source -.-&amp;gt; check
  check --&amp;gt;|Fail| reject[Reject candidate]
  check --&amp;gt;|Pass| review[Cabin and reflection review]
  source -.-&amp;gt; review
  review --&amp;gt;|Unsupported| reject
  review --&amp;gt;|Supported| accept[Accept correction]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A change to a protected pillar fails the copy check even when the window looks better. Passing that check sends the candidate to image review; it does not approve the correction.&lt;/p&gt;

&lt;p&gt;The following is an independent example using NumPy, Python's array library. It demonstrates restricted copying, not the reflection-removal method. Both image arrays must already refer to the same pixel locations and color representation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Demonstrate restricted copying, not glass correction.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndim&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source must be a nonempty H x W x 3 RGB array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source and candidate shapes must match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtype&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtype&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;both images must use float32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtype&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bool_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;editable must be an H x W boolean mask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image values must be finite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example has no blending, resizing, color conversion, or transparency handling. It copies the source and replaces only the permitted RGB samples. Shape validation prevents an incompatible array from being accepted, but matching shapes do not prove that two photographs are aligned.&lt;/p&gt;

&lt;p&gt;The boundary itself can be wrong, though. A region drawn too tightly prevents useful work near the edge. A region drawn too loosely can admit trim or bodywork into an area intended for glass. Restricting edits does not make region selection infallible.&lt;/p&gt;

&lt;p&gt;The consequence of a tight boundary is often a visible remnant of the original scene. That can be inspected and traced back to the region that blocked the correction. Allowing broader edits to hide it would make the system harder to judge, because the apparent improvement could come from changing the car.&lt;/p&gt;

&lt;p&gt;I would rather correct the region that was wrong than give a later stage more freedom to conceal the mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Four tests, and what they cannot establish
&lt;/h2&gt;

&lt;p&gt;These tests use a synthetic four-by-four RGB array, not a captured vehicle or a production training sample. They check an unchanged candidate, a restricted edit, damage outside the permitted region, and damage inside it.&lt;/p&gt;

&lt;p&gt;The byte comparison is deliberate. The requirement is an exact copy, not an image that is numerically close enough to the original.&lt;br&gt;
&lt;/p&gt;

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


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CopyRuleTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unchanged_candidate_preserves_every_byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_only_permitted_pixels_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_array_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_one_protected_change_is_detected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_inside_damage_passes_the_copy_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_array_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save both Python blocks, in order, as &lt;code&gt;glass_copy_example.py&lt;/code&gt;. Install NumPy with &lt;code&gt;python -m pip install numpy&lt;/code&gt;, then run &lt;code&gt;python glass_copy_example.py&lt;/code&gt;. The example runs four tests.&lt;/p&gt;

&lt;p&gt;The last test is the important limit. It darkens a permitted sample and still passes the protected-pixel check. That is expected: the check knows where a change occurred, not whether the changed sample belongs to a headrest. The test makes that limitation explicit rather than treating a passing preservation check as proof of image quality.&lt;/p&gt;

&lt;p&gt;The September 8 review had a separate visual failure. The replacement scene read as a bright inserted opening beside the retained glass and cabin. A wider correction removed more of the original scene, but the preview remained rejected for photographic delivery. Fixing the no-change control did not make that image acceptable.&lt;/p&gt;

&lt;p&gt;That is why image review still needs the source photograph. It has to establish whether the same cabin structures remain, whether they still block the view behind them, and whether old surroundings have been removed without deleting vehicle detail. Neither an accurate glass outline nor a count of changed pixels answers those questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. When the evidence runs out
&lt;/h2&gt;

&lt;p&gt;Part 1 separated vehicle, confirmed background, and uncertainty. That last category remains important here, but the reason for uncertainty has changed.&lt;/p&gt;

&lt;p&gt;At the outer edge, the question was often where the vehicle ended. Inside the window, I can know where the pane is and still lack enough evidence to separate what the photograph contains.&lt;/p&gt;

&lt;p&gt;The practical response is to limit the correction, preserve the parts that are established, and leave unresolved areas visible as unfinished work. A rule about where changes are allowed helps enforce that response. It does not solve the missing-information problem.&lt;/p&gt;

&lt;p&gt;A remaining reflection may make the image less polished. A missing headrest makes it inaccurate. The finished listing still has to show the car I photographed.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>matting</category>
      <category>carsegnet</category>
      <category>automotiveai</category>
    </item>
    <item>
      <title>A Media Asset Is a Stateful Object</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:19:15 +0000</pubDate>
      <link>https://dev.to/romiteld/a-media-asset-is-a-stateful-object-1iag</link>
      <guid>https://dev.to/romiteld/a-media-asset-is-a-stateful-object-1iag</guid>
      <description>&lt;p&gt;A phone can upload a sharp photo and still leave the product with nothing usable. The bytes are there. A thumbnail may exist. A background cutout may even look clean. None of that answers the question the product actually has to answer: can this image be shown, retried, reviewed, or published?&lt;/p&gt;

&lt;p&gt;That is the failure I wanted to avoid. Vehicle listings are made out of media, yet accepting images is the easy part. The hard part is keeping the original, the expensive work, the human decision, and the publish button attached to the same object after retries and partial success.&lt;/p&gt;

&lt;p&gt;This post is separate from the earlier provenance and approval-gate work. Those posts were about receipts and invalid final states. This one is about custody across storage, database jobs, vision workers, derived artifacts, human review, and publication. The model I use is simple: a media asset is a stateful object moving through a bounded vision pipeline. The endpoint bag is the failure mode.&lt;/p&gt;

&lt;p&gt;The curated highlight for this build was Vehicle media intelligence pipeline with bounded multi-stage vision processing. Every expensive computer vision (CV) step has to write back to the same asset story, or the product will eventually show an image whose approval came from somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Intake is proven by an event, not by a timestamp
&lt;/h2&gt;

&lt;p&gt;The listing design starts with one walk-around becoming one finished listing: a hero set, social placements, description, and a spec sheet, with review before publication. Later work reserves room for 360 output and a forty-image set through &lt;code&gt;SceneBundle&lt;/code&gt;. Product-shaped, yes. The important part sits underneath.&lt;/p&gt;

&lt;p&gt;The trigger is a durable row in &lt;code&gt;project_events&lt;/code&gt; named &lt;code&gt;capture.media_secured&lt;/code&gt;. Its idempotency key starts with media-secured, includes the capture session identifier, and ends at version one. The payload is &lt;code&gt;MediaSecuredEvent&lt;/code&gt;. It carries the project identifier, source asset identifiers, required roles satisfied, required roles missing, and &lt;code&gt;processingEnqueued&lt;/code&gt;. Extra fields are rejected.&lt;/p&gt;

&lt;p&gt;That last field is there because false is a real result. The phone may have secured the bytes while the next work item failed to enqueue. I wanted that outcome reported as a first-class condition, instead of being inferred later from silence.&lt;/p&gt;

&lt;p&gt;The design also says the event row is the proof that photos are secured. The &lt;code&gt;media_secured_at&lt;/code&gt; column alone is not enough, because another writer stamps it during egress. Two writers used the same timestamp for different meanings. The event has a name, a payload, and an idempotency key, so the capture handoff can be replayed or audited without guessing.&lt;/p&gt;

&lt;p&gt;Only digest-verified source assets get their first &lt;code&gt;processing_jobs&lt;/code&gt; rows. The first job key begins with preflight and includes the source asset identifier. That is the first custody boundary: work attaches to source asset identity, not to a temporary URL the interface happened to hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Separate endpoints break at the first partial retry
&lt;/h2&gt;

&lt;p&gt;The tempting application programming interface (API) shape is easy to draw. Upload. Remove background. Enhance. Make thumbnail. Ask for status. Each endpoint has a clean request and a clean response, then the first uneven result arrives.&lt;/p&gt;

&lt;p&gt;A derived image exists, while the quality verdict is missing. A retry produces a better frame, while the listing still points at the old one. A social crop finishes before the parent image is accepted. The review queue now contains outputs from different attempts of the same vehicle.&lt;/p&gt;

&lt;p&gt;I did put bounded API surfaces around image work. The bulk per-image operations endpoint accepts image identifiers, optional transformation settings, and a priority hint. Its operation enum covers background removal, enhancement, and thumbnail creation. A separate status shape checks progress by vehicle identifier and can filter by job identifiers.&lt;/p&gt;

&lt;p&gt;Those schemas are useful. They keep requests sane. They do not decide whether a listing is ready. Readiness lives in the persistent asset graph, because the last endpoint to run is often the wrong place to ask.&lt;/p&gt;

&lt;p&gt;The listing build uses Postgres for that spine. It rejected an older five-queue description because the queues did not exist. The table is &lt;code&gt;processing_jobs&lt;/code&gt;, with six check-constrained stages and states for queued, running, succeeded, failed, cancelled, and declined.&lt;/p&gt;

&lt;p&gt;Workers claim rows with a SQL path that uses FOR UPDATE SKIP LOCKED. Stale work can be reclaimed after fifteen minutes. Attempts stop at three. The service role owns the claim path.&lt;/p&gt;

&lt;p&gt;The drainer keeps the same shape. &lt;code&gt;/api/cron/drain-processing&lt;/code&gt; runs from Vercel cron every five minutes. The default budget is ten, the maximum is fifty, and the route ceiling is one hundred twenty seconds. It is guarded by &lt;code&gt;CRON_SECRET&lt;/code&gt; and PROJECT_PROCESSING_DRAIN_V1. Stage dispatch sits in &lt;code&gt;STAGE_RUNNERS&lt;/code&gt; inside processing-drainer.ts.&lt;/p&gt;

&lt;p&gt;That choice costs latency. A poller feels slower than a push system. I accepted that cost because the expensive part is keeping media, review, and publishability in agreement after a retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Four lanes have to agree
&lt;/h2&gt;

&lt;p&gt;I draw the system as four lanes because each lane can tell a partial truth. Storage can prove bytes exist. The database can prove work was requested. A worker can say it produced something. The product can show a button as ready. The useful rule is the binding between them.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  subgraph storageLane
    sourceBytes[Signed source bytes]
    derivedBytes[Derived media bytes]
  end
  subgraph databaseLane
    securedEvent[capture.media_secured event]
    jobRows[processing_jobs rows]
    reviewDecision[append-only review decision]
  end
  subgraph workerLane
    preflight[matte preflight]
    renderStage[studio render]
    listingStage[listing generation]
    marketingStage[marketing generation]
  end
  subgraph productLane
    ownerReadiness[owner visible readiness]
    reviewQueue[human review queue]
    publicationGate[publication gate]
  end
  sourceBytes --&amp;gt; securedEvent
  securedEvent --&amp;gt; jobRows
  jobRows --&amp;gt; preflight
  preflight --&amp;gt; renderStage
  renderStage --&amp;gt; listingStage
  listingStage --&amp;gt; marketingStage
  marketingStage --&amp;gt; derivedBytes
  derivedBytes --&amp;gt; reviewDecision
  reviewDecision --&amp;gt; reviewQueue
  reviewQueue --&amp;gt; publicationGate
  jobRows --&amp;gt; ownerReadiness```



The storage lane begins with signed phone evidence. In `src/project/services/vehicle-project-orchestrator.ts`, the orchestrator keeps digest verification narrow. Digest verification proves transport: the bytes that left the phone are the bytes in storage. It does not prove the image depicts a car. The field named `contentState` can say `not_evaluated`, pending, verified, or refused. It must never translate a digest match into content approval.

The same orchestrator signs URLs in parallel and returns them beside the asset states. That comment matters. A client cannot render a picture without receiving what the platform thinks of it at the same time. The URL is bounded surface area, never the master record.

The database lane carries job ownership and decisions. In `src/project/domain/states.ts`, every asset carries storage, quality, review, and publication as independent fields, plus `machineProduced`. `publicationBlockers` returns all blockers. `mayPublish()` is true only when that list is empty, so the answer and the explanation cannot drift apart.

The product lane has a different problem. Owners need to see work that is still blocked from publication. `src/quality/contracts/project-read-model.ts` calls this out directly: the newest produced asset can be visible to the owner while the hero claim remains reserved for cleared media. `publication_state: blocked` means unpublished. It has never meant hidden from the owner.

That distinction fixed a real product trap. If the dashboard hides every blocked output, the owner cannot review the work. If it treats every visible output as publishable, the public listing can outrun approval. The read model keeps those two surfaces separate.

## 4. Fanout is part of the product shape

The batch image processing API accepts one to twenty remote image URLs. It can request background removal, an enhancement level, responsive outputs, and watermarking. It can also associate the batch with a parent record identifier.

That upper bound controls more than cost. It keeps the work explainable. When a vehicle has a batch in flight, the status response can speak in terms of that vehicle, those image identifiers, and the job identifiers that matter. The video generation request has the same instinct in a stricter form: supplied image identifiers must be exactly three. Exact length makes downstream assembly simpler, and it rejects bad input before compute starts.

Priority needs restraint. The bulk endpoint accepts a priority hint. Elsewhere in the app, cache strategy uses levels where critical outranks high, then medium, then low. Priority helps when interactive work competes with background work. It should remain scheduling metadata. Once priority becomes hidden ownership, the product view and worker view start telling different stories.

The listing route avoids that split by keeping stage claims database-owned. Consumers claim through the remote procedure call or are invoked by cron. There is no pub/sub layer in that design. The capture flow polls.

Polling is a tradeoff, and I would not hide it. It burns time between readiness and the next drain. It also keeps claims, retries, and stale recovery in the same place as the media records. For this system, that was the cleaner failure mode.

## 5. Machines propose, humans accept

The listing generation stage has narrow authority. It takes the accepted hero set and writes proposed records for review. The design rule is blunt: machines propose, and only an actor shaped as `human:&amp;lt;uuid&amp;gt;` confirms. Condition findings follow the same line. Machines may propose findings. They do not confirm, reject, set severity, or drop them.

This matters because vision outputs look finished. A mask has pixels. A generated backdrop can look better than the source lot. A social frame can be well composed. The product still needs to separate produced from accepted.

The marketing generation stage starts from the accepted hero. It produces per-aspect re-renders for one-to-one, four-to-five, nine-to-sixteen, and sixteen-to-nine placements.

The output is letterboxed on dominant tone, so the model extends the scene while the car stays whole. It also stamps International Press Telecommunications Council (IPTC) DigitalSourceType and verifies the stamp on every produced frame. The implementation closed a measured gap where the core before-and-after publish path did not stamp.

That detail belongs inside generation. If a social frame is only an output URL, stamping becomes optional plumbing. Once publication depends on the asset carrying its receipts, verification has to happen where the frame is made.

The database gate is stricter. The project asset check makes published unrepresentable without digest verification, quality passed, and review approved or marked unnecessary. Takedown is never blocked.

I prefer a locked door to an apology in logs. A warning after an unreviewed image is public helps nobody.

## 6. Refusal has to be named

Vision workers fail in ways that ordinary endpoint errors flatten. They time out. They return partial output. A provider can accept a job and then vanish from the caller view. The operator still has to decide whether to retry, decline, or wait.

The framing drainer has one concrete answer. A slow matte becomes a per-item refusal with the note will retry next tick. It does not take the whole invocation down. That worker uses concurrency three and a per-item timeout of ninety thousand milliseconds. The comment explains the budget math: batches stay inside the route three hundred second ceiling at the maximum budget.

The same comment says the underlying fetch is not aborted. The worker may still finish. The next tick can ride a warm cache or record the result. The tradeoff is explicit: at most one orphan per slow item per tick, rather than eight.

That ugly detail belongs in the media custody model because timeout does not always mean failed image. Sometimes it means the drain cycle refused to spend all of its budget on one item. Sometimes the delayed result should be attached to the same attempt on the next pass.

The deletion worker follows the same rule from another direction. An API error or 404 alone does not prove a graphics processing unit (GPU) job stopped. Cleanup is allowed after a same-ID terminal response, a persisted terminal observation, or the provider maximum time-to-live after the fenced request. No endpoint-wide purge.

External systems can be vague. My database should name the uncertainty.

## 7. Derived media stays with its parent

The most dangerous derived asset is the one that looks better than its parent. A thumbnail can be crisp. A crop can be framed well. A studio render can look ready. None of that proves the source set passed review.

The listing work tightened this rule around parentage. Listing photos inherit their alpha at attempt one. Failed rerenders retry as their own family. A listing photo is never retried on its own. Orbit frames are reviewed as a set. A later change requires exact parent 360 approval before listing creation.

Those are product rules expressed as lineage. A child output does not declare independence because it has pixels. If the parent set lacks approval, children wait. If a rerender fails, it belongs to its attempt family. If a 360 has no ruling, the owner may ask for another render, while the system still refuses to treat absence as approval.

The 360 gate makes that concrete in `supabase/migrations/20261119000000_listing_360_approval_gate.sql`. The source-authority function returns a media delivery shape with schema version, delivery identifier, parent job, owner, actor, project, capture session, source set hash, generation, render attempt, source asset identifiers, listing batch, social selection, derived status, and failure code. The derived object carries enough parent identity to prevent a listing from being built from the wrong ring.

This is where the post differs from a pure provenance piece. Provenance tells you where an artifact came from. Custody tells the product what may happen next, who may decide, and which derived outputs must wait for their parent.

Any system that spends real money changing pixels needs ownership to move with the pixels, or the product will eventually believe the wrong thing.

---

🎧 **Listen to the audiobook** — [Spotify](https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D) · [Google Play](https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en) · [All platforms](https://www.craftedbydaniel.com/audiobook)
🎬 [Watch the visual overviews on YouTube](https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6)
📖 [Read the full 13-part series](https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters)&lt;/code&gt;&lt;/pre&gt;

</description>
      <category>mediapipeline</category>
      <category>computervision</category>
      <category>architecture</category>
      <category>statemachines</category>
    </item>
    <item>
      <title>I Had to Build What the Bumper Could See</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 30 Sep 2026 07:12:07 +0000</pubDate>
      <link>https://dev.to/romiteld/i-had-to-build-what-the-bumper-could-see-fkk</link>
      <guid>https://dev.to/romiteld/i-had-to-build-what-the-bumper-could-see-fkk</guid>
      <description>&lt;p&gt;The carrier’s chrome reflected my light card as a white rectangle. I moved the light to the wet road and rebuilt the shot around what the bumper saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The rectangle in the bumper
&lt;/h2&gt;

&lt;p&gt;With the card gone, I traced the bumper’s reflection back to the apron.&lt;/p&gt;

&lt;p&gt;A polished surface reflects along a direction set by its angle and the camera’s position. Roughness spreads that reflection. It cannot bring a lit patch of road into metal that faces somewhere else. I checked the reflected direction before touching the shader again.&lt;/p&gt;

&lt;p&gt;The teaching plate is flat. The bumper is not. Each patch of chrome looks at a different piece of apron, so one lit mark on the road cannot serve the whole bar. I had to use the shot camera and light the ground that bar was actually returning.&lt;/p&gt;

&lt;p&gt;The scripts below use generic geometry and no production assets. They run with Python 3.10 or newer and need no extra packages. In &lt;code&gt;mirror_ray.py&lt;/code&gt;, a vertical plate reflects a flat floor at teaching coordinates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted educational example; no film assets, scene, or production settings.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A direction must have nonzero length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;reflected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;camera_z=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; ground_hit=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python3 mirror_ray.py&lt;/code&gt;. The tested output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera_z=1.4 -&amp;gt; ground_hit=(0.0, -3.0, 0.0)
camera_z=2.2 -&amp;gt; ground_hit=(0.0, -1.5, 0.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plate stayed still. Raising the camera by 0.8 meters moved the reflected floor point 1.5 meters, from three meters ahead of the plate to 1.5 meters ahead. In the carrier scene, I traced the bumper’s reflected directions to the actual ground and put fill there. In the later frame, the chrome held dark ground structure and the card was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Put the road where the reflection lands
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngjvf2kvj75aq3rbfc5o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngjvf2kvj75aq3rbfc5o.jpg" alt="Snow blowing between parked vehicles toward a lit service bay, with two wet tracks in the foreground." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two wet paths lead toward the service bay. Snow is visible near the camera and around the door.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built the winter lot around the service building. Its lit door gave the camera a destination; parked rows kept the route narrow. The road starts broad in the foreground and runs straight to that door. Near the lens, snow crosses in long streaks. At the far wall it becomes flecks against the light. I judged the snowfall by where I could see it.&lt;/p&gt;

&lt;p&gt;A flat dark stripe gave the lamp little to describe. I cut shallow depressions into the terrain and shaped uneven slush at their edges. Their shoulders caught light. The wet middle sent a different reflection toward the camera. A worn path has a cross-section. Snow can remain beside it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsh2ucdm3dzwbkw1nohlw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsh2ucdm3dzwbkw1nohlw.png" alt="Snow-covered vehicles on the upper deck of red carrier #171 in a winter lot." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover: snow on the upper-deck vehicles, amber markers along the carrier, dark ground in the chrome.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I bought a hauler model, then rebuilt it into Online Auto Connection truck #171 with permission from the owner, Mark Subjeck. The other vehicles also began as licensed models that I modified for the film.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oy8g52ivknernhv937s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oy8g52ivknernhv937s.jpg" alt="Overhead view of a loaded carrier beside rows of parked cars in a snow-covered lot." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The loaded carrier beside the parked rows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I placed the overhead camera to check where the deck sat among those rows. At road level, the rows compress toward the service building. I returned to the low camera to judge the paths and the snow.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Build a smaller scene to test the reflection
&lt;/h2&gt;

&lt;p&gt;Three renders, same plate: road light off, road light on, then a second camera with the light left alone. Look at the floor first. If the bright patch is not the point the plate is reflecting, more power only lights the wrong ground. Then look at the metal.&lt;/p&gt;

&lt;p&gt;A short road, a box on wheels, and a polished front plate are enough. The adapted &lt;code&gt;rut_strip.py&lt;/code&gt; writes a 10 m road strip with two shallow troughs. Run &lt;code&gt;python3 rut_strip.py demo-ruts.obj&lt;/code&gt; with a new output filename. It reports 3,321 vertices and a lowest height of 0.007 m. The full generator is under Scripts.&lt;/p&gt;

&lt;p&gt;Import the OBJ into Blender at one unit per meter and check that its upper face normals point up. The surface starts at 0.025 meters. Each track center drops by up to 0.018 meters. Change the track width, then inspect the shoulder from the camera.&lt;/p&gt;

&lt;p&gt;Use a beveled cube for the body and four cylinders for wheels. Check their contact with the road. Put a thin plate at the front, facing back toward the first camera.&lt;/p&gt;

&lt;p&gt;For the plate’s Principled shader, start with metallic &lt;code&gt;1&lt;/code&gt; and roughness around &lt;code&gt;0.06&lt;/code&gt;. Start the exposed track around &lt;code&gt;0.18&lt;/code&gt; roughness and the pale snow around &lt;code&gt;0.65&lt;/code&gt;, with fine Noise Texture feeding a small Bump node. These are starting values for this test.&lt;/p&gt;

&lt;p&gt;To keep snow beside the tracks, take the X coordinate from Geometry Position through Separate XYZ. Measure the distance to track centers at &lt;code&gt;-0.7&lt;/code&gt; and &lt;code&gt;0.7&lt;/code&gt; meters and keep the smaller distance. That drives a Mix between wet material and snow, with a starting blend from &lt;code&gt;0.14&lt;/code&gt; to &lt;code&gt;0.33&lt;/code&gt; meters from either center. If the tracks read as ink marks, inspect the height profile, transition width, and light direction one at a time.&lt;/p&gt;

&lt;p&gt;Put the first camera low and aim it at the plate. Use a broad area light to illuminate the road ahead of the box. Place the second camera about 0.8 meters higher, still aimed at the plate. Compare the third render with the second before changing roughness.&lt;/p&gt;

&lt;p&gt;For a moving test, key a straight camera push and inspect its first, middle, and last frames. Check wheel contact and whether flakes remain visible near the lens and farther into the scene. Place a recorded mechanical contact at a visible action, then listen on speakers and headphones.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Know which frame you judged
&lt;/h2&gt;

&lt;p&gt;At thumbnail size the truck reads and the chrome does not. At 1280 × 720 I could see dark ground in the bumper, snow on the upper deck, and whether a wheel sat on the lot. That is the file I kept a receipt for.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;python3 frame_receipt.py your-render.png&lt;/code&gt; to print image dimensions, byte count, and SHA-256. For the cover it returned 3,219,108 bytes and a hash matching the handoff receipt. The full script is under Scripts. Its &lt;code&gt;pixel_review&lt;/code&gt; field reads &lt;code&gt;NOT_ESTABLISHED_BY_THIS_SCRIPT&lt;/code&gt;. The hash is not the review.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Four questions from the directors
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://theasc.com/article/terminator-2-he-said-he-039-d-be-back/" rel="noopener noreferrer"&gt;Adam Greenberg described wetting the streets for &lt;em&gt;Terminator 2&lt;/em&gt; to darken gray pavement&lt;/a&gt;, and separately described moving reflections in car windows and hoods. Where did my wet road enter the bumper’s reflection?&lt;/p&gt;

&lt;p&gt;Which visible fixture revealed the snow? &lt;a href="https://theasc.com/article/john-wick-chapter-3-slayin-in-the-rain/" rel="noopener noreferrer"&gt;Dan Laustsen’s backlit rain in &lt;em&gt;John Wick: Chapter 3&lt;/em&gt;&lt;/a&gt; sent me to the foreground, then the service door. The overhead posed a different question after &lt;a href="https://filmmakermagazine.com/101633-the-bike-is-going-to-hit-the-camera-chad-stahelski-on-john-wick-chapter-2-stair-falls-and-other-stunts/" rel="noopener noreferrer"&gt;Chad Stahelski’s discussion of readable wide action&lt;/a&gt;: could I place the carrier among the parked rows before cutting closer?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.festival-cannes.com/en/2018/rendez-vous-with-christopher-nolan/" rel="noopener noreferrer"&gt;Christopher Nolan’s account of building sound effects and music together on &lt;em&gt;Dunkirk&lt;/em&gt;&lt;/a&gt; left one question: which mechanical action deserved the sound cut?&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The cold open
&lt;/h2&gt;

&lt;p&gt;The 12-second cold open below is a 390 × 220 phone-size preview with stereo sound. It starts in darkness, then reveals exhaust, falling snow, a headlamp, and red bodywork. Play it with sound on.&lt;/p&gt;


  
  Your browser does not support embedded video.


&lt;p&gt;&lt;a href="https://vzhrmffwrkgvaziiompu.supabase.co/storage/v1/object/public/blog-covers/one-lap/7a19b0f9-4dd2-4bd3-b200-63e5f2b8e329/cold-open-0068048034f80235c01a.mp4" rel="noopener noreferrer"&gt;Open the 12-second preview with sound.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clip is from &lt;em&gt;One Lap&lt;/em&gt;, my commercial for AutoLensAI.&lt;/p&gt;

&lt;p&gt;Glass, wet asphalt, and chrome show pieces of a set that sit outside the frame.&lt;/p&gt;




&lt;h2 id="scripts"&gt;Scripts&lt;/h2&gt;

&lt;p&gt;rut_strip.py — road mesh generator&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted educational OBJ generator; a new toy surface, not film geometry.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;surface_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.025&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.018&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
    &lt;span class="n"&gt;vertices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="nf"&gt;surface_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# Adapted educational rut strip, units are meters&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3321&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.007&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vertices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimum_z_m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;geometry_checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;write_obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;frame_receipt.py — PNG file receipt&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted read-only PNG receipt; integrity is not visual approval.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect_png&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x89&lt;/span&gt;&lt;span class="s"&gt;PNG&lt;/span&gt;&lt;span class="se"&gt;\r\n\x1a\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IHDR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a PNG with an IHDR header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;gt;II&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pixel_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT_ESTABLISHED_BY_THIS_SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspect_png&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cinematography</category>
      <category>film</category>
      <category>blender</category>
      <category>automotive</category>
    </item>
    <item>
      <title>I Had to Build What the Bumper Could See</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:39:50 +0000</pubDate>
      <link>https://dev.to/romiteld/i-had-to-build-what-the-bumper-could-see-17e2</link>
      <guid>https://dev.to/romiteld/i-had-to-build-what-the-bumper-could-see-17e2</guid>
      <description>&lt;p&gt;After I removed the light card, I checked the bumper from the camera view and worked backward to the part of the apron it reflected. The road had to carry the light now. Its surface and the camera angle had to agree.&lt;/p&gt;

&lt;p&gt;That changed how I worked on the winter lot in &lt;em&gt;One Lap&lt;/em&gt;. The bumper needed ground to reflect. The road needed worn paths with actual shape. Snow had to remain visible between the camera, the parked cars, and the lit service building. Each part affected what the next camera view could show.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The rectangle in the bumper
&lt;/h2&gt;

&lt;p&gt;A polished surface responds to the direction it faces and the position of the camera. Roughness spreads a reflection; it cannot bring an unlit patch of ground into a bumper that is looking somewhere else. I had to inspect the truck from the camera position, then work on the part of the lot that appeared in its metal.&lt;/p&gt;

&lt;p&gt;A small numerical exercise makes the camera change plain. It uses a vertical reflective plate and a flat floor. Raise its camera by 0.8 meters and the floor point appearing in the plate moves 1.5 meters. These are &lt;strong&gt;adapted teaching coordinates&lt;/strong&gt;, separate from my film scene. The scripts and Blender exercise below use generic geometry and no production assets or settings.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;mirror_ray.py&lt;/code&gt;. It runs with Python 3.10 or newer and needs no packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted educational example; no film assets, scene, or production settings.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A direction must have nonzero length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;reflected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reflected&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;camera_z=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; ground_hit=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;ground_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python3 mirror_ray.py&lt;/code&gt;. The tested output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera_z=1.4 -&amp;gt; ground_hit=(0.0, -3.0, 0.0)
camera_z=2.2 -&amp;gt; ground_hit=(0.0, -1.5, 0.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plate did not move. At the lower camera height, it showed ground three meters from the plate. At the higher position, it showed ground only 1.5 meters away. If I light the first patch and then raise the camera, the highlight can vanish from the metal. The script computes an intersection; it does not render or judge an image. I still inspect the actual frame at native size.&lt;/p&gt;

&lt;p&gt;For the carrier section, I checked reflected directions against the actual ground surfaces in the scene and put fill on the ground the camera could see through the bumper. In the later carrier view, the chrome carries a broad silver response with dark ground structure through it. I can no longer pick out the card as a rectangle.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Put the road where the reflection lands
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngjvf2kvj75aq3rbfc5o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngjvf2kvj75aq3rbfc5o.jpg" alt="Snow blowing between rows of parked vehicles toward a lit service bay, with two wet paths in the foreground." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The road-level approach. Two wet paths lead toward the service bay; its lamps catch snow at several distances.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The building gives this frame a destination. Parked vehicles close in from both sides. Snow near the camera crosses in long streaks; smaller flecks remain around the lit door. The exposed road pulls the eye through the middle.&lt;/p&gt;

&lt;p&gt;I judged the snowfall by where it could be seen. The nearest flakes cut across the foreground. Around the building they become small marks against the door and work lights. That distance change matters when the storm and the road have to occupy the same space; an even screen of white would cover the scene without telling me how deep the lot is.&lt;/p&gt;

&lt;p&gt;A dark stripe could have occupied those pixels, but a flat stripe gave the lamp little to describe. I shaped the exposed paths as shallow depressions with uneven slush at their edges. Their shoulders catch light. The wet middle sends a different reflection toward the camera. A worn path has a cross-section. Snow can remain beside it.&lt;/p&gt;

&lt;p&gt;The carrier needed the same weather as the lot around it. The overhead view helped me check the arrangement before returning to the low camera:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oy8g52ivknernhv937s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9oy8g52ivknernhv937s.jpg" alt="An overhead view of a carrier loaded with snow-covered vehicles beside rows of parked cars." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Snow stays on the upper-deck roofs while amber markers trace the carrier’s length.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From above, I could see where the loaded deck sat among the parked rows. At road level, those same rows compress toward the service building. I used the two views for different checks: one for the lot’s geography, one for the path and weather that a viewer would feel while approaching it.&lt;/p&gt;

&lt;p&gt;The selected cover let me inspect a third relationship at native size. Snow covered the hoods, roofs, and windshields of vehicles on the upper deck, while the carrier’s small markers still described its length. The weather was present on the payload as well as in the air and on the road. That gave the truck a place in the winter lot instead of treating it as a clean object dropped in front of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Build a smaller scene to test the reflection
&lt;/h2&gt;

&lt;p&gt;Make three renders: the first camera with the road light off, the same camera with it on, then a second camera with the light unchanged. Keep the polished front plate material fixed. That sequence lets you see whether the road enters the reflection and what the camera changes.&lt;/p&gt;

&lt;p&gt;The film scene used licensed vehicle models. For this exercise, make a short road and an unbranded vehicle from simple shapes. The plate at its front is the surface to watch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape the road
&lt;/h3&gt;

&lt;p&gt;Save this second adapted script as &lt;code&gt;rut_strip.py&lt;/code&gt;. It writes a four-meter-wide, ten-meter-long Wavefront Object (OBJ) mesh with two shallow troughs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted educational OBJ generator; a new toy surface, not film geometry.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;surface_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.025&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.018&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
    &lt;span class="n"&gt;vertices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="nf"&gt;surface_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# Adapted educational rut strip, units are meters&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3321&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.007&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vertices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimum_z_m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;geometry_checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;write_obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with a new filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 rut_strip.py demo-ruts.obj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tested run produced 3,321 vertices and 3,200 faces, with a lowest height of 0.007 meters. The generator refuses to overwrite an existing file. I checked its geometry and counts.&lt;/p&gt;

&lt;p&gt;Import the OBJ into Blender with one unit per meter. Check that the upper face normals point up. The height function starts at 0.025 meters and lowers each track center by up to 0.018 meters. Change the width value, then look from the camera again. You are changing the shoulder that catches light, not merely painting a darker line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give the metal a place to look
&lt;/h3&gt;

&lt;p&gt;A beveled cube can stand in for a vehicle body. Put four cylinder wheels under it and check that they meet the road. Add a thin plate at the front, facing back toward the first camera. No badge, grille, or purchased asset is needed.&lt;/p&gt;

&lt;p&gt;For the material test, use a Principled shader on each surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start the front plate at metallic &lt;code&gt;1&lt;/code&gt; and roughness around &lt;code&gt;0.06&lt;/code&gt;. Its reflection should reveal the set around it.&lt;/li&gt;
&lt;li&gt;Make the exposed track darker and smoother, with roughness around &lt;code&gt;0.18&lt;/code&gt;. Give the snow a pale material at roughly &lt;code&gt;0.65&lt;/code&gt; roughness, with fine Noise Texture feeding a small Bump node. Keep the wet road smoother than the snow.&lt;/li&gt;
&lt;li&gt;To place snow beside the tracks, take the X coordinate from Geometry Position and Separate XYZ. Measure its distance to track centers at &lt;code&gt;-0.7&lt;/code&gt; and &lt;code&gt;0.7&lt;/code&gt; meters, keep the smaller distance, and map that value into a Mix between wet material and snow. Start the blend around &lt;code&gt;0.14&lt;/code&gt; to &lt;code&gt;0.33&lt;/code&gt; meters from a center. The mesh supplies the dip; the shader supplies the surface response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those roughness and blend values are starting controls, not measured settings from &lt;em&gt;One Lap&lt;/em&gt;. If the tracks read as two ink marks, inspect their height profile, transition width, and light direction one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move the camera, keep the light
&lt;/h3&gt;

&lt;p&gt;Put one camera low and aimed at the front plate. Place a broad area light so it illuminates the road ahead of the vehicle, rather than presenting a bright card directly to the plate. Make a second camera about 0.8 meters higher or to the side. Keep both cameras aimed at the plate.&lt;/p&gt;

&lt;p&gt;Compare the third render with the second. Its reflection may change even though the light stayed put. Return to the ray script before adjusting roughness.&lt;/p&gt;

&lt;p&gt;For a short moving study, key a straight camera push that clears the wheels and ground. Inspect its first, middle, and last frames before rendering the full range. A sparse Geometry Nodes field of instanced flakes can test whether snow remains legible near the lens and at the building. Record your own wind and one mechanical contact for sound. Place that contact at a visible action, then listen once on speakers and once on headphones.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Know which frame you judged
&lt;/h2&gt;

&lt;p&gt;I inspected the selected carrier render at its native 1280 × 720 size. That let me see the upper-deck snow, marker lamps, bumper response, and dark parts of the lot. I also needed to know that I was opening the selected file rather than an earlier lighting pass or a smaller preview.&lt;/p&gt;

&lt;p&gt;This third adapted script, &lt;code&gt;frame_receipt.py&lt;/code&gt;, records a Portable Network Graphics (PNG) file’s dimensions, byte count, and SHA-256 digest. It reads the file without changing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapted read-only PNG receipt; integrity is not visual approval.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect_png&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x89&lt;/span&gt;&lt;span class="s"&gt;PNG&lt;/span&gt;&lt;span class="se"&gt;\r\n\x1a\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IHDR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a PNG with an IHDR header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;II&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pixel_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT_ESTABLISHED_BY_THIS_SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspect_png&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python3 frame_receipt.py your-render.png&lt;/code&gt;. Under Python 3.10.12, the script was run against the selected cover and another reviewed image. The cover’s dimensions, byte count, and digest matched its handoff record.&lt;/p&gt;

&lt;p&gt;The last field has a narrow meaning. Matching bytes establish which file I inspected. They say nothing about whether the snow has depth or a wheel touches the ground. Open the frame at its actual size for those decisions. For the exercise, save the three comparison renders with their camera and light choices, then make a receipt for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Four questions I brought back from the directors
&lt;/h2&gt;

&lt;p&gt;I used interviews as checks on my own shots. Each source gave me a question I could ask of the lot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adam Greenberg:&lt;/strong&gt; When he described wetting streets for &lt;em&gt;Terminator 2&lt;/em&gt;, he also discussed moving lights appearing in a car’s windows and hood. Where should the road carry light, and what does the carrier reflect? &lt;a href="https://theasc.com/article/terminator-2-he-said-he-039-d-be-back/" rel="noopener noreferrer"&gt;Greenberg in &lt;em&gt;American Cinematographer&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dan Laustsen:&lt;/strong&gt; His account of backlit rain in &lt;em&gt;John Wick: Chapter 3&lt;/em&gt; pushed me to check snow at more than one distance. Which visible fixture reveals it? &lt;a href="https://theasc.com/article/john-wick-chapter-3-slayin-in-the-rain/" rel="noopener noreferrer"&gt;Laustsen in &lt;em&gt;American Cinematographer&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chad Stahelski:&lt;/strong&gt; His discussion of readable wide action sent me back to the overhead image. Can a viewer place the carrier among the parked rows before the cut moves closer? This is how I used the principle, not a copied &lt;em&gt;John Wick&lt;/em&gt; setup. &lt;a href="https://filmmakermagazine.com/101633-the-bike-is-going-to-hit-the-camera-chad-stahelski-on-john-wick-chapter-2-stair-falls-and-other-stunts/" rel="noopener noreferrer"&gt;Stahelski in &lt;em&gt;Filmmaker&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Christopher Nolan:&lt;/strong&gt; His account of building sound effects and music together during &lt;em&gt;Dunkirk&lt;/em&gt; raised an editing question. Which mechanical action deserves the sound cut? &lt;a href="https://www.festival-cannes.com/en/2018/rendez-vous-with-christopher-nolan/" rel="noopener noreferrer"&gt;Nolan at Cannes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Coming soon: the AutoLensAI commercial
&lt;/h2&gt;

&lt;p&gt;The 12-second clip below is a phone-size preview from &lt;em&gt;One Lap&lt;/em&gt;, the upcoming commercial for my AutoLensAI application. It has a stereo audio track. The picture starts in darkness, then reveals exhaust, falling snow, a headlamp, and red bodywork. Play it with sound on.&lt;/p&gt;


  
  Your browser does not support embedded video.


&lt;p&gt;&lt;a href="https://vzhrmffwrkgvaziiompu.supabase.co/storage/v1/object/public/blog-covers/one-lap/7a19b0f9-4dd2-4bd3-b200-63e5f2b8e329/cold-open-0068048034f80235c01a.mp4" rel="noopener noreferrer"&gt;Watch the 12-second preview of the &lt;em&gt;One Lap&lt;/em&gt; commercial with sound.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AutoLensAI commercial is coming soon.&lt;/strong&gt; Glass, wet asphalt, and chrome can all show pieces of a set that sit outside the frame.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cinematography</category>
      <category>film</category>
      <category>blender</category>
      <category>automotive</category>
    </item>
    <item>
      <title>You Want Engineers Using AI on the Job. Why Turn It Off in the Interview?</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:58:52 +0000</pubDate>
      <link>https://dev.to/romiteld/you-want-engineers-using-ai-on-the-job-why-turn-it-off-in-the-interview-3pin</link>
      <guid>https://dev.to/romiteld/you-want-engineers-using-ai-on-the-job-why-turn-it-off-in-the-interview-3pin</guid>
      <description>&lt;p&gt;Forty-five minutes in, I'd walked through systems I built. The architecture, the integrations, the auth decisions, what broke in production and what I did about it. There are working products online with demos anyone can open. Nobody opened them.&lt;/p&gt;

&lt;p&gt;Then came the coding exercise. Share your screen. Open the editor. Turn off AI, disable autocomplete. Write a program that calls an API and returns a picture of a bird and a fact about it.&lt;/p&gt;

&lt;p&gt;The endpoint was supplied. So were the library, the response shape, and the expected output. The product and architecture decisions were already out of the picture. What remained was implementing a tiny, prescribed task without the tools I'd normally use.&lt;/p&gt;

&lt;p&gt;I've shipped a lot of things. I have never once shipped a bird.&lt;/p&gt;

&lt;p&gt;When I talk to someone who knows a field I know, I can usually hear it inside five minutes. They ask about a condition I haven't mentioned. They tell me why an approach works here and turns into a problem two systems over. Change an assumption on them and they follow it downstream without stopping to reload.&lt;/p&gt;

&lt;p&gt;I've found that in software and I found it in a woodworking shop. People who have actually done the work hand you details you can push on. The part that took three times longer than the estimate. The shortcut they took once and will never take again. You always have somewhere left to go.&lt;/p&gt;

&lt;p&gt;A first impression still needs checking. Keep going with the follow-ups, because somebody can stumble over a sentence and still understand the problem cold, and somebody else can be very smooth and very empty. But by the end of a real technical conversation, an interviewer should be holding evidence.&lt;/p&gt;

&lt;p&gt;So why spend forty-five minutes asking about my experience, decline to look at the results, and then let a small unaided API call cast the deciding vote?&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The woodworking shop
&lt;/h2&gt;

&lt;p&gt;I owned a woodworking shop with expensive equipment in it. A jointer, a planer, a table saw with a fence I could trust. That equipment took rough lumber and turned it into square, four-sided, dimensioned stock faster than I could ever do it by hand. That was the entire point of buying it.&lt;/p&gt;

&lt;p&gt;A stack of perfectly milled boards still wasn't furniture. I had to know what I was building, how the joints would carry load, where the material would move across a winter, and whether the finished piece was any good. If I couldn't put it together, the machinery didn't excuse that. The gap was mine.&lt;/p&gt;

&lt;p&gt;Some excellent woodworkers can walk onto a site with basic tools and build something beautiful. I respect that ability. It doesn't follow that using a planer proves somebody lacks it, and making a person repeat every prep operation by hand will not tell you whether they can deliver the finished piece. It'll tell you they can push a hand plane.&lt;/p&gt;

&lt;p&gt;I bought equipment to take back time that didn't need to be spent, and to get consistency I couldn't hold by hand at volume. What I got in exchange was attention I could spend on the design and on the parts of the job the machines hadn't solved.&lt;/p&gt;

&lt;p&gt;AI isn't a planer. It can hand you something confidently wrong, which makes verification part of the engineering problem instead of a formality. But verification is a thing you can design, automate, and hold to a standard.&lt;/p&gt;

&lt;p&gt;The objective is a well-built result. I don't hand out extra credit for preserving unnecessary labor on the way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Nobody panics about the teleprompter
&lt;/h2&gt;

&lt;p&gt;Here's a tool that's been around &lt;a href="https://www.smithsonianmag.com/history/a-brief-history-of-the-teleprompter-88039053/" rel="noopener noreferrer"&gt;since the 1950s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An executive can deliver a prepared speech from a piece of glass. A news anchor can read a newsroom's script. We understand that the person on camera may be working from words prepared with other people. Imagine interrupting a CEO halfway through a town hall: hang on, kill the prompter, let's find out if he actually believes any of this.&lt;/p&gt;

&lt;p&gt;I'd have questions about what he said. Whether he'd memorized it wouldn't settle them.&lt;/p&gt;

&lt;p&gt;Apply the same test to a quantitative analyst: take away the computer and ask for long division on a legal pad. You could learn something about their arithmetic. You'd still have questions about whether they understand the risk they're being asked to model.&lt;/p&gt;

&lt;p&gt;Spellcheck. Route planning. CAD. Compilers, for that matter. We could spend the afternoon removing assistance and congratulate ourselves on how much harder we'd made the job.&lt;/p&gt;

&lt;p&gt;The honest objection is that these tools aren't equivalent. A teleprompter displays a prepared script; a generative model can supply reasoning the person never had. Fine. That difference is worth examining. Tell me which ability this role requires unaided, why this exercise measures it, and I'll take that seriously.&lt;/p&gt;

&lt;p&gt;What I won't accept is the reflex. Finding a tool in someone's hand is the beginning of a question, not the answer to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Walowitz is mine
&lt;/h2&gt;

&lt;p&gt;I've spent the better part of a decade building my own assistant. Walowitz is part of it, in my editor and across the other places I use it. I haven't released it publicly. I built it for myself.&lt;/p&gt;

&lt;p&gt;Cognitive relay is one reason I built it. I can read a raw log, see the relationship in it fast, and still lose the explanation somewhere between knowing the answer and getting it out of my mouth on demand. Anxiety makes that worse. I have understood a problem and blown the explanation in front of people. It doesn't feel like a communication problem. It feels like being found out.&lt;/p&gt;

&lt;p&gt;For recall, the material I want back is my own writing: notes, comments, ledgers, and decisions I recorded while doing the work. Retrieved passages carry references back to their source. That lets me return to the record rather than pretend a new model-generated explanation is something I wrote years ago.&lt;/p&gt;

&lt;p&gt;Exact identifiers and semantic search do different jobs, so I run both and put exact matches first when they're available. Asking for the function where I handled a specific case isn't the same request as asking for everything related to an idea. The combined results are deduplicated before entering the context.&lt;/p&gt;

&lt;p&gt;Walowitz cancels retrieval when a newer request overtakes it and expires temporary context on purpose. Material from the previous subject can be completely accurate and completely wrong for the conversation happening now. Skip that and you've built a very sophisticated way to remember the wrong thing at exactly the right moment.&lt;/p&gt;

&lt;p&gt;Recall isn't permission to act. As I extend the assistant into screens for my kids, a request to trace the letter B shouldn't inherit authority to touch the router. If I ever confuse those scopes, I have considerably larger problems than a coding interview.&lt;/p&gt;

&lt;p&gt;The practice ledger has separate states for aided, unaided, and unrecorded turns. Zero means nobody measured this. It doesn't mean unaided. Within a tracked turn, an aided label doesn't revert merely because the display later clears. The ledger still has paths that don't record aid. I won't pretend it is finished.&lt;/p&gt;

&lt;p&gt;I chose those states because a ledger based only on the session's opening mode would record a preference and call it behavior. A flattering default becomes indistinguishable from a real measurement once somebody writes it down.&lt;/p&gt;

&lt;p&gt;Pick one of those decisions. Keep asking. Change a condition and see whether I can follow the consequences. We can discuss those choices without distributing private source or a former employer's work.&lt;/p&gt;

&lt;p&gt;But first, apparently, we need to establish whether I can fetch the bird unassisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Knowing what not to build
&lt;/h2&gt;

&lt;p&gt;The first decision on anything is whether it should exist. Who needs it, what are they trying to do, what would make it worth using.&lt;/p&gt;

&lt;p&gt;While one team builds a capability from scratch, somebody else may find a maintained service or an open model that already does it and spend those weeks shipping the part nobody has solved. Checking that is part of the job. License, operating cost, what you'd depend on, what happens when it changes under you.&lt;/p&gt;

&lt;p&gt;There are real reasons to build your own. The available option might require sending customer records somewhere they're not allowed to go, or become unaffordable at the volume you're planning for. I want those limits investigated before anybody commits the team to months of work, not discovered afterward.&lt;/p&gt;

&lt;p&gt;That judgment is invisible in a test where the build-versus-buy decision was made for me before I opened the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Companies already changed the test
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://karat.com/engineering-interview-trends-2026/" rel="noopener noreferrer"&gt;Karat surveyed 400 engineering leaders&lt;/a&gt; across the United States, India and China. Seventy-one percent said AI was making technical skills harder to assess, while 62% of organizations still prohibited it in technical interviews.&lt;/p&gt;

&lt;p&gt;Karat co-founder Mo Bhende &lt;a href="https://insight.ieeeusa.org/articles/three-ways-ai-is-reshaping-traditional-technical-interviews-in-2026/" rel="noopener noreferrer"&gt;put it to IEEE-USA&lt;/a&gt; plainly: "the fundamental job has changed, but technical interviews, as we know them, have not."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.canva.dev/blog/engineering/yes-you-can-use-ai-in-our-interviews/" rel="noopener noreferrer"&gt;Canva announced in June 2025&lt;/a&gt; that backend, frontend and machine-learning candidates were expected to use AI tools. Its pilot found that some candidates who could code struggled anyway, because they couldn't guide the tools or recognize a bad suggestion. Canva kept code fluency and technical depth as requirements. Allowing assistance exposed a weakness the old exercise hadn't been designed to assess.&lt;/p&gt;

&lt;p&gt;Meta's July 2025 internal memo gave two reasons for developing AI-enabled interviews: they better represent the working environment, and they make AI-based cheating less effective. &lt;a href="https://www.404media.co/meta-is-going-to-let-job-candidates-use-ai-during-coding-tests/" rel="noopener noreferrer"&gt;404 Media reported the memo with WIRED&lt;/a&gt;. Read that second one twice. Allowing visible assistance was part of the stated rationale for protecting the assessment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://coderpad.io/survey-reports/coderpad-state-of-tech-hiring-2026/" rel="noopener noreferrer"&gt;CoderPad's 2026 survey&lt;/a&gt; found its hiring respondents split 34% prohibiting, 46% allowing broadly or with constraints, and 20% deciding case by case. Asked what shows skill when AI is allowed, 66% picked catching and fixing its mistakes and 56% picked explaining trade-offs and correctness.&lt;/p&gt;

&lt;p&gt;Excluding AI is a choice. It isn't a settled professional requirement, and it shouldn't be presented to a candidate as one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.anthropic.com/candidate-ai-guidance" rel="noopener noreferrer"&gt;Anthropic's candidate guidance&lt;/a&gt;, updated July 2025, prohibits AI in live interviews and take-homes unless permission is given. The same page describes using Claude to develop questions, draft communications, analyze hiring metrics and source candidates. Claude doesn't make the hiring decisions.&lt;/p&gt;

&lt;p&gt;Preparing a question and answering one are different activities, so different rules on the two sides aren't automatically hypocrisy. But the restriction still owes the applicant an explanation. Which ability does this job require unaided, and why does this exercise deserve the weight you're giving it?&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What about cheating
&lt;/h2&gt;

&lt;p&gt;The concern is legitimate and I'm not going to wave at it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fabrichq.ai/blogs/state-of-ai-interview-cheating-in-2026-insights-from-19-368-interviews" rel="noopener noreferrer"&gt;Fabric's January 2026 report&lt;/a&gt; analyzed 19,368 interviews on its own AI interview platform. It flagged 38.5% of candidates for cheating behavior, and roughly 61% of those flagged scored above its passing threshold. That's one vendor's detection and scoring, not an independently established industry rate, and it doesn't establish that every flagged person cheated.&lt;/p&gt;

&lt;p&gt;If the people you believe haven't demonstrated competence are clearing your threshold at that rate, the scoring deserves investigation alongside their behavior. Adding a ban doesn't establish that the score measures the ability you're hiring for.&lt;/p&gt;

&lt;p&gt;There's risk pointed the other way too. A &lt;a href="https://engr.ncsu.edu/news/2020/11/11/tech-sector-job-interviews-assess-anxiety-not-software-skills-2/" rel="noopener noreferrer"&gt;2020 NC State and Microsoft study&lt;/a&gt; put 48 computer science students through a whiteboard problem. The ones solving it while watched and narrating performed about half as well as the ones working privately. Observation and narration both changed at once, and they were students, not principal engineers. The size of that gap is still worth noticing before anyone insists the stressful version is the honest one.&lt;/p&gt;

&lt;p&gt;AI can also undermine learning, and I'm not leaving that out because it complicates my case. In &lt;a href="https://www.anthropic.com/research/AI-assistance-coding-skills" rel="noopener noreferrer"&gt;Anthropic's January 2026 randomized study&lt;/a&gt;, 52 mostly junior engineers learned an unfamiliar Python library. The AI group averaged 50% on a quiz covering concepts they'd used minutes earlier, against 67% unaided. The speed gain wasn't statistically significant.&lt;/p&gt;

&lt;p&gt;Inside the AI group, people who asked conceptual questions or followed generated code with questions to understand it scored better than people who mostly delegated. That comparison was exploratory and can't establish cause. It still gives an interviewer a behavior worth investigating rather than treating every use of assistance as equivalent.&lt;/p&gt;

&lt;p&gt;I'm not claiming AI always makes experienced engineers faster, either. &lt;a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="noopener noreferrer"&gt;METR's February 2026 update&lt;/a&gt; revisited its earlier 19% slowdown finding and reported that the follow-up couldn't reliably estimate the current effect, largely because developers declined to participate or withheld tasks they didn't want to attempt without AI. The experiment was missing people and work that could have changed the answer.&lt;/p&gt;

&lt;p&gt;A fast, plausible mistake is still a mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Keep the birds, then change the response
&lt;/h2&gt;

&lt;p&gt;I'm not asking anybody to stop testing me. I spent part of my twenties in military aviation, where competence was checked by people qualified to check it. I have no objection to proving I can do the work.&lt;/p&gt;

&lt;p&gt;A failure scenario can justify taking a capability away. Then the question is how the person handles that failure in the job they're trained to do. That's an explanation I can understand. "We always turn the tools off" isn't one.&lt;/p&gt;

&lt;p&gt;So keep the bird exercise. Then make it worth the hour.&lt;/p&gt;

&lt;p&gt;Change the response halfway through. Introduce a timeout. Hand me an implementation that looks reasonable and has a defect sitting in it, and ask what needs testing before anyone should trust it. Let me use my tools, including automated review, and watch whether my checks find the problem and what I do once they have. Make me explain why the evidence I produced addresses the changed requirement.&lt;/p&gt;

&lt;p&gt;Then take a problem with real decisions in it. Talk about what the user needs and let me investigate the options. Ask why I'd use an existing service instead of building one. Change a constraint and follow it with me. I can use research or a model to explore it, and you can ask what made me accept one approach and abandon another.&lt;/p&gt;

&lt;p&gt;Work I can legitimately share gives you another place to start. A demo leads into the decisions behind it without exposing a former employer's code.&lt;/p&gt;

&lt;p&gt;Nobody needs to complete an unpaid production project for this. A bounded exercise plus a serious technical conversation gives you several kinds of evidence instead of one convenient proxy. Give candidates comparable access and consistent criteria. That addresses an important part of fairness without pretending the working environment doesn't exist.&lt;/p&gt;

&lt;p&gt;If your process can make time to watch my screen share but not to open what I built, the priorities are backward.&lt;/p&gt;

&lt;p&gt;And if you're using a tool to assess me, own how you interpret its output. That standard runs both directions or it isn't a standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What I'd want to know before I said yes
&lt;/h2&gt;

&lt;p&gt;I wouldn't want to join a company that expects end-to-end AI engineering and treats an unaided API call as the deciding test, particularly if the people running it can't connect it to the role. I'd question whether their understanding of the job had caught up to what they're asking for. I don't need to prove their whole stack is obsolete before deciding to pass.&lt;/p&gt;

&lt;p&gt;There's a question worth asking out loud in either direction. Am I being hired to work inside an established engineering practice, or to modernize the software and change how it gets built? The second one comes with budget decisions, approval processes, and people outside the development team who have to agree. That's a different job, and whoever takes it needs the authority and the resources to do it.&lt;/p&gt;

&lt;p&gt;A paycheck doesn't resolve a mandate nobody defined. I'm not interested in spending my first six months arguing for the methods I was supposedly hired to bring.&lt;/p&gt;

&lt;p&gt;I'm asking you to put the bar where the work is.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>technicalinterviews</category>
      <category>softwareengineering</category>
      <category>engineeringleadership</category>
    </item>
    <item>
      <title>CommandCanvas: Building Around Shared Objects</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Sat, 05 Sep 2026 05:27:41 +0000</pubDate>
      <link>https://dev.to/romiteld/commandcanvas-building-around-shared-objects-45cl</link>
      <guid>https://dev.to/romiteld/commandcanvas-building-around-shared-objects-45cl</guid>
      <description>&lt;p&gt;In CommandCanvas, a sketch can become a structured diagram beside its source. A person can rearrange the result, a collaborator can edit it, and a supported agent host can address it through WebMCP. Each operation refers to objects in the same room, with identities and versions that survive changes in how people interact with them.&lt;/p&gt;

&lt;p&gt;I built the shared canvas mutations around one commit path. Pointer input, voice, and tools can propose changes, but the server must establish which member authorized a change and whether it still applies to the current room state. Adding another input should not introduce another definition of what counts as saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objects that people and tools can both address
&lt;/h2&gt;

&lt;p&gt;A note in CommandCanvas has an identity, a title, a position, dimensions, and a version. Its content follows a note schema. A task board has columns and tasks. A sketch has strokes and points. A structured diagram has its own payload and can retain a reference to the sketch it came from.&lt;/p&gt;

&lt;p&gt;These objects share spatial behavior while keeping their different kinds of content. Moving a board should preserve its tasks. Resizing a sketch should preserve the drawing. An operation can address a particular object and check its version before changing it. The &lt;a href="https://github.com/romiteld/commandcanvas/blob/3cf92d0f919708e738ad213027edf18047b2249d/lib/canvas/object-model.ts" rel="noopener noreferrer"&gt;object model&lt;/a&gt; makes those distinctions explicit.&lt;/p&gt;

&lt;p&gt;That gives a tool request something concrete to address: the selected sketch, a populated board, or an existing object to transform. It also constrains the result. A model response has to fit a supported object schema before the application can submit it as a canvas change.&lt;/p&gt;

&lt;p&gt;The distinction is useful when people and an agent take turns. They can refer to the same object without reconstructing its content from a screenshot or carrying a separate copy into another conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a canvas change commits
&lt;/h2&gt;

&lt;p&gt;The browser holds the visible objects, current selection, and room state. Direct input produces canvas commands. WebMCP and optional embedded voice pass through a capability runtime that validates their arguments and current context before invoking the command adapters.&lt;/p&gt;

&lt;p&gt;WebMCP exposes capabilities to a supported agent host. Embedded voice uses a separate OpenAI Realtime connection and a narrower set of capabilities. Both reach the application's command adapters; neither gets its own route around room membership or revision checks.&lt;/p&gt;

&lt;p&gt;The durable room path looks like this:&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  subgraph browser[Browser workspace]
    direct[Pointer, touch and hand intents] --&amp;amp;gt; command[Canonical canvas command]
    tools[WebMCP and optional voice] --&amp;amp;gt; capability[Capability validation]
    capability --&amp;amp;gt; command
  end
  subgraph server[Server and database]
    api[Authenticated room API] --&amp;amp;gt; guards[Membership and revision checks]
    guards --&amp;amp;gt; commit[Postgres mutation transaction]
    commit --&amp;amp;gt; records[Objects, room revision and receipt]
  end
  command --&amp;amp;gt;|Command, source and base revision| api
  records --&amp;amp;gt;|Authoritative readback| origin[Originating canvas]
  records --&amp;amp;gt;|Revision notification| reload[Other clients reload room state]
  reload --&amp;amp;gt; peers[Collaborator canvases]
  presence[Presence and cursor Broadcast] -.-&amp;amp;gt; origin
  presence -.-&amp;amp;gt; peers&lt;/code&gt;&lt;/pre&gt;




&lt;p&gt;&lt;em&gt;The browser proposes a command; the server establishes who may commit it. Solid arrows trace the durable command and synchronization path. Dotted arrows show participant presence and cursor traffic, which do not create mutation receipts. On narrow screens, scroll the diagram horizontally.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The request carries a command ID, its reported input source, and the room revision the client worked against. The HTTP route verifies the bearer token. The room service loads membership, derives the actor, reads the current canvas, and builds a mutation plan. The database operation checks the expected revision again when committing the change.&lt;/p&gt;

&lt;p&gt;That second check matters because the room can change between the server reading it and attempting the write. A command prepared against revision 12 cannot commit against revision 13 by assuming its original view still applies. The stale request is rejected.&lt;/p&gt;

&lt;p&gt;The shared revision makes the order of committed changes explicit. It also makes conflicts coarser: two people working on different objects can still contend for the same room revision. The application does not automatically merge those edits. That is the cost of using a single room revision as a commit precondition.&lt;/p&gt;

&lt;p&gt;The transaction persists the object changes, advances the room revision, and writes the receipt. The server reloads the authoritative state and checks for the expected receipt before returning success. Other clients receive a compact revision notification and reload the room. The &lt;a href="https://github.com/romiteld/commandcanvas/blob/3cf92d0f919708e738ad213027edf18047b2249d/lib/supabase/room-service.ts" rel="noopener noreferrer"&gt;room service&lt;/a&gt; contains that commit and readback path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why drag previews stay out of history
&lt;/h2&gt;

&lt;p&gt;Applying that commit path to every cursor sample would make ordinary movement compete with object edits for room revisions. It would also fill history with intermediate positions that are of little use when someone wants to undo a completed action.&lt;/p&gt;

&lt;p&gt;Cursors and movement previews therefore remain ephemeral. Supabase Presence describes connected participants, and Broadcast carries the frequent updates. Stable object changes go through the mutation path and produce the receipt that other clients can verify.&lt;/p&gt;

&lt;p&gt;The room can show motion before it has a new committed state. That distinction is necessary for responsive interaction, and it has to survive into the UI: seeing another person's cursor move is not evidence that their edit was saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a sketch into another object
&lt;/h2&gt;

&lt;p&gt;Sketch interpretation introduces a longer gap between preparing a change and trying to commit it. A provider can finish its work after the drawing it received has changed.&lt;/p&gt;

&lt;p&gt;The transformation captures the selected sketch's ID and version, then rasterizes it to a PNG in the browser. A provider interprets that image and returns structured output. The application validates the payload, checks the source reference, and checks that the requested output kind agrees with the result.&lt;/p&gt;

&lt;p&gt;The person can keep drawing while the provider works. Before submitting the new object, the transformation checks the source version again. If the sketch changed, interpretation may have succeeded, but the application refuses to create the diagram from that result. The provider time has already been spent; accepting its answer anyway would attach an outdated interpretation to the current work.&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant C as Canvas
  participant P as Interpretation service
  participant R as Room API
  C-&amp;amp;gt;&amp;amp;gt;C: Capture sketch ID and version
  C-&amp;amp;gt;&amp;amp;gt;P: Rasterized sketch and instructions
  P--&amp;amp;gt;&amp;amp;gt;C: Structured result
  C-&amp;amp;gt;&amp;amp;gt;C: Validate payload and recheck source
  alt Source unchanged
    C-&amp;amp;gt;&amp;amp;gt;R: Create linked diagram at current room revision
    R--&amp;amp;gt;&amp;amp;gt;C: Committed object and receipt
  else Source changed
    C-&amp;amp;gt;&amp;amp;gt;C: Refuse the stale transformation
  end&lt;/code&gt;&lt;/pre&gt;




&lt;p&gt;&lt;em&gt;The generated diagram becomes a separate canvas object. The original sketch remains available, including when interpretation fails or its source has changed. On narrow screens, scroll the diagram horizontally.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Creating the result still uses the ordinary command path. After the save, the transformation looks for the new diagram and its receipt in the authoritative returned state. A provider response alone is not enough to report that the room contains the diagram.&lt;/p&gt;

&lt;p&gt;The source-version check and the room-revision check address different changes. One checks the drawing used for interpretation; the other protects the shared commit. Neither establishes that the model understood the drawing correctly.&lt;/p&gt;

&lt;p&gt;That is why the result appears beside the preserved source. Someone can compare them, point out a missing relationship, or try another interpretation without recovering an overwritten sketch. The output remains a structured object that later commands can address. These checks and the separate-object creation are implemented in the &lt;a href="https://github.com/romiteld/commandcanvas/blob/3cf92d0f919708e738ad213027edf18047b2249d/lib/vision/canvas-transform.ts" rel="noopener noreferrer"&gt;sketch transformation orchestrator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding when a hand is drawing
&lt;/h2&gt;

&lt;p&gt;The commit checks only help after an input has produced the right command. Hand tracking has an earlier decision to make: whether a moving fingertip should create ink at all. A valid room membership and a current revision cannot answer that.&lt;/p&gt;

&lt;p&gt;On the default local path, an opt-in camera feeds MediaPipe Hand Landmarker in a browser worker. The interaction layer receives landmarks and decides what action they may produce. The index fingertip supplies the pen position. A separate thumb-to-middle-finger clutch supplies pen-down intent.&lt;/p&gt;

&lt;p&gt;That separation lets someone move their hand to the start of another stroke without drawing a line across the intervening space. Closing the clutch engages the pen; opening it lifts the pen. A later engagement begins another stroke with its own identity.&lt;/p&gt;

&lt;p&gt;The clutch uses different engagement and release thresholds, along with temporal confirmation. This hysteresis prevents a small amount of motion near one threshold from repeatedly switching the pen on and off. The &lt;a href="https://github.com/romiteld/commandcanvas/blob/3cf92d0f919708e738ad213027edf18047b2249d/lib/gesture/drawing-clutch.ts" rel="noopener noreferrer"&gt;drawing policy&lt;/a&gt; also distinguishes provisional thresholds from calibrated ones.&lt;/p&gt;

&lt;p&gt;Those rules are testable, but a state-machine test cannot tell me whether the interaction feels comfortable after ten minutes, or whether it holds up under poor lighting and partial occlusion. Physical-hand usability remains experimental. Pointer, touch, and typed controls keep the workspace usable while that work continues.&lt;/p&gt;

&lt;p&gt;The default hand path processes camera frames locally. Asking for sketch interpretation is a separate operation: it sends an image of the selected drawing for interpretation. The distinction matters when explaining what leaves the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent is not the actor
&lt;/h2&gt;

&lt;p&gt;Once several inputs could change the same room, the receipt needed to describe both the member responsible for a committed change and the path that requested it.&lt;/p&gt;

&lt;p&gt;The earlier schema already had a separate source column. Its rule was too restrictive: a WebMCP source could only accompany an agent actor. The receipt still retained the authorizing user's ID and required room membership, but its actor classification and display name presented the action as belonging to a generic agent.&lt;/p&gt;

&lt;p&gt;The correction binds durable WebMCP canvas mutations to the authenticated member. A host is classified as human; another room member is classified as participant. The source remains webmcp. The SQL wrapper derives the classification from stored membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;v_effective_actor_type&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;v_member_role&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'human'&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'participant'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'participant'&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This aligns the receipt's visible attribution with the identity that authorized the request. It also allows the reader to distinguish a tool-originated change from direct interaction.&lt;/p&gt;

&lt;p&gt;The compatibility work was less visible. The wrapper accepts request shapes from both releases and canonicalizes them before persistence. The table constraint continues to permit the historical agent-plus-WebMCP shape so older rows remain valid; a stricter validator in the write path enforces the new behavior. The migration adds the broader constraint without scanning historical rows immediately, and a separate migration performs validation. The &lt;a href="https://github.com/romiteld/commandcanvas/blob/3cf92d0f919708e738ad213027edf18047b2249d/supabase/migrations/20260901123500_bind_webmcp_receipts_to_room_members.sql" rel="noopener noreferrer"&gt;attribution migration&lt;/a&gt; shows both responsibilities.&lt;/p&gt;

&lt;p&gt;There are limits to what this receipt says. The source field records an application input path; it does not authenticate a particular external agent host. The current service also normalizes most participant inputs to collaborator, preserving webmcp and system separately. It therefore cannot answer every question about whether a remote participant used touch, voice, or a pointer.&lt;/p&gt;

&lt;p&gt;The shared write path gives those receipts a consistent meaning across supported inputs. Each refers to committed work, identifies the affected objects, and can support guarded undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing the work before it leaves the room
&lt;/h2&gt;

&lt;p&gt;The optional participant filmstrip keeps a conversation beside the canvas. Its small peer-to-peer WebRTC connection uses Supabase signaling and remains separate from canvas persistence and embedded voice.&lt;/p&gt;

&lt;p&gt;Sending a meeting packet introduces another boundary. A saved canvas object does not authorize an email. The packet has its own workflow: preparation creates a snapshot, and approval binds the content and recipients being reviewed. A tool can stage a send request, but the host must perform the final Send action before the configured server transport may submit it.&lt;/p&gt;

&lt;p&gt;That requires an explicit review step after the room has produced useful work. The host approves the packet that will leave the room, with its intended recipients. Actual email delivery remains a separate result to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the recorded checks establish
&lt;/h2&gt;

&lt;p&gt;I built CommandCanvas intending to submit it to the WebMCP Challenge, but didn't submit it.&lt;/p&gt;

&lt;p&gt;The verification ledger records native Chrome WebMCP execution, a real voice-provider call that created a board, a real sketch-interpretation request that created structured output beside its source, and two browser clients converging on a durable mutation and receipt. Those are individual recorded checks, rather than proof that the whole intended experience has passed on physical devices.&lt;/p&gt;

&lt;p&gt;The documented gaps include a ChatGPT built-in-browser Site Tools invocation, final physical-hand and microphone ergonomics, real email delivery, and cross-network TURN traversal. WebMCP's shared-page model is described in the &lt;a href="https://learn.chatgpt.com/docs/webmcp" rel="noopener noreferrer"&gt;official Site Tools documentation&lt;/a&gt;; host support and an observed invocation still have to be established separately for CommandCanvas.&lt;/p&gt;

&lt;p&gt;The next useful rehearsal is a complete session with two people: draw something, explain it, create structured work from it, let the other person change it, and review the outcome together. I want to see whether revision conflicts interrupt that sequence, where selection becomes unclear, and whether the hand controls earn their place beside the pointer. Those are questions about working in the room that individual commit checks cannot settle.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>agents</category>
      <category>collaboration</category>
      <category>interactiondesign</category>
    </item>
    <item>
      <title>The Approval Machine That Refuses Before It Drafts</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 26 Aug 2026 12:10:28 +0000</pubDate>
      <link>https://dev.to/romiteld/the-approval-machine-that-refuses-before-it-drafts-3jkm</link>
      <guid>https://dev.to/romiteld/the-approval-machine-that-refuses-before-it-drafts-3jkm</guid>
      <description>&lt;p&gt;A message can be wrong before anyone writes it.&lt;/p&gt;

&lt;p&gt;That sounds strange until the queue asks for a reply the system has no business drafting. If the member is under a policy flag that disables that intent, the safe answer is not a cautious paragraph. The safe answer is no draft. No retry buffer. No trace payload with forbidden text inside it. Nothing to accidentally send later.&lt;/p&gt;

&lt;p&gt;I built an AI message review console around that shape. A card enters the day’s queue and leaves through exactly one exit: blocked before generation, held by a hard rule, or eligible for score-based release. The large language model can draft, classify, and explain. It does not get the final word.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The spine is the product
&lt;/h2&gt;

&lt;p&gt;The whole system is readable from &lt;code&gt;pipeline/run.py&lt;/code&gt;. That file calls five stages in order: policy, generation, hard rules, scoring, then decision. The ordering matters more than the individual model prompts.&lt;/p&gt;

&lt;p&gt;The first gate runs before any model call. The third gate runs after generation and before scoring. The scoring stage is late on purpose, because a score is a judgement about quality. It is not permission to ignore an eligibility flag, a contraindication, missing equipment, scope drift, a stale thread state, or tone that misses a recent life event.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  queue[Draft request enters queue] --&amp;gt; policy[Pre generation policy]
  policy --&amp;gt;|intent disabled| blocked[Blocked before generation]
  policy --&amp;gt;|allowed| generation[Generate grounded draft]
  generation --&amp;gt; hardRules[Deterministic hard rules]
  hardRules --&amp;gt;|rule fails| held[Held by hard rule]
  hardRules --&amp;gt;|rules pass| scoring[Probabilistic scoring]
  scoring --&amp;gt; decision[Eligible for score based release]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;That diagram is the contract I wanted the code to enforce. The interesting part is the missing arrow: there is no route from scoring back into policy or hard rules. A high score cannot wash out a refusal, which is the same fail-closed shape Abhijat Chaturvedi argues for in &lt;a href="https://dev.to/abhijat_chaturvedi/fail-closed-not-open-designing-an-ai-gateway-for-regulated-enterprises-3ife"&gt;Fail Closed, Not Open: Designing an AI Gateway for Regulated Enterprises&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pre-generation refusal is a privacy boundary
&lt;/h2&gt;

&lt;p&gt;The naive version of this system is easy to build. Generate the message, run checks, discard the message if the checks fail. It feels safe because the user never sees the rejected text.&lt;/p&gt;

&lt;p&gt;It is not, because discarding is not the same as never creating. Once generated, text exists in process memory, logs, traces, retries, recordings, and whatever future evaluation set someone builds from today’s artifacts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pipeline/stage1_policy.py&lt;/code&gt; makes that ordering explicit. The file’s own comment is blunt: if a member has an active policy flag that disables the intent the day’s queue asked for, the stage returns blocked and nothing is generated. The table has two rows, and only one suppresses. A flag can raise scrutiny without disabling generation, which keeps “this topic needs checking” separate from “this topic is not ours.”&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;core/models.py&lt;/code&gt;, the policy row is plain data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PolicyRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One row of the pre-generation policy table.

    disables_intents is the whole mechanism. A flag that disables nothing is
    still recorded in the trace, because the difference between a flag that
    suppresses and one that merely raises scrutiny is exactly what stage 1 is
    demonstrating.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PolicyFlag&lt;/span&gt;
    &lt;span class="n"&gt;disables_intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DraftIntent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;route_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CareRole&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;coach_explanation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I like about this shape is that the refusal is auditable without being clever. &lt;code&gt;disables_intents&lt;/code&gt; is the mechanism. A route and explanation travel with it, so the operator sees why the machine refused instead of getting a silent missing card.&lt;/p&gt;

&lt;p&gt;The cost is that policy has to be modeled before generation. You cannot hide vague eligibility rules in a prompt and hope the model declines. If the rule is meant to prevent text from existing, it belongs before the generator. Ahmed P makes the case in &lt;a href="https://www.assumed-breach.com/blog/llm-moderation-bypass" rel="noopener noreferrer"&gt;An LLM is not a security boundary&lt;/a&gt;: he builds a moderation layer, then walks through it, because a model reads instructions and data from the same token stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generation is allowed only after policy clears it
&lt;/h2&gt;

&lt;p&gt;Stage 2 is the point where the model can do useful work. In this console, generation runs a real tool loop. The generation mode is recorded in the trace and response, because showing generated text while implying it was queued text would overstate what happened.&lt;/p&gt;

&lt;p&gt;That detail matters because the console is reviewing AI-drafted messages, not pretending every draft has the same origin. The downstream gates do not change based on the mode. Policy has already cleared the request. Hard rules still run. Scoring still waits.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pipeline/run.py&lt;/code&gt; records the generation stage with the provider, model, mode, turn count, and tool call count. If the generator proposes different wording while seeded mode is in use, the trace says the queued text was kept. The review layer should not blur authorship just because both paths pass through the same adapter boundary.&lt;/p&gt;

&lt;p&gt;The tradeoff is ceremony. A simple demo could have skipped modes, traces, and tool counts. I kept them because the system is making approval decisions, and approval decisions need receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hard rules are post-generation, deterministic, and score-proof
&lt;/h2&gt;

&lt;p&gt;Some checks need the text. Equipment is a good example: the system cannot know whether a message asks for something impossible until there is a message to inspect. Thread state and tone also depend on the relationship between the draft and recent messages.&lt;/p&gt;

&lt;p&gt;So hard rules live after generation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pipeline/stage3_hard_rules.py&lt;/code&gt; says the rule set ignores every score and fails closed. Five rules run there. Three are pure code. Two use a model to classify something, then keep the decision in Python by combining strict enum values with an &lt;code&gt;and&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The thread-state rule in &lt;code&gt;pipeline/rules/thread_state.py&lt;/code&gt; is deterministic and carries its limit in the file comment. It reads who spoke last, not whether the last reply and the draft are about the same thing. The window is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ANSWERING_INTENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DraftIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reschedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DraftIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_in&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer that kind of visible weakness to a hidden prompt instruction. At seventeen cases the distinction between “coach spoke last” and “coach answered this exact topic” does not bite. At scale, the fix would be topic matching, not stretching the window and pretending the predicate became smarter.&lt;/p&gt;

&lt;p&gt;The hard-rule result model also keeps the decision state concrete. From &lt;code&gt;core/models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What a failed rule costs.

    block: the message must not be sent as written.
    hold: a coach has to look at it. Cheaper to be wrong about.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;hold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;  &lt;span class="c1"&gt;# located by exact substring match; never fabricated
&lt;/span&gt;    &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;movement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;movement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HardRuleResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HardRuleName&lt;/span&gt;
    &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# set only when passed is False
&lt;/span&gt;    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;triggering_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;  &lt;span class="c1"&gt;# engineering detail; stays out of the coach copy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment on &lt;code&gt;start&lt;/code&gt; is a rule about evidence. Where a rule has a span to point at, the offsets are found by exact substring match rather than generated, so the console cannot underline text it invented after the fact.&lt;/p&gt;

&lt;p&gt;The thread-state rule is the exception. Its finding is about who spoke last, not about any phrase, so it has no span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already answered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kind="message"&lt;/code&gt; is the field that matters, and the reference is &lt;code&gt;triggering_message_id&lt;/code&gt;, set beside it to the id of the message that triggered the hold. The zeros mean no span. This rule points at a whole message, which is the right granularity for it and the wrong thing to call an exact offset.&lt;/p&gt;

&lt;p&gt;The cost is that hard rules need measurements. &lt;code&gt;pipeline/stage3_hard_rules.py&lt;/code&gt; does that measuring: it scans the draft for movements and makes the classifier calls. The rule modules receive measurements and return blockers. That shape is why the rules can be tested without a network.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A model can classify without becoming the judge
&lt;/h2&gt;

&lt;p&gt;Two hard rules use a model because pattern matching is the wrong tool for the job. The scope rule in &lt;code&gt;pipeline/rules/scope.py&lt;/code&gt; is about whether a draft interprets a clinical value instead of pointing at a clinician. The failure is semantic: two messages can share most words while one crosses scope and the other routes correctly.&lt;/p&gt;

&lt;p&gt;The tone rule has the same shape. A high-energy push can be fine on most days and wrong when a recent thread contains a hardship. The problem may not be inside the draft alone. It can be in the relationship between the draft and a message from a few days earlier.&lt;/p&gt;

&lt;p&gt;The model returns strict enums. Python decides.&lt;/p&gt;

&lt;p&gt;That choice costs some flexibility. If the enum set is too small, the classifier has to force an edge case into a label that does not fit. But I would rather expand a typed vocabulary than let a free-form model answer become an approval predicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Typed failures are decisions, not missing values
&lt;/h2&gt;

&lt;p&gt;The approval machine also has to handle broken model calls. A timeout, malformed structured output, and an unusable rubric are different failures. Treating all of them as zero would make the system look more numeric while hiding the reason it refused.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;core/errors.py&lt;/code&gt; names the failures below the adapter boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProviderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Anything that went wrong below the adapter boundary.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProviderUnavailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProviderError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Timeout, connection failure, 5xx, or rate limit. Triggers failover.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StructuredOutputError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProviderError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The call returned, but not something that validates.

    raw_excerpt is the first 400 characters of what actually came back. It goes
    in the trace so a reader can see the malformation rather than trust the
    label on it.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_excerpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_excerpt&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TruncatedJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StructuredOutputError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Output stopped mid-token. Unparseable.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IncompleteObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StructuredOutputError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parsed as JSON, but a required field is missing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NullResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StructuredOutputError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The provider returned no content at all, or an explicit null.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutOfRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StructuredOutputError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A value parsed and is the right type but violates its bound.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A provider timeout, output that stopped mid-token, an object missing a required field, and a score of 1.4 on a zero-to-one dimension are four different failures, and each gets a name instead of a fallback value. &lt;code&gt;OutOfRange&lt;/code&gt; is not clamped: rounding that 1.4 down to 1.0 would turn a broken judge into a passing grade.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;core/models.py&lt;/code&gt; carries the same philosophy. The file comment says absence refuses, it never defaults. A missing measurement is a blocker, not a zero. That is why rubric scores are optional on the gate decision and why judge failure has its own type instead of a fallback value.&lt;/p&gt;

&lt;p&gt;This is less convenient than filling blank fields with zeros. It also stops a dashboard from mixing “bad answer” with “no answer.” Those are different operational problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Scoring is late because it is allowed to be uncertain
&lt;/h2&gt;

&lt;p&gt;Only after policy and hard rules pass does the probabilistic part get a vote. The threshold stage clears a card only if the samples agree that it clears, using the minimum of three dimensions rather than an average.&lt;/p&gt;

&lt;p&gt;The judge is not asked once. &lt;code&gt;core/settings.py&lt;/code&gt; sets &lt;code&gt;ensemble_judge: bool = True&lt;/code&gt; and &lt;code&gt;judge_samples: int = 5&lt;/code&gt;. The stage header gives the reason: on this fixture set, five identical calls move the weakest score by up to 0.28, wider than the 0.70-to-0.80 range the threshold slider was built around.&lt;/p&gt;

&lt;p&gt;So the decision comes from how often the samples agree, not from where one of them landed. Three or four usable samples out of five still support an agreement rate, and the failures are recorded. Below three, the stage blocks.&lt;/p&gt;

&lt;p&gt;The numbers below come from one run. &lt;code&gt;make eval&lt;/code&gt; sweeps the threshold across the seventeen-case fixture set in &lt;code&gt;fixtures/cases.yaml&lt;/code&gt; and writes &lt;code&gt;eval/results.json&lt;/code&gt;. It uses recorded provider responses rather than live calls, which is what &lt;code&gt;provider: "replay"&lt;/code&gt; in that file means and what makes the sweep reproducible offline. Everything here is that run at its default threshold of 0.70.&lt;/p&gt;

&lt;p&gt;Of seventeen cards, three clear automatically, nine go to the coach, and five are blocked. Nothing labelled as needing a human was auto-cleared.&lt;/p&gt;

&lt;p&gt;Four of the fourteen that did not clear are over-holds: cards labelled as not needing a human that the gate declined to release. One card is undecidable, where the judge could not resolve it and a coach looks at it instead. &lt;code&gt;eval/metrics.py&lt;/code&gt; excludes those from the over-hold count, because a judge that could not answer and a gate that held a card it should have released are different failures. Mean spread across the samples is 0.148.&lt;/p&gt;

&lt;p&gt;I kept the over-hold cost in the system view and the decision notes instead of tuning it away. A fail-closed system can still be lazy if it hides that cost, because holding too much burns human attention. The point is not to make refusal free. The point is to make the refusal cost visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What the design costs
&lt;/h2&gt;

&lt;p&gt;The cost lands in maintenance, not at runtime.&lt;/p&gt;

&lt;p&gt;Every refusal here is something a person has to keep current: a policy row, an enum member, a threshold, a fixture case proving the rule still fires. That is more surface than a prompt asking a model to be careful. It buys artifacts that can be diffed, tested, and argued with in a pull request. A paragraph of instructions cannot be tested the same way.&lt;/p&gt;

&lt;p&gt;The number I would watch in production is the over-hold count, not the block count. Four out of seventeen is survivable on a fixture day and would be a staffing problem at a thousand cards a morning.&lt;/p&gt;

&lt;p&gt;Fail-closed systems rarely fail by letting something through. They fail by becoming more expensive without anyone writing down why, until somebody raises the threshold to make the queue manageable. That is a decision to make against a measured over-hold rate, which is why the sweep exists and why the over-held column stays on the screen.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aisafety</category>
      <category>approvalsystems</category>
      <category>python</category>
      <category>llmarchitecture</category>
    </item>
    <item>
      <title>Confidence Procedures Are Production Code</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Wed, 26 Aug 2026 11:48:12 +0000</pubDate>
      <link>https://dev.to/romiteld/confidence-procedures-are-production-code-4haf</link>
      <guid>https://dev.to/romiteld/confidence-procedures-are-production-code-4haf</guid>
      <description>&lt;p&gt;A clean sample can sound stronger than it is. Someone checks a handful of items, finds no exceptions, and the sentence wants to become broader than the evidence: the process worked, the control operated, the population was clean.&lt;/p&gt;

&lt;p&gt;That sentence is where I stopped trusting the product unless the math had the same status as the API. If a compliance audit sampling tool emits a confidence claim, the confidence procedure is production code. It needs adversarial tests. A formula in a notebook is too far away from the thing users read.&lt;/p&gt;

&lt;p&gt;This post is about the bound in &lt;code&gt;core/bounds.py&lt;/code&gt;, the inverse sample-size question in the same module, and the evaluation gate in &lt;code&gt;eval/coverage.py&lt;/code&gt; and &lt;code&gt;eval/run.py&lt;/code&gt;. The tool reports a one-sided upper confidence bound on the number of failures in a finite population. Then it tries to prove that bound covers at the confidence it claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The number is a frontier, not a fact
&lt;/h2&gt;

&lt;p&gt;The bound answers a narrow question: after drawing &lt;code&gt;n&lt;/code&gt; items from a population of &lt;code&gt;N&lt;/code&gt; and observing &lt;code&gt;k&lt;/code&gt; exceptions, what is the largest true failure count &lt;code&gt;D&lt;/code&gt; still compatible with the sample at the requested confidence?&lt;/p&gt;

&lt;p&gt;That wording matters. The tool is not estimating the true number of failures. It is stating what the sample rules out. A clean sample does not mean the population is clean. It means failure counts above the frontier are no longer supported by the observed draw at the selected confidence.&lt;/p&gt;

&lt;p&gt;The implementation uses the hypergeometric distribution because audit sampling draws without replacement. The same item cannot be selected twice. That is a small detail in code and a large detail in interpretation when the sample is a meaningful fraction of the population, which is exactly the situation where finite-population correction starts to matter (&lt;a href="https://measuringu.com/finite-population-correction/" rel="noopener noreferrer"&gt;MeasuringU&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DECISIONS.md&lt;/code&gt; has the blunt version of the tradeoff. At &lt;code&gt;N=1,000&lt;/code&gt; with &lt;code&gt;n=25&lt;/code&gt;, the exact finite-population bound is &lt;code&gt;11.1 percent&lt;/code&gt;; the binomial comparison is &lt;code&gt;11.3 percent&lt;/code&gt;. Close enough that the distinction feels academic. At &lt;code&gt;N=64&lt;/code&gt;, the gap opens as the sampling fraction rises: at &lt;code&gt;n=5&lt;/code&gt; the binomial is &lt;code&gt;7 percent&lt;/code&gt; high, at &lt;code&gt;n=25&lt;/code&gt; it is &lt;code&gt;45 percent&lt;/code&gt; high, at &lt;code&gt;n=48&lt;/code&gt; it is &lt;code&gt;94 percent&lt;/code&gt; high, and at a full census of &lt;code&gt;64&lt;/code&gt; it reports &lt;code&gt;4.6 percent&lt;/code&gt; where the true answer is zero.&lt;/p&gt;

&lt;p&gt;That last case decided the design. If I examine every item in a finite population, the upper bound on unobserved failures cannot stay above zero after a clean census. The binomial approximation has forgotten that the population can be exhausted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exact_upper_bound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Bound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What a sample of n from N, showing k exceptions, actually supports.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_max_failures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Bound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_failures&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_failure_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rule_of_three&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;binomial_max_failure_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_clopper_pearson_upper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I kept the binomial value in the returned model as a comparison, never as the answer. That made the approximation visible without letting it drive the verdict.&lt;/p&gt;

&lt;p&gt;The other choice in this function is less obvious: the result carries &lt;code&gt;N&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;k&lt;/code&gt;, and &lt;code&gt;confidence&lt;/code&gt; alongside the bound. &lt;code&gt;core/models.py&lt;/code&gt; says the rule directly: no bare float crosses a signature. A number without its inputs is exactly the failure mode this tool is trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The hypergeometric inversion is the product behavior
&lt;/h2&gt;

&lt;p&gt;The core search lives in &lt;code&gt;_max_failures&lt;/code&gt;. It inverts the probability statement instead of sampling possible worlds. For each possible true failure count &lt;code&gt;D&lt;/code&gt;, the observed exception count &lt;code&gt;K&lt;/code&gt; has a hypergeometric distribution. The reported bound is the largest &lt;code&gt;D&lt;/code&gt; for which observing at most &lt;code&gt;k&lt;/code&gt; exceptions is still probable enough.&lt;/p&gt;

&lt;p&gt;The implementation depends on monotonicity: adding more failures to the population can only make small observed counts less likely. That turns the search into an exact binary search over integer failure counts, rather than a heuristic over percentages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Zero draws exclude nothing. Every item could be a failure.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Every draw was an exception. P(X &amp;lt;= n) == 1 for every D.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;

&lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;
&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;  &lt;span class="c1"&gt;# D = k always satisfies: P(X &amp;lt;= k | D = k) == 1
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_cdf_at_most_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_ALPHA_TOL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge cases are part of the contract. With zero draws, the sample excludes nothing. If every draw is an exception, every possible failure count remains compatible with observing at most &lt;code&gt;n&lt;/code&gt; exceptions, so the upper bound is the whole population.&lt;/p&gt;

&lt;p&gt;The middle is the frontier. For fixed &lt;code&gt;N&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt;, every observed &lt;code&gt;k&lt;/code&gt; cuts the lattice of possible true failure counts into allowed and ruled-out values. The product prints the frontier, not the hidden truth.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  population[Finite Population N] --&amp;gt; draw[Draw n Without Replacement]
  draw[Draw n Without Replacement] --&amp;gt; observed[Observed Exceptions k]
  observed[Observed Exceptions k] --&amp;gt; inversion[Hypergeometric Inversion]
  inversion[Hypergeometric Inversion] --&amp;gt; frontier[Reported Upper Bound D]
  frontier[Reported Upper Bound D] --&amp;gt; claim[Allowed Claim]
  population[Finite Population N] --&amp;gt; lattice[All True Failure Counts D]
  lattice[All True Failure Counts D] -.-&amp;gt; inversion[Hypergeometric Inversion]
  binomial[Binomial Approximation] -.-&amp;gt; wrongFrontier[Forgets Finite Population]
  alwaysN[Always Return N] -.-&amp;gt; vacuous[Always Covers But Says Nothing]
  inversion[Hypergeometric Inversion] ==&amp;gt; exactPath[Exact Finite Population Bound]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;I folded the bad alternatives into the same diagram because they fail for different reasons. The binomial approximation can be too loose when the sampling fraction rises. Always returning &lt;code&gt;N&lt;/code&gt; would pass a naive coverage check, because it covers everything. The exact finite-population inversion has to satisfy both constraints: cover at the stated confidence and still say something.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The inverse question is where product pressure shows up
&lt;/h2&gt;

&lt;p&gt;Once the tool can say what a sample supports, the next question is predictable: what sample size would support a stronger sentence?&lt;/p&gt;

&lt;p&gt;That inverse question also belongs in &lt;code&gt;core/bounds.py&lt;/code&gt;. It uses the same finite-population bound, only flipped around. Instead of asking, “given &lt;code&gt;N&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, and &lt;code&gt;k&lt;/code&gt;, what is the maximum supported failure rate,” it asks what draw size would be needed for a target cap under an assumed exception rate.&lt;/p&gt;

&lt;p&gt;I am careful with that framing because this is where tools start to lie politely. A target cap is not a measurement. An assumed exception rate is an input. If those are treated as facts, the sample-size panel becomes a confidence costume for planning assumptions.&lt;/p&gt;

&lt;p&gt;The model boundary helps here. In &lt;code&gt;core/models.py&lt;/code&gt;, &lt;code&gt;Bound&lt;/code&gt; validates internal consistency: &lt;code&gt;k&lt;/code&gt; cannot exceed &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt; cannot exceed &lt;code&gt;N&lt;/code&gt;, the bound cannot fall below the observed exceptions, and the maximum failures cannot exceed the population. That is plain defensive programming, but in this domain those checks are also argument hygiene.&lt;/p&gt;

&lt;p&gt;A product that emits statistical language has two outputs. There is the value on the screen, and there is the sentence a human will say after reading it. The inverse sample-size path exists because the second output matters. If the current sample cannot support the stronger sentence, the tool should state the cost of that sentence rather than rounding the current evidence upward.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Coverage is a gate, not a chart
&lt;/h2&gt;

&lt;p&gt;The evaluation runner treats coverage as the first measurement. &lt;code&gt;eval/run.py&lt;/code&gt; describes four measurements, but coverage runs first and stops the run if it fails. The target is never adjusted to match the result.&lt;/p&gt;

&lt;p&gt;That is the right shape for a statistical product. Detection rates, draws to first exception, and prior sensitivity are useful after the confidence procedure is valid. Before that, they are decoration.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eval/coverage.py&lt;/code&gt; checks the confidence claim two ways. The exact path enumerates outcomes analytically. The Monte Carlo path replays the same quantity through the random draw mechanism. They are not redundant. Exact enumeration tests the math. Simulation tests that the implementation path agrees with the math.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exact_coverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOMINAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;P(true D falls inside the reported bound), summed over all outcomes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pmf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hypergeom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pmf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;covered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;exact_upper_bound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;max_failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pmf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;covered&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cleanest test in the project. For a fixed &lt;code&gt;N&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, and true failure count &lt;code&gt;D&lt;/code&gt;, every possible observed &lt;code&gt;k&lt;/code&gt; is finite. So the test sums the probability mass for the outcomes where the reported bound contains the true &lt;code&gt;D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no sampling error in that number. If it falls below the nominal confidence, the procedure does not cover. The product is wrong.&lt;/p&gt;

&lt;p&gt;The Monte Carlo version uses NumPy’s hypergeometric draw and then runs the same bound calculation on the observed exception counts. The default is &lt;code&gt;10,000&lt;/code&gt; runs with seed &lt;code&gt;0&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mc_coverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOMINAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MC_RUNS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The same quantity by simulation, through the real draw path.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypergeometric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngood&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nbad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nsample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nf"&gt;exact_upper_bound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;max_failures&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dictionary cache is a small detail, but I like it. A simulation can draw the same &lt;code&gt;k&lt;/code&gt; many times, and the bound for that &lt;code&gt;k&lt;/code&gt; does not change. Cache the unique observed counts, then compare repeated outcomes to the same computed frontier.&lt;/p&gt;

&lt;p&gt;The readme states the result before anything else: verdict passed. Minimum coverage observed was &lt;code&gt;0.9500&lt;/code&gt; against nominal &lt;code&gt;0.9500&lt;/code&gt;, across &lt;code&gt;5,943&lt;/code&gt; exhaustively enumerated configurations. The worst configuration was &lt;code&gt;N=64&lt;/code&gt;, &lt;code&gt;n=25&lt;/code&gt;, &lt;code&gt;D=16&lt;/code&gt;, at &lt;code&gt;0.9505&lt;/code&gt;. The largest disagreement between exact and simulated coverage was &lt;code&gt;0.0039&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those numbers are not marketing. They are the release condition for every sentence that depends on the bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A passing coverage test can still be useless
&lt;/h2&gt;

&lt;p&gt;A malicious implementation can pass a lower-bound coverage check by returning &lt;code&gt;N&lt;/code&gt; for every input. It will always contain the true failure count, because the true count cannot exceed the population.&lt;/p&gt;

&lt;p&gt;That is the trap in testing confidence procedures. “Covers at least 95 percent” is necessary. Alone, it rewards vacuity.&lt;/p&gt;

&lt;p&gt;The suite adds an anti-vacuity ceiling. The readme states it as a test: minimum coverage must not rise above &lt;code&gt;0.96&lt;/code&gt;. A bound that always returned &lt;code&gt;N&lt;/code&gt; would cover at &lt;code&gt;100 percent&lt;/code&gt; and say nothing, so that implementation should fail.&lt;/p&gt;

&lt;p&gt;This is one of those tests that looks strange until you need it. Most test suites only reject undercoverage. Here, overcoverage can also be a bug because the product is supposed to say the strongest true sentence supported by the evidence. Too safe becomes uninformative.&lt;/p&gt;

&lt;p&gt;There is a second guard: shrink the bound by one and confirm coverage breaks. That catches the opposite fake. Without it, the coverage test could be passing while failing to detect that the frontier is load-bearing. If reducing every reported upper bound by one still passed, the original test would not be sharp enough to prove the implemented boundary.&lt;/p&gt;

&lt;p&gt;The two guards create pressure from both sides. The bound cannot be loose enough to say nothing, and it cannot be tightened without losing the claimed confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Ground truth stays outside the estimator
&lt;/h2&gt;

&lt;p&gt;The simulator needs hidden failure rates. The estimator must never see them.&lt;/p&gt;

&lt;p&gt;That boundary is explicit. &lt;code&gt;simulation/draw.py&lt;/code&gt; is the only module permitted to read &lt;code&gt;Stratum.true_failure_rate&lt;/code&gt;. Its docstring says why: everything under &lt;code&gt;core/&lt;/code&gt; estimates, while the simulation knows the answer. If an estimator could read ground truth, the evaluation would be theatre.&lt;/p&gt;

&lt;p&gt;The same file says the boundary is enforced three ways: parse every file under &lt;code&gt;core/&lt;/code&gt; and &lt;code&gt;api/&lt;/code&gt; for the identifier, assert that no estimator module imports the simulation package, and perturb every true rate in the fixtures while checking that estimator output does not move by a single digit.&lt;/p&gt;

&lt;p&gt;I care about this boundary more than I expected to when I started. Statistical code has an especially ugly failure mode: it can look more correct after it cheats. If ground truth leaks into the estimator, the bounds become beautifully calibrated to information the auditor never had.&lt;/p&gt;

&lt;p&gt;The HTTP surface follows the same rule. &lt;code&gt;server/main.py&lt;/code&gt; says every response is a public projection. &lt;code&gt;Stratum&lt;/code&gt; carries simulator ground truth, so nothing of that type is serialized. The browser receives projections, never the domain object that knows the answer.&lt;/p&gt;

&lt;p&gt;That separation costs some convenience. You write more models. You pass more values around. You make tests about identifiers and imports, which feels crude until the first time it prevents a silent category error.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The formula was the easy part
&lt;/h2&gt;

&lt;p&gt;The finite-population bound is compact. The production problem is larger: keep the approximation visible but out of the verdict, carry the inputs with every output, invert the distribution over integer counts, prove coverage by enumeration, replay it through simulation, reject vacuity, and make ground truth unreachable from estimators.&lt;/p&gt;

&lt;p&gt;I do not think of that as “extra validation” around the math. It is the math becoming a product surface.&lt;/p&gt;

&lt;p&gt;A confidence number is a promise about repeated behavior. Once that promise appears in software, the test suite has to attack the promise itself, because the user will build a sentence from the number whether the code earned it or not.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>sampling</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Rehearsal Is Only Cheap In Distribution</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 24 Aug 2026 03:19:27 +0000</pubDate>
      <link>https://dev.to/romiteld/a-rehearsal-is-only-cheap-in-distribution-2a1l</link>
      <guid>https://dev.to/romiteld/a-rehearsal-is-only-cheap-in-distribution-2a1l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; In generative video pipelines, running cheap low-step sketches to pick parameters sounds like free optimization. But when prompts go out-of-distribution, surrogate scorers return noise, turning a \$0.002 check into a bad decision that triggers a \$15 compounding failure. Here's why skipping the cheap step is sometimes the cheapest option.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three numbers run Scenematic's generation loop. A think-frame costs \$0.002. A full render costs \$0.50. A bad scene that slips through and gets built on costs about \$15.50, because the scene chain compounds it before anyone looks. The constant in &lt;code&gt;lib/generation-loop.ts&lt;/code&gt; carries the arithmetic in a comment: &lt;code&gt;15.502, // CALIBRATION_TARGET: 0.002 + 0.50 + 15.00&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most of the pipeline exists to keep spend at the cheap end of that ladder. One module decides when the cheap step should be skipped entirely. A hundred-contract baseline then put numbers on how often that decision was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The rehearsal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/think-frames.ts&lt;/code&gt; generates quick, low-inference-step sketches before committing to a full-quality keyframe. The file header credits DeepGen's think tokens as the inspiration. Each sketch tries a different preservation focus, character, environment, mood, composition, or atmosphere, with its own image-to-image strength and seed. The reward mixer scores the batch and the winner's parameters go to the full render.&lt;/p&gt;

&lt;p&gt;The economics only work if those scores mean something. That assumption fails quietly, and it fails hardest on the prompts where a rehearsal looks most useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where the scores stop meaning anything
&lt;/h2&gt;

&lt;p&gt;Scoring a sketch of &lt;code&gt;A detective leans forward across a metal table, interrogating a nervous suspect under fluorescent lights&lt;/code&gt; works fine. The scorer has seen a thousand shots like it. Scoring &lt;code&gt;A sentient equation writes itself across a blackboard that extends infinitely in all dimensions&lt;/code&gt; does not fail loudly. It returns a number, and the number is noise. Both prompts are verbatim from the baseline harness.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lib/ood-detector.ts&lt;/code&gt; measures the distance instead of hoping. It keeps a reference corpus of 25 in-distribution prompts, roughly 8 dialogue, 9 cinematic, 8 multi-asset, and embeds them with all-MiniLM-L6-v2 through &lt;code&gt;@xenova/transformers&lt;/code&gt;. Local model, no API call.&lt;/p&gt;

&lt;p&gt;The embeddings mean-pool into a unit centroid, computed once per process and cached. An incoming prompt gets embedded the same way. Epistemic uncertainty is one minus the cosine similarity to that centroid, and at or above the threshold the detector sets &lt;code&gt;bypass_surrogate: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bypass means skipping the cheap step, so the prompt the system understands least is the one that goes straight to the expensive render. Backwards from most cost optimizations, and deliberate. Out past the corpus a rehearsal is a \$0.002 lie that steers a \$0.50 decision toward a \$15 mistake, and skipping it buys the removal of a bad witness at the price of one render. The gate recuses an unqualified judge rather than filtering bad prompts.&lt;/p&gt;

&lt;p&gt;Every evaluation writes a row to an &lt;code&gt;ood_events&lt;/code&gt; table: the uncertainty, the threshold that was applied, the bypass flag, and &lt;code&gt;cost_incurred&lt;/code&gt; at either \$0.50 or \$0.002. That table is where everything else in this post comes from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  P["Compiled prompt"] --&amp;gt; U["Uncertainty = 1 - cosine vs corpus centroid"]
  U --&amp;gt; G{"At or above the category threshold?"}
  G --&amp;gt;|"BYPASS"| F["Straight to full render, $0.50"]
  G --&amp;gt;|"SURROG"| T["Think-frame rehearsal, $0.002"]
  T --&amp;gt; W["Full render using the winning sketch's parameters"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. One hundred contracts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/run-phase1-baseline.ts&lt;/code&gt; runs 100 steering contracts through the gate. A steering contract is the typed record the compiler emits for one scene: the prompt, the chosen model and seed, the quality targets, and the audit trail of what happened to it. The batch splits into 80 in-distribution prompts across dialogue, cinematic, and multi-asset, plus 20 out-of-distribution (OOD) prompts, abstract and high-complexity. Seeds are fixed at 42 plus the contract index. The renders are real, on RunPod pods running ltx2, wan22, and hunyuan behind per-model semaphores of 3, 2, and 2. Each console line prints &lt;code&gt;SURROG&lt;/code&gt; or &lt;code&gt;BYPASS&lt;/code&gt; next to the measured uncertainty.&lt;/p&gt;

&lt;p&gt;A caveat before the numbers. The reward heads, the four per-dimension scores that judge a finished render (R_smooth, R_motion, R_semantic, R_narrative), are simulated in this harness: in-distribution signals drawn around 0.65 with jitter, OOD around 0.35, because real per-head scoring of rendered video was not wired up yet. The embeddings, the routing, the renders, and the telemetry are real. The simulation still surfaced two real bugs, covered below.&lt;/p&gt;

&lt;p&gt;The cost waterfall, straight out of &lt;code&gt;fig4_cost_waterfall.csv&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contracts submitted&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OOD bypassed&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;\$0.00 extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU rendered&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;\$15.64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passed quality floor&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HITL flagged&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False negatives&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;\$821.60 compound&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty-two of the 26 human-in-the-loop (HITL) flags blamed the narrative head. The Phase 2 commit message gives the aggregate: a 25.3% false negative rate, with R_narrative dominating 72.4% of HITL flags. Fifty-three contracts finished under the quality floor, and at \$15.50 of modeled compound damage each the ledger prices those misses at \$821.60, against \$15.64 for every render in the run combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Thresholds from the misses
&lt;/h2&gt;

&lt;p&gt;Phase 2 swept the threshold from 0.30 to 0.95 in steps of 0.01 and computed a full confusion matrix at each stop. The F1-optimal global threshold came out at 0.31, which flags 97 of 100 contracts and puts the cost column at \$48.51 per hundred. Flagging nearly everything does maximize F1. It also defeats the point of having a surrogate at all.&lt;/p&gt;

&lt;p&gt;So the shipped config ignored F1. The &lt;code&gt;calibration_metadata&lt;/code&gt; block in &lt;code&gt;quality-thresholds-v2.json&lt;/code&gt; records the target it optimized instead: &lt;code&gt;Catch ALL known FNs per category with margin&lt;/code&gt;. Take the known misses, find the minimum uncertainty among them per category, set the threshold just under it. DIALOGUE's one known false negative sat at 0.631, so DIALOGUE got 0.62. SCENIC's four bottomed out at 0.506, so 0.50. ACTION's five at 0.495, so 0.49. The global fell from 0.72 to 0.55 and now serves as the fallback for categories with no data.&lt;/p&gt;

&lt;p&gt;Per category matters because the sweep exposed an inversion a single global threshold cannot encode. The config file says it in one line: &lt;code&gt;SCENIC and ACTION had LOWER uncertainty but HIGHER FN rates — category thresholds fix this&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A scenic prompt reads as familiar to the embedding space. Sweeping drone shot, golden light, the corpus is full of that texture. The renders still miss. Uncertainty and failure rate are correlated across the whole population and inverted inside two of its slices, and one knob cannot express that.&lt;/p&gt;

&lt;p&gt;ABSTRACT kept the old 0.72, since genuinely weird prompts carry high uncertainty on their own. NARRATIVE got 0.65 with an annotation calling it a conservative estimate with no false negative data behind it. That one is a guess with a label on it, and it stays a guess until a NARRATIVE prompt fails in a logged run.&lt;/p&gt;

&lt;p&gt;The detector loads this file at runtime and falls back to the legacy global 0.72 if it is missing. The sweep range and step size are recorded next to the thresholds they produced, along with the per-category evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Two gates that could not fire
&lt;/h2&gt;

&lt;p&gt;The baseline caught two bugs I would not have found by reading the code, because the code looked fine.&lt;/p&gt;

&lt;p&gt;The HITL gate required the composite score to be under the floor AND at least one reward head to breach a z-score of -2.0. With simulated signals at 0.65 ± 0.1, the commit message for &lt;code&gt;20aa5ec&lt;/code&gt; describes the result: the z-score condition was &lt;code&gt;mathematically impossible to satisfy with simulated signal distribution of 0.65 ± 0.1&lt;/code&gt;. Two conditions joined by AND, one of them unsatisfiable. The gate sat silent while contracts failed under it, and the dashboards looked calm the whole time.&lt;/p&gt;

&lt;p&gt;The fix inverted the roles. The reward floor of 0.65 is now the primary trigger, and per-head z-scores only attribute which head gets blamed.&lt;/p&gt;

&lt;p&gt;The second bug was in the false negative counter itself. It only counted SURROG contracts, on the theory that a false negative means the surrogate path trusted a prompt it should not have. That definition missed 8 contracts the gate bypassed which still rendered below the floor. A bypassed prompt that fails is still the pipeline failing, whatever path it took, so the counter now takes any contract under the floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The head that ran cooler
&lt;/h2&gt;

&lt;p&gt;R_narrative took 72.4% of the HITL blame in the phase 2 baseline. The narrative scoring was doing its job. Its head just runs cooler than the others: population mean 0.55, against 0.60 for R_motion and 0.70 for R_smooth and R_semantic. Four heads held to the same absolute bar, and the coolest one took nearly all the blame.&lt;/p&gt;

&lt;p&gt;The fix in &lt;code&gt;lib/reward-mixer.ts&lt;/code&gt; is z-score normalization per head. &lt;code&gt;normalizeHeadScore&lt;/code&gt; computes (raw − mean) / std against per-head population stats stored in the same versioned config, so a head only triggers when it is unusual for itself. &lt;code&gt;computeSubReason&lt;/code&gt; then cross-references the other heads whenever R_narrative does trigger: motion also low means pacing, semantic also low means fidelity, narrative alone means coherence.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Infra noise is not model failure
&lt;/h2&gt;

&lt;p&gt;Twenty hunyuan events in the baseline were graphics processing unit (GPU) failures, out-of-memory and handler crashes, sitting in &lt;code&gt;ood_events&lt;/code&gt; and dragging the quality numbers around. So the table grew &lt;code&gt;gpu_error&lt;/code&gt; and &lt;code&gt;superseded&lt;/code&gt; columns. &lt;code&gt;evaluateOOD&lt;/code&gt; returns the row id of the event it just logged, and the runner calls &lt;code&gt;markOODEventGpuError&lt;/code&gt; on a GPU error so the dashboard can segment it. Re-running contracts with &lt;code&gt;--rerun-contracts="81,82,100"&lt;/code&gt; marks the old contaminated rows superseded instead of deleting them. &lt;code&gt;queryDashboards&lt;/code&gt; takes a view argument, &lt;code&gt;clean&lt;/code&gt; or &lt;code&gt;all&lt;/code&gt;, and clean excludes both flags.&lt;/p&gt;

&lt;p&gt;Even total infrastructure collapse gets a row: &lt;code&gt;ALL_MODELS_DOWN&lt;/code&gt; logs an OOD event with &lt;code&gt;gpu_error=true&lt;/code&gt;. Without the segmentation, a crashed pod reads as a quality regression, and one afternoon of flaky hardware quietly recalibrates your thresholds for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What is still provisional
&lt;/h2&gt;

&lt;p&gt;The detector landed at 234 lines and sits just over 300 after the Phase 2 changes. The scoring inside it is a cached centroid and a cosine. Nothing is learned and none of it touches a GPU. The cost constants carry a &lt;code&gt;CALIBRATION_TARGET&lt;/code&gt; marker with a comment saying to recalibrate after 100 real runs or a provider switch, so the provisional numbers announce themselves and are greppable.&lt;/p&gt;

&lt;p&gt;Plenty is still provisional. The reference corpus is 25 prompts, and every threshold in the v2 config is calibrated against the centroid those 25 produce. Adding corpus prompts moves the centroid, which shifts every uncertainty measurement, which invalidates the per-category thresholds. Corpus and thresholds have to version together or the calibration quietly stops describing anything. The reward heads are still the simulated ones, so the &lt;code&gt;head_stats&lt;/code&gt; block needs re-deriving from real scores before the z-score triggers mean what they claim. And NARRATIVE still has no false negative data.&lt;/p&gt;

&lt;p&gt;The part I trust is the ledger. Both dead gates were invisible in code review and obvious in the event counts, and the event counts only exist because every decision writes a row, including the decision to spend more.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>videogeneration</category>
      <category>ooddetection</category>
      <category>calibration</category>
      <category>telemetry</category>
    </item>
    <item>
      <title>Calibration Is Bet Sizing</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Sun, 23 Aug 2026 12:19:16 +0000</pubDate>
      <link>https://dev.to/romiteld/calibration-is-bet-sizing-9cm</link>
      <guid>https://dev.to/romiteld/calibration-is-bet-sizing-9cm</guid>
      <description>&lt;p&gt;The last post was about making a number trustworthy. Leakage geometry, purge widths, de-overlap, a baseline that could not cheat. It ended with a minute-scale ceiling that held at 52% across seven configurations and a model family swap.&lt;/p&gt;

&lt;p&gt;This one is about what happens after you trust the number. Because a probability you are going to bet on is a different object from a probability you are going to report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The probabilities are not decorative
&lt;/h2&gt;

&lt;p&gt;The path-passage classifier is a three-class LightGBM. It returns &lt;code&gt;p_up&lt;/code&gt;, &lt;code&gt;p_down&lt;/code&gt;, &lt;code&gt;p_none&lt;/code&gt;. Those go straight into the expected-value score that decides whether to take a trade and how big:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;long_score&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_up&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_down&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_none&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;short_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_up&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_down&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p_none&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;B&lt;/code&gt; is the barrier, &lt;code&gt;C&lt;/code&gt; the cost. Read the arithmetic. Every term is linear in a probability. Scale &lt;code&gt;p_up&lt;/code&gt; by 1.2 and you scale the long score by very nearly 1.2.&lt;/p&gt;

&lt;p&gt;So miscalibration does not stay in the model. It becomes a bet-sizing error, in proportion, in the bins where the gate actually fires. A classifier that is right 70% of the time while claiming 90% is not 20 points wrong. It is sizing every position in that bin as though the edge were far larger than it is.&lt;/p&gt;

&lt;p&gt;Boosted trees are known for uncalibrated softmax output. I had been consuming it as if it were a probability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit
&lt;/h2&gt;

&lt;p&gt;Seven live assets. For each one, fit an Inductive Venn-Abers wrapper on the time-ordered older 80% of that model's training data, 6,988 rows, and evaluate against a 500-row uniform-random sample of the newer 20%, seed 42. The LightGBM models are reloaded from disk and left alone. Only the wrapper is fit.&lt;/p&gt;

&lt;p&gt;Measure Expected Calibration Error and log-loss, before and after.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;ECE before → after&lt;/th&gt;
&lt;th&gt;ECE Δ&lt;/th&gt;
&lt;th&gt;Log-loss Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BTC&lt;/td&gt;
&lt;td&gt;0.1272 → 0.0621&lt;/td&gt;
&lt;td&gt;-51.2%&lt;/td&gt;
&lt;td&gt;-5.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETH&lt;/td&gt;
&lt;td&gt;0.1795 → 0.0298&lt;/td&gt;
&lt;td&gt;-83.4%&lt;/td&gt;
&lt;td&gt;-11.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOL&lt;/td&gt;
&lt;td&gt;0.1680 → 0.0386&lt;/td&gt;
&lt;td&gt;-77.0%&lt;/td&gt;
&lt;td&gt;-10.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XRP&lt;/td&gt;
&lt;td&gt;0.2219 → 0.0645&lt;/td&gt;
&lt;td&gt;-70.9%&lt;/td&gt;
&lt;td&gt;-17.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADA&lt;/td&gt;
&lt;td&gt;0.1419 → 0.0369&lt;/td&gt;
&lt;td&gt;-74.0%&lt;/td&gt;
&lt;td&gt;-8.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LINK&lt;/td&gt;
&lt;td&gt;0.1260 → 0.0737&lt;/td&gt;
&lt;td&gt;-41.5%&lt;/td&gt;
&lt;td&gt;-1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LTC&lt;/td&gt;
&lt;td&gt;0.1508 → 0.0603&lt;/td&gt;
&lt;td&gt;-60.0%&lt;/td&gt;
&lt;td&gt;-14.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every asset has a real gap. That settles the first question, which was whether this was one bad model or a property of the setup. It is systematic.&lt;/p&gt;

&lt;p&gt;The second question is the interesting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The direction is asset-specific, and that rules out the easy fix
&lt;/h2&gt;

&lt;p&gt;ETH fails the way boosted trees are supposed to fail. Its worst reliability bin is [0.90, 1.00]. Eleven samples. Mean stated confidence 93.8%. Empirical accuracy 45.5%.&lt;/p&gt;

&lt;p&gt;Most certain, least reliable. That is the bin where the EV gate fires hardest and the position sizes are largest.&lt;/p&gt;

&lt;p&gt;The other six fail in the opposite direction.&lt;/p&gt;

&lt;p&gt;XRP, in the [0.60, 0.70] bin: 93.1% accuracy at 64.9% stated confidence, n=116. A 28-point understatement.&lt;/p&gt;

&lt;p&gt;LTC, same bin: 90.4% accuracy at 64.7% confidence, n=115.&lt;/p&gt;

&lt;p&gt;That is a suppressed-signal failure. The gate does not fire often enough, because the stated confidence lags what the model actually delivers. It costs money quietly, by declining trades that were good.&lt;/p&gt;

&lt;p&gt;One asset over-confident. Six under-confident.&lt;/p&gt;

&lt;p&gt;Which kills the convenient answer. Platt scaling and temperature scaling apply one monotone correction. They cannot pull ETH's tail down and push XRP's middle up at the same time, because those are corrections in opposite directions. A single global calibrator fits the average of two failure modes and helps neither.&lt;/p&gt;

&lt;p&gt;Per-asset Venn-Abers works here because it fits each asset's own reliability curve and does not assume a shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the split came from
&lt;/h2&gt;

&lt;p&gt;This is the part I did not expect, and it is the reason I keep the training params in the same document as the audit.&lt;/p&gt;

&lt;p&gt;The hardened LightGBM settings were &lt;code&gt;min_data_in_leaf=400&lt;/code&gt;, &lt;code&gt;num_leaves=8&lt;/code&gt;, &lt;code&gt;max_depth=3&lt;/code&gt;. I introduced those specifically to stop the ETH-style saturation, where terminal leaves go to 1.0 and the model claims certainty it has not earned.&lt;/p&gt;

&lt;p&gt;They worked. They also worked too well.&lt;/p&gt;

&lt;p&gt;Constraining the leaves prevented the over-confidence failure and produced a structural under-confidence pattern across the rest of the universe. The fix for one failure mode manufactured the opposite failure mode in six assets.&lt;/p&gt;

&lt;p&gt;ETH is the lone holdout that still saturates, because in the rare cases where it is genuinely certain its leaves still reach the top of the range. Eleven samples in that bin tells the story: sparse and extreme.&lt;/p&gt;

&lt;p&gt;That is a straight tradeoff I made without knowing I was making it. Reliability diagrams are what showed it. An accuracy score would have shown a modest improvement and nothing else, because averaging is exactly the operation that hides a bin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bar was set before the numbers came back
&lt;/h2&gt;

&lt;p&gt;ECE has to improve by at least 50%, and log-loss by at least 5%. Both, not either.&lt;/p&gt;

&lt;p&gt;Six assets clear it. LINK-USD misses both, at -41.5% and -1.2%.&lt;/p&gt;

&lt;p&gt;LINK is not a data problem. It has the same 8,736 training rows as everything else, so this is model quality rather than availability. The LightGBM may simply be better calibrated for LINK already, in which case there is less for the wrapper to do and the small lift is honest. Or the calibrator needs different hyperparameters. Both are worth a follow-up and neither is resolved today.&lt;/p&gt;

&lt;p&gt;So the rollout is selective. Six of seven, and LINK stays on raw softmax until somebody investigates it.&lt;/p&gt;

&lt;p&gt;The mechanism is deliberately boring. The loader falls back to raw softmax for any asset with no calibrator on disk, so excluding LINK means either leaving its pickle off the deployment image or adding an allow-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VENN_ABERS_ASSETS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTC-USD,ETH-USD,SOL-USD,XRP-USD,ADA-USD,LTC-USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One environment variable and three lines in the loader. A rollout that cannot express "these six and not that one" ends up shipping the failure with the successes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this audit does not prove
&lt;/h2&gt;

&lt;p&gt;The status line at the top of the integration guide says prototype, not integrated into the live inference path. That is still true, and the caveats are worth having in the open.&lt;/p&gt;

&lt;p&gt;The holdout is a 500-row uniform sample, seed 42. The 80/20 split is on the model's own training data rather than on live signals.&lt;/p&gt;

&lt;p&gt;There has been no live-data refit, because &lt;code&gt;signals_history&lt;/code&gt; does not yet carry realized 24-hour outcomes for the &lt;code&gt;p_up&lt;/code&gt;/&lt;code&gt;p_down&lt;/code&gt;/&lt;code&gt;p_none&lt;/code&gt; rows. Migration 028 landed on 2026-05-21 to start collecting them.&lt;/p&gt;

&lt;p&gt;BTC's barrier is 150 bps and the rest are 200, so the ECE deltas compare cleanly but the absolute ECE values across assets carry a class-balance shift. I treat those as informal.&lt;/p&gt;

&lt;p&gt;LINK's failure in particular should be re-run once live outcomes exist. A training-data fit can hide a different live picture, and that is exactly the asset where I would expect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the second half of the last post
&lt;/h2&gt;

&lt;p&gt;The geometry work answered whether the measurement could be trusted. Purge, embargo, de-overlap, a split that cannot leak. All of that gets you a number you can believe.&lt;/p&gt;

&lt;p&gt;Believing the number is not the same as being able to bet on it. The ceiling post established that the minute-scale direction signal is capped near 52% and that the cap is real rather than an artifact. This audit establishes something narrower and more immediate: on the horizon where there is signal, the probability the model hands the sizer is not the probability it should act on, and the correction is different for every asset.&lt;/p&gt;

&lt;p&gt;Accuracy averages. A reliability diagram does not. The bin where the model was most confident and least correct is worth more attention than any figure computed across the whole set, because it is the bin where the money goes.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>calibration</category>
      <category>conformalprediction</category>
      <category>quantitativefinance</category>
    </item>
    <item>
      <title>Experimentation Is the Missing Evaluation Layer for Agent Memory</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Sun, 23 Aug 2026 10:43:40 +0000</pubDate>
      <link>https://dev.to/romiteld/experimentation-is-the-missing-evaluation-layer-for-agent-memory-4k7d</link>
      <guid>https://dev.to/romiteld/experimentation-is-the-missing-evaluation-layer-for-agent-memory-4k7d</guid>
      <description>&lt;p&gt;An agent can fail because it missed the right material. It can also fail because I gave it the wrong material with confidence. The worse case is quieter: the wrong material sits beside a successful task, then looks useful afterward.&lt;/p&gt;

&lt;p&gt;A skilled user may ask for architecture notes because they know where to look. Those notes can travel with a good result even when they did not cause it. If the system learns from that trace alone, the next session may get extra text that feels justified and still wastes the agent's attention.&lt;/p&gt;

&lt;p&gt;That failure has a name and a direction. The same expertise that makes someone request the right document also makes them likelier to finish the task without it. Skill causes both the request and the outcome, so skill is a common cause sitting upstream of the thing I am trying to measure. A system that reads the trace and credits the document has attributed to the context what belonged to the person. This is not noise that averages out as traffic grows. More sessions from confident users make the estimate tighter and no less wrong, which is the property that makes it dangerous: the system becomes more certain of a relationship it never established.&lt;/p&gt;

&lt;p&gt;There is no way to subtract that bias afterward from the trace alone, because the trace does not record why the material was requested. The only cheap instrument that removes it is deciding who gets the material by coin flip instead of by request. Randomization makes assignment independent of skill, so whatever difference survives between the two branches is attributable to the material rather than to the person holding it.&lt;/p&gt;

&lt;p&gt;This is the problem I built the active experimentation layer for in Zero Context Loss (ZCL). ZCL is the context learning platform I use for my AI agents. This post is about one part of it: the evaluation layer that tests changes to context provisioning while the agent is doing real work. It is also, by the end, an honest account of how much that layer can currently prove, which is less than its own vocabulary suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A context change becomes a hypothesis
&lt;/h2&gt;

&lt;p&gt;The active learning code lives in &lt;code&gt;zcl_core/learning/active_learning.py&lt;/code&gt;. The central object is &lt;code&gt;Hypothesis&lt;/code&gt;. I made the candidate change carry its own treatment, control, task type, metric, and minimum sample threshold.&lt;/p&gt;

&lt;p&gt;That shape is deliberate. Reordering two documents has a different target from including a document whose value is uncertain. The first can be judged by time. The second can be judged by success. If those choices stay implicit, the system can promote a vague improvement without saying what improved.&lt;/p&gt;

&lt;p&gt;Declaring the metric before the data arrives matters more than it looks. A hypothesis that names its outcome in advance can only be judged on that outcome. A hypothesis that stays vague can be judged on whichever of success, duration or token count happens to have moved, and something almost always has. Writing the metric into the object is the cheapest available guard against choosing the comparison after seeing the result.&lt;/p&gt;

&lt;p&gt;The hypothesis object uses a UUID, a type string, a description, the task kind, treatment and control payloads, a metric name, and &lt;code&gt;min_samples&lt;/code&gt; set to 20. The default threshold sits on the object because I wanted the brake to travel with the proposed change. Section 6 returns to whether that brake is connected to anything.&lt;/p&gt;

&lt;p&gt;The generator in &lt;code&gt;ActiveLearning&lt;/code&gt; stays close to context the system can actually apply. It looks at documents already used for a task type, then builds order hypotheses from the top five documents against the next slice. It also asks for uncertain documents and creates inclusion tests for up to three of them. Each hypothesis takes its id from &lt;code&gt;uuid4()&lt;/code&gt; — called, not referenced.&lt;/p&gt;

&lt;p&gt;A context system with limited traffic can burn every session it has on combinations that will never gather enough evidence to matter, so the generator stops early and some pairings never become live candidates at all, and I gave that coverage up on purpose. The generator is not there to enumerate everything that could be tested. It is there to keep the number of live tests small enough that each one can actually finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Assignment happens while context is being built
&lt;/h2&gt;

&lt;p&gt;The integration point is &lt;code&gt;zcl_core/provision/context_provider.py&lt;/code&gt;. The context provider creates the session id with &lt;code&gt;uuid4()&lt;/code&gt;, decides whether the request should explore, asks value attribution for learned document values, handles cold start, then filters the selected document ids through causal evidence before the final bundle goes back to the agent.&lt;/p&gt;

&lt;p&gt;Assignment has to happen before the agent consumes the material, because the branch has to be decided by the coin rather than by the request, and that ordering is doing more work here than anything else in the file. A report run later can describe what happened, but by then the material has already been chosen by whatever mixture of habit, skill and retrieval score produced it, and the confound from the opening is already baked into the data. Deciding during provisioning is what converts an observation into an experiment.&lt;/p&gt;

&lt;p&gt;The returned &lt;code&gt;ContextBundle&lt;/code&gt; carries &lt;code&gt;is_experimental&lt;/code&gt;, &lt;code&gt;experiment_id&lt;/code&gt;, and &lt;code&gt;experiment_group&lt;/code&gt;. Those fields are plain bookkeeping, and they are the difference between analysis and guesswork. If a session was in treatment, the bundle says so. If it was in control, the evaluator does not have to reconstruct that from log order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
 candidate[Candidate context strategy] --&amp;gt; assignment[Randomized assignment]
 assignment --&amp;gt; treatment[Treatment context]
 assignment --&amp;gt; control[Control context]
 treatment --&amp;gt; outcome[Task outcome]
 control --&amp;gt; outcome
 outcome --&amp;gt; evaluation[Statistical evaluation]
 evaluation --&amp;gt; promoted[Promoted strategy]
 evaluation --&amp;gt; retired[Retired strategy]```



The diagram lies in one place, and it is the place most likely to matter. Randomizing sessions treats sessions as independent units. They are not. The same person returns, so their skill appears in both arms across the week rather than being held constant within one. Learned document values also update between sessions, which means the control arm is not fixed while the experiment runs; the baseline drifts under the test. Randomizing the person rather than the session would remove the first problem, and freezing learned values for the duration of an experiment would remove the second. The current design does neither, and the effect is that the estimate is noisier than a clean trial of the same size.

## 3. How much of the traffic gets spent on trials

`ActiveLearning` starts with `base_exploration_rate = 0.2`, uses `uncertainty_multiplier = 0.5`, and caps the final probability at 0.4. These are configuration values in the implementation. They are not presented as observed production rates, throughput measurements, or hardware-dependent results.

The decision is small: compute task uncertainty for the organization, add the uncertainty bonus to the base rate, cap it, then compare that probability with `random.random()`.

The uncertainty calculation is scoped by organization. That stops one organization's history from making another organization's context appear more certain than it is. In the helper, fewer than 10 sessions returns maximum uncertainty. After that, the code reads outcomes for the task, computes success-rate variance as a Bernoulli variance, and reduces the result as sample count grows using the log of the count. With no outcomes, it uses 0.25 as maximum variance.

It is a cheap signal for where to spend trials, and it makes no claim at all about why a document helped.

There are two traditions tangled together in that decision, and they do not want the same thing. Spending more trials where uncertainty is high is bandit reasoning, and a bandit's goal is to minimise regret: give as many sessions as possible the best-known context while still learning. A controlled experiment has a different goal, which is an unbiased estimate of an effect, and it is happiest with a fixed allocation decided in advance. The two are not the same discipline. Adaptive allocation is known to bias the naive difference in means, because the amount of data each arm receives depends on how the arm has been performing.

Here the two are only loosely coupled: the uncertainty rate governs whether a session explores at all, while assignment within a live experiment is a fair split. That keeps the bias small. But the honest description of this layer is that it uses a bandit to decide when to run trials and a fixed randomization to run them, and if the exploration rate ever starts responding to the results of a specific live experiment, the estimator stops being trustworthy.

Capping it at 0.4 means the areas with the least evidence still spend most of their sessions on the path already known, which slows learning down badly. I kept the cap anyway. Without it a sparse task type turns into churn, every request treated as a trial, and nothing ever settles into a default.

## 4. The schema keeps the branches separate

The experiment tables are defined in `migrations/004_experiments.sql`. The migration separates the proposed change from the per-session assignment. One table stores the hypothesis text, task type, treatment, control, status, results, and timestamps. The other stores the session, experiment id, group name, and assignment time. Both take their ids and timestamps from `uuid_generate_v4()` and `NOW()`, called rather than named.



```sql
CREATE TABLE zcl_experiments (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    hypothesis TEXT NOT NULL,
    task_type VARCHAR(100) NOT NULL,
    treatment JSONB NOT NULL,
    control JSONB NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, completed, cancelled
    results JSONB,
    started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ,

    CONSTRAINT valid_status CHECK (status IN ('active', 'completed', 'cancelled'))
);

CREATE TABLE zcl_experiment_assignments (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    session_id UUID NOT NULL REFERENCES zcl_sessions(id) ON DELETE CASCADE,
    experiment_id UUID NOT NULL REFERENCES zcl_experiments(id) ON DELETE CASCADE,
    group_name VARCHAR(20) NOT NULL, -- treatment, control
    assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE(session_id, experiment_id),
    CONSTRAINT valid_group CHECK (group_name IN ('treatment', 'control'))
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JSONB&lt;/code&gt; is PostgreSQL's binary JSON type. I used it here because treatment and control payloads vary by hypothesis type. Ordering two documents is shaped differently from excluding one document, and forcing those into a rigid column set would add ceremony without improving the evaluator.&lt;/p&gt;

&lt;p&gt;The two constraints are doing real work, and they are doing the kind of work that is easy to skip and expensive to skip. &lt;code&gt;UNIQUE(session_id, experiment_id)&lt;/code&gt; means a session is assigned once to a given experiment. Without it, a retry or a duplicated provisioning call would let one session contribute two rows to the same arm, which inflates the sample count with correlated data and makes the test more confident than the evidence warrants. The group check keeps the result set to treatment or control, so a typo cannot open a third bucket that then silently reduces the size of both real arms.&lt;/p&gt;

&lt;p&gt;The migration also adds indexes for task type, status, active experiments, assignment lookup, session lookup, and group lookup. The write path needs to find active tests during provisioning. The read path needs to count assignments when judging results. Those are different access patterns, so the schema names both.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Outcomes are measured after the task
&lt;/h2&gt;

&lt;p&gt;Retrieval can produce tidy scores before the agent acts. Rank, similarity, token fit, and document presence all arrive early. None of them says whether the work succeeded.&lt;/p&gt;

&lt;p&gt;This is the gap the title is about, and it is worth stating plainly rather than implying it. The standard instruments for retrieval quality — recall at k, mean reciprocal rank, normalised discounted cumulative gain — all score a ranking against a set of documents somebody labelled relevant in advance. They answer whether retrieval found what a human said was relevant. Agent memory has to answer something else: whether the material changed what the agent did. Those questions come apart precisely where the interesting failures live. A document can be topically relevant, rank first, be judged relevant by any labeller, and still cost the agent attention it needed elsewhere. Recall at k cannot see that, because the harm is not in the ranking. It is in what happened next.&lt;/p&gt;

&lt;p&gt;Asking a model to grade the retrieval has the same shape. It scores the plausibility of the material against the request, which is a judgement made before the work and without knowing how the work went. Both instruments measure the retrieval, and both stop exactly where the question starts.&lt;/p&gt;

&lt;p&gt;The migration defines &lt;code&gt;get_experiment_outcomes&lt;/code&gt;. It joins experiment assignments to recorded outcomes, grouped by branch. The function returns group name, total sessions, successful sessions, success rate, and average time in minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_experiment_outcomes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_experiment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;group_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_sessions&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;successful_sessions&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;success_rate&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avg_time_minutes&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_name&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;successful_sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;FLOAT&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;success_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time_minutes&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;FLOAT&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_time_minutes&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;zcl_experiment_assignments&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;zcl_outcomes&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_experiment_id&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the layer stops being retrieval self-scoring. An inclusion hypothesis can be judged on whether the session succeeded; an order hypothesis is better judged on how long it took. Either way the metric was declared up front, and the outcome table supplies whatever was actually measured.&lt;/p&gt;

&lt;p&gt;There is a floor under all of this that randomization cannot reach. &lt;code&gt;zcl_outcomes.success&lt;/code&gt; is a bare &lt;code&gt;BOOLEAN NOT NULL&lt;/code&gt;, and nothing in the schema says who decides it or on what evidence. Randomizing the assignment removes the confound between the user and the branch they got. It does nothing whatsoever about a mismeasured outcome. If success is set by a heuristic, or reported by the same person whose skill I was trying to control for in the first place, then the bias I built this entire layer to remove walks back in through the dependent variable — and this time the system reports it with a p-value attached, which makes it harder to argue with rather than easier.&lt;/p&gt;

&lt;p&gt;If I want to judge some other behavior later, the outcome schema has to carry it first, or the candidate cannot be promoted inside this loop at all. It is a narrow door to walk through, and I would still rather have it than a soft win assembled from whatever trace happened to be lying nearby.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Minimum samples slow the system on purpose
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ActiveLearning&lt;/code&gt; sets &lt;code&gt;alpha = 0.05&lt;/code&gt;, with the code comment &lt;code&gt;p &amp;lt; 0.05 for significance&lt;/code&gt;. The result object carries &lt;code&gt;effect_size&lt;/code&gt;, &lt;code&gt;p_value&lt;/code&gt;, &lt;code&gt;significant&lt;/code&gt;, and &lt;code&gt;recommendation&lt;/code&gt;. Evaluation compares the two arms' success counts with a chi-square test on a two-by-two contingency table, and the migration includes &lt;code&gt;experiment_ready_for_analysis&lt;/code&gt;, which checks whether an experiment has enough samples before evaluation.&lt;/p&gt;

&lt;p&gt;This is the guardrail I wanted most. A document can land in treatment, ride along with one successful session, and look useful if the system is hungry for reinforcement, so the threshold is supposed to make it wait.&lt;/p&gt;

&lt;p&gt;At least that is what I had been telling myself. Writing this section I went to check the number, and the number is not wired to anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Hypothesis.min_samples&lt;/code&gt; is set to 20. Nothing reads it. It sits on the dataclass, rides into the row, and not one code path consults it before a result gets judged. The gate that actually runs is in &lt;code&gt;analyze_experiment&lt;/code&gt;, which bails out only when an arm holds fewer than five outcomes, and its SQL counterpart &lt;code&gt;experiment_ready_for_analysis&lt;/code&gt; takes &lt;code&gt;p_min_per_group INTEGER DEFAULT 5&lt;/code&gt;. So the brake is five, not twenty. The twenty is decoration, and it is the kind that looks clean and fails quietly.&lt;/p&gt;

&lt;p&gt;Five per arm cannot carry the conclusion the code is willing to draw from it. Work the table: five sessions in each branch, chi-square at alpha 0.05, and exactly two of the thirty-six possible outcomes clear significance. Five successes against zero. Zero against five. Both land at p equal to 0.011, and every other cell in that table is a null before the experiment starts. At this size the test is not measuring an effect. It is asking whether the two arms disagreed about every single session, which is not the question I meant to ask.&lt;/p&gt;

&lt;p&gt;Chi-square is the wrong instrument here anyway. It wants expected cell counts of about five or more, and a two-by-two table built from five observations per arm cannot give it that. Fisher's exact test handles tables this small and the swap is one line. Raising the gate to the twenty already written on the object does not rescue it either — at twenty per arm the smallest difference the test can see is still around thirty percentage points.&lt;/p&gt;

&lt;p&gt;Here is the scale I should have worked out before writing any of it. A ten point improvement in success rate, 0.50 to 0.60, at eighty percent power and the same alpha, needs roughly 194 sessions per arm. Five points needs about 782. Those are the numbers at which the words sitting in &lt;code&gt;recommendation&lt;/code&gt; — ADOPT, REJECT — mean what they claim. Ten points is a substantial win for a context change. This layer would need forty times the evidence it currently demands to notice one.&lt;/p&gt;

&lt;p&gt;There is a related exposure in how many tests run at once. The generator can open five order hypotheses and three inclusion hypotheses for a single task type. Eight independent tests at alpha 0.05 carry roughly a 34 percent chance that at least one clears significance by luck alone, and the one that clears is the one that gets promoted into every future session's context. A Bonferroni correction, or simply refusing to run more than one live experiment per task type at a time, is the cheap fix.&lt;/p&gt;

&lt;p&gt;One thing the design gets right by accident of structure is worth crediting, because it is the error most A/B systems make. &lt;code&gt;analyze_experiment&lt;/code&gt; completes the experiment as soon as it evaluates it. There is no path that looks at the data, finds nothing, and looks again next week. Repeated peeking at an accumulating result is how a nominal five percent false positive rate becomes twenty or thirty percent in practice, and this loop cannot do it: one look, then the experiment closes. The cost of that virtue is that the single look happens at the earliest moment it is permitted, which is also the moment with the least evidence behind it.&lt;/p&gt;

&lt;p&gt;Put those two facts side by side and it is worse than slow learning. A completed experiment never reopens; &lt;code&gt;_complete_experiment&lt;/code&gt; writes status completed and nothing anywhere sets it back to active. At five per arm almost every outcome is INCONCLUSIVE by construction. So a context change that genuinely helps gets one underpowered look, comes back as no significant difference because it could hardly come back as anything else, and is closed on that basis permanently. I built the threshold to stop the system adopting noise. What it actually does is retire real improvements the test was never equipped to detect, and there is no route back to retry one.&lt;/p&gt;

&lt;p&gt;The exploration rate decides where this lands hardest, and it picks the worst place. &lt;code&gt;_get_task_uncertainty&lt;/code&gt; returns maximum uncertainty for any task type with fewer than ten recorded sessions, which pushes the exploration probability straight to its 0.4 ceiling, and that count is scoped per organization. A small organization therefore runs the largest share of its sessions as trials while generating the fewest sessions to finish any of them with. It experiments hardest precisely where five outcomes per arm takes longest to accumulate, and every one of those trials still closes after a single look.&lt;/p&gt;

&lt;p&gt;The stored result hides that rather than surfacing it. &lt;code&gt;effect_size&lt;/code&gt; goes into the results JSON as a bare difference between two success rates, with no interval around it, so a gap measured on five sessions per arm is recorded in the same shape, and reads later with the same authority, as one measured on five hundred.&lt;/p&gt;

&lt;p&gt;Waiting costs something. Bad ideas stay alive longer, good ones take more sessions before they become default behavior, and some combinations never get tested at all because the generator cut them off early. That pressure is almost certainly how the number ended up at five: a gate that low returns verdicts fast, and returning verdicts fast is the entire problem with it.&lt;/p&gt;

&lt;p&gt;The operational states are active, completed and cancelled. An active experiment can still receive assignments and a completed one can store results, but the useful one is cancelled: it stops shaping sessions without pretending it ever reached a conclusion. The view &lt;code&gt;v_experiment_results&lt;/code&gt; puts treatment counts, control counts, status, and timestamps together so the operator can see whether a run is still gathering evidence or ready for judgment.&lt;/p&gt;

&lt;p&gt;That is the trade I made for agent memory and retrieval: learn slower, record the branch, measure after work, and require enough samples before future sessions change. Three of those four are built and working. The fourth is a number I wrote on an object and never wired up, and writing this post is what made me look.&lt;/p&gt;

&lt;p&gt;The rule I started with still holds. A learning system that changes future inputs has to earn that change with outcomes, or it will preserve coincidence as policy. Randomization is what makes an outcome mean anything. The sample threshold is what stops it being noise. I built the first one properly. The second I wrote down on a dataclass and never wired up, so the system will go on drawing conclusions from five sessions and filing them with the same confidence it would give five hundred.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agentmemory</category>
      <category>experimentation</category>
      <category>retrieval</category>
      <category>zerocontextloss</category>
    </item>
  </channel>
</rss>
