<?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: Renga</title>
    <description>The latest articles on DEV Community by Renga (@renga154).</description>
    <link>https://dev.to/renga154</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%2F4073404%2Faad84e8a-9902-418f-9a69-8f2490746d86.PNG</url>
      <title>DEV Community: Renga</title>
      <link>https://dev.to/renga154</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/renga154"/>
    <language>en</language>
    <item>
      <title>A finished revision can still leave a stale review copy</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Wed, 04 Nov 2026 04:51:35 +0000</pubDate>
      <link>https://dev.to/renga154/a-finished-revision-can-still-leave-a-stale-review-copy-4g2k</link>
      <guid>https://dev.to/renga154/a-finished-revision-can-still-leave-a-stale-review-copy-4g2k</guid>
      <description>&lt;p&gt;A technical article had been revised locally, and the completion record contained the revised file's hash. The cloud copy provided for review still contained the earlier draft.&lt;/p&gt;

&lt;p&gt;The URL had not changed. That did not establish which text it contained. Comparing the local revision and the review copy as separate artifacts revealed the mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  A filename and a URL do not identify the revision
&lt;/h2&gt;

&lt;p&gt;In the saved records from October 20, 2026, the revised local draft was 5,387 bytes. The review copy was 3,171 bytes. The revision added information about the verification scope, while the review copy retained the earlier explanation.&lt;/p&gt;

&lt;p&gt;The entire review text matched the preserved old draft. The current local manuscript matched the SHA-256 recorded when the revision was completed.&lt;/p&gt;

&lt;p&gt;Those checks established two different states: the local revision existed, and the delivered copy matched the old draft. An accessible URL and an unchanged filename did not distinguish them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the original, the intended snapshot, and the readback
&lt;/h2&gt;

&lt;p&gt;For this UTF-8 Markdown workflow, three artifacts have different jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working original&lt;/td&gt;
&lt;td&gt;The document that can continue to change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manuscript snapshot&lt;/td&gt;
&lt;td&gt;The exact text intended for this handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text read back from the review copy&lt;/td&gt;
&lt;td&gt;An observation of what the destination contains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After updating the review copy, the readback string was encoded as UTF-8 and compared with every byte of the manuscript snapshot. The reported stored size was also 5,387 bytes, and the SHA-256 values matched.&lt;/p&gt;

&lt;p&gt;The simplified check looks like this. This is explanatory pseudocode with the retrieval API and error handling omitted, not a newly validated distribution tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected = bytes of the manuscript snapshot intended for review
observed = complete review-copy text encoded as UTF-8

Check every byte of observed against expected
Compare the reported stored size with the length of expected
Record the SHA-256 of the compared text and the check time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This evidence is a comparison of retrieved UTF-8 text and reported size. It should not be described as a separate raw binary download test of the cloud file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check history and sharing separately from content
&lt;/h2&gt;

&lt;p&gt;Before the update, the existing review text was checked against the preserved old draft. The content was then updated under the same file ID. No new public link or sharing destination was created.&lt;/p&gt;

&lt;p&gt;The saved records show that the filename, ID, parent location, and nonshared permission state remained unchanged. They also show more than an old revision appearing in a history list: that revision was read back and its complete text matched the preserved old manuscript.&lt;/p&gt;

&lt;p&gt;That establishes that the earlier revision was readable at the time of the operation. It does not promise indefinite history retention. The comparison before writing is also not an atomic lock. Safe handling of an edit arriving between the comparison and the write was not established.&lt;/p&gt;

&lt;p&gt;If the destination differs from the preserved draft in an unexpected way, there is insufficient evidence to call it merely stale and replace it. Preserve both versions and resolve which one should be used. Avoiding automatic resends after an uncertain outcome is an operational rule here, not the result of a network-fault injection test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text identity is not factual review or publication approval
&lt;/h2&gt;

&lt;p&gt;A hash or full-text comparison helps identify what was handed over. It does not establish that the claims are correct, that the prose is clear, or that the author has approved publication.&lt;/p&gt;

&lt;p&gt;For this article, I rechecked the saved October 20 operation records and readback text against the manuscripts retained locally. This is not a fresh inspection of the current cloud document. The development tests discussed in the underlying manuscript were not rerun either.&lt;/p&gt;

&lt;p&gt;Revising the original does not prove that the review copy was updated. Linking the intended snapshot to the text read back from its destination made that handoff mismatch observable and allowed it to be resolved in this case.&lt;/p&gt;

&lt;p&gt;AI assisted the operation's comparison process, the organization of the saved records, and the organization and writing of this article.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>An unknown note category should stop the write</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:19:33 +0000</pubDate>
      <link>https://dev.to/renga154/an-unknown-note-category-should-stop-the-write-5cgi</link>
      <guid>https://dev.to/renga154/an-unknown-note-category-should-stop-the-write-5cgi</guid>
      <description>&lt;p&gt;Reorganizing notes into content categories does not update the code that saves the next note. If that code still defaults to the old shared folder, it can recreate the location you just retired.&lt;/p&gt;

&lt;p&gt;My AI assistant had a note tool that appended to a project folder when a project was supplied, and to a shared location otherwise. After reorganizing the notes, I changed that fallback into an explicit destination check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new default would keep the same ambiguity
&lt;/h2&gt;

&lt;p&gt;Replacing the old shared path with the root of the new knowledge folder would still collect unclassified notes in one place.&lt;/p&gt;

&lt;p&gt;Instead, a save without a project must specify an existing content category. Missing both inputs, supplying an unknown category, or supplying both a project and a category now produces an error without saving.&lt;/p&gt;

&lt;p&gt;The implementation follows this simplified order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trim the project and category names
  → Reject both being supplied
  → Keep the existing project route when a project is supplied
  → Otherwise check the category against the existing allowlist
  → Reject a missing or unknown category
  → Only then resolve the destination, create directories, and append
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checks sit before directory creation. That gives the tests a useful negative condition: an unclassified note must not create even the save root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the tool contract and the writer in agreement
&lt;/h2&gt;

&lt;p&gt;I added the category enum to the model-facing tool definition, updated its Japanese and English descriptions, and passed the category through the tool consumer to the saving function.&lt;/p&gt;

&lt;p&gt;The saving function also checks the allowlist. A description telling the model to choose a category is not the only place enforcing the rule. When both a project and a category arrive, the writer rejects the ambiguity rather than silently choosing one.&lt;/p&gt;

&lt;p&gt;This establishes a no-write condition and a model-facing instruction. It does not demonstrate that an interactive clarification screen has worked in the real app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test refusal as well as successful appends
&lt;/h2&gt;

&lt;p&gt;The saved tests use temporary folders and dummy data:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input or operation&lt;/th&gt;
&lt;th&gt;Verified result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Neither project nor category&lt;/td&gt;
&lt;td&gt;Error; the save root is not created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown or path-like category&lt;/td&gt;
&lt;td&gt;Error; the save root is not created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both project and category&lt;/td&gt;
&lt;td&gt;Error; no destination is silently preferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Append to an existing note in a valid category&lt;/td&gt;
&lt;td&gt;Existing filename and original byte prefix remain; the addition is present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category supplied through the tool consumer&lt;/td&gt;
&lt;td&gt;Writes into the selected content category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project supplied through the tool consumer&lt;/td&gt;
&lt;td&gt;Keeps the existing project route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japanese and English tool definitions&lt;/td&gt;
&lt;td&gt;Category input and contract consistency are checked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The assistant-side result is 57 passing tests across three files, including existing tests. These are not 57 newly added tests. The main-layer TypeScript typecheck also passed.&lt;/p&gt;

&lt;p&gt;A separate mock test covers the related one-way note generator. It runs the actual generation body with a temporary home and guarded filesystem access. It checks the new category destination, the location of the entry note, preservation of manually written originals, and absence of the retired folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applied source is a different result from working software
&lt;/h2&gt;

&lt;p&gt;I reviewed the saved patch and results from October 21, 2026, Japan time. The record covers three runtime source files and four related test files, with hashes confirming that the applied bytes matched the tested bytes. I did not rerun these tests for this article.&lt;/p&gt;

&lt;p&gt;The record does not establish a packaged build, a running assistant saving notes in the real vault, or a real synchronization run. The generator's existing cleanup behavior was not redesigned. Before a real sync, preserving manual additions made after the move still needs separate checking.&lt;/p&gt;

&lt;p&gt;Rejecting path-like category strings also does not prove safety against every path operation, symlink, or concurrent write.&lt;/p&gt;

&lt;p&gt;The change is small: when a note's destination cannot be determined, a convenient default must not carry it into a write. The rule belongs in the writer and its no-write tests, as well as in the instructions given to the model.&lt;/p&gt;

&lt;p&gt;This article summarizes implementation and saved validation records. AI assisted the implementation, validation, and the organization and writing of this article.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A generated video still needs a receipt and a stopped runtime</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:44:26 +0000</pubDate>
      <link>https://dev.to/renga154/a-generated-video-still-needs-a-receipt-and-a-stopped-runtime-1lgh</link>
      <guid>https://dev.to/renga154/a-generated-video-still-needs-a-receipt-and-a-stopped-runtime-1lgh</guid>
      <description>&lt;p&gt;The five-second video existed, but the embedded UI had not caught up. Its completion message and receipt field did not update, and its exit button remained disabled.&lt;/p&gt;

&lt;p&gt;This happened in a private prototype running Wan2.2 TI2V-5B on an L4 in Google Colab. The useful result was more specific than “the tool worked”: generation, receipt, and runtime release each had separate evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the existing job before submitting another
&lt;/h2&gt;

&lt;p&gt;The trial generated one silent clip of a paper boat moving across water. It used Python 3.12.13, fixed dependencies, and model files whose hashes had been checked.&lt;/p&gt;

&lt;p&gt;The worker had exited successfully. Rather than submit another generation, the operator used a helper path that read the existing job's state and exported its output. That recovered the MP4 and its settings JSON. The cause of the stale UI remains unconfirmed.&lt;/p&gt;

&lt;p&gt;A button click, a completed worker, and a file received by the user are different events. A successful worker exit does not establish that the file reached the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  A matching hash answers a different question from a valid video
&lt;/h2&gt;

&lt;p&gt;The received MP4 was 571,335 bytes; its settings JSON was 2,301 bytes. Their sizes and SHA-256 hashes matched the generation-side receipt records.&lt;/p&gt;

&lt;p&gt;That established byte identity. Media validation checked the requested output separately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Recorded result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Video encoding&lt;/td&gt;
&lt;td&gt;H.264 / yuv420p&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;832 × 480&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame rate and count&lt;/td&gt;
&lt;td&gt;24 fps, 120 frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration and audio&lt;/td&gt;
&lt;td&gt;5.000 seconds, no audio stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode&lt;/td&gt;
&lt;td&gt;All frames decoded without errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The generation graph handled 121 frames; the export used the first 120 to meet the receipt specification. The graph's frame count and the delivered file's frame count were recorded separately.&lt;/p&gt;

&lt;p&gt;For this article, I checked that the current MP4 and settings JSON still match the saved receipt hashes. I did not rerun generation or the full decode. Successful decoding also does not establish the clip's overall visual or motion quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stopped engine does not establish runtime release
&lt;/h2&gt;

&lt;p&gt;The saved shutdown checks showed no remaining owned generation-engine process, a closed listening endpoint, and a successful worker exit.&lt;/p&gt;

&lt;p&gt;Those checks did not establish that Colab had released the runtime. Automatic disconnection could not be confirmed after receipt verification, so the runtime was disconnected and deleted through Colab's normal menu. The resource panel then showed zero sessions, and the session manager was empty.&lt;/p&gt;

&lt;p&gt;The interval from the pre-allocation clock to release confirmation was about nineteen minutes in this one trial. It is not a completion-time estimate for another model or environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep completion states small enough to prove
&lt;/h2&gt;

&lt;p&gt;The trial supports five separate claims:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation finished&lt;/td&gt;
&lt;td&gt;Successful worker exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The output met the media specification&lt;/td&gt;
&lt;td&gt;Media metadata and the saved full-decode check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same output was received&lt;/td&gt;
&lt;td&gt;Matching MP4 and settings sizes and hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The generation engine stopped&lt;/td&gt;
&lt;td&gt;Owned-process and listening-endpoint checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The runtime was released&lt;/td&gt;
&lt;td&gt;Zero-session checks after normal deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The UI's completion display, automatic shutdown, and continuous playback in a browser remain unverified or unresolved. Recovering a valid video is a narrower result than completing the entire GUI workflow.&lt;/p&gt;

&lt;p&gt;This article draws on implementation and saved verification records. AI assisted development, verification, and the organization and writing of the article.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A CSV comparison result expires when its inputs change</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:58:51 +0000</pubDate>
      <link>https://dev.to/renga154/a-csv-comparison-result-expires-when-its-inputs-change-5860</link>
      <guid>https://dev.to/renga154/a-csv-comparison-result-expires-when-its-inputs-change-5860</guid>
      <description>&lt;p&gt;A CSV comparison has more inputs than two files. It also depends on the encoding, the key columns, the column mappings, and the fields the user wants to protect.&lt;/p&gt;

&lt;p&gt;After a comparison finishes, the user can change any of those inputs. If the old result remains visible until the next comparison finishes, the UI can show a new filename next to an old diff. An enabled save button can turn that mismatch into a report.&lt;/p&gt;

&lt;p&gt;A private browser-local CSV prototype handles this by expiring results as soon as their inputs change. This article describes its implementation and stored test records. It does not describe a customer incident or a public product launch.&lt;/p&gt;

&lt;p&gt;The prototype runs previews, comparisons, and HTML report generation in Workers. A controller on the UI side owns result state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the expiration rules first
&lt;/h2&gt;

&lt;p&gt;Consider this illustrative sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a comparison using configuration A.&lt;/li&gt;
&lt;li&gt;Change the key column to configuration B before A finishes.&lt;/li&gt;
&lt;li&gt;Receive A's result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result can be internally correct and still be unusable in the current UI. The relevant question is which state changes invalidate it.&lt;/p&gt;

&lt;p&gt;The prototype uses these rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Completed comparison&lt;/th&gt;
&lt;th&gt;Unsaved report&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Change a file, encoding, key, mapping, or protected field&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change only the case name&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;td&gt;Clear and regenerate with the new name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start another comparison&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancel or reset&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;td&gt;Clear&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The case name is report metadata. It does not participate in the diff calculation, so a completed comparison can survive a name change. Cancel keeps the selected files; reset removes their references too.&lt;/p&gt;

&lt;p&gt;These transitions clear data from application state. The result display and save button derive their availability from that same state. Hiding a panel while keeping an old report available elsewhere would leave the expiration rule incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  An input epoch and a job token answer different questions
&lt;/h2&gt;

&lt;p&gt;Each input change increments an &lt;code&gt;epoch&lt;/code&gt;. A job receives the current epoch when it starts, and the state reducer rejects an event from another epoch:&lt;/p&gt;

&lt;p&gt;This epoch tracks input and state generations. Changing the case name, canceling, or resetting also advances it. Rerunning a comparison without changing the inputs does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// State-update excerpt: events carry the epoch used to start the job.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rejects configuration A's result after the user moves to B. It does not distinguish two runs started with identical inputs: both can have the same epoch.&lt;/p&gt;

&lt;p&gt;The controller therefore assigns each job a separate &lt;code&gt;token&lt;/code&gt;. The left preview, right preview, comparison, and report have separate slots. A slot accepts only the token it currently owns.&lt;/p&gt;

&lt;p&gt;The controller captures the token in the callback; it is not a field returned by the Worker. With an &lt;code&gt;input&lt;/code&gt; containing the current epoch prepared, the comparison-start code registers the job in its slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextToken&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onCompareEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback is expected to run asynchronously after slot registration. In this implementation, a Worker response triggers it; even Worker-construction failure is deferred with &lt;code&gt;queueMicrotask&lt;/code&gt;. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage" rel="noopener noreferrer"&gt;MDN's Worker.postMessage reference&lt;/a&gt; describes the message mechanism.&lt;/p&gt;

&lt;p&gt;The response callback then checks its captured token against the current slot. These are excerpts, with input validation, cancellation of old jobs, and Worker creation omitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onCompareEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JobEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disposed&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;token&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="nx"&gt;handles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reduceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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 reducer still checks the epoch after the token check. The epoch identifies the input generation. The token identifies the particular operation the application currently accepts.&lt;/p&gt;

&lt;p&gt;Reports need the same ownership rule because accepting a report can trigger a side effect. Starting another comparison cancels an in-flight report. An old report callback must not reach the function that initiates saving, even when the inputs and epoch have not changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation and acceptance have separate checks
&lt;/h2&gt;

&lt;p&gt;The implementation terminates a Worker when its job is canceled. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate" rel="noopener noreferrer"&gt;Worker.terminate&lt;/a&gt; stops the Worker immediately, without giving the running operation a chance to finish.&lt;/p&gt;

&lt;p&gt;The controller separately specifies which callbacks it accepts. That rule can be tested without depending on a real Worker's termination behavior. This is not evidence that a browser delivers messages after Worker.terminate() has been called; the test uses a fake job deliberately capable of producing a canceled job's response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the event order directly
&lt;/h2&gt;

&lt;p&gt;The unit-test harness records cancellation but still lets a test emit a response afterward. This makes the order deterministic; it does not require a large CSV or a carefully chosen delay.&lt;/p&gt;

&lt;p&gt;The stored tests cover these sequences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sequence supplied by the test&lt;/th&gt;
&lt;th&gt;Required outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start comparison A → change to B → emit A's response&lt;/td&gt;
&lt;td&gt;No comparison or report reappears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start a preview → reset → emit the old preview&lt;/td&gt;
&lt;td&gt;Initial state remains free of that preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start a report → cancel → emit its response&lt;/td&gt;
&lt;td&gt;No comparison or generated report remains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start a report → rerun comparison with the same inputs → emit the old report&lt;/td&gt;
&lt;td&gt;The save callback is not called despite the matching epoch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cancel case checks result state; it does not directly monitor calls to the save callback. The last sequence exercises operation ownership within one epoch. The test checks that the array recording calls to the save callback stays empty after the old report responds.&lt;/p&gt;

&lt;p&gt;Stored browser tests check the visible contract too: changing the file, encoding, key, mapping, or protected fields, and resetting, removes old output and the save button. A separate case verifies that changing only the case name keeps the completed comparison and puts the new name into the generated report.&lt;/p&gt;

&lt;p&gt;These checks cover specific transitions. They do not prove every possible scheduling race or confirm that the browser finished writing a download to disk. For this article, I reviewed the implementation and existing logs; I did not rerun the tests.&lt;/p&gt;

&lt;p&gt;Treat an input change as one operation that clears expired output, invalidates stored result data, stops obsolete work, and rejects obsolete responses. The comparison then stays attached to the inputs it actually describes.&lt;/p&gt;

&lt;p&gt;AI assisted development, verification, and the organization and writing of this article. The claims above are grounded in implementation and test records; they do not imply customer adoption or commercial results.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A no-network test needs a request that makes it fail</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:24:20 +0000</pubDate>
      <link>https://dev.to/renga154/a-no-network-test-needs-a-request-that-makes-it-fail-3ejc</link>
      <guid>https://dev.to/renga154/a-no-network-test-needs-a-request-that-makes-it-fail-3ejc</guid>
      <description>&lt;p&gt;A browser-local CSV comparison prototype developed and tested with AI assistance needed a test for network activity after file selection. Its saved records show that the first test failed because it counted local Blob worker loads as HTTP traffic. This article examines the implementation and those records to explain how an unwanted request can check the monitor itself.&lt;/p&gt;

&lt;p&gt;The general rule of breaking a check to verify that it fails appears in an &lt;a href="https://wisp.rengaworks.com/blog/checks-that-cannot-be-loosened/" rel="noopener noreferrer"&gt;earlier article&lt;/a&gt;. Here the focus is specific to browser observation: a local Blob load that was counted as traffic, and a fetch attempt that CSP blocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A count of zero looks the same when the application is quiet and when the monitor misses everything.&lt;/strong&gt; An intentional request provided a check on the monitor itself.&lt;/p&gt;

&lt;p&gt;AI assisted the development, validation, and writing. This article is based on the implementation and saved validation records; the tests were not rerun for the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  An HTTP origin inside a Blob URL is not an HTTP request
&lt;/h2&gt;

&lt;p&gt;The prototype loads static assets at startup, then parses and compares CSV files in a Worker. The test treats startup asset loading separately from activity after file selection.&lt;/p&gt;

&lt;p&gt;The observation log also contained local Worker loads using Blob URLs. Their text includes the creator's origin. Blob URLs represent resources in memory, so an HTTP origin appearing inside one does not establish a server request. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/blob" rel="noopener noreferrer"&gt;MDN's Blob URL reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test counted the prototype's same-origin Blob loads separately. It kept HTTP, HTTPS, and unexpected URLs subject to the original checks. This excerpt shows the classification idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blob:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;appOrigin&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local-blob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unexpected&lt;/span&gt;&lt;span class="dl"&gt;"&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;Classification is only one part of observation. It does not establish privacy by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A blocked call is still an attempt
&lt;/h2&gt;

&lt;p&gt;Counting successful requests alone can miss an API call blocked by Content Security Policy. The &lt;code&gt;connect-src&lt;/code&gt; directive controls connections made through interfaces such as fetch, XHR, and WebSocket. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src" rel="noopener noreferrer"&gt;MDN's connect-src reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The prototype's requirement was zero HTTP requests after CSV selection and zero observed network or storage API attempts. The test therefore combined request events with API-call observations and CSP violation events. Its request observations used Playwright's BrowserContext events. &lt;a href="https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request" rel="noopener noreferrer"&gt;Playwright documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An intentional fetch made the test fail
&lt;/h2&gt;

&lt;p&gt;A temporary &lt;code&gt;fetch('/privacy-probe', ...)&lt;/code&gt; was inserted into the comparison path. It used a fixed synthetic string and the local test server. It did not use real CSV data or an external destination.&lt;/p&gt;

&lt;p&gt;CSP blocked the call. The monitor still recorded fetch attempts and CSP violations, and the zero-attempt assertion failed. The test was capable of rejecting that blocked call.&lt;/p&gt;

&lt;p&gt;After removing the temporary code and rebuilding production assets, the saved observation record contained:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial HTTP asset requests&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP requests after CSV selection&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same-origin local Blob loads&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network or storage API attempts&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The workflow covered file selection, comparison, report download, configuration changes, cancellation, reruns, and reset. The intentional failure and the normal build's success were recorded separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  One mutation does not establish coverage of every path
&lt;/h2&gt;

&lt;p&gt;The evidence establishes detection of the fetch inserted for this check. It does not establish detection of every API or a call made immediately after Worker initialization. Hook timing and observation coverage need their own checks as the monitored paths expand.&lt;/p&gt;

&lt;p&gt;These are Chrome validation records using synthetic data. They do not establish the safety of browser extensions or the operating system, or secure erasure of browser memory.&lt;/p&gt;

&lt;p&gt;For a no-network requirement, keep the evidence that made the monitor fail alongside its zero-count result.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>"Zero matches" and "couldn't read the file" look identical</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:10:29 +0000</pubDate>
      <link>https://dev.to/renga154/zero-matches-and-couldnt-read-the-file-look-identical-2c2b</link>
      <guid>https://dev.to/renga154/zero-matches-and-couldnt-read-the-file-look-identical-2c2b</guid>
      <description>&lt;p&gt;I wrote the checks before touching any of the 671 items I had to fix. &lt;strong&gt;So I wouldn't start fixing by feel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I spent the rest of the project finding defects in my own checks. &lt;strong&gt;They all looked correct when I wrote them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the check before the fix
&lt;/h2&gt;

&lt;p&gt;Reverse the order and it fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix first and the check quietly loosens to match what you produced.&lt;/strong&gt; Your own approach becomes the standard.&lt;/p&gt;

&lt;p&gt;I froze the standard in code without touching a single item first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let the allowlist become an amnesty
&lt;/h2&gt;

&lt;p&gt;Existing matches go into an allowlist. But &lt;strong&gt;both directions must be errors.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A new match &lt;strong&gt;not&lt;/strong&gt; in the allowlist&lt;/td&gt;
&lt;td&gt;the standard has slipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A line still in the allowlist after being fixed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;you only think it's fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one is the load-bearing half. Without it the allowlist never shrinks. &lt;strong&gt;With it, the list decreases monotonically.&lt;/strong&gt; Mine went 802 → 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no command to add entries.&lt;/strong&gt; If you can add, you have a route to escape instead of fix.&lt;/p&gt;

&lt;p&gt;When it empties, &lt;strong&gt;keep the empty file.&lt;/strong&gt; Delete it and the next match starts with "create somewhere to put it." An empty file makes &lt;strong&gt;fix, not add&lt;/strong&gt; the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate and the working view must be different tools
&lt;/h2&gt;

&lt;p&gt;The check only reports matches outside the allowlist. It's a gate — pass or fail.&lt;/p&gt;

&lt;p&gt;Which means &lt;strong&gt;the person doing the fixing cannot see what's in the allowlist.&lt;/strong&gt; That needs a separate command that ignores the list and dumps contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  You cannot see the defects in your own tooling
&lt;/h2&gt;

&lt;p&gt;I handed it to five people and two defects in my check surfaced immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number it displayed and the number it judged on were different.&lt;/strong&gt; Display was raw character count; the judgement excluded identifiers. &lt;strong&gt;Fix by reading the display and you miss.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It stripped punctuation before comparing.&lt;/strong&gt; Strings that only joined up across a sentence boundary were flagged as copied, which pushed people to mangle perfectly natural prose. Replacing punctuation with a separator token means it only matches when the other side has a break in the same place.&lt;/p&gt;

&lt;p&gt;Right after that fix, matches went &lt;strong&gt;0 → 16.&lt;/strong&gt; The separator was being counted toward match length. &lt;strong&gt;A separator is not content.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Carve exceptions at "there is no other way to say it"
&lt;/h2&gt;

&lt;p&gt;Identifiers (&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;) and raw data (log lines) cannot be paraphrased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exclude them from both the length calculation and the text comparison.&lt;/strong&gt; Otherwise things that need no fixing get flagged, and the attempt to fix them makes the writing worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Zero matches" and "couldn't read it" are indistinguishable
&lt;/h2&gt;

&lt;p&gt;This is the dangerous one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero items scanned also prints "zero matches."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I once misread a YAML key name, scanned nothing, and got a green check. &lt;strong&gt;Fail loudly when you cannot read the input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same reasoning: &lt;strong&gt;distrust the counting tool itself.&lt;/strong&gt; My first aggregation used a regex, missed YAML block scalars, and every value was wrong. I nearly reported those numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it fail before you believe it watches
&lt;/h2&gt;

&lt;p&gt;I wrote a guard and &lt;strong&gt;mutation testing passed twice in a row.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searching by the &lt;strong&gt;shape&lt;/strong&gt; of the call missed a second code path. Anything recorded more than one way slips past a shape match&lt;/li&gt;
&lt;li&gt;Matching on name alone then matched &lt;strong&gt;the definition file itself&lt;/strong&gt;. "Is defined" got read as "is used." Exclude the definition side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both looked right when written. &lt;strong&gt;Until you break it and watch it fail, it is not guarding anything.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile what's written against what runs
&lt;/h2&gt;

&lt;p&gt;Policies, terms, and help text drift the day you change the implementation. &lt;strong&gt;Nothing breaks. The page renders. Tests pass. You find out when someone tells you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just before adding analytics, my privacy policy said cookies were used &lt;strong&gt;only&lt;/strong&gt; for keeping you signed in. It became a lie the moment I added it.&lt;/p&gt;

&lt;p&gt;The guard has to fail &lt;strong&gt;if either side changes alone.&lt;/strong&gt; Check "is the tracking code present" against "does the policy say so" in both directions. One direction misses either the stale text or the missing text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't grep HTML and conclude it isn't rendering
&lt;/h2&gt;

&lt;p&gt;Verifying production, I grepped for &lt;code&gt;¥[0-9,]+&lt;/code&gt; and concluded the price wasn't showing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React emits &lt;code&gt;¥&lt;/code&gt; and the number as separate nodes&lt;/strong&gt;, so the HTML reads &lt;code&gt;¥&amp;lt;!-- --&amp;gt;14,800&lt;/code&gt; and never matches as one string. Zero-width spaces inserted by Japanese typesetting do the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the rendered string (&lt;code&gt;innerText&lt;/code&gt;) instead.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some defects only appear when it runs
&lt;/h2&gt;

&lt;p&gt;A missing CSP allowance blocks exactly one request while the page works normally. Reading the code will never show you that.&lt;/p&gt;

&lt;p&gt;That an analytics tag fires an image beacon at a different domain was not knowable until I put a real ID in and opened the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the check first.&lt;/strong&gt; Reverse the order and the standard bends to the result&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both allowlist directions are errors.&lt;/strong&gt; A list that never shrinks is an amnesty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide no way to add.&lt;/strong&gt; Given an escape route, people escape&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot see the defects in your own tooling.&lt;/strong&gt; Hand it to someone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never trust "zero matches."&lt;/strong&gt; Throw when the input can't be read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch it fail.&lt;/strong&gt; Passing is not evidence of guarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile docs and code in both directions.&lt;/strong&gt; One direction misses half&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Three independent proposals won't find the error. Adding a refuter killed one of them.</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:50:43 +0000</pubDate>
      <link>https://dev.to/renga154/three-independent-proposals-wont-find-the-error-adding-a-refuter-killed-one-of-them-364h</link>
      <guid>https://dev.to/renga154/three-independent-proposals-wont-find-the-error-adding-a-refuter-killed-one-of-them-364h</guid>
      <description>&lt;p&gt;I used five AI agents to decide how to rewrite 671 quiz questions. &lt;strong&gt;Three perspectives, one refuter, one synthesizer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shape came out of a five-agent cap, but nothing in it turned out to be waste. &lt;strong&gt;Generating independent proposals is not enough. You need something whose job is to kill them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The refuter killed one of the three
&lt;/h2&gt;

&lt;p&gt;One proposal went down for this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It never ran the verification it most strongly demanded, on its own headline number&lt;/strong&gt; — it had confused the denominator with a subset.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three proposals side by side did not surface that. &lt;strong&gt;I didn't catch it either, reading them myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The refuter's instructions were explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not read this to agree with it&lt;/li&gt;
&lt;li&gt;Find the place where you can say "this will fail in execution"&lt;/li&gt;
&lt;li&gt;Suspect any number quoted without being run&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real win: the assumption all three shared
&lt;/h2&gt;

&lt;p&gt;All three &lt;strong&gt;silently assumed that the metric they measured was the same thing as the problem that had been reported.&lt;/strong&gt; Only the refuter went at that.&lt;/p&gt;

&lt;p&gt;When I actually asked the operator, the answer was two different things entirely. &lt;strong&gt;Fixing only the headline metric would have missed the target.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Splitting the perspectives buys you independence of perspective. &lt;strong&gt;It does not buy you independence of assumption.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let the synthesizer compromise
&lt;/h2&gt;

&lt;p&gt;The synthesizer was told plainly: &lt;strong&gt;discard anything the refuter killed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Allow compromise and a dead proposal comes back diluted. &lt;strong&gt;"Take the best of both" does not un-refute the reasoning that killed one of them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand everyone the same measured data, up front
&lt;/h2&gt;

&lt;p&gt;Let each agent do its own aggregation and they recount the same thing repeatedly and disagree on the results. &lt;strong&gt;I dumped the analysis to a file first and had all of them read it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On top of that: write only numbers you produced by running something. No estimates.&lt;/p&gt;

&lt;p&gt;One agent still got its own recount wrong. &lt;strong&gt;Because the report was fully reproducible, the refuter could determine that the agent was the thing that was off.&lt;/strong&gt; Shared measured data decides &lt;em&gt;who&lt;/em&gt; is wrong, not just &lt;em&gt;that&lt;/em&gt; something is.&lt;/p&gt;

&lt;h2&gt;
  
  
  It also found errors in what I supplied
&lt;/h2&gt;

&lt;p&gt;The refuter flagged &lt;strong&gt;two errors in the measured data I had handed them.&lt;/strong&gt; Both were real.&lt;/p&gt;

&lt;p&gt;Instruct it to suspect the premises it was given, and &lt;strong&gt;it catches your mistakes too.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make "what to ask" a required output field
&lt;/h2&gt;

&lt;p&gt;I required the synthesizer to emit &lt;code&gt;open_question&lt;/code&gt; — things the operator should decide.&lt;/p&gt;

&lt;p&gt;What came back: &lt;strong&gt;ask the operator which items they actually found confusing; ten minutes settles the direction.&lt;/strong&gt; That pays for itself before you gamble 4 to 70 hours.&lt;/p&gt;

&lt;p&gt;All three proposals started from one remark by the operator, and &lt;strong&gt;not one of them proposed going back and asking.&lt;/strong&gt; It doesn't appear unless you require the field.&lt;/p&gt;

&lt;h2&gt;
  
  
  I hand-copied the work split and got a quarter of it wrong
&lt;/h2&gt;

&lt;p&gt;This one is mine.&lt;/p&gt;

&lt;p&gt;Distributing 116 sites across five agents, I &lt;strong&gt;hand-copied the script's assignment output into JSON.&lt;/strong&gt; I got &lt;strong&gt;28 of them — a quarter — wrong.&lt;/strong&gt; Names that didn't exist went out; 28 real ones never did.&lt;/p&gt;

&lt;p&gt;I caught it by diffing against the real data right after launch, before a single edit landed.&lt;/p&gt;

&lt;p&gt;Three fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diff what you handed out against what generated it&lt;/strong&gt;, as the first step after launch&lt;/li&gt;
&lt;li&gt;Verify names down to &lt;strong&gt;file existence&lt;/strong&gt; before handing them over (added an &lt;code&gt;assert&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the transcription step entirely.&lt;/strong&gt; The script now emits a string you paste as-is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cause wasn't carelessness. It was that a copying step existed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the standard in a document before distributing
&lt;/h2&gt;

&lt;p&gt;Before handing out 390 rewrites, &lt;strong&gt;I did 76 myself and wrote the pattern into a document&lt;/strong&gt; — good examples, bad examples, and what must not be touched.&lt;/p&gt;

&lt;p&gt;The thing that breaks first when you split work is consistency of standard. &lt;strong&gt;One dialect per agent is worse than no standard at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Machine checks are the gate; the writing standard travels as prose. I also stated: &lt;strong&gt;do not loosen the check to pass. If it feels too strict, report it — don't edit it.&lt;/strong&gt; Never leave the gate adjustable from the inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  If everyone falls in the same hole, it's your handoff
&lt;/h2&gt;

&lt;p&gt;Six agents, one video cut each: &lt;strong&gt;every one of them hit "the font file isn't here," and solved it four different ways&lt;/strong&gt; — bare font names, embedding a font from another project, fetching from the network.&lt;/p&gt;

&lt;p&gt;I only found out because each report was required to include &lt;strong&gt;"where the instructions and reality diverged."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When all of them raise the same point, it isn't the agents. It's a gap in what you handed them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The next round, I folded the previous four failures into the instructions. &lt;strong&gt;None of the four recurred.&lt;/strong&gt; But sealing holes reveals the next ones — three agents independently fell into a different one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requiring those reports is how you learn what to hand over next time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proposals alone don't find errors. Stand up an independent refuter&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate perspectives still share assumptions.&lt;/strong&gt; Only the refuter attacks those&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let the synthesizer compromise.&lt;/strong&gt; Killed proposals return diluted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribute the same measured data first.&lt;/strong&gt; It decides who is wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell it to suspect your premises.&lt;/strong&gt; It will catch your errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require an "open questions" field.&lt;/strong&gt; It doesn't appear otherwise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never build a step that requires transcription.&lt;/strong&gt; I got a quarter wrong&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everyone in the same hole means the handoff is missing something&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Separately from this, I build a desktop AI agent called &lt;a href="https://rengaworks.gumroad.com/l/wisp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=refuter-agent" rel="noopener noreferrer"&gt;Wisp&lt;/a&gt;. It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I published three posts in one day and two of them were never seen by anyone</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:20:08 +0000</pubDate>
      <link>https://dev.to/renga154/i-published-three-posts-in-one-day-and-two-of-them-were-never-seen-by-anyone-402n</link>
      <guid>https://dev.to/renga154/i-published-three-posts-in-one-day-and-two-of-them-were-never-seen-by-anyone-402n</guid>
      <description>&lt;p&gt;I wrote eight technical posts and put them on dev.to. Then I pulled the view counts from the API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Published&lt;/th&gt;
&lt;th&gt;Posts that day&lt;/th&gt;
&lt;th&gt;Views per post&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aug 11&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aug 14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 62 / 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 16&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 21&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 22&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Every day I published once, the post got views. The one day I published three, two of them got zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not low. &lt;strong&gt;Zero. Never shown to anybody.&lt;/strong&gt; Two out of eight — a quarter of everything I had written — had never reached a single reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  I killed two explanations first
&lt;/h2&gt;

&lt;p&gt;My immediate thought was "one post per author per day." But &lt;strong&gt;believing the first plausible story is how you get this wrong&lt;/strong&gt;, so I went looking for anything else that fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Were the tags too small?
&lt;/h3&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;The zero-view post was tagged &lt;code&gt;ffmpeg, python, video, tutorial&lt;/code&gt;. &lt;strong&gt;&lt;code&gt;python&lt;/code&gt; is the 8th most-followed tag on the platform and &lt;code&gt;tutorial&lt;/code&gt; is 7th.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Big tags and still zero. Tags don't explain it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Was it published at a bad hour?
&lt;/h3&gt;

&lt;p&gt;Also no — if anything, backwards.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Views&lt;/th&gt;
&lt;th&gt;Published (US Eastern)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;00:27 — the middle of the night&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13:43 — prime time&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My best-performing post went out at the worst possible hour, and a zero-view post went out at a good one. The timing hypothesis doesn't survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only the count is left
&lt;/h2&gt;

&lt;p&gt;Not tags, not timing. The only thing that differs is &lt;strong&gt;how many posts I published that day.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forem's scoring, per the open implementation, blends roughly these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;match against tags the reader follows&lt;/li&gt;
&lt;li&gt;reactions the post has accumulated&lt;/li&gt;
&lt;li&gt;whether the reader already follows the author&lt;/li&gt;
&lt;li&gt;language match&lt;/li&gt;
&lt;li&gt;some random jitter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With zero followers and zero reactions, tags are the only lever you have.&lt;/strong&gt; Cap per-author exposure on top of that, and a same-day second post has nothing left to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zenn does the same thing, worse
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zenn has a posting-rate limit.&lt;/strong&gt; Hit it and the article simply doesn't publish.&lt;/p&gt;

&lt;p&gt;The nasty part is that &lt;strong&gt;nothing errors.&lt;/strong&gt; The git push succeeds. Only the deploy log mentions the limit, and the article URL sits at 404.&lt;/p&gt;

&lt;p&gt;I misdiagnosed this the first time as a frontmatter formatting problem, because &lt;strong&gt;the push had succeeded and I took that as evidence of publication.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a machine, not a resolution
&lt;/h2&gt;

&lt;p&gt;Deciding to post once a day isn't enough. &lt;strong&gt;You break it on the day you forget.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The publishing script now carries a 24-hour guard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ dev.to published within the last 24 hours (0h ago: "...").
  A second post the same day never reaches the feed and gets zero views.
  Wait another 23.7 hours.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zenn has the same guard, plus one more condition: &lt;strong&gt;it does not record the post as published until the URL returns 200.&lt;/strong&gt; The success of a push is not evidence that anything shipped.&lt;/p&gt;

&lt;p&gt;I tripped the guard myself within an hour of adding it. Without it, I would have produced another 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish twice in a day and one of them is discarded.&lt;/strong&gt; Nothing errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It isn't tags and it isn't timing.&lt;/strong&gt; Big tags at good hours still went to zero&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 200 from the API is not delivery.&lt;/strong&gt; Zenn returns a successful push and a 404 page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encode it, don't resolve it.&lt;/strong&gt; A countermeasure that depends on remembering fails on the day you don't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure after you ship.&lt;/strong&gt; Line up posts-per-day against views and this shape is visible immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The writing time was not what bothered me. &lt;strong&gt;The time spent writing things that were thrown away&lt;/strong&gt; was.&lt;/p&gt;

</description>
      <category>devto</category>
      <category>writing</category>
      <category>analytics</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The jitter wasn't in the interpolation. It was in the schedule.</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sat, 22 Aug 2026 01:23:52 +0000</pubDate>
      <link>https://dev.to/renga154/the-jitter-wasnt-in-the-interpolation-it-was-in-the-schedule-557p</link>
      <guid>https://dev.to/renga154/the-jitter-wasnt-in-the-interpolation-it-was-in-the-schedule-557p</guid>
      <description>&lt;p&gt;The avatar standing on my desktop looked like it was stuttering. One symptom — but it turned out to have &lt;strong&gt;three causes of completely different kinds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check them in the wrong order and you waste a lot of time. I started with interpolation, which was the wrong end.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. First, suspect overlap
&lt;/h2&gt;

&lt;p&gt;This was the big one. &lt;strong&gt;The next motion was starting before the previous one had finished.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interval was set to 5 seconds. The motion was 9 seconds long. Calling it again mid-playback resets elapsed time to zero, so it snaps back to the first frame partway through. There's no chance to settle back into the idle pose before the next one begins. That reads as "stuttering".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The symptom is jitter, but the cause is the timetable, not the curve.&lt;/strong&gt; No amount of smoothing fixes it.&lt;/p&gt;

&lt;p&gt;Two things to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the next run scheduled when the motion &lt;strong&gt;starts&lt;/strong&gt;, or when it &lt;strong&gt;finishes&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Does your "skip if already playing" guard cover &lt;strong&gt;every&lt;/strong&gt; playback path?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I got caught by the second. The guard only consulted a flag used by the &lt;code&gt;.vrma&lt;/code&gt; path, and the procedural-motion path walked straight past it. &lt;strong&gt;A gate on one of two doors is not a gate.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Then, suspect velocity discontinuity
&lt;/h2&gt;

&lt;p&gt;Eyes pick up discontinuity in &lt;strong&gt;velocity&lt;/strong&gt;, not position. Move at a constant rate and stop abruptly, and the stop itself looks like a jolt.&lt;/p&gt;

&lt;p&gt;Two common sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linear trapezoids&lt;/strong&gt; (ramping with &lt;code&gt;t / rise&lt;/code&gt;) — velocity jumps at both the start and the peak&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Math.abs(Math.sin(x))&lt;/code&gt;&lt;/strong&gt; — forms a sharp point at every zero crossing. &lt;code&gt;(1 - cos) / 2&lt;/code&gt; is the drop-in replacement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Detecting a kink without picking a threshold
&lt;/h3&gt;

&lt;p&gt;This is the practically useful part.&lt;/p&gt;

&lt;p&gt;My first attempt divided the jump by the amplitude and compared against a threshold. &lt;strong&gt;That doesn't work.&lt;/strong&gt; Larger motions are penalised, so legitimate movement trips the check.&lt;/p&gt;

&lt;p&gt;Instead, &lt;strong&gt;double the number of samples and see what happens to the jump.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;When you double the samples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A smooth curve&lt;/td&gt;
&lt;td&gt;the jump &lt;strong&gt;halves&lt;/strong&gt; — you were only sampling curvature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real kink&lt;/td&gt;
&lt;td&gt;it &lt;strong&gt;doesn't change&lt;/strong&gt; — there is an actual corner there&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Independent of amplitude and of speed. And no threshold to guess at.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Last, check whether it's visible at all
&lt;/h2&gt;

&lt;p&gt;If you've fixed the smoothness and something still feels off, &lt;strong&gt;the motion may not be visible in the first place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Movement along the depth axis — leaning forward, for instance — barely changes the image from a front-facing camera. I measured one pose where the top of the head moved &lt;strong&gt;10 px&lt;/strong&gt;. That's &lt;strong&gt;0.65%&lt;/strong&gt; of the screen height.&lt;/p&gt;

&lt;p&gt;"Something feels off" turned out to mean "effectively nothing is happening."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't measure in degrees. Measure how many pixels it moves on screen.&lt;/strong&gt; However far it rotates in 3D, if the camera can't see it move, it didn't move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the lesson down doesn't stop the repeat
&lt;/h2&gt;

&lt;p&gt;About that third one, a confession.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was already written in a comment in the same repository.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Forward/back motion runs along the screen's depth axis, so from a front view it looks like it isn't doing anything&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hit it once with a folded-arms pose and wrote that down at the time. &lt;strong&gt;Then I hit it again, somewhere else, having written it myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping the lesson in a note was not enough. &lt;strong&gt;Unless a machine fails the build, you repeat it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smoothness now has &lt;code&gt;npm run check:smooth&lt;/code&gt;, implementing the sample-doubling test directly.&lt;/p&gt;

&lt;p&gt;Visibility I still measure by hand. That hole isn't closed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suspect overlap first.&lt;/strong&gt; Jitter doesn't imply the interpolation is at fault&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Skip if playing" must see every path.&lt;/strong&gt; A gate on one door is not a gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect kinks by doubling the samples.&lt;/strong&gt; Dividing by amplitude penalises big motions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility is measured in pixels.&lt;/strong&gt; Degrees will lie to you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn lessons into tooling.&lt;/strong&gt; You will hit your own notes twice&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Separately from this, I build a desktop AI agent called &lt;a href="https://rengaworks.gumroad.com/l/wisp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=jitter-is-a-schedule-bug" rel="noopener noreferrer"&gt;Wisp&lt;/a&gt;. It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.&lt;/p&gt;

</description>
      <category>animation</category>
      <category>graphics</category>
      <category>ux</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My VRM was rejected at upload and every viewer I own said it was fine</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Fri, 21 Aug 2026 19:15:46 +0000</pubDate>
      <link>https://dev.to/renga154/my-vrm-was-rejected-at-upload-and-every-viewer-i-own-said-it-was-fine-34m7</link>
      <guid>https://dev.to/renga154/my-vrm-was-rejected-at-upload-and-every-viewer-i-own-said-it-was-fine-34m7</guid>
      <description>&lt;p&gt;I uploaded &lt;code&gt;Mira.vrm&lt;/code&gt; to VRoid Hub and it was rejected.&lt;/p&gt;

&lt;p&gt;Nothing was wrong on my machine. Every viewer opened it correctly. My own VRM validation passed.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POSITION Z: declared -0.391, actual -0.238
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The data was correct. The declaration about the data was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  glTF accessors declare their own range
&lt;/h2&gt;

&lt;p&gt;In glTF, separately from the vertex data itself, each accessor declares &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VEC3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12043&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;-0.512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.391&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.621&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.284&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spec requires that declaration to match the actual data. When it doesn't, the file is invalid — even though nothing about it looks wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you cannot catch this locally
&lt;/h2&gt;

&lt;p&gt;This is the part worth internalising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering reads the data.&lt;/strong&gt; &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; are bounds metadata; they play no part in producing pixels. So when the declaration drifts, &lt;strong&gt;the render is still perfectly correct.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open it in a viewer — fine&lt;/li&gt;
&lt;li&gt;Run your own validator — passes&lt;/li&gt;
&lt;li&gt;Ship it to a device — works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Everything you own reports "fine",&lt;/strong&gt; because this isn't the kind of information you can check by looking.&lt;/p&gt;

&lt;p&gt;The only thing that catches it is a distribution target that runs a real validator. Which means the place you find out is the moment before publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually broke
&lt;/h2&gt;

&lt;p&gt;A script that moves vertices around. On write-back it never recomputed &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Walking the chain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Bounds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input base file&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate build product&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;drifted&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final output&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;drifted&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The input was clean, which pinned the damage to the transform step. Locating &lt;em&gt;where&lt;/em&gt; it broke is what tells you the thing to fix is the script, not the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it must not touch the vertices
&lt;/h2&gt;

&lt;p&gt;Recomputing bounds changes no vertex data at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app/scripts/fix-accessor-bounds.mjs &amp;lt;file.vrm&amp;gt; &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--check&lt;/code&gt; it only inspects, exiting non-zero on drift. Without it, it repairs.&lt;/p&gt;

&lt;p&gt;Afterwards I took the sha256 of the BIN chunk and confirmed it was &lt;strong&gt;byte-identical to before the repair.&lt;/strong&gt; That's the guarantee that only the declaration moved. Skip that check and you're left unable to rule out "I broke the model while trying to fix it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the lesson down is not enough
&lt;/h2&gt;

&lt;p&gt;This is the real point.&lt;/p&gt;

&lt;p&gt;If "recompute bounds after moving vertices" only exists as a note, &lt;strong&gt;you will stop in the same place next time.&lt;/strong&gt; A countermeasure that depends on remembering fails on the day you don't remember.&lt;/p&gt;

&lt;p&gt;I wired it into &lt;code&gt;verify:release-assets&lt;/code&gt;. A broken model now means &lt;strong&gt;the build refuses to start.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I dropped in a deliberately broken model and confirmed it actually fails. &lt;strong&gt;Adding a check isn't finished until you've watched it reject something.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;This trap isn't specific to glTF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata you cannot verify by looking must be recomputed on every transform, or checked by a machine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bounds — min/max, bounding boxes&lt;/li&gt;
&lt;li&gt;Counts — count, length, total&lt;/li&gt;
&lt;li&gt;Hashes — checksums, ETags&lt;/li&gt;
&lt;li&gt;Offsets — byteOffset, index tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them look correct while being wrong. And things that look correct go unquestioned right up until you publish.&lt;/p&gt;




&lt;p&gt;Separately from this, I build a desktop AI agent called &lt;a href="https://rengaworks.gumroad.com/l/wisp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=accessor-bounds-drift" rel="noopener noreferrer"&gt;Wisp&lt;/a&gt;. That &lt;code&gt;Mira.vrm&lt;/code&gt; is its default body. It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.&lt;/p&gt;

</description>
      <category>gltf</category>
      <category>vrm</category>
      <category>threejs</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I set the font to the largest size and found the same bug eleven times</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sun, 16 Aug 2026 00:01:23 +0000</pubDate>
      <link>https://dev.to/renga154/i-set-the-font-to-the-largest-size-and-found-the-same-bug-eleven-times-496m</link>
      <guid>https://dev.to/renga154/i-set-the-font-to-the-largest-size-and-found-the-same-bug-eleven-times-496m</guid>
      <description>&lt;p&gt;I was cleaning up the UI on a side-project iOS app and did one thing: &lt;strong&gt;set Dynamic Type to XXXL and screenshot every screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading the code had turned up nothing. The screenshots showed problems immediately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eleven of them, in the end. &lt;strong&gt;All the same cause.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the conclusion first. &lt;strong&gt;"A parent that pins things side by side" × "text that grows" is not a bug, it's a pattern.&lt;/strong&gt; And in Japanese it breaks reliably worse than in English.&lt;/p&gt;

&lt;h2&gt;
  
  
  English truncates. Japanese stacks vertically.
&lt;/h2&gt;

&lt;p&gt;Same layout, different failure depending on language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;English&lt;/strong&gt; wraps at word boundaries and, failing that, ends in &lt;code&gt;…&lt;/code&gt;. You can't read it, but you can tell something was cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Japanese doesn't do that.&lt;/strong&gt; It can break between almost any two characters, so once a column is squeezed to one character wide, you get &lt;strong&gt;one character per line, stacked downward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Actual output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rendered&lt;/th&gt;
&lt;th&gt;Intended&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Paire / d / Macs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paired Macs (&lt;strong&gt;broken mid-word&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Claud / e / Code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One character per line&lt;/td&gt;
&lt;td&gt;"実行中のセッション" (Running sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three step labels stacked vertically&lt;/td&gt;
&lt;td&gt;A three-step horizontal stepper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;De / mo&lt;/code&gt;, turning the capsule into a circle&lt;/td&gt;
&lt;td&gt;A "Demo" badge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Paire / d / Macs&lt;/code&gt; is English breaking mid-word. Once the column has only a few characters left, even English gets there. &lt;strong&gt;Japanese gets there much earlier.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause had the same shape every time
&lt;/h2&gt;

&lt;p&gt;Nearly all eleven were this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// fixed&lt;/span&gt;
  &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kt"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                 &lt;span class="c1"&gt;// pinned right&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;The 44pt icon and the trailing value claim their width first, leaving the label column a few characters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix: rows that carry a value drop the value to the line below — but &lt;strong&gt;affordances like chevrons and toggles stay on the right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That row component was shared across the whole settings tree, so &lt;strong&gt;fixing one place fixed the entire settings screen.&lt;/strong&gt; Which also means one decision inside a shared component was breaking eleven screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ViewThatFits&lt;/code&gt; is not a general answer
&lt;/h2&gt;

&lt;p&gt;I used &lt;code&gt;ViewThatFits&lt;/code&gt; to switch to a stacked layout. &lt;strong&gt;It broke the standard size.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ViewThatFits&lt;/code&gt; decides using each child's &lt;strong&gt;ideal width&lt;/strong&gt;. A title that wraps reports &lt;strong&gt;its unwrapped full length&lt;/strong&gt; as its ideal width. So the side-by-side candidate was judged "doesn't fit" and the badge dropped below the title &lt;em&gt;at the standard size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could not have reasoned my way to this. I found it by screenshotting the standard size.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where each one belongs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ViewThatFits&lt;/code&gt;&lt;/strong&gt; — when every child is short and has an intrinsic width (badges, two-word metrics)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explicit &lt;code&gt;isAccessibilitySize&lt;/code&gt; branch&lt;/strong&gt; — when one of the children is a sentence that wraps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Only pin things to the right if you operate them in place
&lt;/h2&gt;

&lt;p&gt;I reversed one of my own calls partway through.&lt;/p&gt;

&lt;p&gt;I'd classified a &lt;code&gt;.menu&lt;/code&gt; Picker with Toggle, as something that belongs pinned right. &lt;strong&gt;A screenshot of the audit-log screen disproved that&lt;/strong&gt; — the filter label came out as &lt;code&gt;絞り… / 込み&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule I ended up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pin right&lt;/strong&gt; — controls whose state changes in place (Toggle)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowed to take the full-width row&lt;/strong&gt; — things that merely open something (&lt;code&gt;.menu&lt;/code&gt; Picker, chevron)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The modals had never been tested at all
&lt;/h2&gt;

&lt;p&gt;I had a &lt;code&gt;UITEST_FORCE_ACCESSIBILITY_XXXL&lt;/code&gt; env var for UI tests. It applies &lt;code&gt;.dynamicTypeSize&lt;/code&gt; to the root view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That does not propagate into a &lt;code&gt;.sheet&lt;/code&gt;'s environment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the paywall and the setup guide had &lt;strong&gt;never once been rendered at the largest text size.&lt;/strong&gt; What was passing was "the tests are green," not "the modals were checked."&lt;/p&gt;

&lt;p&gt;Now it's applied from the system side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun simctl ui &amp;lt;udid&amp;gt; content_size accessibility-extra-extra-extra-large
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  I declared convergence twice
&lt;/h2&gt;

&lt;p&gt;After fixing six, I wrote that the pattern had converged. &lt;strong&gt;It hadn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There were simply screens I hadn't looked at. When I did, four more appeared — numbers seven through ten. Then I went through the QR scanner, host-add and pairing flows in Japanese at XXXL, confirmed &lt;strong&gt;zero new findings&lt;/strong&gt;, and only then called it converged at eleven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson is: don't declare convergence until you've actually looked at the screens.&lt;/strong&gt; Judge by how many screens you looked at, not by how many you fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  And, incidentally, zero pluralization
&lt;/h2&gt;

&lt;p&gt;While reading the audit log, &lt;code&gt;1 items need attention&lt;/code&gt; caught my eye. I swept the strings: of &lt;strong&gt;38 English strings with a quantity placeholder, zero had plural variants.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 items need attention&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 saved Macs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1 days free&lt;/code&gt; (on the paywall)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1 sessions, 1 active…&lt;/code&gt; — &lt;strong&gt;in a share-sheet body, i.e. it leaves the device&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thirteen keys got variants. Japanese has no plural agreement, so nothing changed there. Strings like &lt;code&gt;%d of %d Macs&lt;/code&gt;, where the number doesn't agree with the noun, were deliberately left alone.&lt;/p&gt;

&lt;p&gt;The verification order mattered. Add one key, build, confirm &lt;code&gt;1 item needs attention&lt;/code&gt; on a real screen, &lt;em&gt;then&lt;/em&gt; roll out the rest. Finally, &lt;strong&gt;read &lt;code&gt;en.lproj/Localizable.stringsdict&lt;/code&gt; out of the build product&lt;/strong&gt; and confirm &lt;code&gt;NSStringPluralRuleType&lt;/code&gt; was generated correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I deliberately didn't change
&lt;/h2&gt;

&lt;p&gt;Recorded honestly, on the principle of not manufacturing work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The list screens survive XXXL intact&lt;/li&gt;
&lt;li&gt;Onboarding is fully reachable by scrolling at XXXL (clipping at the boundary is normal &lt;code&gt;ScrollView&lt;/code&gt; behaviour)&lt;/li&gt;
&lt;li&gt;The top padding in settings is the standard &lt;code&gt;.large&lt;/code&gt; navigation title layout — the same as Apple's own apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Fixed horizontal layout" × "growing text" is a pattern, not a bug.&lt;/strong&gt; Find one, suspect every screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japanese breaks worse than English.&lt;/strong&gt; English truncates; Japanese wraps anywhere, so it stacks vertically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One decision in a shared component broke eleven screens&lt;/strong&gt; — and fixing one place fixed all of them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ViewThatFits&lt;/code&gt; judges on ideal width.&lt;/strong&gt; A wrapping sentence among its children will break your standard size&lt;/li&gt;
&lt;li&gt;Pin to the right only what the user &lt;strong&gt;operates in place&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Env vars don't reach sheets.&lt;/strong&gt; "Tests are green" is not "I looked at the modal"&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't declare convergence until you've looked at the screens&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reading the code found zero of the eleven.&lt;/p&gt;




&lt;p&gt;Separately from this, I build a desktop AI agent called &lt;a href="https://rengaworks.gumroad.com/l/wisp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=xxxl-broke-japanese-worse" rel="noopener noreferrer"&gt;Wisp&lt;/a&gt;.&lt;br&gt;
It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.&lt;/p&gt;

</description>
      <category>swiftui</category>
      <category>ios</category>
      <category>a11y</category>
      <category>i18n</category>
    </item>
    <item>
      <title>My evidence pipeline was saving Cloudflare block pages as evidence</title>
      <dc:creator>Renga</dc:creator>
      <pubDate>Sat, 15 Aug 2026 15:58:57 +0000</pubDate>
      <link>https://dev.to/renga154/my-evidence-pipeline-was-saving-cloudflare-block-pages-as-evidence-25ml</link>
      <guid>https://dev.to/renga154/my-evidence-pipeline-was-saving-cloudflare-block-pages-as-evidence-25ml</guid>
      <description>&lt;p&gt;I build a web service that preserves evidence of harassment on social platforms.&lt;/p&gt;

&lt;p&gt;The core feature is a single thing: &lt;strong&gt;automatically capture a real screenshot of the offending post.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There was no substitute for it. I built an alternative that pulled the text through an API and rendered a tidy "evidence card" image, and threw it away. &lt;strong&gt;An image you can author freely afterwards proves nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the conclusion first. &lt;strong&gt;Third-party wrappers eventually die, and when they do, the failure comes back as a plausible-looking image rather than an error.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first approach was refused by the other side
&lt;/h2&gt;

&lt;p&gt;I started with Cloudflare Browser Rendering. The wiring worked. The capture didn't.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X blocks headless browsers.&lt;/strong&gt; The request times out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube refuses script injection under a Trusted Types CSP.&lt;/strong&gt; There's no way to make it render the comment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is a bug in my implementation — &lt;strong&gt;that is how they are built.&lt;/strong&gt; So I declared Cloudflare alone impossible for this and moved to a service with a real browser and bot avoidance behind it.&lt;/p&gt;

&lt;p&gt;Both captures started working. For X, open the post page and clip the tweet element. For YouTube, open the URL with &lt;code&gt;&amp;amp;lc=&lt;/code&gt; and screenshot just that comment element.&lt;/p&gt;

&lt;p&gt;Element screenshots have one trap worth knowing: &lt;strong&gt;&lt;code&gt;selector_algorithm=clip&lt;/code&gt; returns a blank image when the element sits below the fold.&lt;/strong&gt; The selector matches, the capture "succeeds," and the file is empty. That took a while to see.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;ytd-comment-thread-renderer&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;href&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;"lc=ID"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A parameter that had worked started returning 400
&lt;/h2&gt;

&lt;p&gt;I wanted timestamps rendered in Japan time, so I passed &lt;code&gt;time_zone: Asia/Tokyo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One day every request started coming back 400.&lt;/strong&gt; Every capture failed.&lt;/p&gt;

&lt;p&gt;The provider had narrowed which timezones they accept. Nothing changed on my side.&lt;/p&gt;

&lt;p&gt;I could diagnose it immediately only because &lt;strong&gt;I was storing the raw error body in the database.&lt;/strong&gt; The response went into &lt;code&gt;rawPayload.screenshotError&lt;/code&gt;, so opening one row told me why. Without that, this starts as "captures stopped working, no idea why."&lt;/p&gt;

&lt;p&gt;The fix was to drop the parameter, leave the rendering in GMT, and &lt;strong&gt;record the legally meaningful capture timestamp on my side, in JST.&lt;/strong&gt; How a third party displays a time is their business and can change. What I record is mine and doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  This was the bad one
&lt;/h2&gt;

&lt;p&gt;For X captures I was going through a third-party embed wrapper called &lt;code&gt;twitframe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That domain had been retired.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is what that looks like from the inside. The capture succeeds. HTTP 200. An image is written to storage. &lt;strong&gt;The image is a Cloudflare block page.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So: as evidence of online harassment, I was &lt;strong&gt;storing error screens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it had failed, I'd have known. It didn't. A stored, plausible-looking image doesn't stand out when you scan a list of them. You find out when someone needs to actually use it.&lt;/p&gt;

&lt;p&gt;Two causes stacked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A dependency on a third-party wrapper.&lt;/strong&gt; When the domain dies, nobody tells you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ignore_host_errors: true&lt;/code&gt; was set.&lt;/strong&gt; That says "continue capturing even if the host returns an error" — which is precisely permission to &lt;strong&gt;save a block page as a success&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;ignore_host_errors&lt;/code&gt; is reasonable when a slightly broken capture beats no capture. &lt;strong&gt;In a feature that handles evidence, it is only a hazard.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;X capture now goes through the official embed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform.twitter.com/embed/Tweet.html?id=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A third-party wrapper ends when that person stops caring. An official embed is maintained because the platform wants it maintained. &lt;strong&gt;Both are "someone else's," but only one has a reason to keep existing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it faster removed the evidence
&lt;/h2&gt;

&lt;p&gt;Scanning was slow, so I moved capture from synchronous to background. It got faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saved evidence stopped having screenshots attached.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The save path assumed "by the time we get here, the screenshot exists" and copied it forward. Make the earlier stage async and that assumption quietly stops holding.&lt;/p&gt;

&lt;p&gt;The fix is two-layered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning responds immediately and captures in the background&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;at the moment evidence is saved, check for the screenshot and capture it right there if missing&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything missed also gets picked up by the next scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you move a synchronous step to the background, audit every later stage that inherited data assuming it was synchronous.&lt;/strong&gt; Optimise on speed alone and this is the shape of the breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party wrappers die.&lt;/strong&gt; Use the official embed&lt;/li&gt;
&lt;li&gt;When they die, &lt;strong&gt;the failure arrives as a plausible image, not an exception&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Flags like &lt;code&gt;ignore_host_errors&lt;/code&gt; that mean "continue even if it looks wrong" are &lt;strong&gt;pure hazard when the artifact is evidence&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider parameters change.&lt;/strong&gt; Keeping the raw error body makes the day it changes a five-minute diagnosis&lt;/li&gt;
&lt;li&gt;Separate values that depend on someone else's rendering (timezone) from &lt;strong&gt;values you must record yourself&lt;/strong&gt; (capture time)&lt;/li&gt;
&lt;li&gt;Move something to async, then &lt;strong&gt;re-check what the later stages assumed&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"The capture succeeded" and "we captured evidence" turned out to be different claims. The first is visible in the status code. The second requires looking at the pixels.&lt;/p&gt;




&lt;p&gt;Separately from this, I build a desktop AI agent called &lt;a href="https://rengaworks.gumroad.com/l/wisp?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=block-pages-as-evidence" rel="noopener noreferrer"&gt;Wisp&lt;/a&gt;.&lt;br&gt;
It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>playwright</category>
      <category>webscraping</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
