<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Level Up Coding - Medium]]></title>
        <description><![CDATA[Coding tutorials and news. The developer homepage gitconnected.com &amp;&amp; skilled.dev &amp;&amp; levelup.dev - Medium]]></description>
        <link>https://levelup.gitconnected.com?source=rss----5517fd7b58a6---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Level Up Coding - Medium</title>
            <link>https://levelup.gitconnected.com?source=rss----5517fd7b58a6---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 26 Sep 2026 10:03:23 GMT</lastBuildDate>
        <atom:link href="https://levelup.gitconnected.com/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Transactional Outbox Pattern in Symfony: Keeping Your Database and RabbitMQ Consistent]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/the-transactional-outbox-pattern-in-symfony-keeping-your-database-and-rabbitmq-consistent-b258708ef4b1?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/2600/0*DdFRzQuv14ooUOqk" width="4752"></a></p><p class="medium-feed-snippet">Your order was saved, but the &#x201C;OrderPlaced&#x201D; event never left the building. Here&#x2019;s how to make that impossible.</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/the-transactional-outbox-pattern-in-symfony-keeping-your-database-and-rabbitmq-consistent-b258708ef4b1?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/the-transactional-outbox-pattern-in-symfony-keeping-your-database-and-rabbitmq-consistent-b258708ef4b1?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/b258708ef4b1</guid>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[php]]></category>
            <category><![CDATA[symfony]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Roman Huliak]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:10:32 GMT</pubDate>
            <atom:updated>2026-09-25T15:10:31.719Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The Hidden Technical Debt of Prompt Hacks]]></title>
            <link>https://levelup.gitconnected.com/the-hidden-technical-debt-of-prompt-hacks-a42a9abc8d61?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/a42a9abc8d61</guid>
            <dc:creator><![CDATA[Mohit Sewak, Ph.D.]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:10:17 GMT</pubDate>
            <atom:updated>2026-09-25T15:10:16.441Z</atom:updated>
            <content:encoded><![CDATA[<h4>Architectural patterns to insulate enterprise pipelines from sudden model deprecations.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*39PNmk5AI50BcjgP" /></figure><p><em>An editorial still life illustrating how superstitious prompt patches conceal critical structural failures beneath superficial semantic tape.</em></p><p>Picture this: It is 2:00 AM, and your flagship enterprise AI agent has just hallucinated a radical corporate discount policy for a tier-one client, triggering a massive, automated financial ledger discrepancy. Your lead engineer scrambles to the rescue, not by refactoring the codebase or analyzing state vectors, but by opening a chat window and appending a frantic, superstitious patch: <em>“Crucial: Never, under any circumstances, invent discounts. Be very careful.”</em> The deployment goes live. The bleeding stops momentarily. Everyone breathes a sigh of relief over lukewarm masala tea. But deep down, you know the truth. You haven’t fixed anything; you’ve just buried another toxic snippet of technical debt under a mountain of semantic quick-fixes.</p><blockquote><em>📊 Executive Summary: </em>Modern generative AI integration significantly spikes software delivery throughput while simultaneously acting as an institutional amplifier for underlying architectural instability (<a href="http://testmuai.com/">testmuai.com</a>, 2024). Traditional software guarantees mathematically deterministic outputs from identical inputs (<a href="http://vincirufus.com/">vincirufus.com</a>, 2024), whereas stochastic AI agents rely on probabilistic reasoning (<a href="http://nintex.com/">nintex.com</a>, 2024). Managing this transition requires Enterprise AI Reliability Engineering (AIRe), utilizing decoupled control planes (<a href="http://elementum.ai/">elementum.ai</a>, 2024), dual-indicator SLIs (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://medium.com/">medium.com</a>, 2024), uncertainty quantification (<a href="http://mit.edu/">mit.edu</a>, 2024), and quality-aware circuit breakers (<a href="http://bigyan.dev/">bigyan.dev</a>, 2024) to curb silent system degradation (<a href="http://nintex.com/">nintex.com</a>, 2024; <a href="http://ve3.global/">ve3.global</a>, 2024).</blockquote><h3>I. The Hook &amp; The Big Promise</h3><p>Since the dawn of assembly language, enterprise software engineering has operated on a comforting, mathematically absolute premise: a specific input reliably produces an identical, deterministic output (<a href="http://vincirufus.com/">vincirufus.com</a>, 2024). From financial core ledgers to regulatory compliance engines, our digital infrastructure was built on the rock-solid foundation of predictability (<a href="http://vincirufus.com/">vincirufus.com</a>, 2024; <a href="http://nintex.com/">nintex.com</a>, 2024). Enter Generative AI. We are now stitching probabilistic reasoning engines — systems that evaluate semantic likelihoods, weigh fluid context, and spit out stochastic variance — directly into the heart of mission-critical pipelines (<a href="http://nintex.com/">nintex.com</a>, 2024). While this integration provides an undeniable surge in delivery velocity, it also serves as a potent amplifier for deep architectural instability (<a href="http://testmuai.com/">testmuai.com</a>, 2024).</p><blockquote>“Superstitious prompt patches conceal architectural rot beneath layers of semantic illusion.”<em> — Mohit Sewak</em></blockquote><p>The industry’s default coping mechanism has been the “prompt hack” — treating system prompts and few-shot examples as magic strings used to patch symptoms on the fly. Engineering teams stitch together ad-hoc string formatting tricks while ignoring the silent structural degradation, unmanaged API rate limits, and catastrophic failure cascades brewing just beneath the application tier (<a href="http://ve3.global/">ve3.global</a>, 2024; <a href="http://testmuai.com/">testmuai.com</a>, 2024). Treating a distributed, probabilistic state vector as a simple string variable is the architectural equivalent of holding up a crumbling skyscraper wall with masking tape.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NeBZr4_5KTlm1vB1" /></figure><p><em>A physical study contrasting rigid deterministic clockwork machinery with turbulent stochastic fluid dynamics, illustrating the vulnerability of enterprise pipelines.</em></p><p>This masterclass outlines Enterprise AI Reliability Engineering (AIRe) — a rigorous framework that adapts classical Site Reliability Engineering (SRE) principles to stochastic, non-deterministic environments (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://medium.com/">medium.com</a>, 2024). By the end of this blueprint, you will possess the operational models, mathematical uncertainty quantification frameworks, and code-level fault-tolerance patterns required to build production-grade, self-healing hybrid systems that survive contact with reality (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://medium.com/">medium.com</a>, 2024).</p><h3>II. The Architectural Convergence: Marrying Deterministic Control Planes with Bounded Probabilistic Agents</h3><p>The modern enterprise cannot choose between deterministic stability and probabilistic intelligence; it requires a disciplined hybrid approach (<a href="http://acceldata.io/">acceldata.io</a>, 2024). Industry data indicates that only a small fraction of organizations achieve enterprise-wide LLM production, primarily because governance bottlenecks choke unconstrained deployments (<a href="http://stonebranch.com/">stonebranch.com</a>, 2024). To solve this, enterprise architecture is coalescing around the “symphony orchestra” model, which strictly segregates deterministic control from probabilistic execution (<a href="http://elementum.ai/">elementum.ai</a>, 2024).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TYc1CEImyENdFtU7" /></figure><p><em>A physical architectural model illustrating the segregation of deterministic control planes from isolated probabilistic execution edge agents.</em></p><blockquote><em>🔍 Fact Check: </em>Only a small fraction of organizations achieve enterprise-wide LLM production due to unmanaged governance bottlenecks and architectural opacity.</blockquote><p>In this hybrid topology, deterministic orchestration engines act as the strict conductor, managing distributed state, enforcing execution sequences, and maintaining immutable audit trails (<a href="http://elementum.ai/">elementum.ai</a>, 2024; <a href="http://stonebranch.com/">stonebranch.com</a>, 2024). Probabilistic agents are securely relegated to the edges of the application to handle unstructured tasks like sentiment extraction, document parsing, and semantic synthesis (<a href="http://nintex.com/">nintex.com</a>, 2024; <a href="http://elementum.ai/">elementum.ai</a>, 2024; <a href="http://kestra.io/">kestra.io</a>, 2024). Financial ledgers, compliance reporting, and loan origination pipelines demand absolute rule consistency, systematically excluding AI reasoning from final rule-execution steps (<a href="http://nintex.com/">nintex.com</a>, 2024; <a href="http://elementum.ai/">elementum.ai</a>, 2024; <a href="http://kestra.io/">kestra.io</a>, 2024).</p><p>Data flows through strict interception checkpoints: the deterministic layer dictates the required JSON schema, and if a model’s output fails validation or dips below a confidence threshold, the control plane intercepts it before downstream propagation (<a href="http://elementum.ai/">elementum.ai</a>, 2024; <a href="http://stonebranch.com/">stonebranch.com</a>, 2024; <a href="http://bigyan.dev/">bigyan.dev</a>, 2024). Never allow an unconstrained agent to orchestrate core business logic. If an identical input must equal an identical output for regulatory audits, bypass the probabilistic layer entirely (<a href="http://elementum.ai/">elementum.ai</a>, 2024).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4Urkyb8lBPNrdZg0" /></figure><p><em>A laboratory balance and dual-column fluid manometer isolating infrastructure uptime from behavioral semantic fidelity and refusal ceilings.</em></p><h3>III. Beyond “Three Nines”: Formulating Probabilistic SLAs, Refusal Ceilings, and Burn-Rate Freezes</h3><p>Traditional SRE relies on binary availability metrics and uptime percentages like “three nines” (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://medium.com/">medium.com</a>, 2024). But an AI model can maintain 100% infrastructure uptime while completely failing semantically through hallucinations or toxic outputs (<a href="http://medium.com/">medium.com</a>, 2024). AIRe resolves this by establishing a two-indicator system that isolates Infrastructure SLIs (computed deterministically at the gateway: TTFT, TPOT, HTTP error codes) from Behavioral SLIs (evaluated via programmatic validators and LLM-as-a-judge frameworks tracking semantic quality and prompt adherence) (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://medium.com/">medium.com</a>, 2024).</p><blockquote><em>💡 ProTip: </em>Isolate infrastructure uptime metrics from behavioral semantic validation using dual SLI indicators to prevent infrastructure health from masking silent model hallucinations.</blockquote><p>To maintain granular visibility without over-provisioning expensive GPU clusters, state fields are tracked explicitly across categories like answered_primary, answered_fallback, answered_ungrounded, refused, and answered_after_retry (<a href="http://testmuai.com/">testmuai.com</a>, 2024; <a href="http://researchgate.net/">researchgate.net</a>, 2023, 2024). Crucially, a distinct “refusal ceiling” ensures that necessary safety guardrails rejecting malicious prompts do not unjustly drain the primary composite error budget (<a href="http://testmuai.com/">testmuai.com</a>, 2024).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MwW3PMwmSHl4rO9l" /></figure><p><em>An optical test bench demonstrating how uncertainty quantification separates reducible epistemic variance from irreducible aleatoric ambiguity.</em></p><p>Alerts are mapped not to single-incident failures — which cause crippling alert fatigue in stochastic environments — but to error budget consumption velocity or “burn rates” (<a href="http://testmuai.com/">testmuai.com</a>, 2024). When burn rates spike past acceptable thresholds, automated freeze policies immediately halt prompt edits, model upgrades, and tool integrations while permitting rollbacks and deterministic fallback routing (<a href="http://testmuai.com/">testmuai.com</a>, 2024). Treat upstream vendor model deprecations or silent quality drops as direct hits to your internal error budget (<a href="http://testmuai.com/">testmuai.com</a>, 2024).</p><h3>IV. Runtime Self-Awareness: Operationalizing Uncertainty Quantification (UQ) and Decision Theory</h3><p>To survive in production, an AI system must possess runtime self-awareness. Uncertainty Quantification (UQ) provides this by appending a numerical confidence score or probability distribution to model predictions (<a href="http://mit.edu/">mit.edu</a>, 2024; <a href="http://arxiv.org/">arxiv.org</a>, 2021). UQ mathematically separates Epistemic Uncertainty (Model Uncertainty) — a lack of training data that is <em>reducible</em> via targeted fine-tuning or RAG — from Aleatoric Uncertainty (Data Uncertainty) — inherent input ambiguity that is completely <em>irreducible</em> (<a href="http://ibm.com/">ibm.com</a>, 2023; <a href="http://medium.com/">medium.com</a>, 2023).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SKJi63QD52rTU15o" /></figure><p><em>Industrial electrical switchgear and heavy steel bulkheads illustrating semantic circuit breakers and idempotent tooling.</em></p><p>Translating these variance metrics into business actions requires decision theory mapping using expected loss functions (Expected_Loss = ∫ Loss(y, a) P(y|x) dy) (<a href="http://medium.com/">medium.com</a>, 2023). Rather than undertaking the massive computational expense of retraining primary foundation models, enterprise architectures deploy lightweight, low-latency companion evaluators that inspect outputs in real-time, compute precise uncertainty bounds, and trigger automated routing paths (<a href="http://mit.edu/">mit.edu</a>, 2024). When epistemic uncertainty is low but aleatoric uncertainty spikes due to a messy user prompt, bypass expensive generation loops and immediately trigger human-in-the-loop routing or deterministic clarification workflows (<a href="http://mit.edu/">mit.edu</a>, 2024; <a href="http://ibm.com/">ibm.com</a>, 2023).</p><h3>V. Defensive Infrastructure: Quality-Aware Circuit Breakers, Model Bulkheads, and Idempotent Tooling</h3><p>Distributed systems fail when external dependencies experience latency spikes or thread exhaustion (<a href="http://ve3.global/">ve3.global</a>, 2024; <a href="http://getmaxim.ai/">getmaxim.ai</a>, 2024; <a href="http://microsoft.com/">microsoft.com</a>, 2020). Intelligent retry logic must map explicitly to HTTP status codes, distinguishing transient 429 rate limits and 5xx server errors from persistent 400 malformed requests (<a href="http://getmaxim.ai/">getmaxim.ai</a>, 2024; <a href="http://udemy.com/">udemy.com</a>, 2022). However, network-level circuit breakers are blind to semantic failure. An HTTP 200 OK carrying a hallucinated payload must trip a breaker just as quickly as a network timeout (<a href="http://bigyan.dev/">bigyan.dev</a>, 2024).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CSjf5URBxBe-iY4w" /></figure><p><em>A terraced architectural staircase and decoupled overhead mechanical gantry modeling operational degradation ladders and task-decoupled planning.</em></p><p>Extended “Quality-Aware Circuit Breakers” integrate sliding quality windows, schema compliance validators, and recovery probes executing known-good inputs during the “Half-Open” state (<a href="http://bigyan.dev/">bigyan.dev</a>, 2024; <a href="http://microsoft.com/">microsoft.com</a>, 2020). Simultaneously, Model Bulkheads enforce physical thread partitioning and asynchronous non-blocking IO wrappers, preventing slow-processing LLM endpoints from holding calling service thread pools hostage (<a href="http://researchgate.net/">researchgate.net</a>, 2023; <a href="http://microsoft.com/">microsoft.com</a>, 2020; <a href="http://studylib.net/">studylib.net</a>, 2014).</p><h3>VI. Bounded Autonomy: Designing Degradation Ladders and Task-Decoupled Planning</h3><p>When systems degrade, they must do so gracefully. Multi-agent workflows risk cascading failures where an error at step nine destroys accumulated reasoning state (<a href="http://github.com/">github.com</a>, 2023). A programmatic degradation ladder steps down capabilities across defined operational tiers — from Full Capability and Constrained Mode (RAG-only, no code execution) to Read-Only Mode, Human Takeover, and Hard Stop termination (<a href="http://substack.com/">substack.com</a>, 2022; <a href="http://udemy.com/">udemy.com</a>, 2022).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VdrlZoxS7eu8chL5" /></figure><p><em>A tactical executive blueprint installation detailing the four-phase implementation runbook for enterprise AI reliability engineering.</em></p><p>To protect execution graphs, Task-Decoupled Planning (TDP) separates supervisor planning nodes from individual execution nodes, enabling localized retries without abandoning safe checkpoints (<a href="http://github.com/">github.com</a>, 2023). Furthermore, mandate idempotency keys on all tool calls that enact state changes, neutralizing the risks of double-billing or duplicate resource provisioning during automated retry cycles (<a href="http://udemy.com/">udemy.com</a>, 2022).</p><h3>VII. The Synthesis &amp; CTO Runbook: Instrumenting OpenInference and Scaling the Autonomy Curve</h3><p>Standardizing observability around OpenTelemetry and OpenInference semantic conventions allows organizations to capture end-to-end evaluation traces (Observe, Evaluate, Learn loops) without vendor lock-in (<a href="http://medium.com/">medium.com</a>, 2024; <a href="http://arize.com/">arize.com</a>, 2023). Deploying AI SRE agents requires scaling intentionally along the “Agentic Autonomy Curve,” moving safely from read-only triage to bounded production remediation (<a href="http://acceldata.io/">acceldata.io</a>, 2024; <a href="http://substack.com/">substack.com</a>, 2022; <a href="http://augmentcode.com/">augmentcode.com</a>, 2024).</p><h3>CTO Phased Implementation Runbook:</h3><ul><li>Phase 1 (Month 1): Instrument all LLM gateways using OpenInference standards to baseline Infrastructure and Behavioral SLIs.</li><li>Phase 2 (Month 2): Deploy lightweight UQ companion models and establish baseline refusal ceilings and composite error budgets.</li><li>Phase 3 (Month 3): Implement Quality-Aware Circuit Breakers and Model Bulkheads across high-risk agentic tool paths.</li><li>Phase 4 (Month 4+): Activate phased degradation ladders and scale agentic autonomy along the governed incident-triage curve.</li></ul><p><em>Download the enterprise-ready AIRe Open-Source Reference Architecture &amp; Circuit Breaker Template on GitHub to audit your current prompt infrastructure and transition from probabilistic chaos to mathematically governed reliability engineering today.</em></p><h3>References &amp; Further Reading</h3><h3>Core Concepts</h3><ul><li>Acceldata. (2024). <em>Enterprise AI governance and hybrid architectures</em>. Acceldata Research. <a href="https://www.acceldata.io/">https://www.acceldata.io</a></li><li>Nintex. (2024). <em>Deterministic vs. probabilistic workflows in modern business automation</em>. Nintex Insights. <a href="https://www.nintex.com/">https://www.nintex.com</a></li><li>TestMu AI. (2024). <em>Software delivery throughput and stability in the generative AI era</em>. TestMu AI Research. <a href="https://www.testmuai.com/">https://www.testmuai.com</a></li><li>Vinci Rufus. (2024). <em>The historical evolution of deterministic computing</em>. Vinci Rufus Tech Blog. <a href="https://www.vincirufus.com/">https://www.vincirufus.com</a></li></ul><h3>Advanced Theory</h3><ul><li>Massachusetts Institute of Technology. (2024). <em>Uncertainty quantification in machine learning models</em>. MIT Research Briefs. <a href="https://www.mit.edu/">https://www.mit.edu</a></li><li>ResearchGate. (2023). <em>Adaptive workload modeling and resource allocation in cloud-native AI pipelines</em>. ResearchGate. <a href="https://www.researchgate.net/">https://www.researchgate.net</a></li><li>ResearchGate. (2024). <em>Dynamic execution and probabilistic metrics for generative systems</em>. ResearchGate. <a href="https://www.researchgate.net/">https://www.researchgate.net</a></li></ul><h3>Practical Applications</h3><ul><li>Bigyan. (2024). <em>Quality-aware circuit breakers for stochastic LLM applications</em>. Bigyan Dev. <a href="https://www.bigyan.dev/">https://www.bigyan.dev</a></li><li>GetMaxim. (2024). <em>Managing third-party LLM API failures and transient rate limits</em>. GetMaxim AI. <a href="https://www.getmaxim.ai/">https://www.getmaxim.ai</a></li><li>Microsoft. (2020). <em>Cloud design patterns: Bulkhead and circuit breaker implementations</em>. Microsoft Learn. <a href="https://www.microsoft.com/">https://www.microsoft.com</a></li><li>Udemy. (2022). <em>Defensive programming and idempotent design for distributed autonomous agents</em>. Udemy Engineering. <a href="https://www.udemy.com/">https://www.udemy.com</a></li></ul><h3>🌐 The GenAI &amp; Quantum Intelligence Ecosystem</h3><p><em>Explore our dedicated sister dispatches tailored to your specific focus lane:</em></p><ul><li>🛡️ AI Safety &amp; Governance: Tracking generative AI security, red teaming, and regulatory compliance? Subscribe to our flagship dispatch in <a href="https://www.linkedin.com/newsletters/responsible-generative-ai-7272218788274540544/">Responsible Generative AI</a>.</li><li>🔬 Deep Technical &amp; arXiv: Want to inspect the formal proofs, benchmarks, and research architecture? Explore <a href="https://www.linkedin.com/newsletters/generative-ai-research-7368625322801438720/">Generative AI Research</a>.</li><li>⚛️ Quantum Security Bridge: Tracking quantum threats to AI security and cryptographic migration? Subscribe to <a href="https://www.linkedin.com/newsletters/quantum-7373935162969153537/">Quantum</a>.</li></ul><blockquote><em>🔔 Never miss a breakthrough: Subscribe to the publications above to build a multi-disciplinary vantage point across AI security, enterprise ROI, research benchmarks, and post-quantum encryption.</em></blockquote><p><em>Disclaimer: The views and opinions expressed in this article are personal and do not necessarily reflect the official policy or position of any associated agencies, organizations, or the India AI Mission. AI assistance was utilized in the research, drafting, and ideation of this article. Licensed under CC BY-ND 4.0.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a42a9abc8d61" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/the-hidden-technical-debt-of-prompt-hacks-a42a9abc8d61">The Hidden Technical Debt of Prompt Hacks</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Vibe Coding Trap: Why AI-Generated MVPs Are Quietly Bankrupting Early-Stage Startups]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/the-vibe-coding-trap-why-ai-generated-mvps-are-quietly-bankrupting-early-stage-startups-f6d06065348e?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/1672/1*DnxK8OlvM4J13jFfW9VmEQ.png" width="1672"></a></p><p class="medium-feed-snippet">A founder friend recently showed me a full-stack SaaS he built over a single weekend using Cursor.</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/the-vibe-coding-trap-why-ai-generated-mvps-are-quietly-bankrupting-early-stage-startups-f6d06065348e?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/the-vibe-coding-trap-why-ai-generated-mvps-are-quietly-bankrupting-early-stage-startups-f6d06065348e?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/f6d06065348e</guid>
            <category><![CDATA[startupş]]></category>
            <category><![CDATA[saas]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[entrepreneurship]]></category>
            <dc:creator><![CDATA[Abul Kalam Azad]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:10:03 GMT</pubDate>
            <atom:updated>2026-09-25T15:10:02.355Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[What to Log Before Your Agent Fails in Production]]></title>
            <link>https://levelup.gitconnected.com/what-to-log-before-your-agent-fails-in-production-aa594d6d134d?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/aa594d6d134d</guid>
            <category><![CDATA[claude-code]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[claude]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[M. Haseeb Hassan]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:09:49 GMT</pubDate>
            <atom:updated>2026-09-25T15:09:48.118Z</atom:updated>
            <content:encoded><![CDATA[<p>An agent that takes 45 seconds to answer a one-line question isn’t obviously broken. It’s just slow, and slow turns into a real problem once it happens on every third request and nobody on the team can say whether the delay came from the model, a stalled tool call, or a retry loop that fired three times before giving up [1]. That question, was it the model or a tool, is the one most teams can’t answer about their own agents, because most agent logging still stops at “here’s what the user typed” and “here’s what came back.”</p><figure><img alt="A lit final step on a dark grid of agent actions, earlier steps unlit" src="https://cdn-images-1.medium.com/max/1024/1*DCn_3DiIfr3XY50X_jAcCw.png" /><figcaption><em>Most agent logs show you the last step. The failure usually happened three steps earlier.</em></figcaption></figure><p><strong>In this article:</strong></p><ul><li>Why a tool result marked success: true doesn&#39;t mean the agent did the right thing</li><li>The three kinds of signal Claude Code and the Claude Agent SDK already expose: metrics, events, and traces</li><li>Using PreToolUse and PostToolUse hooks to log tool calls without slowing the agent down</li><li>How OpenTelemetry’s GenAI conventions turn a multi-step agent run into one readable trace</li><li>What to instrument first if you’re starting from nothing</li></ul><h3>Why agent failures hide behind a green checkmark</h3><p>A tool call that returns success: true tells you the function didn&#39;t throw an exception. It doesn&#39;t tell you the agent picked the right file, queried the right table, or interpreted the result correctly before acting on it. That gap between &quot;the tool ran&quot; and &quot;the agent did the right thing&quot; is where most production agent incidents actually live, and it&#39;s a distinction I&#39;ve written about before in the context of validating model output rather than trusting confident-sounding responses at face value (<a href="https://medium.com/gitconnected/evaluating-ai-output-why-it-sounds-right-isnt-good-enough-f9edd21f7346">Evaluating AI output: why it sounds right isn&#39;t good enough</a>).</p><p>The problem compounds with every additional step in an agent loop. A single LLM call has one obvious failure mode: a bad response. A multi-step agent has a combinatorial one: the model can plan correctly but call the wrong tool, call the right tool with a malformed argument, get a correct result and misread it, or get a correct result and act on stale context from three turns earlier. None of those show up as an error in a basic request log, because nothing errored. Without step-level visibility, “why did the agent do that” becomes a question you answer by asking the model to explain itself after the fact, which is not a debugging method, it’s a guess with extra steps.</p><p>Anthropic’s own Claude Code exports exactly this kind of step-level data when you turn telemetry on, and walking through what it captures is a reasonable template for instrumenting any agent, not just Claude Code itself [1].</p><h3>Three kinds of signal: metrics, events, and traces</h3><p>Claude Code’s OpenTelemetry integration separates what it reports into three categories, and the distinction matters because each answers a different question [1]. Metrics answer “how much”: claude_code.token.usage breaks down tokens by type (input, output, cache read, cache creation), and claude_code.cost.usage reports session cost in USD, both tagged with attributes like model, query_source (main session, subagent, or auxiliary), and mcp_tool.name for per-tool cost attribution [1]. Events answer &quot;what happened, when&quot;: the claude_code.tool_result event carries tool_name, duration_ms, success, and error_type, and every event in a single user turn shares a prompt.id so you can group them without stitching timestamps together yourself [1].</p><p>Traces answer the hardest question, “in what order, and how did the pieces relate.” Claude Code’s beta tracing support builds a span hierarchy with a root claude_code.interaction span containing child claude_code.llm_request and claude_code.tool spans, where each tool span further splits into claude_code.tool.blocked_on_user and claude_code.tool.execution [1]. That hierarchy is the actual answer to &quot;was it the model or the tool&quot;: you can see the LLM request span&#39;s duration next to the tool execution span&#39;s duration in the same trace, rather than inferring it from two separate logs.</p><figure><img alt="Tree diagram of Claude Code’s trace span hierarchy from interaction to tool execution" src="https://cdn-images-1.medium.com/max/1024/1*2IDBXHldn6oC-tCoTNRuFg.png" /><figcaption><em>One trace, three span types: what the model did, what a hook decided, and what the tool actually executed.</em></figcaption></figure><p>This three-layer split isn’t unique to Claude Code. It maps closely onto what OpenTelemetry’s own GenAI semantic conventions define for any LLM or agent system, which is worth knowing if you’re building on a different stack, and it’s the same layering that makes multi-agent systems traceable instead of opaque, something I ran into directly when writing about hub-and-spoke subagent orchestration in the Claude Agent SDK (<a href="https://medium.com/gitconnected/coordinator-subagent-orchestration-in-claude-agent-sdk-a127eacc6f5b">Coordinator-subagent orchestration patterns in the Claude Agent SDK</a>), where a coordinator’s trace and a subagent’s trace need to stay correlated without merging into one indistinguishable blob.</p><h3>Logging tool calls with hooks, without slowing the agent down</h3><p>Metrics and traces tell you what already happened. Hooks let you act on what’s about to happen, and they’re also the simplest way to add your own logging if you’re not ready to stand up a full OTel collector. The Claude Agent SDK fires a PreToolUse hook before a tool call executes and a PostToolUse hook after it returns, and each callback receives the tool name, its input arguments, and a tool_use_id that ties the two events to the same call [2]. A PreToolUse hook can log the call, block it, or rewrite its input before it runs; a PostToolUse hook can only observe and annotate, since the tool has already executed [2].</p><p>The matcher pattern on a hook determines which tools trigger it. An empty matcher runs the callback for every tool call, which is exactly what you want for a blanket audit log, while a matcher like Write|Edit scopes it to file-modifying tools, and MCP tools match the pattern mcp__&lt;server&gt;__&lt;action&gt;, which is worth knowing if your agent&#39;s failures cluster around one MCP integration rather than the built-in tools, a scenario I covered in more depth when the root cause is the tool description itself rather than the logging around it (<a href="https://medium.com/gitconnected/mcp-tool-design-why-your-agent-keeps-calling-the-wrong-tool-d57559dfbbe2">MCP tool design: why your agent keeps calling the wrong tool</a>) [2].</p><p>One detail matters for production use: if your hook’s only job is logging, don’t make the agent wait on it. The SDK supports an async output, {&quot;async&quot;: true, &quot;asyncTimeout&quot;: 30000}, that tells the agent to continue immediately while your callback finishes a webhook call or database write in the background [2]. A synchronous audit hook that calls out to a slow logging service will show up as latency in your claude_code.tool.execution span, which defeats the purpose of instrumenting for speed in the first place.</p><figure><img alt="TypeScript PostToolUse hook returning an async logging response" src="https://cdn-images-1.medium.com/max/1024/1*_bxErwoyXCkhDt8YLCpsLg.png" /><figcaption><em>A logging hook that doesn’t block the agent: fire the callback, return async, keep moving.</em></figcaption></figure><pre>const auditLogger: HookCallback = async (input, toolUseID, { signal }) =&gt; {<br>  if (input.hook_event_name !== &quot;PostToolUse&quot;) return {};<br><br>  // Fire-and-forget: don&#39;t block the agent on your logging pipeline<br>  sendToLoggingService({<br>    tool: input.tool_name,<br>    toolUseId: toolUseID,<br>    timestamp: new Date().toISOString(),<br>  }).catch(console.error);<br><br>  return { async: true, asyncTimeout: 30000 };<br>};</pre><h3>Turning a multi-step run into one trace</h3><p>Claude Code’s span names are specific to Claude Code, but the underlying idea, that an agent run is a tree of spans rather than a flat log, is exactly what OpenTelemetry’s GenAI semantic conventions standardize for any agent framework. The conventions define a required gen_ai.operation.name attribute on every span, with values that map onto the actual shape of agent work: chat for a single inference call, execute_tool for a tool invocation, invoke_agent for a full agent run, invoke_workflow for multi-step orchestration, and plan for reasoning or task decomposition [4][5]. Recommended usage attributes like gen_ai.usage.input_tokens and gen_ai.usage.output_tokens sit on the chat spans, while gen_ai.agent.name and gen_ai.tool.name identify which agent or tool a given span belongs to, which is what makes it possible to filter a trace down to &quot;just the subagent calls&quot; or &quot;just the retrieval steps&quot; in a system with several moving parts [5].</p><p>OpenTelemetry’s own writeup of this frames the payoff in the same terms as the 45-second example at the start of this piece: a single trace with a root invoke_agent span containing child chat and execute_tool spans lets you see, in one view, where the time actually went, rather than reconstructing it from separate application logs after the fact [3]. That&#39;s a meaningfully different debugging experience from grepping through a session transcript, which is built to answer &quot;what did the agent say,&quot; not &quot;how long did each step take,&quot; a distinction worth keeping in mind if you&#39;re relying on transcript storage as your only record of what an agent did (<a href="https://medium.com/gitconnected/the-three-layers-of-memory-in-the-claude-agent-sdk-d1c5437a9451">The Three Layers of Memory in the Claude Agent SDK</a>).</p><figure><img alt="Table mapping OpenTelemetry gen_ai.operation.name values to their meaning" src="https://cdn-images-1.medium.com/max/1024/1*8R_0FUf1AVP4vjurogUK0g.png" /><figcaption><em>Six operation types, one naming convention, whichever framework you’re tracing.</em></figcaption></figure><h3>What to instrument first if you’re starting from nothing</h3><p>You don’t need a collector, a dashboard, and a retention policy on day one. Start with whichever layer answers the question you get asked most often. If it’s “how much did that session cost,” turn on Claude Code’s metrics exporter and watch claude_code.token.usage and claude_code.cost.usage, both of which work with a plain console exporter before you touch OTLP [1]. If it&#39;s &quot;did the agent do the thing it said it did,&quot; a single blanket PostToolUse hook with an empty matcher, logging tool name, input, and result to a flat file, gets you further than most people expect before you need a real observability platform [2].</p><figure><img alt="Pull quote: a tool result marked success doesn’t mean the agent did the right thing" src="https://cdn-images-1.medium.com/max/1024/1*aSWpRhziD30iW_cvuxLbXw.png" /><figcaption><em>The line worth remembering before you trust a green checkmark in an agent log.</em></figcaption></figure><p>If the question is “why did this specific run take so long, or go sideways three steps in,” that’s when tracing earns its setup cost, because a flat event log makes you reconstruct order and parent-child relationships by hand, and a trace hands them to you [1][3]. The honest failure mode to watch for either way is the same one that makes success: true misleading in the first place: a log that confirms a step executed without confirming it executed correctly. Tracing tells you where the agent spent its time. It doesn&#39;t tell you whether the agent was right, and no amount of span data replaces actually checking the output.</p><h3>Sources</h3><ol><li><a href="https://code.claude.com/docs/en/monitoring-usage">Monitoring — Claude Code Docs</a></li><li><a href="https://code.claude.com/docs/en/agent-sdk/hooks">Intercept and control agent behavior with hooks — Claude Code Docs</a></li><li><a href="https://opentelemetry.io/blog/2026/genai-observability/">Inside the LLM Call: GenAI Observability with OpenTelemetry</a></li><li><a href="https://github.com/open-telemetry/semantic-conventions-genai">OpenTelemetry Semantic Conventions for Generative AI (semantic-conventions-genai)</a></li><li><a href="https://www.dash0.com/knowledge/opentelemetry-genai-semantic-conventions-explained">OpenTelemetry GenAI Semantic Conventions Explained — Dash0</a></li></ol><p>If you enjoyed the article and wish to show your support, make sure to:</p><ul><li>👏 Give a round of applause (50 claps) to help get featured</li><li>👥 Follow me on <a href="https://medium.com/@thehaseebhassan">Medium</a> to stay updated with my latest content</li><li>📰 Explore more articles on my <a href="https://medium.com/@thehaseebhassan">Medium Profile</a></li><li>🔔 Connect with me on <a href="http://linkedin.com/in/thehaseebhassan">LinkedIn</a>, <a href="http://twitter.com/thehaseebhassan">Twitter</a>, and <a href="https://instagram.com/the_haseebhassan">Instagram</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aa594d6d134d" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/what-to-log-before-your-agent-fails-in-production-aa594d6d134d">What to Log Before Your Agent Fails in Production</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oracle Signed $288B of Leases. Then Came the Jupiter Notice]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/oracle-signed-288b-of-leases-then-came-the-jupiter-notice-5767a9aba5e4?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/1024/1*q3vUhj_yb6UO6DxLEDVkFA.jpeg" width="1024"></a></p><p class="medium-feed-snippet">On September 11, Oracle filed a quarterly report counting $288 billion of data center leases it has signed but not yet started.</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/oracle-signed-288b-of-leases-then-came-the-jupiter-notice-5767a9aba5e4?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/oracle-signed-288b-of-leases-then-came-the-jupiter-notice-5767a9aba5e4?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/5767a9aba5e4</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[Ahmed M. Abdelfattah]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:09:33 GMT</pubDate>
            <atom:updated>2026-09-25T15:09:31.980Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[OCR as a Document, Not Just a String]]></title>
            <link>https://levelup.gitconnected.com/ocr-as-a-document-not-just-a-string-c434660fef86?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/c434660fef86</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[ocr]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kmp]]></category>
            <dc:creator><![CDATA[Andrew Malitchuk]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:09:14 GMT</pubDate>
            <atom:updated>2026-09-25T15:09:13.012Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*clcnoEQv0saB2ku0" /><figcaption>Photo by <a href="https://unsplash.com/@ervan_me?utm_source=medium&amp;utm_medium=referral">Ervan M Wirawan</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>I have a pet project: a <a href="https://github.com/andrew-malitchuk/inkcast-kmp">companion app for Xteink X4</a>, a deeply modified ESP32 with an e-ink display. Like most e-readers, it reads EPUB and nothing else. My books are PDFs — <em>bought legally, in the only format the sellers ship</em> — and my reader will not open a single one of them.</p><p>That is the bad half. The good half is that it pushed me into OCR, and into writing the thing this article is about: a PDF goes in, a structured document comes out, and an EPUB comes out of that. Everything below runs against PDFs I paid for or ones that are freely available.</p><h3>Why not an LLM</h3><p>This is the question I get asked the most, so it goes first. Two reasons.</p><p>First, I have not worked out how to run a fully local LLM inside a Kotlin Multiplatform project. Until that is solved, the whole thing stops being on-device, which was the point. Second, OCR is old, settled technology, and spending inference budget on work a mature recognizer already does well is a poor case for it. In a pipeline with a local model, OCR belongs in front of it: it prepares structured text cheaply instead of competing with the model for the same resources.</p><h3>What OCR actually does</h3><p>The pipeline has not changed much in thirty years, whatever engine you pick.</p><ul><li>Binarization — the page is reduced to ink and paper, because everything downstream operates on shapes rather than shades.</li><li>Layout analysis — the page is split into blocks, blocks into lines, lines into words. This is where a two-column scan is either understood or ruined.</li><li>Classification — individual glyphs, or whole lines, are turned into characters. Modern Tesseract does this with an LSTM over a whole line rather than character by character.</li><li>Language model — a dictionary and character statistics correct what the classifier was unsure about. This is why feeding an engine the wrong language is worse than feeding it none.</li></ul><p>Each platform ships its own implementation of that pipeline, and they are not the same implementation.</p><p>On iOS there is Vision, Apple’s own framework, available since iOS 13 and free with the OS. On Android there is Google’s ML Kit, and there is Tesseract through the Tesseract4Android binding. On a desktop JVM there is Tesseract again, through its C API.</p><p>So the choice made itself. Vision on iOS, because it is already there, costs nothing to ship, and uses hardware the phone already has. Tesseract on Android, because it is open, and because its language packs include various languages as a file I can bundle or download rather than a capability I have to hope for.</p><p>And the third target came free. Tesseract is the same engine on Android and on a desktop JVM, so supporting a JVM target cost one more binding rather than one more engine — which is also the only place I can run recognition end to end while developing, without a device in my hand.</p><h3>What each engine gives you</h3><h3>Tesseract</h3><p>Word and line boxes, a confidence per word, and language packs you control: .traineddata files you bundle, download at runtime, or point at an existing system installation. Through libtesseract&#39;s C API it also reports the script type of each line, the page orientation, and the skew angle. It needs to be told the real resolution of the image, through SetSourceResolution. That call must come after SetImage, or it is quietly ignored.</p><h3>Vision</h3><p>Word and line boxes, a confidence per observation, and two recognition levels, fast and accurate, that are a genuine speed-for-quality switch. It has no language packs to manage, because the languages are the ones the OS has. It does not report script type, orientation, or skew at all. Word boxes are not native either - they come from boundingBoxForRange, derived per range rather than reported directly.</p><p>The difference that matters is not in that list. It is that the same engine gives you different things depending on which binding you use: the Tesseract4Android Java wrapper does not surface the per-line script type, orientation and skew that the C API does. Two bindings of one library, two sets of capabilities.</p><h3>Reducing three targets to one denominator</h3><p>A cross-platform SDK has to answer this honestly, and there are only two honest answers: expose the intersection, or expose the difference. Uncial does both.</p><p>The intersection is what every target can do, and it is the default shape of the result: pages, lines, words, boxes, and a confidence on each.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e6eb45b6f54d6ea3f0dcf12101fa96e2/href">https://medium.com/media/e6eb45b6f54d6ea3f0dcf12101fa96e2/href</a></iframe><p>The droppedAnyOf method matters more than it appears. An engine that cannot do a requested language drops it rather than failing, and asking is the only way to find out.</p><p>The difference is reported rather than smoothed over. It would be possible to return 0f for skew on every platform and call the API uniform. Uncial exposes UncialClient.capabilities instead, so the difference can be asked about. A uniform API that quietly returns a made-up value does not abstract the difference away - it just hides it.</p><p>Vision turns that from an inconvenience into a trap. Its set of supported languages is a property of the request, not of the framework — the documentation for supportedRecognitionLanguages says a language available at one recognition level may be absent at another. There is no Ukrainian at all before iOS 16.</p><h3>The document, not the string</h3><p>Text recognition takes an image and returns a string. Abstract, but workable. It is usually enough, too: a receipt, a meter reading, anything that goes straight into an input field. But the moment you need to highlight a matched word on the scan itself, the position is not in that string, and there is nowhere left to get it from.</p><ul><li>Position — every word’s box on the page. Without it you cannot highlight a search hit, crop a field out of a form, or draw anything at all over the scan.</li><li>Confidence — how sure the engine is of each word. A string does not carry that at all.</li><li>Reading order — in a two-column scan, reading straight down mixes lines from both columns together.</li><li>Type size — the one signal that separates a heading from a paragraph, and it exists only upstream of the string.</li></ul><p>That last one is what makes an EPUB possible at the other end. Without type size there are no headings, and without headings there is no table of contents — only one long undifferentiated wall of text.</p><p>Here is exactly what comes back. That is the whole contract:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e6eb45b6f54d6ea3f0dcf12101fa96e2/href">https://medium.com/media/e6eb45b6f54d6ea3f0dcf12101fa96e2/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e82a4899edb460cc44094f1bd160245a/href">https://medium.com/media/e82a4899edb460cc44094f1bd160245a/href</a></iframe><p>document.pages[0].lines[0] is an OcrLine: its text, its box in page pixels, its fontSize, a Confidence, and the OcrWord list underneath it. blocks is the same content in reading order as Heading(level) and Paragraph.</p><blockquote>I am an Android developer, not an OCR researcher. The structure heuristics — the ratio of a line’s type size to the document’s median that promotes it to a heading, the repeat frequency that marks a running header as page furniture — were tuned by hand and live in StructureOptions precisely because they are document-dependent. A technical manual with 11 pt body and 12 pt headings needs different numbers than a novel does.</blockquote><p>One of those heuristics has a trap under it worth naming. PDFBox reports a font size of 0 pt for Type3 fonts and degenerate text matrices, so a real document can mix genuine sizes with zeros. Let the zeros vote in the median and the median sinks below the body size, at which point ordinary prose gets promoted to headings. NaN is the same trap from the other side, since every comparison against it is false: a line with an unreported size would sail past the ratio test and become a heading.</p><p>The fix is a filter ahead of the median — only finite sizes above zero get a vote.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4adb8a572bc54bbf95beaf5d93126584/href">https://medium.com/media/4adb8a572bc54bbf95beaf5d93126584/href</a></iframe><p>The case where nothing survives that filter is handled separately: a document from a recognizer that reports no type sizes at all yields a median of 0f, and then no line is promoted and everything becomes a Paragraph. That is deliberately better than returning an empty list, because the text is there - there is simply no basis for calling any of it a heading.</p><h3>One page, three coordinate systems, pick one</h3><p>Vision reports normalized coordinates with the origin at the bottom-left. PDF is bottom-up too, but in points rather than pixels. PdfRenderer and PdfBox give you top-down pixels.</p><p>If the SDK handed back each platform’s coordinates unchanged, the conversion would land in the app — with different arithmetic for Android, iOS and the JVM.</p><p>Uncial picks one model — top-down pixels, origin at the page’s top-left — and each engine converts at its own edge. Here is that conversion in Vision, written once:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/22c4de4b7adca9a739dee0bfa711766e/href">https://medium.com/media/22c4de4b7adca9a739dee0bfa711766e/href</a></iframe><p>Three lines of arithmetic that would otherwise live in every consuming app. Nobody downstream ever branches on the platform to read a box. The digital text path does the same thing from the other direction: pdf-text scales PDF points by the render scale, so a page that came out of the embedded text layer and a page that came out of OCR are directly comparable inside one document.</p><p>Two constants govern how a page becomes pixels in the first place, and both are load-bearing.</p><p>renderDpi defaults to 200. Below roughly 150 dpi Tesseract&#39;s accuracy on printed text falls off a cliff, because the LSTM was trained on glyphs of a certain pixel height and small type stops clearing it. Above roughly 300 you pay memory and time for detail the recognizer discards during binarization anyway. 200 is the flat part of that curve for printed documents, which is what this SDK is for.</p><p>maxPageSide caps the longer side of a rasterized page at 2600 px and is applied after the dpi, scaling down when it has to. This is purely an out-of-memory (OOM) guard: an A0 poster at 200 dpi is over 9000 px across, and a grayscale buffer that size is well past what a cheap phone will hand you in one allocation. Grayscale is the default pixel format for the same reason - engines binarize regardless, and one channel is worth a quarter of ARGB, which is the difference between a 300-page scan finishing and dying.</p><p>The consequence is a trap I paid for twice: the dpi you must give Tesseract is not renderDpi - it is the effective dpi after the maxPageSide clamp. Raster.sourceDpi carries that real figure, and it is deliberately left null for an image the caller supplied, because in that case nobody rendered anything and the honest move is to say nothing and let Tesseract estimate.</p><h3>The runtime module does not know that engines exist</h3><p>runtime has no compile-time dependency on any recognizer. Engines announce themselves through a registry in core: on Android the Tesseract engine registers itself via androidx.startup, and everywhere else the consumer calls installTesseractEngine() or installVisionEngine() once.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e938ead22a4064fb0ed735ea48b0640f/href">https://medium.com/media/e938ead22a4064fb0ed735ea48b0640f/href</a></iframe><p>The reason is practical. Tesseract on Android means a native library plus language data measured in megabytes, and somebody who only ever OCRs camera photographs on iOS should not link any of it. The same rule is why the PDF text-layer reader is a separate module that you opt into by setting digitalTextExtractor = pdfTextExtractor() - on Android it pulls in several MB of PDFBox-Android, which is a real cost to force on a consumer who never opens a PDF.</p><p>Rasterizing and recognizing are also separate roles, PageRasterizer and TextRecognizer. That split is what makes it possible to feed Uncial a PNG instead of a PDF, to test the whole pipeline against a fake recognizer with no native code in sight, and to swap Tesseract for Vision without the calling code noticing.</p><p>This is also the place to say why KMP rather than a Rust core with thin platform wrappers, which is the other common shape for an SDK like this. It buys nothing here: Vision belongs to Apple and will not link into a Rust core, so you would still be writing two native bindings, and you would have added a third language to the build.</p><p>There is no DI framework inside the SDK, on purpose. A DI dependency in a library is a version conflict you inflict on every consumer, so the composition root is a hand-written builder.</p><h3>One codebase, two completely different deliveries</h3><p>An Android consumer can assemble a set of Gradle modules. An iOS consumer gets one binary. That asymmetry produces two artifact shapes with almost nothing in common, over one codebase.</p><p>iOS is an umbrella. A separate module builds a static framework and re-exports everything that appears in the public API:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/40219562621c3d12de3298da24eae736/href">https://medium.com/media/40219562621c3d12de3298da24eae736/href</a></iframe><p>export is not cosmetic there. Without it Swift gets types that exist but cannot be named, and the whole document model becomes unreachable. This is also the one place where the rule that runtime knows nothing about engines stops applying: a distribution artifact is exactly where they are supposed to meet.</p><p>A thin UncialIos wrapper sits next to it and covers the three places where the generated Objective-C API gets awkward: ByteArray, the builder lambda, and Result, which does not survive the export at all.</p><p>Android is a fat AAR. The reason is not convenience; it is that the alternative does not work. Tesseract4Android lives on JitPack, and as a plain api dependency it would force every consumer to add the JitPack repository to their own settings.gradle.kts or watch resolution fail. That is unacceptable in an SDK. On top of that: nine artifacts for one SDK, and native code and assets that have to arrive together with the classes, or TessBaseAPI.init() fails at runtime with an unhelpful error.</p><p>The first release ships the twelve modules on their own; the fat AAR is the step after it, and until then an Android consumer still adds the JitPack repository for Tesseract4Android — which is the very thing it exists to remove.</p><p>The rule for splitting is simple. Embed what is our implementation detail and what the consumer will never name in their own code. Keep external what they almost certainly already have, or what needs the build system’s participation.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ed4dd2209c40b8e9b188c53bcf5e4400/href">https://medium.com/media/ed4dd2209c40b8e9b188c53bcf5e4400/href</a></iframe><p>Two consequences are easy to miss. Native code reaches Java through JNI reflection, which R8 cannot see, so consumer ProGuard rules are mandatory — without them the release builds and then crashes. And if the jar merge loses META-INF/*.kotlin_module, the consumer quietly loses default arguments, suspend breaks, and top-level functions disappear. That is not a hypothetical, it is the standard way to get a fat AAR wrong.</p><p>This is why the acceptance criterion is not just “it built”. A clean new Android project, google() and mavenCentral() only, one dependency line, R8 enabled, a release build - and OCR of a Ukrainian scan returns text. Until that passes, the fat AAR is not done.</p><p>Where it lives. The modules publish to Maven Central under the io.github.andrew-malitchuk group, the namespace verified through the GitHub account itself — free and immediate. Publication goes through com.vanniktech.maven.publish, because Kotlin Multiplatform turns 12 Gradle modules into 58 Maven coordinates — a root module per library plus one per target — and nobody should hand-write that many publications. Its own uploader, though, could not be used. It reads credentials only through providers.gradleProperty, and a project-local secrets file can never become one: Gradle 9 loads its properties before the settings script runs, so injecting them from settings.gradle.kts silently does nothing. The upload therefore goes through nmcp, which takes credentials as ordinary values and aggregates all 58 coordinates into a single Portal deployment instead of twelve.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/660c799017573bf95cfea18d6f8efc3d/href">https://medium.com/media/660c799017573bf95cfea18d6f8efc3d/href</a></iframe><p>iOS travels a separate road, and SPM dictates the order of it. The manifest is read at the git tag, and the checksum pins the exact bytes of the zip — so you commit a URL to a GitHub Release asset that does not exist yet, then tag, then upload. That looks backwards and is the only order that works. Package.swift lives in this repository rather than in one of its own, so a single tag carries both the Maven release and the Swift one and no second manifest can drift away from the artifact it names.</p><ul><li><a href="https://github.com/andrew-malitchuk/uncial-kmp">https://github.com/andrew-malitchuk/uncial-kmp</a></li><li><a href="https://central.sonatype.com/artifact/io.github.andrew-malitchuk/uncial-core/0.0.1">https://central.sonatype.com/artifact/io.github.andrew-malitchuk/uncial-core/0.0.1</a></li></ul><p>The one part already acting as a release gate is the ABI dump. Every published module has strict explicitApi() and a committed dump, and checkKotlinAbi runs as part of build. A change to the public API fails the build until somebody regenerates the dump and reads the diff.</p><h3>Green tests that prove nothing</h3><p>Vision text recognition returns zero observations on the iOS Simulator. There is no Neural Engine there, so performRequests succeeds, error is null, and results is an empty array. Every assertion I had written passed. The pipeline had recognized precisely nothing.</p><p>That produced the rule the tests are written to now, and it generalizes to any KMP library over a native dependency: an assertion may only check what the environment is actually able to perform. The iOS tests assert the pipeline and the capabilities, and check text only when Vision actually returned some. A green iOS run says the pipeline is assembled correctly. It says nothing about recognition quality, and only a physical device can. In code:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fa25ed44b68337005be9793a9872b1ed/href">https://medium.com/media/fa25ed44b68337005be9793a9872b1ed/href</a></iframe><p>The out-of-page check is not cosmetic. If the coordinate conversion above ever breaks, boxes land outside the page, and this is the only assertion that catches it.</p><p>There are more places like it:</p><ul><li>org.gradle.caching=true means a green build can be entirely cache hits with no test actually executed, which is what --rerun-tasks is for.</li><li>Android host tests cannot construct a Bitmap, so the fake rasterizer produces a placeholder that carries dimensions and no pixels.</li><li>Unannotated Objective-C and JNA signatures type as non-null in Kotlin while genuinely returning null, so the compiler will call your ?: throw dead code right up until it fires.</li></ul><p>The useful artifact that came out of all this is not a test suite. It is a written list of what the test suite does not check, kept in the repository next to the code.</p><h3>What this was actually worth</h3><p>Not wanting to buy an e-ink Android reader, and not wanting to read on a tablet, is the entire reason a KMP OCR library exists on my disk.</p><p>There is no claim of novelty here. I did not invent a recognition algorithm, alone or with a model’s help — I am not competent to, and there are solutions on the market that do the recognition part far better than anything I would produce. Everything above is assembly, not research.</p><p>The real value of this experience lies in the second half. This is a clean, almost textbook case of building a cross-platform SDK over a problem where every platform already has its own answer and no two answers match. You end up building a facade that has to deliver something coherent anyway: trade-offs, palliative decisions, and a lot of knowledge that only arrives by running the thing. As an exercise in what a KMP SDK costs to build, it was worth every hour.</p><p>And it closed the need it started from. A PDF I own goes in, and an EPUB my reader will open comes out.</p><p>Source:</p><ul><li><a href="https://github.com/andrew-malitchuk/uncial-kmp">https://github.com/andrew-malitchuk/uncial-kmp</a></li><li><a href="https://github.com/andrew-malitchuk/inkcast-kmp">https://github.com/andrew-malitchuk/inkcast-kmp</a></li></ul><p><a href="https://github.com/andrew-malitchuk">GitHub</a> | <a href="http://www.linkedin.com/in/andrew-malitchuk">LinkedIn</a> | <a href="https://x.com/AndrewMalitchuk">X</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c434660fef86" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/ocr-as-a-document-not-just-a-string-c434660fef86">OCR as a Document, Not Just a String</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making a “Hello World” in Lean]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/making-a-hello-world-in-lean-10871f2b93c3?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/2600/1*l7IiLmctxp3lmGRNWYLAXQ.png" width="2730"></a></p><p class="medium-feed-snippet">The Language That Proves Math</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/making-a-hello-world-in-lean-10871f2b93c3?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/making-a-hello-world-in-lean-10871f2b93c3?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/10871f2b93c3</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[mathematics]]></category>
            <category><![CDATA[lean]]></category>
            <dc:creator><![CDATA[Dmitrii Eliuseev]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:08:06 GMT</pubDate>
            <atom:updated>2026-09-25T15:08:05.464Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Recursive CTEs: SQL’s Hidden Graph Traversal Engine]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/recursive-ctes-sqls-hidden-graph-traversal-engine-60978661c72f?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/2600/1*NSrFU1RcyvGcw4rkBheOWQ.png" width="2816"></a></p><p class="medium-feed-snippet">Navigate hierarchies, find routes, detect cycles and calculate degrees of separation</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/recursive-ctes-sqls-hidden-graph-traversal-engine-60978661c72f?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/recursive-ctes-sqls-hidden-graph-traversal-engine-60978661c72f?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/60978661c72f</guid>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[Thomas Reid]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:07:51 GMT</pubDate>
            <atom:updated>2026-09-25T15:07:49.929Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Your Codebase Is Part of the Prompt: How AI Coding Agents Read Your Code]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/your-codebase-is-part-of-the-prompt-how-ai-coding-agents-read-your-code-7521315ac6c9?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/1672/1*qP-6uotau_sotcXlDF2RPg.png" width="1672"></a></p><p class="medium-feed-snippet">AI coding agents don&apos;t just follow your instructions. They read your code, structure, types, names, and tests to figure out what you mean.</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/your-codebase-is-part-of-the-prompt-how-ai-coding-agents-read-your-code-7521315ac6c9?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/your-codebase-is-part-of-the-prompt-how-ai-coding-agents-read-your-code-7521315ac6c9?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/7521315ac6c9</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Addepalle Nikhil Varma]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:07:34 GMT</pubDate>
            <atom:updated>2026-09-25T15:07:33.605Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Chat Answers. An Agent Acts.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/chat-answers-an-agent-acts-5ccb8c35a0bd?source=rss----5517fd7b58a6---4"><img src="https://cdn-images-1.medium.com/max/2600/1*mwgE_-byQPIrV_LU2WaQjw.png" width="2800"></a></p><p class="medium-feed-snippet">What changes when your AI tool can read your repository, run your tests, and decide what to do next</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/chat-answers-an-agent-acts-5ccb8c35a0bd?source=rss----5517fd7b58a6---4">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/chat-answers-an-agent-acts-5ccb8c35a0bd?source=rss----5517fd7b58a6---4</link>
            <guid isPermaLink="false">https://medium.com/p/5ccb8c35a0bd</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Dragan Rapić]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:07:23 GMT</pubDate>
            <atom:updated>2026-09-25T15:07:21.873Z</atom:updated>
        </item>
    </channel>
</rss>