# Tailoring **Thesis:** a tailored resume should change *selection and order*, never *content*. Every line on every resume comes from a Fact Bank entry the user wrote and can defend in an interview. The composer decides which facts to use and in what order. It can't invent a claim. ## Inputs | File | Contents | |---|---| | `profile/fact_bank.json` | `facts` (id → sentence), `roles` (company, title, location, dates, fact ids), `projects`, `skills` lines, `education`, `_sources`, `_overlaps` | | `profile/lanes.json` | resume **lanes** (backend, platform, fullstack, ai, data, embedded, sre, ...): headline, summary opener, ordered fact ids, projects, skills order; routing rules title/JD → lane | | the JD | fetched per job, HTML-unescaped | ## Pipeline ```mermaid flowchart LR JD[JD text] --> R[route → lane] R --> C[compose
JD-first] FB[(Fact Bank)] --> C C --> V{validate
every id ∈ Fact Bank} V -->|reject| X[error, no PDF] V --> F[fit to one page
6 type steps, then drop a project] F --> PDF[Resume__.pdf] PDF --> ATS[ATS read-back
pypdf text ∩ JD terms] ATS --> EV[(resume event:
facts, projects, skills, coverage)] ``` ### Routing Lane rules are regexes over title, and then the JD, scoped per rule. The first match wins, then the default lane. Lanes encode the user's real tracks; they are not personas. ### JD-first composer (`engine/tailoring/compose.py`) - **Vocabulary:** the JD's terms ∩ the Fact Bank's skill vocabulary, counted by frequency and spelled as the JD spells them (for example "PostgreSQL" vs "Postgres"). - **Headline:** the role title + the three skill items the posting mentions most. - **Summary:** the lane opener + the best-matching sentences the user wrote, kept in one voice. - **Skills lines:** reordered so the JD's terms come first; nothing is added that isn't in the Fact Bank. - **Roles and projects:** fact ids ranked by JD-term overlap within each role; projects titled per lane. - **Overlap groups:** facts that describe the same achievement in different words never appear twice. ### Validator (`resume.validate`) Rejects any spec that references a fact, project or skill id that isn't in the Fact Bank. This check is the guarantee that the composer can't fabricate. ### One-page fit (`resume.fit`) Six leading/size steps (11.6/9.3 → 10.0/8.8). If it still runs to two pages, drop the least relevant (last) project and retry, recursively. If it still doesn't fit, the job is skipped (the batch continues). ### ATS read-back (`compose.ats_check`) The PDF is parsed back with `pypdf` and the score is the share of the JD terms you have that a parser can actually read. Live runs score **77–100%**, typically high 80s to 100. The score is logged per resume, and the engine room shows it per job. ## Transparency Each `resume` event records the lane, the role fact ids, projects, skills and coverage (`on_resume`, `jd_terms_you_have`, `ats.score`). `/job?u=` in the web app shows exactly which facts a given application used. ## Cost Routing O(R) regexes; composing O(F·T) for F facts and T JD terms; PDF build ~tens of ms; read-back one parse. Tailoring was made ×3 faster in v0.8 (precompiled vocab, single pass), so it costs little next to a page load. Related: [Fit gate](Fit-Gate.md) · [Apply engine](Apply-Engine.md) · [Truthfulness and safety](Truthfulness-and-Safety.md) · [Algorithms](Algorithms.md)