Thanks for picking up a piece of this. offlinecv is a browser-side PDF
parser audit and job-search workbench. Code lives under src/, license is
Apache-2.0 (patent grant included; see NOTICE). See
README.md for what offlinecv is and what it surfaces;
see CLAUDE.md for the pipeline shape and tier layout; and see
docs/CONTRIBUTING-PROCESS.md for the shipping
process this file summarizes — the test-fixture PII policy (mandatory before
you add any PDF), AI attribution, squash messages, and deploy.
npm install
npm run dev # vite dev server on http://localhost:5173
npm run test # vitest run (189 tests, ~1s)
npm run typecheck # tsc -b --noEmitRequires Node 20+. .env and .env.deploy are both gitignored; neither
is needed to develop, run tests, or build. They only matter for opt-in
PostHog telemetry and deploying a build — see the Telemetry
and Deploy sections of the README if you need
those.
Branch from main. Name branches <your-initials>/<short-slug>, e.g.
am/fix-two-column-rendering or sa/wire-up-webllm-pilot. One branch
per issue. Keep branches short-lived; rebase on main before opening a
PR if it has drifted.
The repo refuses manual git commit via a Claude Code hook
(scripts/hooks/block_commit.sh). The canonical commit path is
commit-all.sh from ~/tools/scripts (on $PATH for contributors
using the shared tools repo), which handles formatting, staging,
running the test suite, and writing a structured commit message.
If you are not on Claude Code, or you do not have commit-all.sh
available, do the equivalent by hand:
- Run
npm run typecheck && npm run testand confirm both pass. - Write a single-purpose commit using a conventional-commit-ish prefix
(
feat:,fix:,chore:,docs:,refactor:). Seescripts/hooks/check_conventions.pyfor the exact prefixes the project enforces. - To skip the Claude Code commit-block hook for the session, export
OFFLINECV_SKIP_HOOKS=1in the shell before launchingclaude. Inline prefixing (OFFLINECV_SKIP_HOOKS=1 git commit ...) does not work — the env assignment is part of the command string the hook never executes. The constraint is documented at the top ofscripts/hooks/block_commit.sh.
If you are not using Claude Code at all, the hooks do not fire and no escape hatch is needed.
Open PRs against main. Title in conventional-commit form
(feat: add WebLLM bullet rewrite). In the body, describe what changed
and how you verified it. Reference the issue with Resolves #<n> (for
a complete fix) or Refs #<n> (for partial work) so GitHub auto-links
on merge.
CI must pass before merge — typecheck, tests, and build all run on
every PR (see .github/workflows/ci.yml).
New logic ships with a *.test.ts next to the file it tests. Tests
run under vitest — npm run test once, or npm run test:watch for
the watcher. Match the existing pattern in
src/lib/heuristics/*.test.ts and src/lib/score/*.test.ts; the
heuristics tests use the mkItem helper in
src/lib/heuristics/__test-utils__/ for synthetic pdfjs items.
src/lib/heuristics/corpus.test.ts runs the full cascade + scorer
against every PDF under tests/fixtures/pdfs/<category>/ and diffs
the result against a co-located *.expected.json snapshot. The
snapshot only captures counts and structural flags — never raw text
or field values — so committed fixtures stay free of PII even when
the source PDFs include personas. Add new PDFs and re-bake snapshots
with npm run bake-fixtures. Full workflow + sourcing guidance in
tests/fixtures/pdfs/README.md.
Every new .ts / .tsx file under src/ carries the 3-line SPDX
header — see the canonical form in CLAUDE.md under
"Exemplars":
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The offlinecv AuthorsTypeScript strict mode is on. Default to writing no comments — add one only when the why is non-obvious (a hidden constraint, a workaround for a specific bug, behavior that would surprise a reader). Don't explain what the code does; the names already do that.
offlinecv enforces a strict component hierarchy. Before writing any interactive element or styled surface, check whether a shared primitive already exists:
<Button>(src/components/ui/Button.tsx) — the one and only button. Never reach for a raw<button>in feature code.<StatusBadge>(src/components/shared/StatusBadge.tsx) — label- colour pill for any status / confidence indicator.
<ErrorState>(src/components/shared/ErrorState.tsx) — the canonical full-bleed error card. Do not hand-roll a red warning box.
No hardcoded colours. Style with semantic Tailwind tokens
(bg-surface-card, text-content-primary, border-border-light,
text-accent-primary, …). Raw palette classes (bg-red-500,
text-slate-400) and manual dark: variants are anti-patterns —
dark mode is handled by the token layer, not inline overrides.
These architecture rules are enforced at two layers:
- Blocking CI gate —
npm run lint(eslint .) runs in theverifyCI job on every PR and fails the check if a violation is introduced. The same three checks (raw<button>, palette classes, dark: colour variants, hardcoded hex) are encoded as ESLint errors ineslint.config.js. - Advisory inline nudge — a
PostToolUseClaude Code hook (scripts/hooks/style_guard.sh) fires immediately after a file edit inside Claude Code, giving you instant feedback before commit. Warnings are non-blocking; bypass for one session withexport OFFLINECV_SKIP_HOOKS=1.
Check the labels before you start. An issue carrying status:claimed
already has someone on it — pick another, or comment and ask where it stands.
Do not open a PR against a claimed issue without talking to the assignee
first; you will almost certainly duplicate their work.
To take an unclaimed issue, comment on it saying you're starting. A
maintainer will assign it to you and add status:claimed, which is what makes
the claim visible to everyone else. Two signals are needed because the
assignee alone is not enough: gh issue list prints labels and not assignees,
and on the web list the assignee is a small avatar in the right margin.
Contributors — and tooling that scans the backlog — read the label.
If you have write access and assign yourself, add status:claimed too.
Self-assignment skips the maintainer step that would normally attach the label,
and an assignee on its own is exactly the invisible claim described above — it
is how #681 got built twice.
Claims lapse. If a claimed issue goes 7 days with no linked PR and no
activity, a scheduled job
(stale-claims.yml) unassigns it,
drops the label, and comments to say so. Any activity resets the clock, and an
open PR exempts the claim entirely — so if you are mid-work and quiet, a
one-line comment keeps it. If yours does lapse and you still want it, say so
and we'll assign it straight back.
If a maintainer assigns you an issue you didn't ask for, that's a request, not an obligation. Say no and it goes back to the pool.
Apply at least one type label (bug, feature, improvement,
chore, refactor) plus any matching domain labels
(documentation, testing, etc.). For bugs, include a reproducer PDF
or describe how to reproduce — fonts_unmappable and two-column cases
are the most common and the hardest to repro without a sample.
Issues are tracked on the OfflineCV v1 board
(offlinecv/projects/1),
grouped by Phase — the milestone each issue belongs to (M1 Parser Hardening, M2 UI Parity, M3 JD Matching, M4 AI Rewrite, v1.1 Post-Launch). v1.0 ships when M1–M4 land; v1.1 holds post-launch work.
After filing an issue, place it on the roadmap. With Claude Code, run
/triage-issue <number> — it picks the milestone, adds the issue to the
board, and sets its Phase. By hand:
gh issue edit <number> --repo offlinecv/OfflineCV --milestone "<milestone title>"Adding an issue to the board (the Phase column) needs the project
token scope, which gh does not grant by default — run
gh auth refresh -s project once if you want to write the board.
Without it, just set the milestone (the durable signal) and a maintainer
can add it to the board. Maintainers: /triage-issue also documents the
one-time milestone/board setup in
.claude/skills/triage-issue/SKILL.md.
If you use Claude Code and want to cut token usage, you can enable
caveman — a compressed-output
mode that keeps full technical accuracy while trimming roughly 75% of the
prose. It's entirely optional, opt-in, and not required to contribute; it's
deliberately left out of the repo's committed .claude/settings.json so it
only activates for contributors who choose it.
Install (one time):
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bashToggle within a session with /caveman (levels: lite, full, ultra).
Turn it off by typing normal mode or stop caveman.
Tag @s-annam on the PR or issue.