{"slug": "building-intelligent-document-parsing-systems-balancing-ai-capabilities-with", "title": "Building Intelligent Document Parsing Systems: Balancing AI Capabilities with Deterministic Fail-Safes", "summary": "Jawad Ul Hadi, a backend lead and architect, built a NestJS module that converts timesheets in six formats — PDF, DOCX, XLSX, XLS, PNG and JPG — into a fixed eight-column payroll export, using deterministic parsing as the primary path and an LLM only as a bounded, redacted, budget-capped fallback. The AI path redacts PII before any external call, runs at temperature 0 with forced tool use, and its output is re-parsed and re-scored by the same deterministic pipeline, winning only if it scores strictly higher. Hadi estimates the fallback fires on roughly 40% of documents, putting AI cost at about $22 per month for 10,000 documents, and notes an AI code review caught ten real bugs before merge.", "body_md": "- *Jawad Ul Hadi, Backend Lead / Architect — AI-First Systems Design*\n\nI built a NestJS module that turns timesheets in six formats (PDF, DOCX, XLSX, XLS, PNG, JPG) into one fixed eight-column payroll export. \"AI-first\" meant weighing AI at every design decision, and then using it only where it pays for itself. Deterministic parsing does the work. An LLM is a bounded, redacted, budget-capped fallback that can never fail an upload. AI was also part of how the module was built: planning, implementation, and an AI code review that caught ten real bugs before merge.\n\n## HR operations receive timesheets from client sites in whatever format each site uses: weekly grids with repeated In/Out/Hours blocks, individual timecards with dated punches, monthly day-numbered sheets, scans and phone photos. Payroll needs one thing: an eight-column workbook with **one row per employee per worked day**.\n\n```\nEmployee Number | First Name | Last Name | Full Name | Final Department | Employee Start | Final End | Final Hours\n8092            | Jane       | Doe       | Jane Doe  | Afternoon Shift  | 8/24/2026 14:30| 8/25/2026 0:00 | 9.00\n```\n\nThe rules are simple to state and easy to get wrong:\n\n- **If the source has it, extract it. If not, leave it blank.** Nothing is inferred and nothing is back-filled from the HR database.\n- AM/PM must survive. An overnight shift's end time moves to the next day.\n- Start and end are real Excel datetimes, not strings. Hours are decimals.\n\n## AI-first does **not** mean \"send everything to an LLM\". It means AI is considered first at every design decision, and then placed where it earns its cost, latency and privacy risk. I applied that in two places.\n\n``` php\nflowchart TD\n    U[Upload] --> V{Validate MIME + size}\n    V --> R{Route by format}\n    R -- XLSX / XLS / DOCX --> N[Native table extraction]\n    R -- Digital PDF --> T[Text layer to grid]\n    R -- PNG / JPG --> O[OCR to grid]\n    N & T & O --> P[Deterministic parser<br/>weekly grid / timecard]\n    P --> Q{Quality gate<br/>confident?}\n    Q -- yes --> F[Flatten to 8 columns]\n    Q -- no --> K{AI enabled?}\n    K -- no --> F\n    K -- yes --> AI[Redact PII, then LLM tool call,<br/>then re-parse and re-score]\n    AI --> B{Better than deterministic?<br/>ties go to deterministic}\n    B --> F\n    F --> X[Specimen XLSX + audit record]\n```\n\nTimesheets are *tabular*. A parser that understands the layout is faster, free and reproducible, and it is right most of the time. The LLM handles the leftovers: noisy OCR, odd layouts, ambiguous rows. So the cost model is:\n\n```\ncost per document = P(AI invoked) × cost per AI call\n                  ≈ 0.40 × $0.0055  (small model, ~3k in / ~500 out tokens)\n                  ≈ $0.0022   →  about $22 per month at 10,000 documents\n```\n\nQuoting a flat \"cost per LLM call\" would have overstated the bill 2–10×, because clean digital files never reach the model.\n\n**Guardrails on the AI path:**\n\n| Concern | Control | \n|---|---|\n| Privacy | Emails, URLs, SSNs and phone numbers are redacted before any external call. Only a text grid is sent, never the file and never a signed URL. | \n| Determinism | Temperature 0, forced tool use, JSON-schema output, versioned prompt | \n| Trust | The AI output is re-parsed and re-scored by the *same* deterministic pipeline, and it wins only if it scores strictly higher | \n| Cost | A pre-flight token estimate is checked against a per-upload USD budget. If it's over, the call is skipped | \n| Availability | Short timeout plus a circuit breaker. Any failure returns `null` , so the deterministic result ships | \n| Audit | Provider, model, prompt version, tokens, cost estimate, confidence and decision reason are stored per upload | \n| Logs | Status codes only: no prompts, no document text, no model output | \n\nWith no API key configured, the module runs fully deterministic at zero AI cost. AI is a switch, not a dependency.\n\n| Phase | How AI was used | What I kept for myself | \n|---|---|---|\n| Proposal | Drafted the cost model and compared providers (Anthropic, OpenAI, Google, open-weight) per invoked call | Scope boundaries, the decision to keep AI off the critical path, the provider recommendation | \n| Contract | Turned the sample export into explicit rules (blank-not-inferred, overnight, native datetimes) | Signing off the contract as the source of truth | \n| Build | Claude Code implemented the layered NestJS module against the house patterns (guards, response envelopes, repositories) | Architecture, the simplification calls, reviewing every diff | \n| Review | An AI code review of the rewrite found **10 real bugs** | Triage, fix design, regression tests | \n| Verification | Ran the parser against the real input template instead of trusting unit tests | Deciding what \"done\" means | \n\nThe review is the part I'd highlight. A \"lean\" rewrite removed about 8,800 lines: extra provider abstractions, a Python OCR sidecar, dead indirection. Simplifying that aggressively also dropped things that mattered:\n\n- The **JWT guard disappeared** from the controller, so every route was unauthenticated and crashed on`user._id` .\n- An SDK import had no matching dependency, so the app would have **crashed on boot** .\n- Storage returned `file://` paths instead of private cloud URLs.\n- Scanned PDFs produced **empty exports marked \"completed\"** : silent data loss.\n- A phone-number redaction regex also matched ISO dates and runs of decimal hours, so the LLM received corrupted data.\n- Weekday detection matched on a 3-letter prefix, so \"**Mon** thly Total\" became a second Monday and hours were counted twice.\n- A timezone \"correction\" shifted every exported time by five hours, because the Excel library writes dates from UTC.\n\nEach fix came with a regression test that fails on the old code. Then I ran the parser against the real supplied template, not synthetic fixtures. It returned **zero rows**: that layout uses day numbers (`1 In / 1 Out / 1 Hours`), not weekday names. The unit tests were green, and the acceptance criterion was not. That gap is now the top item on the backlog instead of a production surprise.\n\n| Decision | Alternative | Why | \n|---|---|---|\n| Deterministic-first, LLM fallback | LLM extracts everything | Cheaper, reproducible, auditable. The LLM covers the long tail | \n| Re-parse the AI output with the same parser | Trust the model's JSON | One definition of \"correct\", and it makes AI and non-AI results comparable | \n| Deterministic wins ties | Prefer the newer or smarter path | Stable output. AI has to be *better* to be adopted | \n| Store floating wall-clock times (printed time in UTC fields) | Apply a business-timezone offset | Excel writers serialise from UTC. The export must show what was printed | \n| Reject scanned PDFs with a clear 422 | OCR them via a vision model | No rasteriser in the stack, and sending raw files breaks the \"text only\" privacy rule. A clear error beats a silent empty export | \n| One concrete AI service | Provider interface + factory + DI tokens | One active vendor. Swapping means rewriting ~150 lines. The export contract doesn't change | \n| Blank beats inferred | Look up missing IDs or departments in HR | The source document is the source of truth, and the export stays auditable | \n\nNestJS 11 · TypeScript 5 · MongoDB (Mongoose) · Google Cloud Storage (private, signed URLs) · ExcelJS · SheetJS · mammoth · pdf-parse · tesseract.js · Anthropic Messages API (tool use) · Jest\n\nDated: 27-Sep-2026", "url": "https://wpnews.pro/news/building-intelligent-document-parsing-systems-balancing-ai-capabilities-with", "canonical_source": "https://gist.github.com/JawadulHadi/0ab89d96e0abb545779522b82263e728", "published_at": "2026-09-27 05:14:55+00:00", "updated_at": "2026-09-27 05:30:22.070049+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "natural-language-processing", "mlops", "developer-tools"], "entities": ["Jawad Ul Hadi", "NestJS"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-intelligent-document-parsing-systems-balancing-ai-capabilities-with", "markdown": "https://wpnews.pro/news/building-intelligent-document-parsing-systems-balancing-ai-capabilities-with.md", "text": "https://wpnews.pro/news/building-intelligent-document-parsing-systems-balancing-ai-capabilities-with.txt", "jsonld": "https://wpnews.pro/news/building-intelligent-document-parsing-systems-balancing-ai-capabilities-with.jsonld"}}