<?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: Kalpesh Sharma</title>
    <description>The latest articles on DEV Community by Kalpesh Sharma (@kalpesh_sharma_202b11cc3b).</description>
    <link>https://dev.to/kalpesh_sharma_202b11cc3b</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%2F4093697%2F64f0bfc1-627b-4b1a-87ee-2e3f7458cb4a.png</url>
      <title>DEV Community: Kalpesh Sharma</title>
      <link>https://dev.to/kalpesh_sharma_202b11cc3b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kalpesh_sharma_202b11cc3b"/>
    <language>en</language>
    <item>
      <title>Five Integration Failures I Hit While Building on SuperDocs</title>
      <dc:creator>Kalpesh Sharma</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:01:05 +0000</pubDate>
      <link>https://dev.to/kalpesh_sharma_202b11cc3b/five-integration-failures-i-hit-while-building-on-superdocs-39kp</link>
      <guid>https://dev.to/kalpesh_sharma_202b11cc3b/five-integration-failures-i-hit-while-building-on-superdocs-39kp</guid>
      <description>&lt;p&gt;I built two document workflows on top of SuperDocs: a closing-document reconciliation workflow and PaperForge, a multi-document research/reporting system with a human-reviewed editing step.&lt;/p&gt;

&lt;p&gt;The happy path was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document
→ SuperDocs edit
→ proposed change
→ human review
→ approval
→ export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting engineering work showed up around that path.&lt;/p&gt;

&lt;p&gt;The five problems below were not hypothetical edge cases. They came from real implementation and live integration work. None of them required replacing SuperDocs. They required being much more precise about the boundary between an external document API and the application built around it.&lt;/p&gt;

&lt;p&gt;The biggest lesson was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A document-editing API can give you the right primitives, but the surrounding application still owns state, contracts, retry behavior, review UX, and the definition of “done.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scope note:&lt;/strong&gt; I use “integration failure” deliberately. Some problems below exposed API-contract details I had misunderstood; others were defects in my own application assumptions around SuperDocs. I am not treating every item as a SuperDocs product bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build context: Closing Document Reconciliation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnq8vqq0x1paafg8660h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnq8vqq0x1paafg8660h.png" alt="Closing Document Reconciliation workflow" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The reconciliation build where I first exercised SuperDocs document upload, asynchronous editing, explicit approval, and export in a real workflow.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Additive provider metadata broke my strict pending-change model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I was trying to do
&lt;/h3&gt;

&lt;p&gt;PaperForge stores SuperDocs proposals in a strict immutable model.&lt;/p&gt;

&lt;p&gt;The application only needs a small set of fields for each proposed change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change_id
operation
chunk_id
document_id
old_html
new_html
ai_explanation
insertion anchors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately configured that internal Pydantic model with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent was good: malformed persisted review state should fail loudly instead of quietly entering the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;p&gt;A live &lt;code&gt;pending_changes&lt;/code&gt; response contained every field PaperForge expected &lt;strong&gt;plus additional provider-side diagnostics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One response included a &lt;code&gt;machine_checks&lt;/code&gt; object containing values such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claims
resolved
words_new
words_old
blocks_new
blocks_old
kept_chars_ratio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those fields were useful metadata, but they were not part of PaperForge's domain model.&lt;/p&gt;

&lt;p&gt;Because I was validating the entire provider object directly against a model with &lt;code&gt;extra="forbid"&lt;/code&gt;, an otherwise valid proposed change could fail validation simply because the provider had added more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bad assumption
&lt;/h3&gt;

&lt;p&gt;My mental model was effectively:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A SuperDocs pending-change object and my application's &lt;code&gt;PendingChange&lt;/code&gt; object are the same contract.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They were not.&lt;/p&gt;

&lt;p&gt;The external response belongs to the provider. The persisted domain object belongs to PaperForge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;I put strictness at the wrong boundary.&lt;/p&gt;

&lt;p&gt;The real shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SuperDocs response
        ↓
integration boundary
        ↓
PaperForge-owned model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider should be allowed to add fields that PaperForge does not use.&lt;/p&gt;

&lt;p&gt;PaperForge should still be strict about the fields it &lt;em&gt;does&lt;/em&gt; depend on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I kept the internal model strict and added an explicit projection at the integration boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_PENDING_CHANGE_FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old_html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai_explanation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert_after_chunk_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert_before_chunk_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before validating the object, PaperForge now selects only the fields it owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_normalize_pending_change&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&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="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SuperDocsProtocolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SuperDocs returned invalid pending changes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;key&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_PENDING_CHANGE_FIELDS&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The projected value is then validated by the strict model.&lt;/p&gt;

&lt;p&gt;So the boundary is tolerant of additive provider metadata, while the persisted contract remains strict.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I proved it
&lt;/h3&gt;

&lt;p&gt;I added a regression test using the same additive &lt;code&gt;machine_checks&lt;/code&gt; shape from the live response.&lt;/p&gt;

&lt;p&gt;The test verifies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the change still reaches &lt;code&gt;awaiting_approval&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the expected review fields survive;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;machine_checks&lt;/code&gt; does not leak into persisted &lt;code&gt;review.json&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the internal model remains strict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accept provider evolution at the edge
→ project the fields you own
→ validate those fields strictly
→ persist the smaller stable contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/app/models/review.py" rel="noopener noreferrer"&gt;PaperForge review model&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/app/services/review_service.py" rel="noopener noreferrer"&gt;PaperForge review normalization&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/tests/test_review_service.py" rel="noopener noreferrer"&gt;Regression test&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. A retry could have started the same SuperDocs workflow twice
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What I was trying to do
&lt;/h3&gt;

&lt;p&gt;Starting a review is an external side effect.&lt;/p&gt;

&lt;p&gt;PaperForge sends a request to SuperDocs, receives a &lt;code&gt;job_id&lt;/code&gt;, and polls that asynchronous job until it becomes actionable.&lt;/p&gt;

&lt;p&gt;I wanted this endpoint to be safe to retry.&lt;/p&gt;
&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;p&gt;There is a dangerous failure window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PaperForge starts review
        ↓
SuperDocs accepts it and returns job-1
        ↓
local process/request is interrupted
        ↓
client retries POST /review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the retry simply starts again from the beginning, it can create a second provider job for the same report.&lt;/p&gt;

&lt;p&gt;That can mean duplicated work, duplicated usage, two independent change sets, and uncertainty over which job local state should trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bad assumption
&lt;/h3&gt;

&lt;p&gt;The incorrect assumption was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If my HTTP request did not finish, the external operation did not finish either.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A local timeout or exception tells me almost nothing about whether an external side effect already occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;I had been thinking of review creation as a normal request-response operation.&lt;/p&gt;

&lt;p&gt;It is actually a small distributed state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A. no provider job exists

B. provider job exists and is processing

C. provider job reached an actionable or terminal state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A retry in state B must not behave like a request in state A.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;job_id&lt;/code&gt; is therefore not disposable response metadata. It is the recovery handle for the external side effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;PaperForge persists review state and checks it before starting anything new.&lt;/p&gt;

&lt;p&gt;If the state is already &lt;code&gt;processing&lt;/code&gt; and has a &lt;code&gt;job_id&lt;/code&gt;, the service polls the existing job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_read_optional_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Only a report with no recoverable review state is allowed to start a new SuperDocs job.&lt;/p&gt;

&lt;p&gt;The session ID is also deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paperforge-review-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Persist external identity before treating a retry as new work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How I proved it
&lt;/h3&gt;

&lt;p&gt;The regression test preloads &lt;code&gt;review.json&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;ReviewState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paperforge-review-existing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it calls &lt;code&gt;start_review()&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;The critical assertion is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_calls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry polls &lt;code&gt;job-1&lt;/code&gt;. It does not create another one.&lt;/p&gt;

&lt;p&gt;This changed how I think about idempotency.&lt;/p&gt;

&lt;p&gt;It is not just “support the same request twice.” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Know exactly when the external side effect happened, persist its identity, and recover that same work on retry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/app/services/review_service.py" rel="noopener noreferrer"&gt;Review recovery logic&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/tests/test_review_service.py" rel="noopener noreferrer"&gt;Recovery regression test&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. I inferred the approval write contract from the read contract
&lt;/h2&gt;

&lt;p&gt;This was a smaller bug, but a useful integration lesson.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I was trying to do
&lt;/h3&gt;

&lt;p&gt;The review job returned a list of proposed changes, each with a &lt;code&gt;change_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the natural approval payload looked like this:&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;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"change_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"change-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each change already had an explicit decision. That looked semantically complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;p&gt;The approval endpoint also required a &lt;strong&gt;top-level &lt;code&gt;approved&lt;/code&gt; value&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The working request therefore needed both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;changes&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 nested decisions described the human intent, but the endpoint still had its own required envelope.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bad assumption
&lt;/h3&gt;

&lt;p&gt;I assumed the write shape would mirror the read shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending_changes[]
        ↓
changes[{ change_id, approved }]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But read and write contracts are not automatically symmetrical.&lt;/p&gt;

&lt;p&gt;The actual request had another required field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approval request
├── job_id
├── approved
└── changes[]
    ├── change_id
    └── approved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;I inferred the provider's transport schema from the application data I wanted to send.&lt;/p&gt;

&lt;p&gt;That is fragile because action endpoints often contain workflow-level fields that appear redundant from the domain model's point of view.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I moved provider-specific request construction into the SuperDocs client.&lt;/p&gt;

&lt;p&gt;The workflow decides &lt;strong&gt;what the human chose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The client decides &lt;strong&gt;how that decision must be encoded on the wire&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the Closing Document Reconciliation build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# Required even when every individual change
&lt;/span&gt;    &lt;span class="c1"&gt;# below has its own approval decision.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;changes&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;Later, PaperForge used a narrower one-change-at-a-time approval request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;change_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;approved&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;In both cases, the provider adapter owns the exact request contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;Do not infer a write schema from a read schema, especially for action endpoints such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approve
reject
continue
commit
cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A field that looks redundant may still be part of the endpoint's actual workflow contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/superdocs-builds/blob/closing-document-reconciliation/use-cases/Kalpesh1Sharma/closing-document-reconciliation/superdocs/client.py" rel="noopener noreferrer"&gt;Closing-document SuperDocs client&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/superdocs-builds/blob/closing-document-reconciliation/use-cases/Kalpesh1Sharma/closing-document-reconciliation/review_export_memo.py" rel="noopener noreferrer"&gt;Human-review demo flow&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/app/integrations/superdocs/client.py" rel="noopener noreferrer"&gt;PaperForge SuperDocs client&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. A targeted edit produced too much HTML to be a useful human diff
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What I was trying to do
&lt;/h3&gt;

&lt;p&gt;PaperForge uses SuperDocs for a deliberately narrow editing step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SuperDocs proposes one Executive Summary revision
        ↓
human sees BEFORE / AFTER
        ↓
human approves or rejects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I initially expected a targeted edit to naturally produce a small review payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;p&gt;The semantic change was small, but the &lt;code&gt;old_html&lt;/code&gt; and &lt;code&gt;new_html&lt;/code&gt; could include a large amount of surrounding document structure.&lt;/p&gt;

&lt;p&gt;Dumping that raw content into the UI technically showed the provider response, but it made the human-review step worse.&lt;/p&gt;

&lt;p&gt;A reviewer should not have to scan a document-sized block to discover what changed.&lt;/p&gt;

&lt;p&gt;The product requirement became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show the smallest useful human-readable window around the changed text, while keeping the underlying provider proposal untouched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The bad assumption
&lt;/h3&gt;

&lt;p&gt;I had treated the provider's edit representation as if it were also the correct presentation model.&lt;/p&gt;

&lt;p&gt;Those are different responsibilities.&lt;/p&gt;

&lt;p&gt;The provider representation answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What document content changed?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The review UI must answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What does the human need to read to decide?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I added a dedicated presentation transform.&lt;/p&gt;

&lt;p&gt;First, the frontend parses the HTML and extracts normalized text:&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;plainText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&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;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;??&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &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="nf"&gt;trim&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;Then it finds the common prefix and common suffix between before and after.&lt;/p&gt;

&lt;p&gt;Finally, it keeps only a bounded amount of context around the changed region:&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;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI renders the result as normal React text under explicit &lt;strong&gt;Before&lt;/strong&gt; and &lt;strong&gt;After&lt;/strong&gt; labels.&lt;/p&gt;

&lt;p&gt;It does not render provider HTML with &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I did not modify the provider change
&lt;/h3&gt;

&lt;p&gt;The original &lt;code&gt;PendingChange&lt;/code&gt; still contains the complete &lt;code&gt;old_html&lt;/code&gt; and &lt;code&gt;new_html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The bounded diff is only a presentation.&lt;/p&gt;

&lt;p&gt;That separation matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider representation
        ↓
domain representation
        ↓
human presentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The approval workflow keeps the original change intact while giving the reviewer a much smaller and safer view of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the review UI looked like
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyo9i39xm6nn6k9t1kilg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyo9i39xm6nn6k9t1kilg.png" alt="PaperForge human review screen" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The live PaperForge review surface: the human sees the proposed before/after change and must explicitly approve or reject it before the reviewed DOCX becomes available.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How I proved it
&lt;/h3&gt;

&lt;p&gt;The frontend test provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;old_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Before sentence.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;After sentence.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and checks that the interface renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before sentence.
After sentence.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then approves only the selected &lt;code&gt;change_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The real end-to-end run used the same presentation path against a much larger change block and produced a readable review card rather than a raw HTML dump.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;A provider's wire representation is not automatically a good human interface.&lt;/p&gt;

&lt;p&gt;For review systems, I now think in three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. provider representation
   enough structure to describe/apply the edit

2. application representation
   stable fields needed to track the decision

3. review presentation
   the minimum readable context needed by a human
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human approval is only meaningful if the change is easy enough to inspect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.12.0/frontend/src/utils/diff.ts" rel="noopener noreferrer"&gt;Bounded diff logic&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.12.0/frontend/src/components/DiffView.tsx" rel="noopener noreferrer"&gt;Diff UI&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.12.0/frontend/src/App.test.tsx" rel="noopener noreferrer"&gt;Frontend regression test&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  5. &lt;code&gt;completed&lt;/code&gt; did not prove the exported DOCX contained the approved edit
&lt;/h2&gt;

&lt;p&gt;This was the most important correctness lesson.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I was trying to do
&lt;/h3&gt;

&lt;p&gt;The end of the workflow looked reassuring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;human approves
        ↓
SuperDocs job reaches completed
        ↓
export endpoint returns DOCX bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would be natural to display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reviewed document ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;at that point.&lt;/p&gt;

&lt;p&gt;I decided that PaperForge needed one more check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What could go wrong
&lt;/h3&gt;

&lt;p&gt;A provider can report a workflow as completed and still leave room for artifact-level failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale session state;&lt;/li&gt;
&lt;li&gt;wrong export target;&lt;/li&gt;
&lt;li&gt;change not reflected in the file;&lt;/li&gt;
&lt;li&gt;structurally valid DOCX with old text;&lt;/li&gt;
&lt;li&gt;delete operation whose text remains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful HTTP response proves that bytes came back.&lt;/p&gt;

&lt;p&gt;A provider status proves that the provider workflow finished.&lt;/p&gt;

&lt;p&gt;Neither directly proves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The human-approved edit is present in the file PaperForge is about to offer for download.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;I was defining success too early.&lt;/p&gt;

&lt;p&gt;There are three different levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. transport success
   the API call worked

2. workflow success
   the provider job completed

3. artifact success
   the exported file reflects the approved decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PaperForge's user-facing promise depends on level 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;PaperForge independently verifies the exported DOCX before setting:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;A DOCX is a ZIP container, so the verifier opens it and reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;word/document.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It parses the WordprocessingML text nodes and normalizes the resulting document text.&lt;/p&gt;

&lt;p&gt;Then it checks the approved operation.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;edit&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;new_text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;new_text&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ReviewVerificationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The approved change was not found &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in the DOCX export.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;delete&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;old_text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;old_text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;document_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ReviewVerificationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The deleted change remains &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in the DOCX export.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the DOCX is not a usable ZIP, the XML is missing, or the document cannot be parsed, verification fails closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I proved it
&lt;/h3&gt;

&lt;p&gt;I added a deliberately bad export test.&lt;/p&gt;

&lt;p&gt;The approved change is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original executive summary.
        ↓
Clear revised executive summary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fake provider reports:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;but exports a DOCX containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The old text remains.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReviewVerificationError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ch-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then verifies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So PaperForge refuses to claim success even though the simulated provider workflow itself reached &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important limitation
&lt;/h3&gt;

&lt;p&gt;This verifier does &lt;strong&gt;not&lt;/strong&gt; prove complete DOCX fidelity.&lt;/p&gt;

&lt;p&gt;It does not prove every style, page break, list, table, or layout detail survived.&lt;/p&gt;

&lt;p&gt;It proves the narrower property PaperForge promises at this boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approved replacement/create text must be present
approved deleted text must be absent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That postcondition is deterministic and testable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;External status should not automatically become application-level truth.&lt;/p&gt;

&lt;p&gt;For consequential artifacts, define a postcondition your own system can verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider says completed
        ↓
export artifact
        ↓
inspect artifact independently
        ↓
verify the promised property
        ↓
only then expose "completed"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/app/services/review_service.py" rel="noopener noreferrer"&gt;DOCX verification&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Kalpesh1Sharma/paperforge/blob/v0.11.0/backend/tests/test_review_service.py" rel="noopener noreferrer"&gt;Fail-safe regression test&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What changed in my integration design
&lt;/h2&gt;

&lt;p&gt;These bugs look unrelated at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extra JSON fields
duplicate jobs
approval payload shape
oversized diffs
DOCX verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But they all came from the same mistake: letting the external API boundary leak too far into the rest of the application.&lt;/p&gt;

&lt;p&gt;The more robust architecture became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                SuperDocs
                   │
                   ▼
          narrow provider adapter
                   │
         normalize + validate
                   │
                   ▼
        PaperForge-owned state
                   │
          explicit state machine
                   │
                   ▼
            human review UI
                   │
                   ▼
          explicit human decision
                   │
                   ▼
              DOCX export
                   │
          independent verification
                   ▼
               completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important word there is &lt;strong&gt;owned&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SuperDocs owns the document-editing primitives.&lt;/p&gt;

&lt;p&gt;PaperForge owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what provider data becomes persistent application state;&lt;/li&gt;
&lt;li&gt;how retries recover external jobs;&lt;/li&gt;
&lt;li&gt;how a human sees a proposed change;&lt;/li&gt;
&lt;li&gt;when a decision is allowed;&lt;/li&gt;
&lt;li&gt;and what evidence is required before the application says the workflow is complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation made the integration much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The five lessons I would carry into the next external API integration
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be tolerant at the provider boundary, strict inside your own domain.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Additive upstream metadata should not break a stable application model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persist external operation identity before relying on retries.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A failed local request does not prove an external side effect did not happen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not infer write contracts from read contracts.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Action endpoints often have envelope-level semantics that are easy to miss.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separate machine representations from human review representations.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Correct payloads can still create unusable approval UX.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify the product-level postcondition yourself when you can.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;200 OK&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, and “file returned” are different claims.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integration became much stronger once I stopped treating SuperDocs as a black-box call inside a happy-path script and started treating it as one component in a stateful, human-reviewed system.&lt;/p&gt;

&lt;p&gt;That is also the main thing I would do differently if I started again: define the boundary contracts and state transitions first, before wiring the first live provider call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repositories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PaperForge: &lt;a href="https://github.com/Kalpesh1Sharma/paperforge" rel="noopener noreferrer"&gt;https://github.com/Kalpesh1Sharma/paperforge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Closing Document Reconciliation: &lt;a href="https://github.com/Kalpesh1Sharma/superdocs-builds/tree/closing-document-reconciliation/use-cases/Kalpesh1Sharma/closing-document-reconciliation" rel="noopener noreferrer"&gt;https://github.com/Kalpesh1Sharma/superdocs-builds/tree/closing-document-reconciliation/use-cases/Kalpesh1Sharma/closing-document-reconciliation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>debugging</category>
      <category>python</category>
      <category>react</category>
    </item>
  </channel>
</rss>
