<?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: Byte Chap</title>
    <description>The latest articles on DEV Community by Byte Chap (@sourcebento).</description>
    <link>https://dev.to/sourcebento</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%2F4142891%2Fe0715329-96f5-4971-b0e2-33516f861b50.png</url>
      <title>DEV Community: Byte Chap</title>
      <link>https://dev.to/sourcebento</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sourcebento"/>
    <language>en</language>
    <item>
      <title>A Small State Machine for Leave Approval Policy</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Fri, 02 Oct 2026 04:15:03 +0000</pubDate>
      <link>https://dev.to/sourcebento/a-small-state-machine-for-leave-approval-policy-2cej</link>
      <guid>https://dev.to/sourcebento/a-small-state-machine-for-leave-approval-policy-2cej</guid>
      <description>&lt;p&gt;&lt;em&gt;Separate request lifecycle from eligibility rules, balance accounting, approval authority, and recorded exceptions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A leave request looks like a form with two buttons until the rules become interesting. An employee changes teams while a request is pending. Two requests compete for the same balance. A manager approves an exception, then somebody changes the dates. If those cases are handled through scattered conditionals, the application gradually loses a coherent answer to a basic question: what did this approval authorize?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, then fact-checked and edited by the developer behind WorkBento.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A small state machine gives the request a stable lifecycle. It does not eliminate policy code. It creates an explicit boundary around that code, so eligibility, accounting, authority, and exceptions can evolve without redefining what “approved” means in every endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the request a deliberately small lifecycle
&lt;/h2&gt;

&lt;p&gt;Start with six states: &lt;strong&gt;draft&lt;/strong&gt;, &lt;strong&gt;pending&lt;/strong&gt;, &lt;strong&gt;approved&lt;/strong&gt;, &lt;strong&gt;rejected&lt;/strong&gt;, &lt;strong&gt;cancelled&lt;/strong&gt;, and &lt;strong&gt;completed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A draft is editable and has no balance reservation. Submission moves it to pending after eligibility checks, balance reservation, and approver resolution succeed. Pending requests can be approved, rejected, or cancelled. Approved requests can be cancelled or completed. Rejected, cancelled, and completed requests are terminal in this model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2Fd3a49fa101a44d99%2Fcover.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%2Fdraft-content.sourcebento.com%2Fgenerated%2Fd3a49fa101a44d99%2Fcover.png" title="Six request states and their allowed transitions; submission reserves balance, rejection or cancellation releases it, and completion converts the reservation to consumption." alt="Leave request states: draft submits to pending; pending approves to approved, rejects to rejected, or cancels to cancelled; approved cancels to cancelled or completes to completed." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The state machine should answer which commands are structurally valid. A separate policy evaluator answers whether a particular actor may execute a command against a particular request.&lt;/p&gt;

&lt;p&gt;That distinction matters. “Approve is valid from pending” is a lifecycle rule. “This manager may approve this employee’s request for these dates” is an authorization decision. “This leave type requires a second approver” is a routing rule. Combining all three into one status field produces states such as &lt;code&gt;pending_manager_then_hr&lt;/code&gt;, which become difficult to maintain as routing changes.&lt;/p&gt;

&lt;p&gt;Keep approval stages in a separate approval plan. A request remains pending until that plan is satisfied. Individual decisions belong to the plan; approved is the aggregate outcome.&lt;/p&gt;

&lt;p&gt;I would also prohibit direct edits to submitted dates or leave type in this first model. Cancel and submit a replacement, with a link between the requests. That costs an extra action, but preserves the object that was actually evaluated. If amendment becomes necessary, introduce request revisions explicitly rather than quietly mutating an approved row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make policy produce decisions, not side effects
&lt;/h2&gt;

&lt;p&gt;Policy is more than a table of settings. Eligibility may depend on employment dates, leave category, work schedule, location, and the dates requested. Some rules fit declarative configuration; others need ordinary, tested code.&lt;/p&gt;

&lt;p&gt;The useful separation is between evaluating a request and changing the system. An evaluator receives explicit inputs and returns a decision with reasons. It does not reserve balance, email a manager, or update the request.&lt;/p&gt;

&lt;p&gt;A compact Python result type can make that contract concrete:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nd"&gt;@dataclass&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubmissionDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;policy_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;reason_codes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;required_units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;approver_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eligible&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_units&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approver_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes every submitted request requires positive units and at least one approver. An automatic approval policy would need a different contract. &lt;code&gt;Decimal&lt;/code&gt; avoids binary floating-point arithmetic for fractional units, but the surrounding model must still define whether those units are hours or days, and which rounding rules apply.&lt;/p&gt;

&lt;p&gt;The result deliberately does not claim that balance is available. A policy evaluator can calculate the required amount; availability must be established against authoritative accounting data inside the submission transaction.&lt;/p&gt;

&lt;p&gt;Persist the policy version and relevant evaluated facts with the decision. A version label alone is insufficient if its configuration can be overwritten or if the evaluator reads mutable employee records. Retain an immutable policy definition and a bounded snapshot of the inputs needed to explain the result.&lt;/p&gt;

&lt;p&gt;Avoid turning every rule into an editable expression language. Configuration works well for thresholds, eligible categories, and approval routes. Calendar calculations and interactions between rules often deserve code with a clear interface. The objective is a visible policy boundary, not the removal of all policy from software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat balances as accounting with concurrency
&lt;/h2&gt;

&lt;p&gt;For this model, reserve balance at submission. That prevents multiple pending requests from independently spending the same available units. Approval keeps the reservation in place. Rejection or cancellation releases it. Completion replaces the reservation with consumption.&lt;/p&gt;

&lt;p&gt;A useful projection is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;available = posted entitlement − consumed units − active reservations&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here, posted entitlement includes grants and adjustments, while consumption and reservations are tracked separately. The signs and entry types must be unambiguous. Do not subtract an approved request twice by treating it as both consumption and an active reservation.&lt;/p&gt;

&lt;p&gt;Reservation at submission has a real cost: a slow approval can tie up an employee’s balance. Reserving only at approval reduces that cost, but permits several apparently valid requests to compete until somebody approves one. Either approach can work. The UI and policy must reflect the chosen semantics.&lt;/p&gt;

&lt;p&gt;The balance check and reservation must be atomic. Reading available balance, returning to application code, and inserting a reservation later leaves a race. One practical PostgreSQL design locks a stable balance-account row, calculates availability, inserts the reservation, and updates the request to pending within one transaction. Every operation that changes that account must follow the same locking protocol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2Fd3a49fa101a44d99%2Fscreenshot.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%2Fdraft-content.sourcebento.com%2Fgenerated%2Fd3a49fa101a44d99%2Fscreenshot.png" title="An illustrative concurrency trace: the account lock makes the second submission evaluate balance after the first reservation commits." alt="Illustrative submission race: requests A and B each need four days from a five-day balance; A reserves four days, B rechecks one available day and remains draft." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give submission commands idempotency keys and enforce uniqueness for the reservation associated with a request revision. A retried HTTP request must return the existing outcome rather than reserve again.&lt;/p&gt;

&lt;p&gt;Some categories may allow negative balances or need no balance account. Those are explicit accounting policies. They should not appear as unexplained bypasses in an endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve approvers, then define what can change
&lt;/h2&gt;

&lt;p&gt;“Send it to the manager” leaves several engineering questions unanswered. Which manager: the one at submission, approval time, or the start of leave? What happens when that person leaves the organization? Can a delegate act? Can an employee approve their own request through a second role?&lt;/p&gt;

&lt;p&gt;At submission, resolve an approval plan containing the required stages and the eligible approvers. Store the organizational facts used to build it. At decision time, also check the actor’s current authority. A historical assignment should not allow a deactivated account to approve a request.&lt;/p&gt;

&lt;p&gt;This creates a useful distinction: the plan records the intended route, while current authorization determines whether someone may act now. If a team change invalidates the route, use an explicit rerouting command. Record the old plan, replacement plan, actor, and reason. Do not recompute the route invisibly each time the page loads.&lt;/p&gt;

&lt;p&gt;For multiple stages, define whether approvals are sequential, whether any one approver can satisfy a stage, and whether rejection ends the request immediately. Keep those semantics in the approval plan rather than inferring them from how many approval rows happen to exist.&lt;/p&gt;

&lt;p&gt;Use optimistic concurrency on the request as well as balance locking. An approval command should supply the revision it examined. If another command cancelled the request or changed its plan, the approval fails with a conflict and the actor must review the current request. A transaction can then record the decision and advance the state together.&lt;/p&gt;

&lt;p&gt;Notifications follow the committed transition. If reliable delivery matters, write an outbox event in that transaction and deliver it asynchronously. An email failure should not leave the request half approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make overrides and history part of the model
&lt;/h2&gt;

&lt;p&gt;An override should identify the rule being waived, the actor authorized to waive it, and the reason. “Administrator” is too broad a substitute for those details.&lt;/p&gt;

&lt;p&gt;For example, an exception might permit a negative balance while leaving employment eligibility and separation of duties intact. Store that exception as a scoped authorization linked to the request revision and policy decision. A broad &lt;code&gt;force=True&lt;/code&gt; flag makes it difficult to tell which safeguards still ran.&lt;/p&gt;

&lt;p&gt;Some constraints should remain outside the override system: valid date ranges, supported units, referential integrity, and accounting consistency. Other constraints may be configurable as non-waivable. The boundary should be explicit enough that a reviewer can understand what an exception permits.&lt;/p&gt;

&lt;p&gt;History needs both lifecycle events and accounting entries. A request event might record its identifier and revision, command, previous and resulting states, actor, timestamp, policy version, decision reasons, and any override reference. A balance entry records the reservation, release, consumption, or adjustment with a link back to the originating command.&lt;/p&gt;

&lt;p&gt;Write these records in the same transaction as the transition. Application logs are useful for debugging, but are a weak substitute for durable business history.&lt;/p&gt;

&lt;p&gt;Completion deserves particular care. Decide whether it follows scheduled dates or confirmed attendance, and what happens when actual leave differs from the request. A background job should not silently consume units merely because a date passed unless that is the agreed policy. In this model, a completed request stays terminal; corrections use compensating accounting entries and linked correction records.&lt;/p&gt;

&lt;p&gt;You do not need full event sourcing to achieve this. A current request row, an append-only event table, and explicit balance entries can provide a comprehensible audit trail. Readers get a fast current view; reviewers can reconstruct the decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the boundary in an HR platform
&lt;/h2&gt;

&lt;p&gt;These boundaries are useful whether leave approval is a standalone service or one module in a larger HR application. The crucial implementation question is whether submission, authorization, accounting, and history meet in a transaction with clearly defined inputs. A collection of configurable settings cannot supply that guarantee by itself.&lt;/p&gt;

&lt;p&gt;My own product, &lt;strong&gt;WorkBento — Python HR and Payroll Platform&lt;/strong&gt;, provides a concrete application context for these ideas. It is a self-hosted FastAPI and React platform using PostgreSQL with pgvector, MongoDB, and Meilisearch. Its modules include employees, org chart, attendance, shifts, leave approvals, payroll, and role permissions. Those areas provide the surrounding context in which leave decisions must make sense; the state machine described here is an engineering design, not a claim about WorkBento’s internal implementation.&lt;/p&gt;

&lt;p&gt;Docker Compose deployment and full source code are included, so a builder evaluating the platform can inspect how its implementation handles those boundaries and adapt it to their requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentoassist.gumroad.com/l/workbento" rel="noopener noreferrer"&gt;WorkBento on Gumroad&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>statemachines</category>
      <category>python</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Ranking Scanned PDFs While Preserving OCR Uncertainty</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Thu, 01 Oct 2026 02:31:10 +0000</pubDate>
      <link>https://dev.to/sourcebento/ranking-scanned-pdfs-while-preserving-ocr-uncertainty-2228</link>
      <guid>https://dev.to/sourcebento/ranking-scanned-pdfs-while-preserving-ocr-uncertainty-2228</guid>
      <description>&lt;p&gt;&lt;em&gt;Combine lexical and semantic retrieval, then use region evidence and page metadata to rank results without concealing weak extraction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A scanned PDF presents two separate search problems: finding the relevant passage and deciding how much to trust the text extracted from its pixels. Hybrid retrieval helps with the first. It does not automatically solve the second. A strong semantic match can still point to a misread amount, an incomplete table, or a paragraph assembled from the wrong reading order.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, then fact-checked and edited by the developer behind DocBento.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I would treat OCR evidence as part of the retrieval contract. Search should return useful candidates while preserving enough provenance for the reader to inspect doubtful text. That requires a deliberate distinction between relevance, extraction quality, and human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each retrieval channel can recover
&lt;/h2&gt;

&lt;p&gt;Full-text retrieval operates on extracted tokens. It is useful when the query contains distinctive terminology, a clause heading, or an identifier that survives OCR. Ranking can incorporate frequency, proximity, and field weights. PostgreSQL provides &lt;code&gt;ts_rank&lt;/code&gt; and &lt;code&gt;ts_rank_cd&lt;/code&gt; for lexical ranking; its native full-text search should not be casually described as BM25. &lt;a href="https://www.postgresql.org/docs/17/textsearch-controls.html" rel="noopener noreferrer"&gt;PostgreSQL’s text-search documentation&lt;/a&gt; explains these ranking controls.&lt;/p&gt;

&lt;p&gt;Identifiers need additional care. Language-aware tokenization and stemming are designed for prose. A reference such as &lt;code&gt;AB-1047&lt;/code&gt; may deserve a separate normalized identifier field, with explicit rules for punctuation and case. Otherwise, an apparently exact search may be exact only after transformations the user cannot see.&lt;/p&gt;

&lt;p&gt;Vector retrieval compares an embedded query with embedded passages. It can recover a passage whose wording differs from the query: “ending the agreement early” might retrieve a section headed “Termination.” But the embedding represents the extracted text, including its errors. It cannot establish whether a faint digit was recognized correctly. Corrupted reading order can also produce a plausible embedding of a passage that never existed as a coherent paragraph.&lt;/p&gt;

&lt;p&gt;Both channels therefore fail differently. Lexical retrieval may miss a damaged query term. Semantic retrieval may overlook the distinction between neighboring reference numbers or related contractual concepts. Keeping both candidate lists gives the ranking stage more evidence, but agreement between them is evidence of relevance to the extraction, not proof of transcription accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve evidence at the region boundary
&lt;/h2&gt;

&lt;p&gt;A document-wide OCR average is too coarse for ranking. A clean title page says little about a faint payment schedule on page 18. Even a page average can hide one uncertain amount surrounded by clear boilerplate.&lt;/p&gt;

&lt;p&gt;Store evidence at a region boundary that can be mapped back to the scan: a paragraph, table cell, or another bounded area. Search chunks may include several regions, but they should retain that mapping. Useful fields include the document version, extraction revision, physical page index, bounding box, OCR engine and configuration, raw confidence values, and assessment state.&lt;/p&gt;

&lt;p&gt;Keep raw engine confidence separate from any derived quality estimate. A confidence value should not be treated as a portable probability of correctness. Its usefulness depends on the engine, language, layout, and material being processed. Missing confidence is also different from low confidence. A vision-based extraction that supplies no comparable score belongs in an unknown-quality category, not at an invented midpoint.&lt;/p&gt;

&lt;p&gt;For ranking, derive a region-quality estimate only after evaluating representative scans. For lexical hits, inspect the regions containing the matched terms. For semantic hits, use the contributing chunk regions conservatively unless a later alignment step identifies a narrower passage. Do not claim word-level certainty from a chunk-level similarity score.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F587ce40c94634216%2Fcover.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F587ce40c94634216%2Fcover.png" title="Two retrieval channels share region evidence; replacement extraction resets assessment." alt="Retrieval path from scan regions through lexical and vector candidates to evidence-aware ranking, alongside unassessed, usable, uncertain, and reviewed evidence states." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make review belong to a specific extraction
&lt;/h2&gt;

&lt;p&gt;Use four evidence states: &lt;strong&gt;unassessed&lt;/strong&gt;, &lt;strong&gt;usable&lt;/strong&gt;, &lt;strong&gt;uncertain&lt;/strong&gt;, and &lt;strong&gt;reviewed&lt;/strong&gt;. These are workflow states, not four numerical confidence bands.&lt;/p&gt;

&lt;p&gt;A new extraction starts unassessed. An automated assessment can mark a region usable or uncertain under a documented policy. A human can inspect either state against the scan and mark that region reviewed. A reviewer can also assess an unassessed region directly. Reviewed means the recorded transcription was checked within a defined scope; it does not certify the document’s factual claims or legal meaning.&lt;/p&gt;

&lt;p&gt;The revision boundary matters. In the policy proposed here, every replacement extraction creates new unassessed regions, including when it incorporates a correction. Review status never transfers automatically. A reviewer must check the new region before it becomes reviewed. This costs an additional assessment, but avoids the ambiguity of attaching an old review to changed text or coordinates.&lt;/p&gt;

&lt;p&gt;Retain the earlier extraction and review record for provenance. The active search index should point to the current extraction, while a result opened from an older search session can identify that its revision has been superseded. Updates to text, lexical index entries, embeddings, and evidence should become visible together; otherwise a result can combine an old embedding with a new snippet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fuse relevance before applying a bounded penalty
&lt;/h2&gt;

&lt;p&gt;Lexical scores and vector similarity scores generally have different scales. Adding them directly makes ranking depend on incidental score ranges. Reciprocal rank fusion offers a practical starting point: each candidate receives contributions based on its position in each list, with absent candidates contributing zero. &lt;a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion" rel="noopener noreferrer"&gt;Elastic’s RRF documentation&lt;/a&gt; describes this method for combining independently ranked results.&lt;/p&gt;

&lt;p&gt;Fuse candidates using stable chunk identities within one extraction revision. Then apply a bounded quality adjustment. I prefer a modest demotion over a hard confidence cutoff because the only relevant passage may be difficult to read.&lt;/p&gt;

&lt;p&gt;The following Python example expresses that policy. &lt;code&gt;base&lt;/code&gt; is an already-fused relevance score; &lt;code&gt;quality&lt;/code&gt; is an application-derived region estimate between zero and one, not raw OCR confidence.&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;evidence_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&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;quality&lt;/span&gt; &lt;span class="ow"&gt;is&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;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;  &lt;span class="c1"&gt;# Preserve rank; display unknown quality separately.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quality must be between 0 and 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;max_penalty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;max_penalty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multiplier stays between 0.8 and 1.0. Missing quality leaves the relevance score unchanged and requires a visible unknown-quality label. That is an explicit starting policy, not an assertion that unknown extraction is reliable. Tune it against retrieval judgments and transcription checks.&lt;/p&gt;

&lt;p&gt;Consider this &lt;strong&gt;hypothetical&lt;/strong&gt; candidate set. The numbers demonstrate the formula; they are not measured performance or recommended parameters.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Fused relevance&lt;/th&gt;
&lt;th&gt;Region quality&lt;/th&gt;
&lt;th&gt;Adjusted score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: uncertain clause&lt;/td&gt;
&lt;td&gt;0.032&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.02688&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: clear related clause&lt;/td&gt;
&lt;td&gt;0.030&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.02970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: unique reference hit&lt;/td&gt;
&lt;td&gt;0.024&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;0.01968&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before adjustment, the order is A, B, C. After adjustment, B overtakes A. C remains available despite weak extraction. Preserving that unique hit also requires candidate-generation and display policies: include exact identifier candidates in the union, retain a protected result slot when appropriate, and label their uncertainty. A bounded penalty alone cannot prevent a later top-k cutoff from discarding them.&lt;/p&gt;

&lt;p&gt;Avoid giving reviewed passages an unconditional relevance boost. A carefully checked but irrelevant paragraph should not outrank the requested clause. Review describes evidence quality; relevance still determines whether the passage answers the query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page metadata changes both eligibility and presentation
&lt;/h2&gt;

&lt;p&gt;Apply authorization and explicit user filters consistently to both retrieval channels before fusion. Folder access, selected document versions, and requested document types determine eligibility. A stale vector index must not reintroduce a passage excluded by the lexical path.&lt;/p&gt;

&lt;p&gt;Other metadata can inform ranking more softly. Repeated headers and footers can generate many nearly identical hits; identifying those regions allows downweighting without erasing potentially meaningful text. Page position can support a heading or appendix heuristic, but “earlier page wins” is a poor general rule. A signature block or technical schedule may be the most relevant material near the end.&lt;/p&gt;

&lt;p&gt;Group redundant chunk hits for presentation while keeping their underlying evidence. Ten overlapping chunks from one page should not crowd out a distinct page with a useful match. Deduplicate by region identity or overlap, not merely by equal text, because identical wording on two pages may have different significance.&lt;/p&gt;

&lt;p&gt;Return the physical page index and, when available, the printed page label as separate fields. The third PDF page might be labeled “ii”; treating these as interchangeable produces misleading navigation. A snippet should also carry its extraction revision and uncertainty annotation. Readers need to know whether the doubtful text is the matched identifier, a neighboring sentence, or a table region used by the semantic candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the path from uncertain hit to corrected result
&lt;/h2&gt;

&lt;p&gt;A useful evaluation set includes exact references, paraphrased clauses, faint numbers, tables, rotated pages, and scans with no comparable confidence output. Judge retrieval separately from transcription: did the relevant page enter the candidate set, and does its displayed text accurately represent the scan?&lt;/p&gt;

&lt;p&gt;Measure candidate recall, final ordering, page navigation, and the effect of quality penalties. Check whether low-quality unique hits survive. Also inspect how often an unknown-quality candidate outranks an assessed one and whether that reflects relevance or an avoidable scoring artifact.&lt;/p&gt;

&lt;p&gt;The correction path needs its own test. In this hypothetical trace, an uncertain reference remains discoverable. A reviewer finds an OCR error, replacement extraction creates revision r2, and r2 requires assessment before review. The index then publishes the corrected text and evidence together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F587ce40c94634216%2Fscreenshot.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F587ce40c94634216%2Fscreenshot.png" title="A correction creates an unassessed revision; review follows a fresh scan check." alt="Six-step hypothetical trace showing an uncertain hit, a bounded penalty, scan inspection, an unassessed replacement revision, review of the new region, and publication of matching indexes." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test interruption between those steps. If extraction succeeds but embedding fails, keep the previous coherent index active or explicitly expose the new revision’s limited retrieval availability. Never present mixed revisions as one settled result. Log the policy version used for assessment and ranking so that a changed order can be explained later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete document-management implementation
&lt;/h2&gt;

&lt;p&gt;My own product, DocBento — Python AI Document Management, is a self-hosted application built with FastAPI and React, using PostgreSQL with pgvector or MariaDB. Its OCR choices include offline PaddleOCR on ONNX Runtime, Tesseract, optional Docling, and OpenAI-compatible vision models. Hybrid keyword and semantic search returns filters, snippets, and page numbers. The optional assistant searches and reads documents before answering and provides page citations.&lt;/p&gt;

&lt;p&gt;Those capabilities provide a concrete setting for the retrieval problems discussed here. The region evidence lifecycle and bounded ranking policy above are engineering proposals; the listed product facts do not establish that DocBento implements them. When assessing a document-search system, I would inspect how it preserves extraction revisions, represents unknown confidence, and connects a result to the scanned page before choosing a quality penalty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentoassist.gumroad.com/l/docbento" rel="noopener noreferrer"&gt;DocBento on Gumroad&lt;/a&gt;&lt;/p&gt;

</description>
      <category>informationretrieval</category>
      <category>ocr</category>
      <category>python</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Reproducible Payroll Starts With a Versioned Calculation Manifest</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:30:06 +0000</pubDate>
      <link>https://dev.to/sourcebento/reproducible-payroll-starts-with-a-versioned-calculation-manifest-54ke</link>
      <guid>https://dev.to/sourcebento/reproducible-payroll-starts-with-a-versioned-calculation-manifest-54ke</guid>
      <description>&lt;p&gt;&lt;em&gt;Keep payroll arithmetic deterministic, preserve its dependencies, and let AI explain results within the reader’s permissions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A payroll result is useful only if you can explain where it came from. Given a disputed deduction or a corrected timesheet, an engineer should be able to recover the inputs, run the applicable calculation, and identify precisely what changed. A persuasive explanation cannot substitute for that evidence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, then fact-checked and edited by the developer behind WorkBento.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The engineering boundary is straightforward: deterministic code calculates payroll; an assistant explains recorded results. The difficult work lies underneath that boundary. Employee records change, attendance arrives late, rules acquire new effective dates, and software deployments replace yesterday’s implementation. Reproducibility requires preserving the dependencies of a calculation, not merely improving the prompt that describes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calculation is a function of a preserved world
&lt;/h2&gt;

&lt;p&gt;Consider a salaried employee with an approved unpaid absence and a recurring deduction. Even this modest calculation depends on more than a salary field. It needs a pay period, a compensation agreement, the absence record, the relevant proration policy, the deduction schedule, and rounding rules.&lt;/p&gt;

&lt;p&gt;Reading those values from current tables creates a moving target. If someone corrects the absence tomorrow, a rerun may return a different amount while claiming to represent the same payroll run. A rule identifier is equally weak if the rule’s contents can change under that identifier.&lt;/p&gt;

&lt;p&gt;I would define a payroll calculation as a function of three preserved dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An input snapshot containing the facts used for that calculation.&lt;/li&gt;
&lt;li&gt;A rule bundle containing the applicable policies, parameters, and effective-date decisions.&lt;/li&gt;
&lt;li&gt;A calculator artifact identifying the implementation that executed those rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output should include line items and calculation evidence, not just a net amount. A deduction line can record the input references, rule reference, intermediate amounts, and rounding operation that produced it. That gives both a human reviewer and an explanation service something concrete to inspect.&lt;/p&gt;

&lt;p&gt;Determinism also requires controlling hidden dependencies. The calculator should not consult the current clock, fetch a live rate, or read a mutable employee record halfway through execution. Resolve those dependencies before calculation and include their values in the snapshot. Where dates matter, preserve the payroll calendar and timezone interpretation as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2Ffd69c202eaf94ee2%2Fcover.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%2Fdraft-content.sourcebento.com%2Fgenerated%2Ffd69c202eaf94ee2%2Fcover.png" title="Preserved inputs, rules, and calculator feed the manifest. Calculation proceeds through approval and release; explanation reads recorded evidence through a permission scope." alt="Dependency cards feed a payroll manifest, followed by calculated, approved, and released states, with a separate permission-scoped explanation path." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Version the facts, rules, and implementation separately
&lt;/h2&gt;

&lt;p&gt;A single version number for the payroll service cannot describe everything that changed. An unchanged binary may calculate different results because a compensation agreement changed. An unchanged input snapshot may produce different results because the rule bundle changed. Those are different causes, and an audit should distinguish them.&lt;/p&gt;

&lt;p&gt;The input snapshot needs the values actually consumed: compensation terms, attendance and leave quantities, applicable deductions, currency, period boundaries, and any external parameters. Preserve units. A quantity of &lt;code&gt;8&lt;/code&gt; is ambiguous unless its meaning is recorded as hours, days, or another defined unit. Keep source record identifiers and revision references alongside copied values so a reviewer can trace their origin.&lt;/p&gt;

&lt;p&gt;Rules need both identity and content. Preserve the selected rule bundle, its effective interval, and the resolved parameters. If a policy chooses a divisor based on a calendar, retaining only the policy name leaves a dependency missing. The calculation must retain the calendar or the resolved divisor and sufficient evidence of its selection.&lt;/p&gt;

&lt;p&gt;The implementation needs more than a Git commit if the executable depends on libraries or build configuration. An immutable build artifact, dependency lockfile, and relevant runtime configuration make a stronger execution record. An artifact digest identifies what ran; it does not by itself ensure that the artifact remains available. Retention is part of the design.&lt;/p&gt;

&lt;p&gt;Finally, preserve the results and the review history. Approval should refer to the exact calculation being approved, including its manifest and result digests. Otherwise an input edit can quietly detach the approved amount from the amount subsequently released.&lt;/p&gt;

&lt;p&gt;This does not require copying an entire HR database for every employee. Store the calculation’s dependency closure: the facts and artifacts it actually used. Shared immutable rule bundles can be referenced by many runs. Sensitive snapshots still need access controls and a retention policy; reproducibility is not a reason to retain unrelated personal data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give each run a small, explicit manifest
&lt;/h2&gt;

&lt;p&gt;The manifest ties these dependencies together. It is an execution record, not a substitute for the referenced data. A compact example might look 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;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_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;"payrun-2026-09-r2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"supersedes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payrun-2026-09-r1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"period"&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="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end_exclusive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input_snapshot_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;"inputs-184"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule_bundle_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;"rules-27"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"calculator_artifact_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;"calculator-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rounding_policy_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;"rounding-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result_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;"result-184"&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;Here, every referenced identifier must resolve to an immutable stored object. The period uses an explicit exclusive end to remove a common boundary ambiguity. The &lt;code&gt;supersedes&lt;/code&gt; field records a correction without overwriting its predecessor. The identifiers are illustrative; a production implementation should also retain verified content digests and the serialization scheme used to compute them.&lt;/p&gt;

&lt;p&gt;Serialization matters because equivalent data can have different byte representations. Define field ordering, date formats, decimal encoding, and schema versions before hashing a manifest. Use decimal arithmetic or appropriately scaled integers for money, with explicit rounding at the stages required by the applicable rules. A digest cannot repair an unspecified rounding policy.&lt;/p&gt;

&lt;p&gt;There is a practical tradeoff here. Storing resolved values simplifies replay but duplicates data. Storing references reduces duplication but makes replay depend on reliable historical storage. I prefer immutable references for shared artifacts and explicit snapshots for employee-specific calculation facts. The important property is that neither path silently resolves to the latest value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval must bind to a calculation, not a period
&lt;/h2&gt;

&lt;p&gt;A useful main state model is &lt;strong&gt;Prepared → Calculated → Approved → Released&lt;/strong&gt;. Each state describes a different kind of evidence.&lt;/p&gt;

&lt;p&gt;Prepared means the manifest and its dependencies have been assembled and frozen. Calculated means the engine has produced a stored result against that manifest. Approved means an authorized reviewer has accepted that exact manifest and result. Released means the approved result has been handed to the configured downstream process, with a release record identifying what was sent.&lt;/p&gt;

&lt;p&gt;Release does not automatically mean that money settled successfully. If the system needs settlement tracking, model it separately using evidence from the payment process. Avoid giving a local state a stronger meaning than its evidence supports.&lt;/p&gt;

&lt;p&gt;Any calculation-affecting correction creates a successor run at Prepared. It should not move an Approved run backward while replacing its contents. The successor can refer to the predecessor and explain the changed inputs. Whether a correction requires a replacement payment or an adjustment in another period depends on the applicable process; preserve that decision rather than burying it in arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2Ffd69c202eaf94ee2%2Fscreenshot.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%2Fdraft-content.sourcebento.com%2Fgenerated%2Ffd69c202eaf94ee2%2Fscreenshot.png" title="Illustrative correction workflow: the predecessor remains preserved while a successor is prepared, calculated, compared, approved, and released." alt="An illustrative correction workflow shows a released predecessor, a corrected attendance input, successor preparation and calculation, a result comparison, new approval, and release." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A review screen should show the difference between runs at the line-item level. “Net pay changed” is insufficient. A reviewer needs to see that an absence quantity changed, which rule consumed it, and which amounts consequently changed. The comparison itself should be reproducible from the two preserved results.&lt;/p&gt;

&lt;p&gt;Test this contract with known calculation fixtures and replay tests. Also test the failure boundaries: a missing artifact should fail replay explicitly; changing an input should invalidate an existing approval reference; repeated release requests should not create duplicate downstream work. These tests exercise the lifecycle around the arithmetic, where otherwise-correct payroll engines can lose their audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let AI explain evidence without becoming a dependency
&lt;/h2&gt;

&lt;p&gt;An assistant can help answer “Why is this deduction higher than last period?” It should retrieve the relevant stored line items, their rule references, and the authorized comparison data. It can then turn that evidence into readable prose.&lt;/p&gt;

&lt;p&gt;It should not invent a missing rule, infer an unrecorded agreement, or calculate an authoritative replacement amount from a conversation. If the stored result lacks enough evidence, the useful answer identifies what is missing. Fluency is not a reason to fill the gap.&lt;/p&gt;

&lt;p&gt;Permissions apply before retrieval. An employee may be allowed to inspect their own payslip while a payroll reviewer can inspect a wider set of records. The explanation path should receive only the evidence permitted for the requesting user. Restricting the final wording after unrestricted retrieval is a fragile boundary, especially when comparisons involve other employees.&lt;/p&gt;

&lt;p&gt;Keep the assistant outside the calculation state machine. Generating an explanation must not approve a run, change an input, or release a payment. A failed explanation request should have no effect on the stored payroll result.&lt;/p&gt;

&lt;p&gt;There are also two different reproducibility goals. The financial result should replay from its preserved dependencies. Generated prose may vary across model versions or inference settings. If an explanation must be audited, retain the displayed text, evidence references, requesting identity, model identifier, and relevant prompt configuration under an appropriate retention policy. Recording a prompt alone does not guarantee identical future prose.&lt;/p&gt;

&lt;p&gt;I would start with deterministic explanations for common line items and use AI where questions span multiple records or need contextual phrasing. That keeps routine payslip evidence available even when the assistant is disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the boundary in an HR platform
&lt;/h2&gt;

&lt;p&gt;My own product, WorkBento — Python HR and Payroll Platform, is a concrete setting for this separation. It is a self-hosted FastAPI and React platform using PostgreSQL with pgvector, MongoDB, and Meilisearch. Its modules include employees, attendance, shifts, leave approvals, and payroll, among other business functions. Those domains illustrate why a calculation must preserve its dependencies across mutable operational records.&lt;/p&gt;

&lt;p&gt;WorkBento’s optional assistant answers questions from company data through a guarded read-only SQL layer and applies the requesting user’s permissions. That provides a concrete retrieval boundary for an explanation service. It does not, by itself, establish deterministic payroll replay or approval binding; those require the manifest, immutable dependencies, and lifecycle contracts described above.&lt;/p&gt;

&lt;p&gt;Docker Compose deployment and full source code are included, allowing builders to inspect how the platform is assembled and evaluate the boundaries for their own requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentoassist.gumroad.com/l/workbento" rel="noopener noreferrer"&gt;WorkBento on Gumroad&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>python</category>
      <category>dataengineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Reseller Wallet Balance Is a Ledger Projection</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:29:19 +0000</pubDate>
      <link>https://dev.to/sourcebento/a-reseller-wallet-balance-is-a-ledger-projection-3e71</link>
      <guid>https://dev.to/sourcebento/a-reseller-wallet-balance-is-a-ledger-projection-3e71</guid>
      <description>&lt;p&gt;&lt;em&gt;Model credits, purchases, commissions and refunds as durable postings, then serialize the decisions that spend them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A reseller wallet looks like a number until someone asks why it changed. A prepaid top-up, an order charge, a commission credit and a refund can all affect the same balance, but they carry different evidence and reversal rules. Updating &lt;code&gt;wallet.balance&lt;/code&gt; preserves the answer to “how much?” while discarding much of the answer to “why?”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, then fact-checked and edited by the developer behind eSIMQR.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The balance is an answer, not the record
&lt;/h2&gt;

&lt;p&gt;Consider an illustrative USD wallet. A reseller adds $100, buys a product for $30 and earns a $3 commission credited to the wallet. Its posted balance is now $73. If the purchase is fully refunded and the commission is reversed, the wallet returns to $100.&lt;/p&gt;

&lt;p&gt;A mutable balance can represent every one of those outcomes. It cannot, by itself, distinguish the correct sequence from an accidental $27 adjustment. Nor does it tell you whether a second refund notification has already been processed.&lt;/p&gt;

&lt;p&gt;An append-only ledger records each accepted financial event and the accounts it affects. The displayed balance is a projection of those postings. A cached balance is still useful, especially for authorization and fast reads, but it must be reproducible from the record underneath it.&lt;/p&gt;

&lt;p&gt;I would use balanced entries even for a modest reseller system. They force each movement to have a counterpart. A wallet credit must come from somewhere; a wallet debit must go somewhere. That discipline catches missing legs and makes reconciliation more explicit. It does not prove that the transaction was legitimate: an erroneous transaction can balance perfectly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F858447f45c3f47ad%2Fcover.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F858447f45c3f47ad%2Fcover.png" title="Illustrative USD postings. Each pair sums to zero; wallet postings determine the displayed balance." alt="Five balanced posting pairs for a top-up, purchase, commission, refund and commission reversal" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every movement a counterpart
&lt;/h2&gt;

&lt;p&gt;For a wallet subledger, define a signed movement convention: positive amounts increase an account's tracked position, and negative amounts decrease it. This convention is not a substitute for debit and credit rules in a general ledger. Document how these accounts map into the operator's accounting system.&lt;/p&gt;

&lt;p&gt;The example uses four accounts: reseller wallet, funding clearing, order clearing and commission expense. Its postings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-up: wallet +$100; funding clearing −$100.&lt;/li&gt;
&lt;li&gt;Purchase: wallet −$30; order clearing +$30.&lt;/li&gt;
&lt;li&gt;Commission: wallet +$3; commission expense −$3.&lt;/li&gt;
&lt;li&gt;Purchase refund: wallet +$30; order clearing −$30.&lt;/li&gt;
&lt;li&gt;Commission reversal: wallet −$3; commission expense +$3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clearing accounts identify amounts awaiting reconciliation with other records. They should not become miscellaneous buckets that hide unexplained differences. A payment processor's settlement and fees may require further entries; a wallet top-up alone does not establish that the operator has received settled cash.&lt;/p&gt;

&lt;p&gt;The commission policy matters just as much as the arithmetic. Here, the $30 purchase debit and $3 commission credit are separate events, and the commission becomes immediately spendable. Another business might charge a net wholesale price or keep commissions pending until an eligibility date. Choose one policy explicitly. Crediting a commission and also discounting the same purchase by that commission would count the benefit twice.&lt;/p&gt;

&lt;p&gt;Keep currency inside the account identity. USD and EUR postings cannot balance each other by adding their integers. Currency conversion needs an explicit exchange transaction, a recorded rate and a rounding policy. For this example, all amounts are integer cents in USD.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small schema with deliberate limits
&lt;/h2&gt;

&lt;p&gt;A useful starting point separates accounts, journal entries and postings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ledger_account&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^[A-Z]{3}$'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;journal_entry&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;operation_key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^[A-Z]{3}$'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;source_ref&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reverses_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;journal_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;recorded_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;posting&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;entry_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_minor&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_minor&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;journal_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ledger_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&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 composite foreign keys keep an entry and its accounts in the same currency. The posting key permits one aggregated movement per account per entry. &lt;code&gt;source_ref&lt;/code&gt; connects the financial event to an order, payment or adjustment record; &lt;code&gt;reverses_id&lt;/code&gt; preserves the relationship to an earlier entry.&lt;/p&gt;

&lt;p&gt;This is a schema skeleton, not a complete enforcement mechanism. It does not enforce a zero sum across postings, require two distinct accounts or prevent later edits. Ordinary row checks cannot validate an aggregate across a posting set. Use a controlled posting function or a deferred constraint trigger to reject incomplete or unbalanced entries at commit, and restrict direct table writes. The application role should not be able to update or delete posted history.&lt;/p&gt;

&lt;p&gt;The wallet balance is the sum of its &lt;code&gt;amount_minor&lt;/code&gt; values. If you maintain a cached balance, update it in the same database transaction as the postings. Reconciliation should periodically compare that cache with the sum and surface any difference.&lt;/p&gt;

&lt;p&gt;Make &lt;code&gt;operation_key&lt;/code&gt; identify the business effect, such as &lt;code&gt;order:123:purchase&lt;/code&gt;, rather than an individual HTTP delivery. Duplicate requests must return the existing result. Store enough request information to reject reuse of a key with a different amount, currency or source; uniqueness alone cannot detect that mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serialize available funds, including reservations
&lt;/h2&gt;

&lt;p&gt;A ledger preserves history. It does not automatically prevent overspending.&lt;/p&gt;

&lt;p&gt;Suppose two orders each try to spend $60 from a $100 wallet. If both read the balance before either commits, both may authorize. Their entries can be individually balanced while leaving the wallet overdrawn.&lt;/p&gt;

&lt;p&gt;One practical PostgreSQL protocol uses a stable wallet account row as the serialization point. Start a transaction at &lt;code&gt;READ COMMITTED&lt;/code&gt;, lock that row with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;, then read the current posted balance and active reservations in subsequent statements. Validate the operation, write its financial or reservation changes, update any cache and commit. PostgreSQL holds the row lock until transaction end, so competing transactions using that same lock wait. See the &lt;a href="https://www.postgresql.org/docs/18/explicit-locking.html" rel="noopener noreferrer"&gt;PostgreSQL documentation on explicit locking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every operation that changes spendability must follow this protocol. That includes top-ups, purchases, commissions, refunds and adjustments, plus reservation creation, release and consumption. Locking only ledger writes leaves a race between a new hold and a purchase authorization.&lt;/p&gt;

&lt;p&gt;A reservation has three states in this design: active, consumed and released. Available funds equal posted balance minus active reservations. Consuming a $30 hold must atomically create the $30 purchase debit and mark the hold consumed. Otherwise, an intermediate state can either charge and hold the amount simultaneously or make the held funds available before the charge exists. Releasing a hold changes no posted balance, but it still changes authorization capacity and requires the same lock.&lt;/p&gt;

&lt;p&gt;Do not hold a database transaction open across a supplier HTTP call. Commit the reservation, perform the external work, then consume or release it in another serialized transaction. An ambiguous timeout needs investigation or an idempotent status check; elapsed time alone is weak evidence that no purchase occurred.&lt;/p&gt;

&lt;p&gt;For operations involving multiple wallets, acquire locks in a consistent order. A single wallet lock also imposes a throughput limit on a busy account. That is a concrete tradeoff: straightforward authorization correctness in exchange for serialized mutations per wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refunds append corrections to history
&lt;/h2&gt;

&lt;p&gt;A refund should add entries rather than delete the purchase. In the example, the refund credits $30 and the commission reversal debits $3, producing a net wallet increase of $27.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F858447f45c3f47ad%2Fscreenshot.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F858447f45c3f47ad%2Fscreenshot.png" title="Illustrative full refund: the wallet moves from $73 to $100, and a duplicate request creates no new postings." alt="Five-step refund trace showing request validation, wallet locking, purchase refund, commission reversal and retry handling" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If policy requires both effects together, commit the refund and commission reversal in one transaction. Link each to its original entry, and give each effect a stable operation key derived from the refund identity. A duplicate callback then finds the existing refund result instead of minting another credit.&lt;/p&gt;

&lt;p&gt;Partial refunds need more than a reversal pointer. Under the wallet lock, check the cumulative refunded amount against the original charge. Calculate the commission reversal from the original commission policy, including its rounding rule, rather than from today's configuration. Preserve that calculation's inputs with the refund record.&lt;/p&gt;

&lt;p&gt;The reseller may already have spent the commission. Decide whether a reversal may create debt, whether another refund credit offsets it, or whether the amount becomes a separate receivable. An append-only ledger exposes this business decision; it cannot make it disappear. Silently capping the reversal at the current balance changes the economic result and needs its own explicit policy and record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditability needs evidence outside the postings
&lt;/h2&gt;

&lt;p&gt;Balanced entries answer whether the arithmetic is internally consistent. Reconciliation asks whether those entries agree with independent evidence.&lt;/p&gt;

&lt;p&gt;For each top-up, retain the payment identity, currency, amount and relevant payment status. For each order debit, retain the order and fulfillment relationship. For commissions, record the policy version and eligible amount. Administrative adjustments need an actor and reason, not just a free-form “correction” label.&lt;/p&gt;

&lt;p&gt;Record when the system accepted an entry separately from when the underlying event occurred. A late payment notification can describe an earlier event; rewriting its timestamp to look timely obscures what the system knew when it authorized spending.&lt;/p&gt;

&lt;p&gt;An append-only application interface is not tamper-proof storage. Database administrators can have broader powers. Restricted roles, retained backups and access logs provide additional evidence, while external payment and supplier records provide independent checks. Start with useful reconciliation queries: unbalanced entries, cache differences, unmatched funding, over-refunded orders and consumed reservations without a corresponding charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying this to a reseller platform
&lt;/h2&gt;

&lt;p&gt;My own product, eSIMQR, is a self-hosted platform for selling travel eSIMs under the operator's brand. Its reseller portal includes prepaid wallets and commissions, making these accounting boundaries directly relevant. Its PostgreSQL backend and built-in mock supplier and payment gateway provide a setting for local end-to-end testing without charging money or issuing a real eSIM.&lt;/p&gt;

&lt;p&gt;Those product facts do not establish that the illustrative schema or locking protocol above is its implementation. They define useful acceptance tests for a wallet system: repeated payment delivery, competing purchases, reservation consumption and refunds with commission reversals. Real provider access, approval, funding and production verification remain the operator's responsibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentoassist.gumroad.com/l/esimqr" rel="noopener noreferrer"&gt;eSIMQR on Gumroad&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>databasedesign</category>
      <category>backendengineering</category>
      <category>paymentsystems</category>
    </item>
    <item>
      <title>Page Citations as a Document Assistant’s Interface</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:27:24 +0000</pubDate>
      <link>https://dev.to/sourcebento/page-citations-as-a-document-assistants-interface-2add</link>
      <guid>https://dev.to/sourcebento/page-citations-as-a-document-assistants-interface-2add</guid>
      <description>&lt;p&gt;&lt;em&gt;Designing retrieval evidence, stable page anchors and verifiable answers around a practical request flow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A document assistant’s most useful output is often the small page reference beside a sentence. That reference is where a reader can inspect the evidence, resolve an ambiguity and decide whether to act. If it opens the wrong version, lands several pages away or supports only half the claim, fluent prose does little to repair the interface.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, then fact-checked and edited by the developer behind DocBento.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For an experienced software builder, this changes the design problem. The answer is one representation of an evidence trail. Retrieval, document reading, answer generation and page navigation must agree on what that trail contains. The architecture below is a proposed design, using an illustrative request about an approval deadline; it does not describe verified internals of any particular application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin with a claim the reader can check
&lt;/h2&gt;

&lt;p&gt;Consider this request: “When does the supplier renewal need approval, and who can approve it?” A useful answer needs at least two claims: a deadline and an authorized approver. Those claims may come from different pages, and they may have different levels of support.&lt;/p&gt;

&lt;p&gt;The request flow begins by establishing the authorized search scope, retrieving candidate passages and reading the relevant document content. Only then should the assistant decide what it can say. A search hit is a pointer to possible evidence. A passage that contains “approval” and “renewal” might describe an unrelated process, an obsolete policy or a historical example.&lt;/p&gt;

&lt;p&gt;I would make evidence classification explicit before composing the answer. For each requested claim, use four states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sufficient:&lt;/strong&gt; readable source material directly supports the claim within the requested scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial:&lt;/strong&gt; the material supports some of the claim, but leaves a necessary detail unresolved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflicting:&lt;/strong&gt; relevant sources disagree and the available context does not resolve the disagreement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No evidence:&lt;/strong&gt; a completed search and reading process found no usable support within the authorized scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These states are branches, not stages. A claim does not advance from partial to conflicting on its way to sufficient. The deadline might be sufficient while the approver remains partial. Classifying the entire answer with one status would hide that distinction.&lt;/p&gt;

&lt;p&gt;Sufficient evidence leads to a supported answer. The other three states lead to an answer with limits: identify the supported portion, expose the disagreement or say that the requested fact could not be established. Page verification is available when there is readable cited evidence. An answer with no evidence has no supporting page to open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F6605658b9e754686%2Fcover.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F6605658b9e754686%2Fcover.png" title="Four evidence branches determine the answer; page verification follows only when readable citations exist." alt="Request and retrieval flow branching into Sufficient, Partial, Conflicting and No evidence, then leading to Supported answer or Answer with limits and conditional Verify page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval should preserve evidence identity
&lt;/h2&gt;

&lt;p&gt;Hybrid retrieval can combine exact terms with semantic similarity. Exact matching helps with identifiers, names and distinctive clauses; semantic matching helps when the question and document use different language. Neither method establishes that a passage supports a claim. Ranking produces candidates for inspection.&lt;/p&gt;

&lt;p&gt;Preserve identity through that inspection. At minimum, an evidence record should identify the document, its version, the physical page position and the relevant passage. A chunk identifier helps debugging, but it should not be the reader’s only route to the source. Rebuilding an index may change chunks while leaving the underlying document unchanged.&lt;/p&gt;

&lt;p&gt;Here is a &lt;strong&gt;proposed interface schema&lt;/strong&gt;, expressed as JSON, for one evidence record:&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;"evidence_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;"ev_17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"document_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;"doc_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version_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;"ver_3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"span_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Renewals require approval by the procurement lead."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claim_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorized_approver"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"support"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct"&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 values and passage are illustrative. &lt;code&gt;page_index&lt;/code&gt; is zero-based, so it identifies the seventh physical page. &lt;code&gt;page_label&lt;/code&gt; records the printed label, which can differ because of cover sheets or front matter. &lt;code&gt;claim_ids&lt;/code&gt; connects evidence to the particular assertion it supports. Here, the passage supports an approver claim but says nothing about the deadline.&lt;/p&gt;

&lt;p&gt;This record is a contract between retrieval, generation and navigation. It is not proof by itself. The application still needs to validate that the version exists, the page position is valid and the passage matches the source representation used for reading. If &lt;code&gt;span_text&lt;/code&gt; comes from OCR, preserve that provenance elsewhere in the record or its associated extraction metadata.&lt;/p&gt;

&lt;p&gt;An evidence identifier also gives the answer generator a constrained reference vocabulary. The model can cite &lt;code&gt;ev_17&lt;/code&gt;; application code resolves it into a navigable reference. Reject unknown evidence identifiers instead of allowing generated document names and page numbers to become authoritative links.&lt;/p&gt;

&lt;h2&gt;
  
  
  A page anchor must survive the next document version
&lt;/h2&gt;

&lt;p&gt;A citation such as “Policy, page 5” is readable but underspecified. Which policy? Which version? Does “5” refer to the fifth page in the viewer or the number printed on the page?&lt;/p&gt;

&lt;p&gt;Use a version-specific target with an explicit physical page position. The visible label can remain friendly: “Renewal policy · version 3 · printed page 5.” The viewer should navigate using the physical position. If a newer version becomes available, the old answer should continue pointing to the version it actually used.&lt;/p&gt;

&lt;p&gt;There is a retention tradeoff here. Keeping cited versions makes historical answers easier to inspect, but consumes storage and may conflict with a document deletion policy. When a cited version is no longer available, show that condition explicitly. Quietly redirecting to the latest version replaces the evidence beneath an unchanged claim.&lt;/p&gt;

&lt;p&gt;A text highlight improves navigation when it is reliable. For scans, bounding boxes can help readers locate the relevant line. But coordinates depend on a rendering convention: page rotation, cropping and scale must agree between extraction and preview. A misleading highlight is worse than opening the correct page without one.&lt;/p&gt;

&lt;p&gt;Access control remains part of resolution. Check permission when retrieving evidence and again when opening a citation. A reader’s access can change after an answer is created. If access is revoked, the citation should report that the source is unavailable to that reader; it should not reveal a passage through a tooltip or cached preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose the answer around the evidence boundary
&lt;/h2&gt;

&lt;p&gt;Once the assistant has read candidate sources, answer generation should preserve the distinction between source text and interpretation. “The document names the procurement lead as approver” is a different assertion from “the procurement lead is available to approve this renewal.” The second needs additional evidence.&lt;/p&gt;

&lt;p&gt;Attach citations close to the claims they support. A single reference at the end of a paragraph containing a deadline, an approver and an exception makes verification unnecessarily difficult. Splitting the paragraph into two sentences may be the most effective interface improvement available.&lt;/p&gt;

&lt;p&gt;For partial evidence, state the established fact and the missing fact separately. For conflicting evidence, cite both sources and explain the disagreement in ordinary language. Do not turn retrieval rank into a policy precedence rule. A newer upload timestamp, for example, does not necessarily establish which document governs.&lt;/p&gt;

&lt;p&gt;The following trace illustrates a conflict in the deadline claim. It is a hypothetical workflow, not a reported incident.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdraft-content.sourcebento.com%2Fgenerated%2F6605658b9e754686%2Fscreenshot.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%2Fdraft-content.sourcebento.com%2Fgenerated%2F6605658b9e754686%2Fscreenshot.png" title="Two retrieved pages give different deadlines; the answer preserves the conflict and links both pages for inspection." alt="Six-step hypothetical deadline request: Request, Retrieve, Read, Classify as Conflicting, Answer with limits and Verify page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the application has a documented rule for resolving precedence, apply it explicitly and show the reason. Otherwise, retain the conflicting state. The assistant can still provide value by locating the exact disagreement and reducing the reader’s search work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat verification failures as interface outcomes
&lt;/h2&gt;

&lt;p&gt;Clicking a citation is part of the request flow. It should open the cited version at the expected page, with enough document context to assess the passage. The reader may need the heading, neighboring paragraph or footnote to understand a condition omitted from the extracted text.&lt;/p&gt;

&lt;p&gt;Several failures require distinct treatment. An extraction problem may leave a readable scanned page with unusable text. A source may disappear after retrieval. A viewer may fail to load. None of these proves that the document lacks the requested fact.&lt;/p&gt;

&lt;p&gt;Keep execution failures separate from the four evidence states. “No evidence” means the intended search and reading work completed without usable support. A search timeout means that work did not complete. Likewise, a citation that cannot be opened is a verification failure, even if the earlier answer had sufficient evidence when generated.&lt;/p&gt;

&lt;p&gt;This distinction should reach the reader. “The search could not complete” invites a retry. “The cited version is unavailable” explains why inspection failed. “No supporting passage was found in the searched documents” describes a bounded evidence result. Collapsing all three into “I don’t know” hides the action that could resolve the problem.&lt;/p&gt;

&lt;p&gt;Tests should exercise these boundaries. Check that an approver passage cannot support a deadline claim, that printed page labels resolve to the right physical pages and that a version replacement leaves earlier citations intact. Include permission changes and missing versions. These checks assess whether the reader can inspect the promised evidence; they do not require an invented universal score for answer quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete implementation: DocBento
&lt;/h2&gt;

&lt;p&gt;DocBento is my own self-hosted document management application, built with FastAPI and React and using PostgreSQL with pgvector or MariaDB. Its hybrid keyword and semantic search returns filters, snippets and page numbers. Its optional assistant searches and reads documents before answering and provides page citations.&lt;/p&gt;

&lt;p&gt;Document versions and previews are relevant to source inspection, while roles, groups and restricted folders define access boundaries. Those are verified product capabilities. The proposed evidence schema, per-claim state model and version-specific citation contract in this article are architectural recommendations; the listed capabilities do not establish that DocBento implements every detail of that design.&lt;/p&gt;

&lt;p&gt;For a document assistant, the useful acceptance question is concrete: can a reader follow this claim to the evidence that justified it, understand its limits and recognize when verification fails? That question belongs in the interface specification alongside the answer endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bentoassist.gumroad.com/l/docbento" rel="noopener noreferrer"&gt;DocBento on Gumroad&lt;/a&gt;&lt;/p&gt;

</description>
      <category>documentmanagement</category>
      <category>retrievalaugmentedai</category>
      <category>apidesign</category>
      <category>python</category>
    </item>
    <item>
      <title>What should work offline in a Flutter document scanner?</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Mon, 28 Sep 2026 23:35:58 +0000</pubDate>
      <link>https://dev.to/sourcebento/what-should-work-offline-in-a-flutter-document-scanner-2hmo</link>
      <guid>https://dev.to/sourcebento/what-should-work-offline-in-a-flutter-document-scanner-2hmo</guid>
      <description>&lt;p&gt;A mobile scanner is often used at the exact moment connectivity is inconvenient: a receipt at a counter, a form in a warehouse, or a page photographed away from a desk.&lt;/p&gt;

&lt;p&gt;That makes “works offline” a workflow question. Can the user capture, correct, reorder, and export pages without a server? Or does the app merely open while every useful action waits for a network request?&lt;/p&gt;

&lt;p&gt;Disclosure: we build Bento Scan at SourceBento. It is a commercial Flutter document-scanner source package for Android and iOS, with a self-hosted Python backend. Its division of work between device and server is a useful starting point for this discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the immediate feedback loop on the phone
&lt;/h2&gt;

&lt;p&gt;Finding the document boundary and showing a crop preview are interactive tasks. The user needs to see the effect while the document is still in front of the camera.&lt;/p&gt;

&lt;p&gt;Bento Scan performs document edge detection on-device, offers automatic capture, corrects perspective, and includes filters and AI page flattening. The mobile workflow also supports rotation, page reordering, multipage scans, and PDF export. Core capture and enhancement work offline without Google Play Services.&lt;/p&gt;

&lt;p&gt;This does not mean every feature runs locally. It means the capture-to-export path can remain useful on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91car4e4pz0u38x241oc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91car4e4pz0u38x241oc.jpg" alt="Bento Scan mobile document scanning interface" width="576" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish perspective correction from page flattening
&lt;/h2&gt;

&lt;p&gt;A photograph of a flat sheet can look trapezoidal because the camera is tilted. Perspective correction addresses that geometry.&lt;/p&gt;

&lt;p&gt;A folded receipt or a curved book page presents a different problem: the content itself is no longer on a single flat plane. Flattening attempts to correct that deformation. It is worth testing both operations separately, with text near the edges as well as in the center.&lt;/p&gt;

&lt;p&gt;Automatic processing should still leave a human correction path. Cropping the wrong corner or making small text harder to read is more consequential than producing a less dramatic before-and-after image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the backend for shared document services
&lt;/h2&gt;

&lt;p&gt;Bento Scan's FastAPI backend adds OCR in 100+ languages, searchable PDF generation, a cloud library, folders, tags, full-text search, and S3-compatible storage. A web dashboard supports uploads, preview, OCR text, and downloads.&lt;/p&gt;

&lt;p&gt;Those capabilities introduce different operational questions: where uploaded documents live, how access is controlled, how much storage a user can consume, and how the OCR worker is monitored. The package includes authentication, quotas, deployment documentation, and Docker Compose with Caddy for HTTPS.&lt;/p&gt;

&lt;p&gt;A useful product description must distinguish local enhancement from server-side OCR. Calling the whole application “offline AI” would hide a dependency that matters to buyers and end users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate the difficult documents
&lt;/h2&gt;

&lt;p&gt;Before adapting a scanner for a client, build a small test set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clean printed page photographed at an angle.&lt;/li&gt;
&lt;li&gt;A receipt with a visible fold and small numbers.&lt;/li&gt;
&lt;li&gt;A page with a strong shadow or low contrast.&lt;/li&gt;
&lt;li&gt;Several pages that need reordering before export.&lt;/li&gt;
&lt;li&gt;A document in the actual language your users scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then repeat capture and PDF export with the network disconnected. Check legibility, boundaries, page order, and file size. Reconnect and evaluate OCR and search separately. These checks help you decide whether a problem belongs to capture, enhancement, recognition, or storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buying source still leaves release work
&lt;/h2&gt;

&lt;p&gt;Bento Scan includes Flutter, Python, web, model, and deployment source plus a white-label guide. Buyers can adapt the name, icons, colors, application identifiers, and backend settings within the purchased license.&lt;/p&gt;

&lt;p&gt;You still need to configure hosting and storage, own your signing credentials, test on physical devices, and follow the relevant store release process. A browser-based device demo is helpful for exploring screens, but it cannot fully reproduce camera behavior on the phones your customers use.&lt;/p&gt;

&lt;p&gt;Explore the &lt;a href="https://sourcebento.com/products/bento-scan" rel="noopener noreferrer"&gt;product page and mobile demos&lt;/a&gt;, or inspect the &lt;a href="https://www.codester.com/items/71446/bento-scan-ai-document-scanner-flutter-app" rel="noopener noreferrer"&gt;Bento Scan source package on Codester&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For your use case, is the harder problem capturing a readable page or finding it again later?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prepared with AI assistance from Bento Scan's published documentation; published by SourceBento.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>Read-only is not enough: designing an HR assistant around permissions</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Mon, 28 Sep 2026 23:26:43 +0000</pubDate>
      <link>https://dev.to/sourcebento/read-only-is-not-enough-designing-an-hr-assistant-around-permissions-46mn</link>
      <guid>https://dev.to/sourcebento/read-only-is-not-enough-designing-an-hr-assistant-around-permissions-46mn</guid>
      <description>&lt;p&gt;“Show me this month's payroll” looks like a straightforward request. In an HR application, the first question is who is asking.&lt;/p&gt;

&lt;p&gt;An employee looking for their own payslip, a manager checking a team, and an administrator preparing a payroll run are using the same system with different responsibilities. Giving an AI assistant a read-only database connection does not, by itself, preserve those boundaries.&lt;/p&gt;

&lt;p&gt;Disclosure: we build WorkBento at SourceBento. It is a commercial, self-hosted Python HR and workplace platform with editable source code. Here are the engineering concerns worth evaluating when adding an assistant to this kind of application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-only prevents one class of damage
&lt;/h2&gt;

&lt;p&gt;A read-only query layer limits what can be changed. It does not determine which rows or columns a person may see.&lt;/p&gt;

&lt;p&gt;Even an aggregate can reveal sensitive information. A departmental total may effectively expose one person's salary if that department contains a single employee. Permission checks therefore need to consider the requested operation and its data scope, not just whether the SQL begins with SELECT.&lt;/p&gt;

&lt;p&gt;WorkBento's optional assistant uses a guarded, read-only SQL layer with permission-aware access. The application also scopes queries to the workspace and provides a role and permission matrix. These controls belong in the application and data-access path; a prompt asking the model to behave is not a replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test a question across roles
&lt;/h2&gt;

&lt;p&gt;A useful evaluation starts with sample data and several accounts. For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Boundary to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Employee asks for a payslip&lt;/td&gt;
&lt;td&gt;Access remains limited to permitted employee data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manager asks about leave&lt;/td&gt;
&lt;td&gt;Results respect the manager's team scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User names a different workspace&lt;/td&gt;
&lt;td&gt;The request cannot cross the workspace boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt asks to update a salary&lt;/td&gt;
&lt;td&gt;The assistant's read-only path cannot perform the change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User asks for hidden fields&lt;/td&gt;
&lt;td&gt;Responses do not bypass the application's permission policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are suggested acceptance checks for your deployment, not a claim that any software is immune to every prompt or configuration mistake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyxl805dyh6f03hdy5ole.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyxl805dyh6f03hdy5ole.jpg" alt="WorkBento HR and workplace interface" width="800" height="722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI depends on ordinary workflow quality
&lt;/h2&gt;

&lt;p&gt;A leave summary is only as useful as the underlying requests and approvals. Payroll depends on contracts, attendance, approved leave, adjustments, and the rules an organization actually uses.&lt;/p&gt;

&lt;p&gt;WorkBento brings those records together with employees, departments, shifts, recruitment, projects, tasks, and CRM. Its payroll workflow includes periods, payslips, adjustments, and CSV export. The assistant is an optional way to query information; the core workflows remain the foundation.&lt;/p&gt;

&lt;p&gt;This is particularly important for a source-code buyer. Before customizing screens, agree on what a pay period means, which records are authoritative, and who can approve corrections. Test those decisions with sample records before importing real employee data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting is an operating responsibility
&lt;/h2&gt;

&lt;p&gt;The package includes Python and web application source, Docker Compose, installation documentation, HTTPS guidance, and backup tooling. AI use requires configuring an OpenRouter key; consider what data the chosen model provider receives.&lt;/p&gt;

&lt;p&gt;Owning the deployment means planning access reviews, backup restoration, upgrades, and operational monitoring. Payroll calculations also require validation against the target organization's rules and local requirements. WorkBento is a customizable software foundation, not a guarantee of payroll or tax compliance in every country.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explore the workflow before the code
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://work-demo.sourcebento.com/" rel="noopener noreferrer"&gt;WorkBento demo&lt;/a&gt; lets you explore the application with sample data. The &lt;a href="https://www.codester.com/items/71496/workbento-python-hr-and-payroll-platform" rel="noopener noreferrer"&gt;Codester package&lt;/a&gt; lists the source, features, and deployment requirements.&lt;/p&gt;

&lt;p&gt;When evaluating an HR assistant, which boundary would you test first: individual records, team scope, or aggregate reporting?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prepared with AI assistance from WorkBento's published documentation; published by SourceBento.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Document AI needs a path back to the page: inside DocBento</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Mon, 28 Sep 2026 23:20:37 +0000</pubDate>
      <link>https://dev.to/sourcebento/document-ai-needs-a-path-back-to-the-page-inside-docbento-39lo</link>
      <guid>https://dev.to/sourcebento/document-ai-needs-a-path-back-to-the-page-inside-docbento-39lo</guid>
      <description>&lt;p&gt;A document assistant can produce a fluent answer and still leave you with the real work: finding the passage that supports it.&lt;/p&gt;

&lt;p&gt;For a question such as “When does this agreement renew?”, a useful answer needs more than a date. You need to know which document version supplied it, which page contains it, and whether the system could actually read that page.&lt;/p&gt;

&lt;p&gt;Disclosure: we build DocBento at SourceBento. It is a commercial, self-hosted Python document management application with editable source code. This article explains the design considerations behind its workflow, along with checks you can apply to other document systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate reading, finding, and answering
&lt;/h2&gt;

&lt;p&gt;Think of the workflow as three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the document.&lt;/strong&gt; Extract text while keeping a connection to its original page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find relevant evidence.&lt;/strong&gt; Retrieve the pages that match the question and that the user is allowed to access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer with references.&lt;/strong&gt; Give the reader a route back to the evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the first stage loses a number in a scanned table, a stronger language model cannot reliably repair that loss. If retrieval selects last year's agreement, a well-written answer can still be wrong.&lt;/p&gt;

&lt;p&gt;DocBento offers four OCR paths: PaddleOCR for local CPU processing, Tesseract, optional Docling for layout and tables, and a vision-model option. The point of having choices is to match the input: a clean digital page, a table-heavy scan, and a photograph of handwriting are different problems.&lt;/p&gt;

&lt;p&gt;A practical evaluation set should include those difficult inputs, not just a pristine PDF. Compare extracted text against the image before judging the assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact terms and natural-language questions both matter
&lt;/h2&gt;

&lt;p&gt;An invoice number calls for an exact match. “Documents about renewing a supplier agreement” calls for a broader search.&lt;/p&gt;

&lt;p&gt;DocBento combines keyword and semantic search with filters, snippets, and page numbers. Its optional assistant searches and reads documents, then returns answers with page citations. A citation is a verification aid, not proof that the interpretation is correct: open the cited page and check that it supports the specific claim.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5dg3bpbyy2dmg3gvn5z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5dg3bpbyy2dmg3gvn5z.jpg" alt="DocBento document management interface" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions must also cover the answer
&lt;/h2&gt;

&lt;p&gt;Hiding a restricted folder in the sidebar is insufficient if its contents can still appear in search or a generated summary. DocBento applies restricted-folder access to search and its AI features as well as the document interface.&lt;/p&gt;

&lt;p&gt;When evaluating a deployment, repeat the same question with two accounts that have different access. Test previews, downloads, search snippets, and answers. Include a document that one account can open and the other cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep document work useful without AI
&lt;/h2&gt;

&lt;p&gt;Folders, previews, versions, OCR, reviews, reminders, and templates remain useful when an AI provider is unavailable or deliberately disabled. DocBento makes AI optional and supports an OpenAI-compatible endpoint when enabled. Hosting locally and choosing an external AI provider are separate decisions; review what your configured provider receives.&lt;/p&gt;

&lt;p&gt;The package includes a Python API and background worker, a React client, Docker deployment, and PostgreSQL with pgvector or MariaDB configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small evaluation before a large migration
&lt;/h2&gt;

&lt;p&gt;Try ten representative documents. Ask one exact-reference question, one question spanning several documents, and one whose answer is absent. Check extraction, citations, permissions, and the missing-answer behavior. This tells you more than a polished answer to an easy question.&lt;/p&gt;

&lt;p&gt;You can explore the &lt;a href="https://doc.sourcebento.com/" rel="noopener noreferrer"&gt;DocBento demo&lt;/a&gt; and inspect the &lt;a href="https://www.codester.com/items/71502/docbento-python-ai-document-management" rel="noopener noreferrer"&gt;source-code package on Codester&lt;/a&gt;. Use sample documents in the public demo.&lt;/p&gt;

&lt;p&gt;Which input causes the most trouble in your document workflow: tables, handwriting, or finding the right version?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prepared with AI assistance using the product's published documentation; published by SourceBento.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>A cron job can exit successfully and still fail your business workflow</title>
      <dc:creator>Byte Chap</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:20:00 +0000</pubDate>
      <link>https://dev.to/sourcebento/a-cron-job-can-exit-successfully-and-still-fail-your-business-workflow-333</link>
      <guid>https://dev.to/sourcebento/a-cron-job-can-exit-successfully-and-still-fail-your-business-workflow-333</guid>
      <description>&lt;p&gt;A scheduled process can exit with code zero while the business task is still incomplete.&lt;/p&gt;

&lt;p&gt;A backup script may finish before its upload is durable. An import may return successfully after skipping every malformed row. A billing job may write invoices but fail before notifying customers. Process success and workflow success are different states.&lt;/p&gt;

&lt;p&gt;Logs help explain a failure after someone knows to look. A completion heartbeat answers the earlier question: &lt;strong&gt;did the expected work finish within the expected window?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Send the heartbeat after the critical work
&lt;/h2&gt;

&lt;p&gt;Put the heartbeat at the end of the business-critical path. If a script pings before the backup reaches remote storage, the monitor confirms only that the script started.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

create_backup
upload_backup
verify_remote_object

curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-monitor.example/ping/&amp;lt;job-token&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ping should follow verification: the object exists, the export contains accepted rows, or the downstream API acknowledged the operation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A heartbeat should prove the outcome you care about, not merely that a process woke up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This loose coupling works across shell scripts, containers, queues, serverless functions, and old applications that are difficult to instrument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an interval and a grace period
&lt;/h2&gt;

&lt;p&gt;A daily job rarely completes at the exact same second. Queue delays, database locks, larger inputs, deployments, and daylight-saving changes can shift completion.&lt;/p&gt;

&lt;p&gt;Model two values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the expected run interval;&lt;/li&gt;
&lt;li&gt;a grace period based on observed variance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a job normally completes in 12 minutes and occasionally needs 20, a 30-minute grace period may be reasonable. If the business deadline is stricter, the workflow needs more capacity rather than a quieter monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open one incident and close it on recovery
&lt;/h2&gt;

&lt;p&gt;A missed heartbeat should open a single incident instead of sending the same notification every polling cycle. When the heartbeat returns, close the incident and send a recovery message.&lt;/p&gt;

&lt;p&gt;The pair gives operators a useful duration and prevents an inbox full of duplicate symptoms. Keep the check idempotent so that multiple scheduler passes do not create duplicate incidents for the same missed window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the alert path outside the failed component
&lt;/h2&gt;

&lt;p&gt;A monitor running on the same machine as the job cannot report a full host failure. For important work, run the monitor on another host and configure more than one notification path, such as email plus Telegram or a generic webhook.&lt;/p&gt;

&lt;p&gt;The monitor itself also needs observation. An external uptime check for its public endpoint closes the most obvious blind spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the recovery procedure before the alert
&lt;/h2&gt;

&lt;p&gt;Include the job name, expected schedule, last heartbeat, incident start time, and a link to a short runbook. The runbook should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is rerunning the job safe?&lt;/li&gt;
&lt;li&gt;How do we detect partial work?&lt;/li&gt;
&lt;li&gt;Who owns the downstream system?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with one job whose silent failure already has a real cost. Measure normal completion time for a week, choose a defensible grace period, and run a controlled failure. If the alert arrives but the recovery steps remain ambiguous, the monitoring is not finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  A working reference implementation
&lt;/h2&gt;

&lt;p&gt;I build SourceBento and sell the complete source code for a small self-hosted Cron Monitor implementing this pattern with TypeScript, Express, SQLite, Docker Compose, missed-run incidents, recovery alerts, and email, Telegram, or webhook notifications.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://sourcebento.com/demo/cron/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=launch_2026_09&amp;amp;utm_content=cron_workflow" rel="noopener noreferrer"&gt;public Cron Monitor demo&lt;/a&gt; does not require registration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I am the author and seller of SourceBento. The operational guidance above stands on its own whether or not the product fits your stack.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
