* feat(ats): add ATS checker and replace resume analysis
Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.
Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.
Also bumps the version to 5.2.9 and adds the changelog entry.
* chore(deps): bump workspace dependencies
* fix(ats-checker): keep negation inside each 'what this does not do' bullet
The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.
Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
* fix(pdf): resolve bold text weight from the family's bold face
Bold text (<strong>, rich-text bold, template bold styles) previously
rendered at the last stored body weight, which is ambiguous: families
are commonly stored as ["400","600"] (the typography picker's default
pairing), so bold rendered at SemiBold — nearly indistinguishable from
Regular for faces like Open Sans (#3310).
Add resolveBoldFontWeight() to the fonts package: keep a deliberate
stored bold-class choice (>= 700), else prefer the family's true Bold
face ("700"), else the heaviest >= 600 face; return null so callers
keep their existing fallback when the family has no bold-class face.
Wire it through use-register-fonts, the shared base-template-styles
builder, base-styles and the Scizor template. Default body IBM Plex
Serif ["400","500"] now renders bold at 700 (base-reset-fidelity
expectation updated accordingly).
Fixes#3310
* fix(pdf): register bold fallback faces for CJK glyph substitution
When resolveBoldFontWeight maps stored weights like ["400","600"] to the
family's 700 face, register that weight on each PDF fallback font too so
glyph-level substitution keeps bold glyphs instead of snapping to 600.
Also reorder @reactive-resume/fonts imports per Biome convention.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): render emoji via a Noto Emoji script fallback
Emoji in resume content (flags, globe, pictographs) rendered as mojibake
in the preview and PDF export because the per-codepoint fallback chain
registered no emoji-capable font: every font in the stack lacked the
glyphs, so layout fell through to single-byte standard-font encoding —
each UTF-16 code unit truncated to its low byte (#3321).
Follows the #2986/#3190 script-fallback pattern: detect emoji content
(regional indicators unioned with Extended_Pictographic), map it to the
monochrome Noto Emoji web font (TrueType glyf outlines, PDF-embeddable),
and register it in the fallback stack for both serif and sans stacks.
Out-of-range weight requests alias to the nearest served weight (300-700)
so registration never falls back to the preview subset.
* fix(pdf): detect keycap emoji via the combining enclosing keycap
Greptile review on #3351: keycap sequences like 1\uFE0F\u20E3 carry no
regional indicator and no Extended_Pictographic codepoint, so they
bypassed the emoji detector and rendered garbled — the exact class of
bug #3321 fixes. Union U+20E3 into the detector; every valid keycap
sequence contains it.
* Update packages/utils/src/locale.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* [autofix.ci] apply automated fixes
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* fix(fonts): register Vazirmatn in webfont catalog for JSON imports
Imported resumes can set typography.fontFamily to Vazirmatn, but the
popularity-sorted Google Fonts slice omits it so PDF registration fell
back to IBM Plex Serif and Persian/Arabic glyphs stacked or tofu (#3098).
Add Vazirmatn as a locale-coverage manual entry (same pattern as Carlito)
and cover catalog resolution with unit tests.
* test(pdf): keep Vazirmatn as primary family for fa-IR registration
Prove JSON-imported Vazirmatn is handed to Font.register instead of
being rewritten to IBM Plex Serif (#3098).
* fix(fonts): address CodeRabbit review on Vazirmatn catalog
Assert getWebFontSource resolves files["400"] for Vazirmatn instead of
only matching the preview fallback, and split the font-generation log
line to satisfy the 120-column Biome limit.
Change richListItemContent from flex: initial to flex: 1 with minWidth: 0.
This makes the content area fill remaining space after the marker and
columnGap, so text wraps within the user-set margin instead of overflowing.
Fixes#3336
`SectionItemHeader` only rendered its own box when a template opted into
`mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the
header children and attached the resolved `item-header` style to the first
descendant that happened to be a literal `View` or `InlineItemHeader`.
Sections whose header starts with anything else — certifications, awards,
projects, publications, references — matched nothing, so the style was
silently dropped; stacked headers such as experience matched only their
first row, so a second row went unstyled.
The header now always renders its own `Div`, so `item-header` covers the
whole header row of every section on every template. `Div` rather than
`View` keeps the base row gap the rows used to inherit from the item box,
and Ditgar keeps its tight header via `rowGap: 0` on its own
`sectionItemHeader` slot, so rendered output is unchanged apart from the
newly styled rows. `mainItemHeaderBorder` is now dead and removed.
Fixes#3349
Section item headers render the title and its trailing date inside a shared
split row styled with `flex-wrap: wrap`. When the title is long the date wraps
onto its own line and left-aligns instead of staying pinned right, which reads
as inconsistent down a list of certifications.
That row had no selector. It is a bare View, so it never reached the semantic
tree: `item-header` matches the box around the row, and the title and date are
text nodes that layout properties do not apply to. There was no stylesheet that
could reach it.
Expose it as `template-part[name="item-header-row"]`, shared by every template
because the row comes from the shared section components. Awards,
certifications, projects and publications are covered; experience, education
and volunteer stack two rows and hand their headers to the
`inline-item-header-*` parts on some templates, so they are left alone.
Readers can now write:
@version 1;
template-part[name="item-header-row"] { flex-wrap: nowrap; }
* docs(agents): align Redis compose commands with development guide
- Problem: AGENTS.md omitted Redis from dev infrastructure compose commands
while docs/contributing/development.mdx starts redis for local dev.
- Fix: document full postgres/redis/seaweedfs compose command and note that
REDIS_URL and ENCRYPTION_SECRET are required for AI agent features.
- Verification: preflight upstream fetch; manual diff against development.mdx
and compose.dev.yml redis service; duplicate PR gate passed.
* fix(pdf): ignore phantom gengar skill text nodes
- Problem: gengar template resumes with skills keywords fail semantic CSS activation because the legacy renderer emits a harmless empty text node that parity treats as a mismatch.
- Fix: treat the specific empty text artifact as presentation-neutral in the legacy parity comparator and add a regression test for the phantom fontSize 9 node.
- Verification: pnpm test src/semantic/legacy-parity.test.ts in packages/pdf passed (31 tests).
* docs: clarify host and container Redis URLs
- Problem: the development guide only showed the Docker Redis hostname, which fails for host-run development.\n- Fix: document localhost for host execution and redis for Docker execution.\n- Verification: pnpm test src/semantic/legacy-parity.test.ts (31 passed).
* fix(pdf): omit empty skill proficiency text
* test(pdf): cover blank skill proficiency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): register Noto punctuation fallback for missing glyphs
- Problem: U+2022 bullet characters render as garbled glyphs when the body
font (e.g. IBM Plex Serif) lacks the glyph and no PDF fallback is registered.
- Fix: append Noto Serif/Sans to the PDF fallback stack as a general-purpose
punctuation source covering General Punctuation (U+2000–U+206F).
- Verification: pnpm --filter @reactive-resume/fonts test;
pnpm --filter @reactive-resume/pdf test src/hooks/use-register-fonts.test.ts
* test(fonts): clarify zh-CN fallback test description
- Problem: getPdfFallbackFontFamilies("Times-Roman", { locale: "zh-CN" }) now
returns ["Noto Serif SC", "Noto Serif"] (the general-purpose punctuation
fallback is appended), so the test description "returns only the Simplified
Chinese font for zh-CN (unchanged behavior)" is no longer accurate.
- Fix: rename the test to describe that it uses the Simplified Chinese font
plus the punctuation fallback. The assertion is unchanged.
- Verification: pnpm --filter @reactive-resume/fonts test -> 45/45 passing.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
The server PDF preflight spawned a fresh worker per semantic-CSS edit, each
racing a 15s startup deadline to cold-load the ~721kB+5MB PDF runtime. That
load is super-linear in CPU (~3s at 1 vCPU, >15s on a throttled/shared vCPU),
so on a constrained box every edit hit the startup-timeout path and returned
STYLESHEET_PREFLIGHT_WORKER_FAILED. Since the service only advances the applied
stylesheet when preflight passes, custom styles never applied and the editor
stuck on Checking.
Warm one worker at boot and reuse it (message-based input, respawn on
crash/timeout), so the cold load is paid once instead of per edit. Raise the
render deadline 5s->30s (a rich resume renders ~5-18s on a slow box) and the
readiness ceiling to 120s so the one-time warm completes even when throttled.
Surface worker load failures instead of an unhandled-rejection crash, and log
runner-side failure paths so the previously opaque failure is diagnosable.
Verified in node:24-slim under --cpus=0.25/0.35/0.5: all reused requests pass.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
Move 14 identical style slots (text/heading/div/inline/link/small/bold/
richParagraph/richListItemRow/richListItemMarker/richListItemContent/
splitRow/alignEnd/picture) from all 15 template Page.tsx files into a
single createBaseTemplateStyles factory in templates/shared. Each template
now spreads …base and keeps only its real overrides. Resolved StyleSheet
values are identical to before. Update rtl-fixture and rich-text-template-
styles tests to guard the factory file rather than each template directly.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Add three shared contact-item components to packages/pdf/src/templates/shared/
contact-item.tsx alongside the existing WebsiteContactItem and CustomFieldContactItem.
Replace ~18 lines of inline email/phone/location JSX in all 15 template headers
with the shared components (EmailContactItem accepts an optional iconName prop
for ditgar's "at" variant; rhyhorn's array-push pattern also updated).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Two match chains in sections.tsx are replaced with plain lookup maps typed
via `satisfies Record<CustomSectionType, ...>` which preserves compile-time
exhaustiveness without the ts-pattern dependency. Remove ts-pattern from
packages/pdf/package.json.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Export parseFiniteNumber and parsePxValue from icon-size.ts (already the
canonical home of these helpers). Remove the three private copies in
rich-text-spacing.ts and import the shared ones instead.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
overflow: hidden on Text elements in @react-pdf/renderer v4.x clips
content at its initial computed height, hiding any text after a line
break. minWidth/maxWidth/flexShrink already handle horizontal
containment so nothing else breaks.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use language-specific Noto fonts for CJK PDF fallback
* feat: extend fallback to Arabic/Hebrew/Thai
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(editor): add multicolor highlight with auto-contrast text
Enable the Tiptap Highlight extension in multicolor mode, replacing the
single-color yellow toggle with a full color picker (16 presets + custom).
When the chosen highlight color is perceptually dark, text inside the mark
automatically renders white for readability.
Changes span the full pipeline:
- Editor: ColorPicker UI, extended renderHTML for contrast detection
- PDF: normalizeMarkElements preserves data-color as inline style
- DOCX: mergeStyle reads actual background-color from <mark>
- Utils: new isDarkColor() luminance helper
Backward-compatible: legacy <mark> without data-color still renders yellow.
Resolves#3109
* fix: handle multicolor highlight edge cases
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat: add section heading icons to PDF templates
Add customizable Phosphor icons before section titles in PDF output.
Users can toggle visibility globally via a new "Hide section heading icons"
switch (independent of item-level icons) and customize individual section
icons through the builder sidebar icon picker.
- Add `icon` field to `baseSectionSchema` and `summarySchema`
- Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat)
- Implement `SectionHeadingIcon` component with heading font-size scaling
- Support "none" sentinel for per-section icon hiding
- Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data
- Add icon picker to builder sidebar sections and custom section dialogs
Closes#2632
* test: add unit tests for section heading icons
- Add tests for getResumeSectionIcon() covering built-in sections,
summary, custom sections, "none" sentinel, and default fallbacks
- Add schema tests for baseSectionSchema icon field, summarySchema icon,
and pageSchema hideSectionIcons default behavior
* refactor: minor updates to icon display
* Update apps/web/locales/es-ES.po
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix(pdf): apply custom style fontSize to icon and level indicator sizes
Map fontSize from Icon and Level Indicator custom style slots to Phosphor
icon size and level indicator dimensions, since react-pdf icons ignore
fontSize in favor of the size prop.
* fix: separate global icon and scoped level indicator font sizes
Icon slot fontSize now drives all resume icons plus level display
decorations. Level indicator fontSize overrides only within level display.
Shared sizing logic lives in schema; design sidebar preview uses global rules.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>