<?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: Nilamadhab Senapati</title>
    <description>The latest articles on DEV Community by Nilamadhab Senapati (@nilamadhab47).</description>
    <link>https://dev.to/nilamadhab47</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1304665%2Fb654bb77-a6a6-4727-bc4d-3d8ce5d198cc.jpeg</url>
      <title>DEV Community: Nilamadhab Senapati</title>
      <link>https://dev.to/nilamadhab47</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nilamadhab47"/>
    <language>en</language>
    <item>
      <title>I Resurrected a Dead F1 Project and Accidentally Built a Race Intelligence OS</title>
      <dc:creator>Nilamadhab Senapati</dc:creator>
      <pubDate>Sun, 24 May 2026 20:30:28 +0000</pubDate>
      <link>https://dev.to/nilamadhab47/i-resurrected-a-dead-f1-project-and-accidentally-built-a-race-intelligence-os-2886</link>
      <guid>https://dev.to/nilamadhab47/i-resurrected-a-dead-f1-project-and-accidentally-built-a-race-intelligence-os-2886</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;F1 Intelligence Studio&lt;/strong&gt; — a full-stack Formula 1 race intelligence dashboard that turns raw telemetry data into a living, breathing visualization of any race from 2024 to 2026.&lt;/p&gt;

&lt;p&gt;Think of it as a race engineer's war room. Twenty animated cars chase each other around circuits drawn from real GPS telemetry. An AI race engineer (Claude) analyzes strategy in real-time. A spring-physics camera zooms into wheel-to-wheel battles like an actual broadcast. ElevenLabs voices the commentary. A strategy simulator answers F1's eternal question: &lt;em&gt;pit or stay out?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can scrub through any moment of any race with frame-perfect precision. You can compare two drivers' telemetry traces side-by-side, watch tyre stints unfold, monitor team radio, and get AI-powered insights on developing battles.&lt;/p&gt;

&lt;p&gt;It started as a single API call dumping data into a table. It ended as a 12-panel drag-and-drop dashboard with twelve interactive components. Somewhere in between, I lost track of where the line was — and that's the whole point of this story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this project means to me:&lt;/strong&gt; It's the first side project I've actually shipped in years. My GitHub is a graveyard of half-built ideas. This one made it out alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🏎️ &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://raceosf1.one/" rel="noopener noreferrer"&gt;https://raceosf1.one/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;a href="https://github.com/nilamadhab47/raceosf1" rel="noopener noreferrer"&gt;https://github.com/nilamadhab47/raceosf1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick screenshots:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🟢 &lt;em&gt;The animated track map with 20 cars on a real telemetry-derived circuit&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fwgm965xoe2ift2lau6q4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwgm965xoe2ift2lau6q4.png" alt="animated track" width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
🟢 &lt;em&gt;Telemetry comparison — two drivers, speed/throttle/brake overlaid on the same distance axis&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fa0gxvf68ra6nfbsykbvd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0gxvf68ra6nfbsykbvd.png" alt="Telemetry comparison" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🟢 &lt;em&gt;The AI insights panel — Claude analyzing strategy in real-time and Strategy simulator showing pit vs stay-out delta&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fjsvsbu1vi43wag81fa5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsvsbu1vi43wag81fa5x.png" alt="The AI insights panel" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14, TypeScript, Zustand, GSAP, Recharts, react-grid-layout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI (Python 3.12), FastF1, WebSocket broadcasting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Anthropic Claude (race insights + chat), ElevenLabs (voice commentary)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Vercel (free tier) + Railway ($5/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infrastructure cost: less than my monthly coffee budget. The spring-damper camera system took more math than my engineering degree.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;Here's the honest version.&lt;/p&gt;

&lt;p&gt;My GitHub looks like a graveyard. Landing pages with no backend. ChatGPT chats about apps that never left the chat. Ideas rotting in a documents folder. I'm a full-stack engineer who builds production systems for a living — but my own projects? Couldn't finish a README.&lt;/p&gt;

&lt;p&gt;The F1 project was no exception. I started it months ago when Instagram served me a dev reel about FastF1, that incredible Python package for Formula 1 telemetry data. My brain did its usual thing: &lt;em&gt;"Oh that's cool, I should build something."&lt;/em&gt; I made a repo. Wrote a few API endpoints. Got driver data into a table.&lt;/p&gt;

&lt;p&gt;Then? Procrastination. The classic excuses kicked in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Who's going to use this?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"There's no monetization."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"You're a backend engineer pretending to do frontend."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo sat there for weeks. Untouched. Just another tombstone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I made myself one rule. Sit down after work. Fifteen minutes minimum. No "let me plan the architecture first" (the ultimate procrastination disguise). No "I'll start fresh on Monday." Just open the laptop and ship one small thing.&lt;/p&gt;

&lt;p&gt;The beginning was ugly. Just ugly. But I kept showing up.&lt;/p&gt;

&lt;p&gt;Then the escalation started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week 1:&lt;/strong&gt; Tables turned into graphs. Slightly less boring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2:&lt;/strong&gt; Graphs turned into driver comparisons. Wait, this is actually interesting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3:&lt;/strong&gt; Comparisons turned into full race simulations. Now I need actual circuit maps?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4:&lt;/strong&gt; Drawing SVG tracks from raw GPS telemetry. I googled "what is a viewBox" at 11pm. No shame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 5:&lt;/strong&gt; Twenty animated cars chasing each other at 60fps. Bypassed React's render cycle entirely because &lt;code&gt;setState&lt;/code&gt; 60 times a second is a war crime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 6:&lt;/strong&gt; Added an AI race engineer. Then voice commentary. Then a spring-physics camera that zooms into battles like an actual TV broadcast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I looked up from my keyboard and realized what started as "let me show F1 data in a table" had turned into a complete Race Intelligence Operating System. My scope creep could lap Verstappen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The finish-up grind:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When this challenge dropped, the project was &lt;em&gt;mostly&lt;/em&gt; working but full of rough edges — the kind of rough edges that keep you from actually showing it to anyone. The "I'll polish it later" backlog. Sound familiar?&lt;/p&gt;

&lt;p&gt;Here's what I cleaned up for the final push:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation.&lt;/strong&gt; The README was a single sentence. Now it's a proper onboarding doc with setup, architecture diagrams, and contribution guidelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error boundaries on every panel.&lt;/strong&gt; Before, one panel crashing could take down the whole dashboard. Now each panel fails gracefully on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading skeletons.&lt;/strong&gt; Previously the dashboard flashed empty boxes during data fetch. Now everything has proper loading states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The YouTube content-ID disaster.&lt;/strong&gt; F1 videos kept showing "Video unavailable" in production because FOM blocks third-party embeds. Built a three-tier fallback: Dailymotion → non-blocked YouTube → thumbnail cards with external links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment.&lt;/strong&gt; Three Dockerfile failures on Railway. Path resolution, build context, and the infamous &lt;code&gt;$PORT&lt;/code&gt; variable not expanding because Railway's startCommand doesn't run through a shell. Finally got everything green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polish pass.&lt;/strong&gt; Onboarding tour, keyboard shortcuts, mobile-responsive grid presets, dark mode that doesn't look like an afterthought.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The before-and-after gap is the difference between "a thing on my laptop" and "a thing I can show people without apologizing."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest lesson&lt;/strong&gt; wasn't technical. It's that the beginning lies to you. It whispers &lt;em&gt;"this is pointless"&lt;/em&gt; and &lt;em&gt;"you're not good enough"&lt;/em&gt; — and if you listen, you add another repo to the graveyard and open Instagram instead.&lt;/p&gt;

&lt;p&gt;The only answer is to keep showing up. Fifteen minutes at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;I used Copilot heavily during the finishing-up phase, and honestly? It's where it shined the most.&lt;/p&gt;

&lt;p&gt;The interesting thing about reviving an abandoned project is that the &lt;em&gt;fun&lt;/em&gt; parts are already built. What's left is the unsexy stuff — polish, edge cases, drag-and-resize logic, design system consistency. Things I'd normally rage-quit before finishing. This is exactly where Copilot earned its keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Copilot genuinely helped:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Drag-and-resize architecture for the dashboard panels.&lt;/strong&gt; This was the single biggest unlock. I needed every panel to be draggable, resizable, and auto-adjustable based on its container — without breaking the internal components inside each one. Copilot helped me architect the layout system and walked through how to wire &lt;code&gt;react-grid-layout&lt;/code&gt; with my existing panel components. The hardest part was making sure that resizing didn't break the SVG track map, the Recharts graphs, or the WebSocket-driven animations inside. Copilot suggested the right patterns — &lt;code&gt;ResizeObserver&lt;/code&gt; for container-aware children, debounced resize handlers, key-based remounting for stubborn charts — without me having to re-architect each panel from scratch.&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Type definitions for FastF1 responses.&lt;/strong&gt; FastF1 returns deeply nested pandas DataFrames that I was serializing into JSON. Writing TypeScript types for these by hand was tedious. Copilot inferred most of them from my Python serializer code and saved me from manually transcribing field names.&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Design system consistency + performance tuning.&lt;/strong&gt; When I was unifying the visual language across twelve panels (spacing, colors, typography, motion timings), Copilot was great at suggesting consistent token-based patterns and flagging where I'd diverged. It also helped with performance decisions — when to memoize, when to use refs over state, when to virtualize, when not to. Not always right, but a useful second opinion.&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Edge case handling.&lt;/strong&gt; When I was hardening the API endpoints, Copilot was great at suggesting validation cases I hadn't considered. &lt;em&gt;"What if &lt;code&gt;lap_number&lt;/code&gt; is negative?"&lt;/em&gt; &lt;em&gt;"What if the session hasn't loaded yet?"&lt;/em&gt; The kind of paranoid checks that production code needs but you forget when you're prototyping.&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Test stubs.&lt;/strong&gt; I wrote one test for the gap-calculation logic. Copilot generated the rest of the test cases by varying the inputs. About 70% were useful, 30% were noise — but the useful ones caught two real bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Copilot was less useful:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🔴 &lt;strong&gt;The creative architecture decisions.&lt;/strong&gt; The spring-damper camera, the 1000-point SVG sampling trick, the ref-based animation loop bypassing React — these required actually thinking about the problem. Copilot suggested generic solutions when I needed weird ones. That's fine. It's a tool, not a teammate.&lt;/p&gt;

&lt;p&gt;🔴 &lt;strong&gt;Anything involving FastF1's quirks.&lt;/strong&gt; FastF1 has a lot of session-specific behavior (sprint weekends, qualifying formats, telemetry availability) that Copilot's training data didn't cover well. It would suggest plausible-looking code that didn't actually work for the data shape.&lt;/p&gt;

&lt;p&gt;🔴 &lt;strong&gt;Genuinely novel logic.&lt;/strong&gt; The first time I wrote the gap-to-track-fraction conversion (&lt;code&gt;offset = gap_seconds / avg_lap_time&lt;/code&gt;), Copilot wasn't going to help me derive it. I had to actually understand the math first.&lt;/p&gt;

&lt;p&gt;🔴 &lt;strong&gt;Hallucinations when my prompt was vague.&lt;/strong&gt; This is the honest catch. Whenever I got lazy with my prompting — vague intent, no constraints, no examples — Copilot confidently hallucinated APIs that didn't exist, function signatures from imaginary library versions, or completely overengineered a solution I didn't ask for. I'd ask for a small utility and get back a 200-line abstraction with three layers of inheritance. The lesson learned the hard way: the quality of Copilot's output is directly tied to how precisely I describe what I want. Vague in, garbage out. It's not the AI's fault — it's mine for not being specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest takeaway:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copilot is at its best when you know what you want and need to type less to get there. It's at its worst when you don't know what you want and hope the autocomplete will figure it out for you. For finishing up an abandoned project — where the hard creative work is already done and what remains is execution polish — it's nearly perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  It didn't write my project. But it absolutely helped me finish it.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The graveyard still has occupants. This is the first exhumation, not the last. I've got a backlog of half-built ideas and I'm coming for every single one of them.&lt;/p&gt;

&lt;p&gt;Because it's not about the money. &lt;em&gt;~ Brad Pitt, F1&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Massive shoutout to &lt;a href="https://github.com/theOehrly" rel="noopener noreferrer"&gt;theOehrly&lt;/a&gt; — the FastF1 maintainer. This entire project exists because you built something incredible and open-sourced it. That's the energy.&lt;/p&gt;

&lt;p&gt;If you're sitting on an abandoned project right now, this is your sign. Open the laptop. Fifteen minutes. The beginning is lying to you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with too many late-night qualifying sessions, more cans of energy drink than I'm willing to admit, and a refusal to add one more tombstone to the GitHub graveyard.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubchallenge</category>
      <category>devchallenge</category>
      <category>githubcopilot</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a tool that shows you exactly what an ATS reads from your resume — here's how it works</title>
      <dc:creator>Nilamadhab Senapati</dc:creator>
      <pubDate>Sun, 24 May 2026 18:57:23 +0000</pubDate>
      <link>https://dev.to/nilamadhab47/i-built-a-tool-that-shows-you-exactly-what-an-ats-reads-from-your-resume-heres-how-it-works-3c98</link>
      <guid>https://dev.to/nilamadhab47/i-built-a-tool-that-shows-you-exactly-what-an-ats-reads-from-your-resume-heres-how-it-works-3c98</guid>
      <description>&lt;p&gt;&lt;em&gt;Most resume checkers score your file against keywords. Legible runs the actual parsing pipeline an ATS uses — and shows you the raw output, line by line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've spent months helping friends apply to jobs and watching the same thing happen over and over: a great candidate, a beautiful resume, and silence. No response. No rejection email. Just nothing.&lt;/p&gt;

&lt;p&gt;Eventually I started asking the question nobody asks: &lt;strong&gt;does the company even see this resume?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer, increasingly, is no. An Applicant Tracking System (ATS) sees it first. And ATS systems don't see what you see.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an ATS actually does
&lt;/h2&gt;

&lt;p&gt;When you upload a PDF to Greenhouse, Workday, Lever, Taleo, iCIMS, or any other major ATS, the system runs your file through a five-stage parser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Text extraction&lt;/strong&gt; — PDF/DOCX bytes converted to a character stream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout analysis&lt;/strong&gt; — columns, tables, images, header/footer regions detected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section segmentation&lt;/strong&gt; — Experience, Education, Skills, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field extraction&lt;/strong&gt; — name, email, dates, job titles, companies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured storage&lt;/strong&gt; — fields written to a database the recruiter searches&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any stage fails, your resume becomes invisible. Not rejected. &lt;em&gt;Invisible.&lt;/em&gt; Your file is still in the system, but the recruiter searching for "Kubernetes" never finds you because the parser dropped your skills section.&lt;/p&gt;




&lt;h2&gt;
  
  
  The five most common silent failures
&lt;/h2&gt;

&lt;p&gt;In rough order of how frequently I saw them in testing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Multi-column layouts&lt;/strong&gt;&lt;br&gt;
Parsers read top-to-bottom, left-to-right across the full page width. Two columns interleave into garbled lines. &lt;code&gt;"Skills Work Experience Python Senior Engineer SQL Acme Corp"&lt;/code&gt; — parsed as one job title.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tables for critical content&lt;/strong&gt;&lt;br&gt;
Most parsers strip table structure entirely. Skills in a table → gone from the recruiter's keyword search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Image-based PDFs&lt;/strong&gt;&lt;br&gt;
Canva exports, some Adobe Illustrator templates — these flatten text into a picture. The parser sees a blank page. All your content is invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Contact info in the PDF header region&lt;/strong&gt;&lt;br&gt;
The visual top of the page is fine. The actual &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; XML element in the document structure is not — most parsers ignore it entirely. Many popular resume templates use the document header for name and email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Creative section headings&lt;/strong&gt;&lt;br&gt;
"Where I've Worked" instead of "Experience", "My Toolbox" instead of "Skills" — the parser's section segmenter fails to classify the section correctly.&lt;/p&gt;

&lt;p&gt;Every resume coach knows these patterns exist. But nobody could tell you whether &lt;strong&gt;your specific resume&lt;/strong&gt; failed any of them. You'd just keep applying and hoping.&lt;/p&gt;


&lt;h2&gt;
  
  
  So I built Legible
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://legible.live/legible" rel="noopener noreferrer"&gt;legible.live&lt;/a&gt;&lt;/strong&gt; — free, no signup, anonymous, ~8 seconds.&lt;/p&gt;

&lt;p&gt;Upload a PDF or DOCX. It runs the same five-stage pipeline a real ATS uses, then shows you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exact text the parser extracted, line by line, side-by-side with your original&lt;/li&gt;
&lt;li&gt;Which sections it detected with what confidence — and which it missed&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;strict score&lt;/strong&gt; and a &lt;strong&gt;lenient score&lt;/strong&gt; (explained below)&lt;/li&gt;
&lt;li&gt;The top three concrete fixes, ranked by estimated point gain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No login. No email gate. No "unlock your full report for $29".&lt;/p&gt;


&lt;h2&gt;
  
  
  Strict vs lenient — and why the gap is the interesting number
&lt;/h2&gt;

&lt;p&gt;Most ATS scanners give you one number. That number is meaningless because &lt;strong&gt;the same vendor can be configured very differently across companies.&lt;/strong&gt; Workday with the modern AI screening layer behaves one way; Workday without it behaves another. Taleo at a Fortune 500 with strict filters is brutal; Taleo at a smaller employer with default settings is forgiving.&lt;/p&gt;

&lt;p&gt;So Legible runs two parallel scorers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict mode&lt;/strong&gt; simulates legacy keyword-matching behaviour: exact string matches, no semantic equivalence, low tolerance for layout deviation. Worst-case enterprise ATS configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lenient mode&lt;/strong&gt; simulates modern NLP-based parsing: semantic equivalence (so "Kubernetes" matches "container orchestration"), skills taxonomies, more layout tolerance. Best-case modern configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The gap between the two scores is your parser-dependent risk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your strict score is 45 and your lenient score is 88, your resume is a lottery ticket — it'll pass at modern tech companies and fail at legacy enterprises. If both are above 80, you're robust. If both are below 60, the file itself is broken, not the content.&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.amazonaws.com%2Fuploads%2Farticles%2Fr7wom28gf77057gc62tk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr7wom28gf77057gc62tk.png" alt="Strict vs Lenient" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;The whole pipeline runs in 3–8 seconds on a 1–2 page PDF.&lt;/p&gt;
&lt;h3&gt;
  
  
  Text extraction — two engines, cross-checked
&lt;/h3&gt;

&lt;p&gt;I run PyMuPDF and pdfminer.six in parallel and compare outputs. They disagree about 6% of the time, usually on PDFs with embedded fonts or non-standard encodings. When they diverge significantly, I prefer pdfminer's output and surface a warning:&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;cross_check_extraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExtractionResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pymupdf_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_with_pymupdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pdfminer_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_with_pdfminer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;larger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pymupdf_text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfminer_text&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;larger&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="nc"&gt;ExtractionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_text_layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;divergence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pymupdf_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfminer_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;larger&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;divergence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ExtractionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pdfminer_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extractors disagree by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;divergence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ExtractionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pymupdf_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Column detection
&lt;/h3&gt;

&lt;p&gt;This is the single most important check. I cluster the x-coordinates of every text box on the page and look for a real gap between clusters:&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;detect_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ColumnInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;x_starts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LTTextBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;x_starts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ColumnInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page_width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;
    &lt;span class="n"&gt;clusters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cluster_by_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_starts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page_width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Real two-column resumes have ~30-50% of text boxes in each cluster.
&lt;/span&gt;    &lt;span class="c1"&gt;# Single-column docs with one indented quote don't count.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;min_cluster_share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ColumnInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ColumnInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is the &lt;code&gt;min_cluster_share&lt;/code&gt; check. A single-column resume with one indented quote will produce two x-position clusters, but one of them contains only 2% of the text. A real two-column resume has roughly balanced clusters. This single check eliminated most of my false positives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Section segmentation
&lt;/h3&gt;

&lt;p&gt;Uses fuzzy matching against a known-header vocabulary scraped from a few hundred real resumes. I considered training an NER model, but the gain over a well-tuned dictionary lookup didn't justify the complexity at this scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hardest bug to find
&lt;/h3&gt;

&lt;p&gt;PDFs from certain templates put contact info in the actual &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; XML element of the document — not as regular text in the first paragraph, but in the document structure's header region.&lt;/p&gt;

&lt;p&gt;PyMuPDF returns this text by default. pdfminer's high-level API doesn't. So on these files, one extractor saw a name and the other didn't, and the cross-check flagged an "encoding mismatch" that wasn't really an encoding issue at all. Fixing this required reading both extractors' layout output and detecting whether contact fields lived in header regions specifically — which turned out to be one of the most useful diagnostics in the tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the corpus showed
&lt;/h3&gt;

&lt;p&gt;I ran the final pipeline on a corpus of &lt;strong&gt;20&lt;/strong&gt; anonymised resumes from public sources and personal contributions. &lt;strong&gt;34%&lt;/strong&gt; had at least one critical parsing issue. The most common: contact info in document headers (silently dropped by most ATS), followed by two-column layouts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it doesn't claim
&lt;/h2&gt;

&lt;p&gt;I don't have access to the actual parsers inside Workday, Greenhouse, Taleo, or any other commercial ATS. Nobody outside those companies does.&lt;/p&gt;

&lt;p&gt;Legible simulates the documented behaviour of the parsing pipeline these systems share — the failure modes are real, the detection logic is honest, but the scores are not "what Workday literally returned." The &lt;a href="https://legible.live/legible/methodology" rel="noopener noreferrer"&gt;methodology page&lt;/a&gt; documents every check and every limitation explicitly.&lt;/p&gt;

&lt;p&gt;This honesty is the point. Most ATS scanners quote correlations like "99% match with real employer ATS scores." I don't know how they could possibly verify that. Legible tells you what its pipeline found, what that pipeline shares with real ATS behaviour, and what it cannot tell you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js on Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI + async Postgres on Railway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF extraction:&lt;/strong&gt; PyMuPDF + pdfminer.six, cross-checked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout analysis:&lt;/strong&gt; custom heuristics over pdfminer's &lt;code&gt;LT*&lt;/code&gt; objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring:&lt;/strong&gt; two independent rule-based scorers running in parallel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendations:&lt;/strong&gt; GPT-4o-mini pass over ranked deductions, with a deterministic fallback if the call fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One container per service. Nothing fancy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://legible.live/legible" rel="noopener noreferrer"&gt;legible.live&lt;/a&gt;&lt;/strong&gt; — free, no signup, ~8 seconds.&lt;/p&gt;

&lt;p&gt;If you're job hunting, or you know someone who is, send them the link. Thirty seconds and they might find out the resume they've been sending out for three months is being read as a blank page.&lt;/p&gt;

&lt;p&gt;I'd love feedback — especially edge cases that break the parser. Reply here, or open an issue on GitHub.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, the &lt;a href="https://legible.live/legible/methodology" rel="noopener noreferrer"&gt;methodology page&lt;/a&gt; goes deeper on how each check works and where the limits are. And if Legible finds something surprising in your resume, I'd genuinely like to hear about it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>resume</category>
      <category>ats</category>
    </item>
  </channel>
</rss>
