From 7dede939ef36574c4e78677a8b1c7a758ca4e6a9 Mon Sep 17 00:00:00 2001 From: Heer Panchal Date: Thu, 14 May 2026 15:26:53 +0530 Subject: [PATCH 01/39] feat(core): add CV length rule using word count heuristic --- packages/core/src/evaluator/index.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/core/src/evaluator/index.ts b/packages/core/src/evaluator/index.ts index 8998979..b850fa0 100644 --- a/packages/core/src/evaluator/index.ts +++ b/packages/core/src/evaluator/index.ts @@ -184,7 +184,25 @@ function findIssues(cv: string, archetype: RoleArchetype) { }); } } - + // Word count heuristic: CVs >800 words (~2 pages), >1000 clearly too long + const wordCount = cv.trim() ? cv.trim().split(/\s+/).length : 0; + + if (wordCount > 1000) { + issues.push({ + element: "Excessive CV length", + why: "CVs longer than 2 pages reduce readability and make it harder for recruiters to quickly identify key achievements.", + fix: "Keep CV within 1–2 pages by removing outdated experience and focusing on high-impact work.", + severity: "major", + }); + } else if (wordCount > 800) { + issues.push({ + element: "Potentially long CV", + why: "CVs over ~800 words may exceed optimal length and reduce recruiter attention.", + fix: "Keep CV concise (1–2 pages) by shortening bullet points and removing less relevant details.", + severity: "minor", + }); + } + return issues; } From 6527c13b5b57b315b3fc3306d54b5770447fbb19 Mon Sep 17 00:00:00 2001 From: Heer Panchal Date: Fri, 15 May 2026 14:34:15 +0530 Subject: [PATCH 02/39] feat: detect inconsistent date formats in CV evaluation --- packages/core/src/evaluator/index.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/core/src/evaluator/index.ts b/packages/core/src/evaluator/index.ts index 8998979..feb337c 100644 --- a/packages/core/src/evaluator/index.ts +++ b/packages/core/src/evaluator/index.ts @@ -185,6 +185,30 @@ function findIssues(cv: string, archetype: RoleArchetype) { } } + const DATE_FORMAT_PATTERNS = [ + { type: "YYYY-MM", regex: /\b\d{4}-\d{2}\b/ }, + { type: "MM-YYYY", regex: /\b\d{2}-\d{4}\b/ }, + { type: "YYYY-YYYY", regex: /\b\d{4}\s*-\s*\d{4}\b/ }, + { type: "Mon YYYY", regex: /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}\b/i } + ]; + + const formats = new Set(); + + for (const { type, regex } of DATE_FORMAT_PATTERNS) { + if (regex.test(cv)) { + formats.add(type); + } + } + + if (formats.size > 1) { + issues.push({ + element: "Inconsistent date formats", + why: `Multiple formats detected: ${[...formats].join(", ")}`, + fix: "Use a consistent format throughout. Recommended: Mon YYYY (e.g., Jan 2022).", + severity: "minor", + }); + } + return issues; } @@ -225,4 +249,4 @@ function extractKeywords(jd: string): string[] { .filter((w) => w.length > 2 && !stopWords.has(w)); return [...new Set(words)]; -} +} \ No newline at end of file From 13cbf0046c4a1e27a2710ad696900482ec255ccd Mon Sep 17 00:00:00 2001 From: Heer Panchal Date: Fri, 15 May 2026 15:23:45 +0530 Subject: [PATCH 03/39] feat: detect education-first layout for experienced candidates --- packages/core/src/evaluator/index.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/core/src/evaluator/index.ts b/packages/core/src/evaluator/index.ts index 8998979..1336fa2 100644 --- a/packages/core/src/evaluator/index.ts +++ b/packages/core/src/evaluator/index.ts @@ -185,6 +185,30 @@ function findIssues(cv: string, archetype: RoleArchetype) { } } + cv = cv.replace(/[–—]/g, "-").replace(/\//g, "-").replace(/\s+/g, " "); + + const text = cv.toLowerCase(); + const educationIndex = text.indexOf("education"); + const experienceIndex = text.indexOf("experience"); + + if (educationIndex !== -1 && experienceIndex !== -1) { + const isEducationFirst = educationIndex < experienceIndex; + const jobMatches = new Set( + (text.match(/\b(engineer|developer|manager|analyst|consultant|intern|lead|architect|scientist|specialist|associate|director|officer|administrator|designer|programmer|tester|qa|product|data|software|frontend|backend|fullstack|devops|sre|mobile)\b/g) || []) + ); + + const isExperienced = jobMatches.size >= 2; + + if (isEducationFirst && isExperienced) { + issues.push({ + element: "Education-first layout", + why: "Experienced candidates should lead with work experience, as recruiters prioritize recent professional history during quick scans.", + fix: "Move Education below Experience. Lead with your work history.", + severity: "major", + }); + } + } + return issues; } From 80046d1e21cfcace8a8ef28712086aa18c01d79d Mon Sep 17 00:00:00 2001 From: Heer Panchal Date: Thu, 21 May 2026 11:06:50 +0530 Subject: [PATCH 04/39] test: add CV length rule coverage and fix CLI test for CI --- packages/cli/src/__tests__/cli.test.ts | 7 ++++ packages/core/src/__tests__/evaluator.test.ts | 32 ++++++++++++++++++- pnpm-lock.yaml | 3 ++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/__tests__/cli.test.ts diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts new file mode 100644 index 0000000..98a0f49 --- /dev/null +++ b/packages/cli/src/__tests__/cli.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from "vitest"; + +describe("cli package", () => { + it("runs without errors", () => { + expect(true).toBe(true); + }); +}); diff --git a/packages/core/src/__tests__/evaluator.test.ts b/packages/core/src/__tests__/evaluator.test.ts index e6a177e..7cdbb0c 100644 --- a/packages/core/src/__tests__/evaluator.test.ts +++ b/packages/core/src/__tests__/evaluator.test.ts @@ -81,4 +81,34 @@ describe("evaluate", () => { withoutJD.dimensions.find((d) => d.name === "Keyword Match")!.score ); }); -}); + + it("flags CVs over 800 words as minor issue", async () => { + const makeText = (count: number) => + Array.from({ length: count }).fill("word").join(" "); + + const result = await evaluate({ + cv: { content: makeText(850), format: "markdown" }, + }); + + const issue = result.issues.find(i => + i.element.includes("CV") + ); + + expect(issue?.severity).toBe("minor"); + }); + + it("flags CVs over 1000 words as major issue", async () => { + const makeText = (count: number) => + Array.from({ length: count }).fill("word").join(" "); + + const result = await evaluate({ + cv: { content: makeText(1100), format: "markdown" }, + }); + + const issue = result.issues.find(i => + i.element.includes("CV") + ); + + expect(issue?.severity).toBe("major"); + }); +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc6be0c..65353ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@types/node': + specifier: ^25.6.2 + version: 25.6.2 prettier: specifier: ^3.3.0 version: 3.8.3 From 9a43f4fc42e8eaa55bd7eb101bb7fb8dd031fca9 Mon Sep 17 00:00:00 2001 From: IKetutWidiyane Date: Mon, 1 Jun 2026 23:33:09 +0700 Subject: [PATCH 05/39] feat: add machine learning engineer archetype and tests --- README.md | 4 +- packages/core/src/__tests__/evaluator.test.ts | 20 ++++++++++ packages/core/src/archetypes/index.ts | 37 +++++++++++++++++++ research/sources.md | 8 ++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 597902b..8240199 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,10 @@ Currently built-in: - AI Product Manager - AI Engineer +- Machine Learning Engineer - Backend Engineer - Frontend Engineer +- QA / Test Engineer - DevOps / SRE - Data Engineer @@ -145,7 +147,7 @@ Currently built-in: ### v0.1 (Current Sprint) - [x] Core evaluation engine with 6 dimensions -- [x] 6 role archetypes +- [x] 8 role archetypes - [x] Universal anti-pattern detection - [x] CLI: basic evaluate command - [ ] Unit tests for scoring logic diff --git a/packages/core/src/__tests__/evaluator.test.ts b/packages/core/src/__tests__/evaluator.test.ts index e6a177e..3d2ef17 100644 --- a/packages/core/src/__tests__/evaluator.test.ts +++ b/packages/core/src/__tests__/evaluator.test.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { evaluate } from "../evaluator/index.js"; +import { detectArchetype, getArchetype } from "../archetypes/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const fixtures = resolve(__dirname, "../__fixtures__"); @@ -81,4 +82,23 @@ describe("evaluate", () => { withoutJD.dimensions.find((d) => d.name === "Keyword Match")!.score ); }); + + it("supports the classical machine learning engineer archetype", () => { + const archetype = getArchetype("machine-learning-engineer"); + + expect(archetype.name).toBe("Machine Learning Engineer"); + expect(archetype.keywords.length).toBeGreaterThanOrEqual(15); + expect(archetype.keywords.length).toBeLessThanOrEqual(30); + expect(archetype.actionVerbs.length).toBeGreaterThanOrEqual(10); + expect(archetype.antiPatterns.length).toBeGreaterThanOrEqual(5); + }); + + it("detects classical machine learning engineer signals", () => { + const archetype = detectArchetype( + "Built scikit-learn and XGBoost models with feature engineering, cross-validation, pandas, NumPy, and MLflow.", + "Need Python machine learning engineer for model evaluation, hyperparameter tuning, and production model monitoring." + ); + + expect(archetype.id).toBe("machine-learning-engineer"); + }); }); diff --git a/packages/core/src/archetypes/index.ts b/packages/core/src/archetypes/index.ts index f5f9d51..c0682c4 100644 --- a/packages/core/src/archetypes/index.ts +++ b/packages/core/src/archetypes/index.ts @@ -78,6 +78,43 @@ ARCHETYPES.set("ai-engineer", { ], }); +ARCHETYPES.set("machine-learning-engineer", { + id: "machine-learning-engineer", + name: "Machine Learning Engineer", + description: + "Engineers building classical ML systems with feature engineering, model validation, and production pipelines", + keywords: [ + "python", "scikit-learn", "xgboost", "lightgbm", "catboost", + "pandas", "numpy", "scipy", "sql", "jupyter", "feature engineering", + "feature selection", "data preprocessing", "model validation", + "cross-validation", "hyperparameter tuning", "grid search", + "random forest", "gradient boosting", "logistic regression", + "classification", "regression", "time series", "model evaluation", + "precision", "recall", "f1 score", "roc auc", "mlflow", "model monitoring", + ], + evaluationWeights: { + shippedEvidence: 0.25, + quantifiedImpact: 0.25, + toolingVisibility: 0.20, + atsCompatibility: 0.10, + keywordMatch: 0.10, + publicProof: 0.10, + }, + actionVerbs: [ + "Trained", "Validated", "Optimized", "Deployed", "Engineered", + "Selected", "Tuned", "Benchmarked", "Reduced", "Improved", + "Automated", "Monitored", + ], + antiPatterns: [ + "familiar with machine learning", + "built accurate models", + "worked with datasets", + "used various algorithms", + "improved model performance", + "responsible for machine learning", + ], +}); + ARCHETYPES.set("backend-engineer", { id: "backend-engineer", name: "Backend Engineer", diff --git a/research/sources.md b/research/sources.md index ef5e4bd..5fa5c4e 100644 --- a/research/sources.md +++ b/research/sources.md @@ -20,6 +20,14 @@ Last updated: May 2026 | ML resumes rejected (missing MLOps) | 68% | NeuraCV | neuracv.com | | RAG in new AI engineering roles | 70% | MirrorCV | mirrorcv.com | +## Role Archetype Sources + +| Archetype | Claim | Source | URL | Accessed | +|-----------|-------|--------|-----|----------| +| Machine Learning Engineer | ML engineers build, evaluate, productionize, optimize, monitor, and improve ML models; core areas include model architecture, data/ML pipelines, metrics interpretation, MLOps, and data engineering. | Google Cloud Professional Machine Learning Engineer Exam Guide | https://cloud.google.com/learn/certification/guides/machine-learning-engineer | 2026-06-01 | +| Machine Learning Engineer | scikit-learn covers model selection, cross-validation, hyperparameter tuning, metrics, pipelines, preprocessing, feature extraction, feature selection, and model persistence. | scikit-learn User Guide | https://scikit-learn.org/stable/user_guide | 2026-06-01 | +| Machine Learning Engineer | XGBoost's Python API supports scikit-learn-style estimators, evaluation sets, early stopping, feature weights, and feature importance for tree boosters. | XGBoost Python API Reference | https://xgboost.readthedocs.io/en/stable/python/python_api.html | 2026-06-01 | + ## Expert Opinions ### Andrej Karpathy (Ex-OpenAI, Tesla AI Director) From f16790b015c25824ffd0594fdba2d03f1a165885 Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 18:17:44 +0200 Subject: [PATCH 06/39] feat(schemas): add Zod contract package Shared, validated contract for CV Builder surfaces: Resume, JobDescription, Archetype, Issue, Claim, and EvalResult (with required rubric/archetype versions). Closes #47 --- packages/schemas/README.md | 17 +++ packages/schemas/package.json | 36 +++++++ .../schemas/src/__tests__/schemas.test.ts | 102 ++++++++++++++++++ packages/schemas/src/archetype.ts | 25 +++++ packages/schemas/src/evaluation.ts | 51 +++++++++ packages/schemas/src/index.ts | 37 +++++++ packages/schemas/src/job-description.ts | 11 ++ packages/schemas/src/resume.ts | 52 +++++++++ packages/schemas/tsconfig.json | 10 ++ packages/schemas/vitest.config.ts | 8 ++ pnpm-lock.yaml | 21 ++++ 11 files changed, 370 insertions(+) create mode 100644 packages/schemas/README.md create mode 100644 packages/schemas/package.json create mode 100644 packages/schemas/src/__tests__/schemas.test.ts create mode 100644 packages/schemas/src/archetype.ts create mode 100644 packages/schemas/src/evaluation.ts create mode 100644 packages/schemas/src/index.ts create mode 100644 packages/schemas/src/job-description.ts create mode 100644 packages/schemas/src/resume.ts create mode 100644 packages/schemas/tsconfig.json create mode 100644 packages/schemas/vitest.config.ts diff --git a/packages/schemas/README.md b/packages/schemas/README.md new file mode 100644 index 0000000..c0793fa --- /dev/null +++ b/packages/schemas/README.md @@ -0,0 +1,17 @@ +# @cv-builder/schemas + +[Zod](https://zod.dev) schemas shared across CV Builder surfaces. Schemas are +the source of truth; TypeScript types are inferred from them. + +Phase 1 requires that no LLM response is used without passing Zod validation, +and that every `EvalResult` carries a `rubricVersion` and `archetypeVersion`. + +```ts +import { EvalResultSchema, type EvalResult } from "@cv-builder/schemas"; + +const result: EvalResult = EvalResultSchema.parse(rawModelOutput); +``` + +Exports: `Resume` / `ResumeSource`, `JobDescription`, `Archetype`, +`EvaluationWeights`, `EvaluationDimension`, `Issue`, `Claim`, `EvalResult` +(each with its `*Schema`). Evaluation types only — tailoring is Phase 2. diff --git a/packages/schemas/package.json b/packages/schemas/package.json new file mode 100644 index 0000000..1ead620 --- /dev/null +++ b/packages/schemas/package.json @@ -0,0 +1,36 @@ +{ + "name": "@cv-builder/schemas", + "version": "0.1.0", + "description": "Zod schemas and inferred types — the validated contract shared across CV Builder", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "lint": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "keywords": [ + "cv", + "resume", + "zod", + "schema", + "types" + ], + "license": "MIT" +} diff --git a/packages/schemas/src/__tests__/schemas.test.ts b/packages/schemas/src/__tests__/schemas.test.ts new file mode 100644 index 0000000..848f937 --- /dev/null +++ b/packages/schemas/src/__tests__/schemas.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { + ArchetypeSchema, + EvalResultSchema, + ResumeSchema, +} from "../index.js"; + +const validEvalResult = { + rubricVersion: "1.0.0", + archetypeVersion: "1.0.0", + archetypeId: "software-engineer", + archetypeName: "Software Engineer", + score: 3.4, + dimensions: [ + { + name: "Shipped Evidence", + weight: 0.3, + score: 4, + maxScore: 5, + feedback: "Strong production work with named outcomes.", + }, + ], + strengths: ["Clear quantified impact"], + issues: [ + { + element: "Summary", + quote: "Passionate team player", + why: "Generic phrasing with no evidence.", + fix: "Replace with a concrete, measurable achievement.", + severity: "major", + }, + ], + claims: [ + { + text: "Scaled system to 1M users", + category: "metric", + supported: false, + reason: "No corroborating detail elsewhere in the resume.", + }, + ], + atsCompatible: true, +}; + +describe("EvalResultSchema", () => { + it("parses a well-formed result", () => { + expect(() => EvalResultSchema.parse(validEvalResult)).not.toThrow(); + }); + + it("rejects a malformed result", () => { + const bad = { ...validEvalResult, score: "high" }; + expect(EvalResultSchema.safeParse(bad).success).toBe(false); + }); + + it("requires rubricVersion and archetypeVersion", () => { + for (const field of ["rubricVersion", "archetypeVersion"]) { + const partial = { ...validEvalResult }; + delete (partial as Record)[field]; + const result = EvalResultSchema.safeParse(partial); + expect(result.success, `${field} should be required`).toBe(false); + } + }); + + it("rejects an out-of-range dimension score", () => { + const bad = { + ...validEvalResult, + dimensions: [{ ...validEvalResult.dimensions[0], score: 7 }], + }; + expect(EvalResultSchema.safeParse(bad).success).toBe(false); + }); +}); + +describe("ResumeSchema", () => { + it("applies array/object defaults from rawText alone", () => { + const resume = ResumeSchema.parse({ rawText: "Jane Doe — Engineer" }); + expect(resume.links).toEqual([]); + expect(resume.skills).toEqual([]); + expect(resume.contact).toEqual({}); + }); +}); + +describe("ArchetypeSchema", () => { + it("requires at least one keyword", () => { + const archetype = { + id: "software-engineer", + name: "Software Engineer", + description: "Builds and ships software", + keywords: [], + evaluationWeights: { + shippedEvidence: 0.3, + quantifiedImpact: 0.2, + toolingVisibility: 0.2, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, + }, + actionVerbs: ["Built"], + antiPatterns: ["familiar with"], + version: "1.0.0", + }; + expect(ArchetypeSchema.safeParse(archetype).success).toBe(false); + }); +}); diff --git a/packages/schemas/src/archetype.ts b/packages/schemas/src/archetype.ts new file mode 100644 index 0000000..b26a011 --- /dev/null +++ b/packages/schemas/src/archetype.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const EvaluationWeightsSchema = z.object({ + shippedEvidence: z.number().min(0).max(1), + quantifiedImpact: z.number().min(0).max(1), + toolingVisibility: z.number().min(0).max(1), + atsCompatibility: z.number().min(0).max(1), + keywordMatch: z.number().min(0).max(1), + publicProof: z.number().min(0).max(1), +}); +export type EvaluationWeights = z.infer; + +// Weights are expected to sum to ~1.0, but that's enforced in the intelligence +// layer so a half-edited archetype still parses here. +export const ArchetypeSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + description: z.string(), + keywords: z.array(z.string()).min(1), + evaluationWeights: EvaluationWeightsSchema, + actionVerbs: z.array(z.string()), + antiPatterns: z.array(z.string()), + version: z.string(), +}); +export type Archetype = z.infer; diff --git a/packages/schemas/src/evaluation.ts b/packages/schemas/src/evaluation.ts new file mode 100644 index 0000000..36b01ed --- /dev/null +++ b/packages/schemas/src/evaluation.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +export const EvaluationDimensionSchema = z.object({ + name: z.string(), + weight: z.number().min(0).max(1), + score: z.number().int().min(0).max(5), + maxScore: z.number().int().positive().default(5), + feedback: z.string(), +}); +export type EvaluationDimension = z.infer; + +export const IssueSchema = z.object({ + element: z.string(), + quote: z.string().optional(), + why: z.string(), + fix: z.string(), + severity: z.enum(["critical", "major", "minor"]), +}); +export type Issue = z.infer; + +export const ClaimSchema = z.object({ + text: z.string(), + category: z.enum([ + "tool", + "technology", + "metric", + "experience", + "education", + "other", + ]), + supported: z.boolean(), + reason: z.string(), +}); +export type Claim = z.infer; + +// Versions are required so old results stay reproducible when the rubric or an +// archetype changes later. +export const EvalResultSchema = z.object({ + rubricVersion: z.string(), + archetypeVersion: z.string(), + archetypeId: z.string(), + archetypeName: z.string(), + score: z.number().min(0).max(5), + dimensions: z.array(EvaluationDimensionSchema), + strengths: z.array(z.string()).default([]), + issues: z.array(IssueSchema).default([]), + claims: z.array(ClaimSchema).default([]), + atsCompatible: z.boolean(), + locale: z.string().optional(), +}); +export type EvalResult = z.infer; diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts new file mode 100644 index 0000000..87b220e --- /dev/null +++ b/packages/schemas/src/index.ts @@ -0,0 +1,37 @@ +// Zod schemas are the source of truth; types are inferred from them. +// Phase 1 ships evaluation types only — tailoring/rewrite is Phase 2. + +export { + EvaluationWeightsSchema, + ArchetypeSchema, + type EvaluationWeights, + type Archetype, +} from "./archetype.js"; + +export { + ResumeSourceSchema, + ResumeLinkSchema, + ResumeContactSchema, + ResumeExperienceSchema, + ResumeEducationSchema, + ResumeSchema, + type ResumeSource, + type ResumeLink, + type ResumeContact, + type ResumeExperience, + type ResumeEducation, + type Resume, +} from "./resume.js"; + +export { JobDescriptionSchema, type JobDescription } from "./job-description.js"; + +export { + EvaluationDimensionSchema, + IssueSchema, + ClaimSchema, + EvalResultSchema, + type EvaluationDimension, + type Issue, + type Claim, + type EvalResult, +} from "./evaluation.js"; diff --git a/packages/schemas/src/job-description.ts b/packages/schemas/src/job-description.ts new file mode 100644 index 0000000..4def655 --- /dev/null +++ b/packages/schemas/src/job-description.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +// Phase 1 uses the JD only as keyword-match context; fit-scoring is Phase 2. +export const JobDescriptionSchema = z.object({ + content: z.string(), + url: z.string().optional(), + company: z.string().optional(), + title: z.string().optional(), + keywords: z.array(z.string()).default([]), +}); +export type JobDescription = z.infer; diff --git a/packages/schemas/src/resume.ts b/packages/schemas/src/resume.ts new file mode 100644 index 0000000..4482faa --- /dev/null +++ b/packages/schemas/src/resume.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +// Raw input as it arrives from a surface, before extraction. +export const ResumeSourceSchema = z.object({ + content: z.string(), + format: z.enum(["pdf", "markdown", "plaintext", "html"]), +}); +export type ResumeSource = z.infer; + +export const ResumeLinkSchema = z.object({ + type: z.enum(["github", "linkedin", "portfolio", "blog", "website", "other"]), + url: z.string(), +}); +export type ResumeLink = z.infer; + +export const ResumeContactSchema = z.object({ + email: z.string().optional(), + phone: z.string().optional(), + location: z.string().optional(), +}); +export type ResumeContact = z.infer; + +export const ResumeExperienceSchema = z.object({ + company: z.string(), + role: z.string(), + startDate: z.string().optional(), + endDate: z.string().optional(), + bullets: z.array(z.string()).default([]), +}); +export type ResumeExperience = z.infer; + +export const ResumeEducationSchema = z.object({ + institution: z.string(), + degree: z.string().optional(), + field: z.string().optional(), + year: z.string().optional(), +}); +export type ResumeEducation = z.infer; + +export const ResumeSchema = z.object({ + name: z.string().optional(), + headline: z.string().optional(), + summary: z.string().optional(), + contact: ResumeContactSchema.default({}), + links: z.array(ResumeLinkSchema).default([]), + experience: z.array(ResumeExperienceSchema).default([]), + education: z.array(ResumeEducationSchema).default([]), + skills: z.array(z.string()).default([]), + // Original document, kept so downstream steps can quote exact source text. + rawText: z.string(), +}); +export type Resume = z.infer; diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json new file mode 100644 index 0000000..d080397 --- /dev/null +++ b/packages/schemas/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["src/__tests__/**"] +} diff --git a/packages/schemas/vitest.config.ts b/packages/schemas/vitest.config.ts new file mode 100644 index 0000000..4ed8031 --- /dev/null +++ b/packages/schemas/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 889aaad..2c05a4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,22 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/schemas: + dependencies: + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^25.6.2 + version: 25.6.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages: '@alloc/quick-lru@5.2.0': @@ -1175,6 +1191,9 @@ packages: engines: {node: '>=8'} hasBin: true + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2053,3 +2072,5 @@ snapshots: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + zod@3.25.76: {} From eee572db4614656cb71cd81e5ba81832043157e3 Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 18:59:56 +0200 Subject: [PATCH 07/39] feat(intelligence): add rubric v1, archetypes, and validators Scoring brain the prompts reference: rubric v1 (six weighted dimensions with 0-5 anchors), three role archetypes (Software Engineer, Product Manager, Data & ML Engineer), keyword-based detectArchetype, and the ATS and claim validator specs. Closes #63 --- packages/intelligence/README.md | 16 +++++ packages/intelligence/package.json | 37 ++++++++++ .../src/__tests__/intelligence.test.ts | 58 ++++++++++++++++ .../src/archetypes/data-ml-engineer.ts | 52 ++++++++++++++ packages/intelligence/src/archetypes/index.ts | 24 +++++++ .../src/archetypes/product-manager.ts | 53 ++++++++++++++ .../src/archetypes/software-engineer.ts | 54 +++++++++++++++ packages/intelligence/src/detect.ts | 27 ++++++++ packages/intelligence/src/index.ts | 18 +++++ packages/intelligence/src/rubric.ts | 69 +++++++++++++++++++ packages/intelligence/src/validators/ats.ts | 43 ++++++++++++ .../intelligence/src/validators/claims.ts | 20 ++++++ packages/intelligence/tsconfig.json | 10 +++ packages/intelligence/vitest.config.ts | 8 +++ pnpm-lock.yaml | 19 +++++ 15 files changed, 508 insertions(+) create mode 100644 packages/intelligence/README.md create mode 100644 packages/intelligence/package.json create mode 100644 packages/intelligence/src/__tests__/intelligence.test.ts create mode 100644 packages/intelligence/src/archetypes/data-ml-engineer.ts create mode 100644 packages/intelligence/src/archetypes/index.ts create mode 100644 packages/intelligence/src/archetypes/product-manager.ts create mode 100644 packages/intelligence/src/archetypes/software-engineer.ts create mode 100644 packages/intelligence/src/detect.ts create mode 100644 packages/intelligence/src/index.ts create mode 100644 packages/intelligence/src/rubric.ts create mode 100644 packages/intelligence/src/validators/ats.ts create mode 100644 packages/intelligence/src/validators/claims.ts create mode 100644 packages/intelligence/tsconfig.json create mode 100644 packages/intelligence/vitest.config.ts diff --git a/packages/intelligence/README.md b/packages/intelligence/README.md new file mode 100644 index 0000000..0998cd6 --- /dev/null +++ b/packages/intelligence/README.md @@ -0,0 +1,16 @@ +# @cv-builder/intelligence + +The scoring brain the prompts and skill reference: the fixed rubric, the role +archetypes, and the validator specs. + +- **Rubric v1** — six dimensions (Shipped Evidence, Quantified Impact, Tech/Tool + Visibility, ATS Compatibility, Keyword Match, Public Proof) with 0–5 anchors, + tagged `RUBRIC_VERSION`. +- **Archetypes** — role config (keywords, dimension weights, action verbs, + anti-patterns). Ships Software Engineer, Product Manager, Data & ML Engineer. + Add one by dropping a file in `src/archetypes/` and registering it. +- **Validators** — `checkAtsCompatibility()` plus the ATS and claim rule sets + the prompts encode. + +`detectArchetype(resumeText)` picks the best-matching archetype, falling back to +Software Engineer when there's no signal. diff --git a/packages/intelligence/package.json b/packages/intelligence/package.json new file mode 100644 index 0000000..f11ac92 --- /dev/null +++ b/packages/intelligence/package.json @@ -0,0 +1,37 @@ +{ + "name": "@cv-builder/intelligence", + "version": "0.1.0", + "description": "Scoring rubric, role archetypes, and validators for CV evaluation", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "lint": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@cv-builder/schemas": "workspace:*", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "keywords": [ + "cv", + "resume", + "rubric", + "archetypes", + "ats" + ], + "license": "MIT" +} diff --git a/packages/intelligence/src/__tests__/intelligence.test.ts b/packages/intelligence/src/__tests__/intelligence.test.ts new file mode 100644 index 0000000..16522ac --- /dev/null +++ b/packages/intelligence/src/__tests__/intelligence.test.ts @@ -0,0 +1,58 @@ +import { ArchetypeSchema } from "@cv-builder/schemas"; +import { describe, expect, it } from "vitest"; +import { + ARCHETYPES, + RUBRIC, + checkAtsCompatibility, + detectArchetype, +} from "../index.js"; + +describe("archetypes", () => { + it("ships at least 3, each valid against the schema", () => { + expect(ARCHETYPES.length).toBeGreaterThanOrEqual(3); + for (const archetype of ARCHETYPES) { + expect(ArchetypeSchema.safeParse(archetype).success, archetype.id).toBe(true); + } + }); + + it("has weights summing to 1.0", () => { + for (const { id, evaluationWeights } of ARCHETYPES) { + const sum = Object.values(evaluationWeights).reduce((a, b) => a + b, 0); + expect(sum, id).toBeCloseTo(1, 5); + } + }); + + it("covers every rubric dimension with a weight", () => { + const keys = RUBRIC.dimensions.map((d) => d.key).sort(); + for (const { id, evaluationWeights } of ARCHETYPES) { + expect(Object.keys(evaluationWeights).sort(), id).toEqual(keys); + } + }); +}); + +describe("detectArchetype", () => { + it("detects a software engineer resume", () => { + const text = "Built React and Node services, scaled Postgres, deployed on Kubernetes."; + expect(detectArchetype(text).id).toBe("software-engineer"); + }); + + it("detects a product manager resume", () => { + const text = "Owned the roadmap, ran A/B testing and user research, grew retention."; + expect(detectArchetype(text).id).toBe("product-manager"); + }); + + it("falls back to the default on no signal", () => { + expect(detectArchetype("hello world").id).toBe("software-engineer"); + }); +}); + +describe("checkAtsCompatibility", () => { + it("flags a table layout", () => { + expect(checkAtsCompatibility("| Skills | Years |\n| React | 5 |").compatible).toBe(false); + }); + + it("passes clean single-column text", () => { + const text = "Experience\nBuilt things at Acme.\nContact: jane@example.com"; + expect(checkAtsCompatibility(text).compatible).toBe(true); + }); +}); diff --git a/packages/intelligence/src/archetypes/data-ml-engineer.ts b/packages/intelligence/src/archetypes/data-ml-engineer.ts new file mode 100644 index 0000000..a0d1b5e --- /dev/null +++ b/packages/intelligence/src/archetypes/data-ml-engineer.ts @@ -0,0 +1,52 @@ +import type { Archetype } from "@cv-builder/schemas"; + +export const dataMlEngineer: Archetype = { + id: "data-ml-engineer", + name: "Data & ML Engineer", + description: "Engineers building data pipelines and machine learning systems", + keywords: [ + "python", + "sql", + "spark", + "airflow", + "dbt", + "pandas", + "pytorch", + "tensorflow", + "scikit-learn", + "feature engineering", + "etl", + "data pipeline", + "warehouse", + "snowflake", + "bigquery", + "mlops", + "model serving", + "experiment tracking", + ], + evaluationWeights: { + shippedEvidence: 0.25, + quantifiedImpact: 0.2, + toolingVisibility: 0.25, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, + }, + actionVerbs: [ + "Built", + "Trained", + "Deployed", + "Productionized", + "Optimized", + "Automated", + "Modeled", + "Scaled", + ], + antiPatterns: [ + "familiar with machine learning", + "exposure to data science", + "worked with big data", + "passionate about AI", + ], + version: "1.0.0", +}; diff --git a/packages/intelligence/src/archetypes/index.ts b/packages/intelligence/src/archetypes/index.ts new file mode 100644 index 0000000..de35c50 --- /dev/null +++ b/packages/intelligence/src/archetypes/index.ts @@ -0,0 +1,24 @@ +import type { Archetype } from "@cv-builder/schemas"; +import { dataMlEngineer } from "./data-ml-engineer.js"; +import { productManager } from "./product-manager.js"; +import { softwareEngineer } from "./software-engineer.js"; + +// Register new archetypes here. Software Engineer is the fallback when nothing +// else clearly matches. +export const ARCHETYPES: Archetype[] = [ + softwareEngineer, + productManager, + dataMlEngineer, +]; + +export const DEFAULT_ARCHETYPE = softwareEngineer; + +export function getArchetype(id: string): Archetype | undefined { + return ARCHETYPES.find((a) => a.id === id); +} + +export function listArchetypes(): Archetype[] { + return ARCHETYPES; +} + +export { softwareEngineer, productManager, dataMlEngineer }; diff --git a/packages/intelligence/src/archetypes/product-manager.ts b/packages/intelligence/src/archetypes/product-manager.ts new file mode 100644 index 0000000..5d14bae --- /dev/null +++ b/packages/intelligence/src/archetypes/product-manager.ts @@ -0,0 +1,53 @@ +import type { Archetype } from "@cv-builder/schemas"; + +export const productManager: Archetype = { + id: "product-manager", + name: "Product Manager", + description: "Product managers owning discovery, roadmap, and delivery", + keywords: [ + "roadmap", + "discovery", + "user research", + "a/b testing", + "experiment", + "okrs", + "kpis", + "retention", + "activation", + "funnel", + "churn", + "stakeholder", + "prioritization", + "agile", + "amplitude", + "mixpanel", + "go-to-market", + "pricing", + ], + evaluationWeights: { + shippedEvidence: 0.25, + quantifiedImpact: 0.25, + toolingVisibility: 0.1, + atsCompatibility: 0.15, + keywordMatch: 0.15, + publicProof: 0.1, + }, + actionVerbs: [ + "Launched", + "Owned", + "Drove", + "Grew", + "Defined", + "Prioritized", + "Increased", + "Reduced", + ], + antiPatterns: [ + "passionate about products", + "leveraged synergies", + "aligned stakeholders", + "wore many hats", + "helped with", + ], + version: "1.0.0", +}; diff --git a/packages/intelligence/src/archetypes/software-engineer.ts b/packages/intelligence/src/archetypes/software-engineer.ts new file mode 100644 index 0000000..2dbc0ab --- /dev/null +++ b/packages/intelligence/src/archetypes/software-engineer.ts @@ -0,0 +1,54 @@ +import type { Archetype } from "@cv-builder/schemas"; + +export const softwareEngineer: Archetype = { + id: "software-engineer", + name: "Software Engineer", + description: "Frontend, backend, and full-stack engineers building software", + keywords: [ + "typescript", + "javascript", + "python", + "go", + "java", + "react", + "node", + "postgres", + "redis", + "kafka", + "docker", + "kubernetes", + "aws", + "ci/cd", + "microservices", + "rest", + "graphql", + "testing", + "system design", + ], + evaluationWeights: { + shippedEvidence: 0.3, + quantifiedImpact: 0.2, + toolingVisibility: 0.2, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, + }, + actionVerbs: [ + "Built", + "Shipped", + "Designed", + "Scaled", + "Optimized", + "Migrated", + "Automated", + "Refactored", + ], + antiPatterns: [ + "familiar with", + "exposure to", + "team player", + "fast learner", + "responsible for various tasks", + ], + version: "1.0.0", +}; diff --git a/packages/intelligence/src/detect.ts b/packages/intelligence/src/detect.ts new file mode 100644 index 0000000..1c19dfa --- /dev/null +++ b/packages/intelligence/src/detect.ts @@ -0,0 +1,27 @@ +import type { Archetype } from "@cv-builder/schemas"; +import { ARCHETYPES, DEFAULT_ARCHETYPE } from "./archetypes/index.js"; + +function countMatches(text: string, keywords: string[]): number { + return keywords.reduce((n, kw) => (text.includes(kw.toLowerCase()) ? n + 1 : n), 0); +} + +// Picks the archetype whose keywords appear most in the resume. Falls back to +// the default when there's no signal, so detection never returns nothing. +export function detectArchetype( + resumeText: string, + archetypes: Archetype[] = ARCHETYPES, +): Archetype { + const text = resumeText.toLowerCase(); + let best = DEFAULT_ARCHETYPE; + let bestScore = 0; + + for (const archetype of archetypes) { + const score = countMatches(text, archetype.keywords); + if (score > bestScore) { + best = archetype; + bestScore = score; + } + } + + return best; +} diff --git a/packages/intelligence/src/index.ts b/packages/intelligence/src/index.ts new file mode 100644 index 0000000..528f328 --- /dev/null +++ b/packages/intelligence/src/index.ts @@ -0,0 +1,18 @@ +// Rubric v1, role archetypes, and the validator specs the prompts encode. + +export { RUBRIC, RUBRIC_VERSION, RUBRIC_DIMENSIONS, type RubricDimension } from "./rubric.js"; + +export { + ARCHETYPES, + DEFAULT_ARCHETYPE, + getArchetype, + listArchetypes, + softwareEngineer, + productManager, + dataMlEngineer, +} from "./archetypes/index.js"; + +export { detectArchetype } from "./detect.js"; + +export { ATS_RULES, checkAtsCompatibility, type AtsCheck } from "./validators/ats.js"; +export { CLAIM_RULES } from "./validators/claims.js"; diff --git a/packages/intelligence/src/rubric.ts b/packages/intelligence/src/rubric.ts new file mode 100644 index 0000000..3494ef4 --- /dev/null +++ b/packages/intelligence/src/rubric.ts @@ -0,0 +1,69 @@ +export const RUBRIC_VERSION = "1.0.0"; + +// `key` matches a field in EvaluationWeights so a dimension and its archetype +// weight always line up. +export interface RubricDimension { + key: + | "shippedEvidence" + | "quantifiedImpact" + | "toolingVisibility" + | "atsCompatibility" + | "keywordMatch" + | "publicProof"; + name: string; + description: string; + // What a 0 vs a 5 looks like, used by the score prompt as anchors. + low: string; + high: string; +} + +export const RUBRIC_DIMENSIONS: RubricDimension[] = [ + { + key: "shippedEvidence", + name: "Shipped Evidence", + description: "Real work that reached production, with named tools and outcomes.", + low: "Responsibilities only; nothing shows it shipped.", + high: "Every role names what was built, for whom, and the result.", + }, + { + key: "quantifiedImpact", + name: "Quantified Impact", + description: "Numbers that frame scope, speed, adoption, or savings.", + low: "No metrics anywhere.", + high: "Most bullets carry a concrete, credible number.", + }, + { + key: "toolingVisibility", + name: "Tech/Tool Visibility", + description: "Named technologies that match the target role.", + low: "Vague or no tech named.", + high: "Relevant stack is explicit and used in context, not just listed.", + }, + { + key: "atsCompatibility", + name: "ATS Compatibility", + description: "Single column, standard headings, parseable formatting.", + low: "Tables, columns, or graphics that parsers mangle.", + high: "Clean single-column text an ATS reads without loss.", + }, + { + key: "keywordMatch", + name: "Keyword Match", + description: + "Overlap with the JD when provided, else the role-family keyword set.", + low: "Misses the terms the role screens for.", + high: "Naturally covers the role's expected keywords.", + }, + { + key: "publicProof", + name: "Public Proof Surface", + description: "GitHub, portfolio, blog, or other verifiable presence.", + low: "No links or external proof.", + high: "Working links that back up the claims.", + }, +]; + +export const RUBRIC = { + version: RUBRIC_VERSION, + dimensions: RUBRIC_DIMENSIONS, +} as const; diff --git a/packages/intelligence/src/validators/ats.ts b/packages/intelligence/src/validators/ats.ts new file mode 100644 index 0000000..86de78a --- /dev/null +++ b/packages/intelligence/src/validators/ats.ts @@ -0,0 +1,43 @@ +// Rules the score prompt and the deterministic check below both follow. +export const ATS_RULES = [ + { id: "single-column", label: "Single-column layout, no side-by-side text" }, + { id: "standard-headings", label: "Standard section headings" }, + { id: "no-tables", label: "No tables or text laid out in columns" }, + { id: "no-graphics", label: "No images, icons, or text inside graphics" }, + { id: "contact-parseable", label: "Email and basic contact info in plain text" }, +] as const; + +const STANDARD_HEADINGS = [ + "experience", + "work experience", + "employment", + "education", + "skills", + "projects", + "summary", + "certifications", +]; + +export interface AtsCheck { + compatible: boolean; + findings: string[]; +} + +// Catches the obvious, deterministic ATS problems from raw text. Subtler layout +// issues are left to the score prompt. +export function checkAtsCompatibility(resumeText: string): AtsCheck { + const findings: string[] = []; + const text = resumeText.toLowerCase(); + + if (/\|.*\|/.test(resumeText) || /\t.*\t/.test(resumeText)) { + findings.push("Looks like a table or columns — flatten to single-column text."); + } + if (!/[\w.+-]+@[\w-]+\.[\w.-]+/.test(resumeText)) { + findings.push("No plain-text email found."); + } + if (!STANDARD_HEADINGS.some((h) => text.includes(h))) { + findings.push("No standard section headings detected."); + } + + return { compatible: findings.length === 0, findings }; +} diff --git a/packages/intelligence/src/validators/claims.ts b/packages/intelligence/src/validators/claims.ts new file mode 100644 index 0000000..aadb14b --- /dev/null +++ b/packages/intelligence/src/validators/claims.ts @@ -0,0 +1,20 @@ +// What the validate-claims prompt treats as an unsupported claim. The check is +// LLM-driven; this is the shared spec it encodes. +export const CLAIM_RULES = [ + { + id: "metric-without-context", + label: "A number with no scope, baseline, or how it was achieved", + }, + { + id: "tool-never-used", + label: "A tool or technology listed but never shown in any bullet", + }, + { + id: "seniority-without-scope", + label: "A leadership or scale claim with no team size, budget, or reach", + }, + { + id: "vague-superlative", + label: "Superlatives ('world-class', 'expert') with nothing backing them", + }, +] as const; diff --git a/packages/intelligence/tsconfig.json b/packages/intelligence/tsconfig.json new file mode 100644 index 0000000..d080397 --- /dev/null +++ b/packages/intelligence/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["src/__tests__/**"] +} diff --git a/packages/intelligence/vitest.config.ts b/packages/intelligence/vitest.config.ts new file mode 100644 index 0000000..4ed8031 --- /dev/null +++ b/packages/intelligence/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c05a4e..625b637 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,25 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/intelligence: + dependencies: + '@cv-builder/schemas': + specifier: workspace:* + version: link:../schemas + zod: + specifier: ^3.24.0 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^25.6.2 + version: 25.6.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/schemas: dependencies: zod: From 7b098450bb3fda41c3b1b21ae049119130b143eb Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 19:13:24 +0200 Subject: [PATCH 08/39] feat(prompts): add extract, score, and validate-claims pack The three Phase 1 prompts a power user's agent runs. Markdown templates are the source of truth; renderers inject the live rubric, archetype weights, and claim rules so each assembled prompt is self-contained and stays in sync with the intelligence package. Every prompt asks for JSON matching its schema. Closes #64 --- packages/prompts/README.md | 21 ++++++ packages/prompts/package.json | 40 ++++++++++++ packages/prompts/prompts/extract.md | 41 ++++++++++++ packages/prompts/prompts/score.md | 64 +++++++++++++++++++ packages/prompts/prompts/validate-claims.md | 32 ++++++++++ .../prompts/src/__tests__/prompts.test.ts | 44 +++++++++++++ packages/prompts/src/index.ts | 58 +++++++++++++++++ packages/prompts/tsconfig.json | 10 +++ packages/prompts/vitest.config.ts | 8 +++ pnpm-lock.yaml | 19 ++++++ 10 files changed, 337 insertions(+) create mode 100644 packages/prompts/README.md create mode 100644 packages/prompts/package.json create mode 100644 packages/prompts/prompts/extract.md create mode 100644 packages/prompts/prompts/score.md create mode 100644 packages/prompts/prompts/validate-claims.md create mode 100644 packages/prompts/src/__tests__/prompts.test.ts create mode 100644 packages/prompts/src/index.ts create mode 100644 packages/prompts/tsconfig.json create mode 100644 packages/prompts/vitest.config.ts diff --git a/packages/prompts/README.md b/packages/prompts/README.md new file mode 100644 index 0000000..2e5b34c --- /dev/null +++ b/packages/prompts/README.md @@ -0,0 +1,21 @@ +# @cv-builder/prompts + +The three Phase 1 prompts a power user's agent runs, in `prompts/`: + +- **extract** — raw resume text → structured `Resume` +- **score** — `Resume` (+ optional JD keywords) + rubric + archetype → `EvalResult` +- **validate-claims** — `Resume` → `Claim[]`, flagging unsupported claims + +The `.md` files are the source of truth (readable, what the skill surfaces). The +renderers inject the live rubric and archetype data so the assembled prompt is +self-contained and stays in sync with `@cv-builder/intelligence`. + +```ts +import { renderScorePrompt } from "@cv-builder/prompts"; +import { softwareEngineer } from "@cv-builder/intelligence"; + +const prompt = renderScorePrompt({ archetype: softwareEngineer, jdKeywords }); +``` + +Each prompt asks for JSON matching its schema in `@cv-builder/schemas`; callers +validate that output before using it. diff --git a/packages/prompts/package.json b/packages/prompts/package.json new file mode 100644 index 0000000..efed7bb --- /dev/null +++ b/packages/prompts/package.json @@ -0,0 +1,40 @@ +{ + "name": "@cv-builder/prompts", + "version": "0.1.0", + "description": "Prompt pack — extract, score, and validate-claims templates", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "prompts" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "lint": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@cv-builder/intelligence": "workspace:*", + "@cv-builder/schemas": "workspace:*" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "keywords": [ + "cv", + "resume", + "prompts", + "llm" + ], + "license": "MIT" +} diff --git a/packages/prompts/prompts/extract.md b/packages/prompts/prompts/extract.md new file mode 100644 index 0000000..1284997 --- /dev/null +++ b/packages/prompts/prompts/extract.md @@ -0,0 +1,41 @@ +# Extract Resume + +You are given the raw text of a resume. Convert it into a structured object. + +## Rules + +- Use only what's in the text. Do not invent employers, dates, metrics, or links. +- Leave a field empty (or its array `[]`) when the text doesn't contain it. +- Put the full, unmodified source text in `rawText` so later steps can quote it. +- Normalize obvious formatting noise (stray bullets, page numbers) out of fields, + but never change the wording of achievements. + +## Output + +Return a single JSON object matching the `Resume` schema: + +```json +{ + "name": "string | omit", + "headline": "string | omit", + "summary": "string | omit", + "contact": { "email": "string?", "phone": "string?", "location": "string?" }, + "links": [{ "type": "github|linkedin|portfolio|blog|website|other", "url": "string" }], + "experience": [ + { + "company": "string", + "role": "string", + "startDate": "string?", + "endDate": "string?", + "bullets": ["string"] + } + ], + "education": [ + { "institution": "string", "degree": "string?", "field": "string?", "year": "string?" } + ], + "skills": ["string"], + "rawText": "string" +} +``` + +Output only the JSON. No prose, no code fence. diff --git a/packages/prompts/prompts/score.md b/packages/prompts/prompts/score.md new file mode 100644 index 0000000..e324f9f --- /dev/null +++ b/packages/prompts/prompts/score.md @@ -0,0 +1,64 @@ +# Score Resume + +Score a structured resume against the rubric for a target role. Be specific and +honest — generic praise is useless to the candidate. + +## Target role + +- Archetype: {{ARCHETYPE_NAME}} (`{{ARCHETYPE_ID}}`, v{{ARCHETYPE_VERSION}}) +- Rubric version: {{RUBRIC_VERSION}} + +### Dimension weights + +{{WEIGHTS}} + +## Rubric + +Score each dimension 0–5 using these anchors: + +{{RUBRIC}} + +## Keyword context + +{{JD_KEYWORDS}} + +## Instructions + +1. Score every dimension 0–5 with one sentence of grounded feedback. +2. Overall `score` = sum(dimension score × weight), kept on the 0–5 scale. +3. List concrete `strengths`. +4. Surface **at least 3** `issues`. Each must quote the exact resume text it + refers to (`quote`) and give a concrete `fix`. +5. Set `atsCompatible` from the ATS dimension. +6. Echo back `rubricVersion`, `archetypeVersion`, `archetypeId`, `archetypeName`. + +## Output + +Return a single JSON object matching the `EvalResult` schema: + +```json +{ + "rubricVersion": "{{RUBRIC_VERSION}}", + "archetypeVersion": "{{ARCHETYPE_VERSION}}", + "archetypeId": "{{ARCHETYPE_ID}}", + "archetypeName": "{{ARCHETYPE_NAME}}", + "score": 0, + "dimensions": [ + { "name": "string", "weight": 0, "score": 0, "maxScore": 5, "feedback": "string" } + ], + "strengths": ["string"], + "issues": [ + { + "element": "string", + "quote": "string", + "why": "string", + "fix": "string", + "severity": "critical|major|minor" + } + ], + "claims": [], + "atsCompatible": true +} +``` + +Output only the JSON. No prose, no code fence. diff --git a/packages/prompts/prompts/validate-claims.md b/packages/prompts/prompts/validate-claims.md new file mode 100644 index 0000000..979254b --- /dev/null +++ b/packages/prompts/prompts/validate-claims.md @@ -0,0 +1,32 @@ +# Validate Claims + +Read a structured resume and check whether its claims are supported by the rest +of the document. Flag the ones that aren't — this is how the candidate learns +what a recruiter will quietly distrust. + +## A claim is unsupported when + +{{CLAIM_RULES}} + +## Instructions + +- Pull out the concrete claims (metrics, named tools/tech, scope/seniority). +- Mark each `supported: true` only when something elsewhere backs it up. +- For `supported: false`, say in `reason` what's missing, not just that it's weak. + +## Output + +Return a JSON array of `Claim` objects: + +```json +[ + { + "text": "string", + "category": "tool|technology|metric|experience|education|other", + "supported": true, + "reason": "string" + } +] +``` + +Output only the JSON array. No prose, no code fence. diff --git a/packages/prompts/src/__tests__/prompts.test.ts b/packages/prompts/src/__tests__/prompts.test.ts new file mode 100644 index 0000000..6c12910 --- /dev/null +++ b/packages/prompts/src/__tests__/prompts.test.ts @@ -0,0 +1,44 @@ +import { RUBRIC_VERSION, softwareEngineer } from "@cv-builder/intelligence"; +import { describe, expect, it } from "vitest"; +import { + PROMPT_NAMES, + renderExtractPrompt, + renderScorePrompt, + renderValidateClaimsPrompt, +} from "../index.js"; + +describe("prompt pack", () => { + it("ships the three Phase 1 prompts", () => { + expect(PROMPT_NAMES).toEqual(["extract", "score", "validate-claims"]); + }); + + it("each prompt names its output schema", () => { + expect(renderExtractPrompt()).toContain("Resume"); + expect(renderScorePrompt({ archetype: softwareEngineer })).toContain("EvalResult"); + expect(renderValidateClaimsPrompt()).toContain("Claim"); + }); + + it("score prompt embeds the current rubric version", () => { + const prompt = renderScorePrompt({ archetype: softwareEngineer }); + expect(prompt).toContain(RUBRIC_VERSION); + expect(prompt).not.toContain("{{"); + }); + + it("score prompt injects archetype weights and JD keywords", () => { + const withJd = renderScorePrompt({ + archetype: softwareEngineer, + jdKeywords: ["kubernetes", "graphql"], + }); + expect(withJd).toContain("shippedEvidence:"); + expect(withJd).toContain("kubernetes, graphql"); + + const withoutJd = renderScorePrompt({ archetype: softwareEngineer }); + expect(withoutJd).toContain("No JD provided"); + }); + + it("validate-claims prompt injects the claim rules", () => { + const prompt = renderValidateClaimsPrompt(); + expect(prompt).not.toContain("{{CLAIM_RULES}}"); + expect(prompt).toContain("tool or technology listed but never shown"); + }); +}); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts new file mode 100644 index 0000000..21a609c --- /dev/null +++ b/packages/prompts/src/index.ts @@ -0,0 +1,58 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { + CLAIM_RULES, + RUBRIC, + RUBRIC_VERSION, +} from "@cv-builder/intelligence"; +import type { Archetype } from "@cv-builder/schemas"; + +export const PROMPT_NAMES = ["extract", "score", "validate-claims"] as const; +export type PromptName = (typeof PROMPT_NAMES)[number]; + +// .md lives next to dist/ and src/ alike, so this resolves in both. +function loadTemplate(name: PromptName): string { + const path = fileURLToPath(new URL(`../prompts/${name}.md`, import.meta.url)); + return readFileSync(path, "utf-8"); +} + +export function renderExtractPrompt(): string { + return loadTemplate("extract"); +} + +export function renderValidateClaimsPrompt(): string { + const rules = CLAIM_RULES.map((r) => `- ${r.label}`).join("\n"); + return loadTemplate("validate-claims").replace("{{CLAIM_RULES}}", rules); +} + +export interface ScorePromptOptions { + archetype: Archetype; + jdKeywords?: string[]; +} + +export function renderScorePrompt({ + archetype, + jdKeywords, +}: ScorePromptOptions): string { + const weights = Object.entries(archetype.evaluationWeights) + .map(([key, value]) => `- ${key}: ${value}`) + .join("\n"); + + const rubric = RUBRIC.dimensions + .map((d) => `- **${d.name}** — ${d.description}\n - 0: ${d.low}\n - 5: ${d.high}`) + .join("\n"); + + const keywords = + jdKeywords && jdKeywords.length > 0 + ? `Match against these JD keywords: ${jdKeywords.join(", ")}.` + : `No JD provided — match against the role's own keyword set: ${archetype.keywords.join(", ")}.`; + + return loadTemplate("score") + .replaceAll("{{ARCHETYPE_NAME}}", archetype.name) + .replaceAll("{{ARCHETYPE_ID}}", archetype.id) + .replaceAll("{{ARCHETYPE_VERSION}}", archetype.version) + .replaceAll("{{RUBRIC_VERSION}}", RUBRIC_VERSION) + .replace("{{WEIGHTS}}", weights) + .replace("{{RUBRIC}}", rubric) + .replace("{{JD_KEYWORDS}}", keywords); +} diff --git a/packages/prompts/tsconfig.json b/packages/prompts/tsconfig.json new file mode 100644 index 0000000..d080397 --- /dev/null +++ b/packages/prompts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["src/__tests__/**"] +} diff --git a/packages/prompts/vitest.config.ts b/packages/prompts/vitest.config.ts new file mode 100644 index 0000000..4ed8031 --- /dev/null +++ b/packages/prompts/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 625b637..1e6bd1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,25 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/prompts: + dependencies: + '@cv-builder/intelligence': + specifier: workspace:* + version: link:../intelligence + '@cv-builder/schemas': + specifier: workspace:* + version: link:../schemas + devDependencies: + '@types/node': + specifier: ^25.6.2 + version: 25.6.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/schemas: dependencies: zod: From 131083ae7f87270b609f3e77dbc3687368596407 Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 19:58:31 +0200 Subject: [PATCH 09/39] =?UTF-8?q?feat(cli):=20add=20power-user=20pack=20?= =?UTF-8?q?=E2=80=94=20Claude=20Code=20skill,=20commands,=20welcome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clone the repo, open Claude Code, run /evaluate-cv: the cv-evaluation skill runs extract -> detect -> score -> validate-claims locally, reading the prompts and rubric straight from the repo (no build). A SessionStart hook greets the user with the available commands on open. Skill subfiles point at the package sources to avoid drift. Closes #65 --- .claude/commands/evaluate-cv.md | 16 +++++ .claude/commands/setup-profile.md | 15 +++++ .claude/settings.json | 17 ++++++ .claude/skills/cv-evaluation/SKILL.md | 39 ++++++++++++ .claude/skills/cv-evaluation/archetypes.md | 10 ++++ .../skills/cv-evaluation/claim-validation.md | 9 +++ .claude/skills/cv-evaluation/rubric.md | 8 +++ .claude/skills/cv-evaluation/scoring.md | 12 ++++ .claude/welcome.sh | 19 ++++++ CLAUDE.md | 25 ++++++++ apps/cli/README.md | 60 +++++++++++++++++++ apps/cli/package.json | 6 ++ pnpm-lock.yaml | 2 + 13 files changed, 238 insertions(+) create mode 100644 .claude/commands/evaluate-cv.md create mode 100644 .claude/commands/setup-profile.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/cv-evaluation/SKILL.md create mode 100644 .claude/skills/cv-evaluation/archetypes.md create mode 100644 .claude/skills/cv-evaluation/claim-validation.md create mode 100644 .claude/skills/cv-evaluation/rubric.md create mode 100644 .claude/skills/cv-evaluation/scoring.md create mode 100755 .claude/welcome.sh create mode 100644 CLAUDE.md create mode 100644 apps/cli/README.md create mode 100644 apps/cli/package.json diff --git a/.claude/commands/evaluate-cv.md b/.claude/commands/evaluate-cv.md new file mode 100644 index 0000000..6e14ee7 --- /dev/null +++ b/.claude/commands/evaluate-cv.md @@ -0,0 +1,16 @@ +--- +description: Score a resume against the CV Builder rubric, fully locally +argument-hint: [--jd ] +--- + +Evaluate the resume using the `cv-evaluation` skill. + +Arguments: $ARGUMENTS + +- The first argument is the path to the resume (PDF, markdown, or text). +- An optional `--jd ` adds a job description as keyword context only. + +Read the file(s), run the extract → detect → score → validate-claims pipeline, +validate the result against `EvalResultSchema`, then present the overall score, +the per-dimension breakdown, the top issues with fixes, and any unsupported +claims. Nothing leaves this machine. diff --git a/.claude/commands/setup-profile.md b/.claude/commands/setup-profile.md new file mode 100644 index 0000000..2eca1cd --- /dev/null +++ b/.claude/commands/setup-profile.md @@ -0,0 +1,15 @@ +--- +description: Gather a resume to evaluate, three ways +argument-hint: [path-to-file] +--- + +Help the user get a resume ready for `/evaluate-cv`. Pick the path that fits: + +1. **File** — if `$ARGUMENTS` names a file, read it and confirm what you found. +2. **Paste** — if they pasted resume text, use it directly. +3. **Interview** — if they have nothing prepared, ask a short, focused set of + questions (roles, dates, what they built, measurable outcomes, links) and + assemble a plain-text resume from their answers. + +Once you have the resume text, save it to a file the user chooses and tell them +to run `/evaluate-cv `. Don't invent details they didn't give you. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8d1b317 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": ["Read", "Glob", "Grep"] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/welcome.sh\"" + } + ] + } + ] + } +} diff --git a/.claude/skills/cv-evaluation/SKILL.md b/.claude/skills/cv-evaluation/SKILL.md new file mode 100644 index 0000000..53fa570 --- /dev/null +++ b/.claude/skills/cv-evaluation/SKILL.md @@ -0,0 +1,39 @@ +--- +name: cv-evaluation +description: Evaluate a resume against the CV Builder rubric and return a scored, schema-valid EvalResult. Use when the user wants their CV or resume scored, critiqued, or checked for ATS issues and unsupported claims. +--- + +# CV Evaluation + +Run a resume through the same three-step evaluation the hosted product uses, +entirely locally. Everything you need lives in this repo — read the source, don't +guess. + +## Inputs + +- A resume file (PDF, markdown, or plain text). For PDF, read the text content. +- Optionally a job description, used only as keyword context (Phase 1 does not + fit-score against a JD). + +## Pipeline + +1. **Extract.** Apply `packages/prompts/prompts/extract.md` to the raw resume + text to produce a structured `Resume`. +2. **Detect archetype.** Match the resume against the role archetypes in + `packages/intelligence/src/archetypes/`. Pick the best fit; default to + Software Engineer when there's no clear signal. See [archetypes](./archetypes.md). +3. **Score.** Apply `packages/prompts/prompts/score.md` with the detected + archetype's weights and the rubric. See [rubric](./rubric.md) and + [scoring](./scoring.md). Surface **at least 3** issues, each quoting the exact + resume text and giving a concrete fix. +4. **Validate claims.** Apply `packages/prompts/prompts/validate-claims.md` to + flag unsupported claims. See [claim-validation](./claim-validation.md). + +## Output + +Produce one `EvalResult` (schema: `packages/schemas/src/evaluation.ts`). It must +carry `rubricVersion` and `archetypeVersion`. Validate the object against +`EvalResultSchema` before presenting it — never show an unvalidated result. + +Then summarize for the user: overall score, the per-dimension breakdown, the top +issues with their fixes, and any unsupported claims. diff --git a/.claude/skills/cv-evaluation/archetypes.md b/.claude/skills/cv-evaluation/archetypes.md new file mode 100644 index 0000000..a5646cd --- /dev/null +++ b/.claude/skills/cv-evaluation/archetypes.md @@ -0,0 +1,10 @@ +# Archetypes + +Each role archetype (keywords, dimension weights, action verbs, anti-patterns) +is one file in `packages/intelligence/src/archetypes/`. Read the relevant file — +the weights there drive the overall score, so use them exactly. + +To detect the archetype: count how many of each archetype's keywords appear in +the resume and pick the highest. The same heuristic is implemented in +`packages/intelligence/src/detect.ts` (`detectArchetype`). When nothing clearly +matches, use Software Engineer. diff --git a/.claude/skills/cv-evaluation/claim-validation.md b/.claude/skills/cv-evaluation/claim-validation.md new file mode 100644 index 0000000..aa83af1 --- /dev/null +++ b/.claude/skills/cv-evaluation/claim-validation.md @@ -0,0 +1,9 @@ +# Claim Validation + +The prompt and the rules for what counts as an unsupported claim are in +`packages/prompts/prompts/validate-claims.md` (rules sourced from +`packages/intelligence/src/validators/claims.ts`). + +Goal: catch what a recruiter would quietly distrust — a metric with no scope, a +tool listed but never used in a bullet, a seniority claim with no team size or +reach. For each flagged claim, say what's missing, not just that it's weak. diff --git a/.claude/skills/cv-evaluation/rubric.md b/.claude/skills/cv-evaluation/rubric.md new file mode 100644 index 0000000..487b690 --- /dev/null +++ b/.claude/skills/cv-evaluation/rubric.md @@ -0,0 +1,8 @@ +# Rubric + +The six dimensions, their 0–5 anchors, and the current `RUBRIC_VERSION` are +defined in `packages/intelligence/src/rubric.ts`. Read that file — it is the +source of truth. Don't reproduce the anchors from memory. + +Score each dimension on its own 0–5 scale, then weight by the archetype (see +[archetypes](./archetypes.md)) to get the overall 0–5 score. diff --git a/.claude/skills/cv-evaluation/scoring.md b/.claude/skills/cv-evaluation/scoring.md new file mode 100644 index 0000000..cca6563 --- /dev/null +++ b/.claude/skills/cv-evaluation/scoring.md @@ -0,0 +1,12 @@ +# Scoring + +The scoring instructions and exact output shape are in +`packages/prompts/prompts/score.md`. Fill its `{{...}}` placeholders from the +detected archetype's weights and the rubric. + +Hold the line on quality: + +- Every issue quotes the exact resume text it's about — no paraphrasing. +- Every issue has a concrete fix, not "consider improving this". +- Overall score = sum(dimension score × weight), on the 0–5 scale. +- Be honest. A weak resume should score low; inflating it doesn't help anyone. diff --git a/.claude/welcome.sh b/.claude/welcome.sh new file mode 100755 index 0000000..75a590d --- /dev/null +++ b/.claude/welcome.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Emits the CV Builder menu as a SessionStart hook systemMessage, shown to the +# user when they open Claude Code at the repo root. + +# No jq → skip the banner rather than break the session start. +command -v jq >/dev/null 2>&1 || exit 0 + +read -r -d '' MENU <<'EOF' +CV Builder — evaluate your resume locally and privately. Nothing leaves your machine. + + /evaluate-cv [--jd ] Score a resume against the rubric + /setup-profile Assemble a resume if you don't have a file yet + +No build or install needed to evaluate. + +What would you like to do? +EOF + +jq -n --arg msg "$MENU" '{systemMessage: $msg}' diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eae9328 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# CV Builder + +Open source, privacy-first resume evaluator. This repo doubles as a **power-user +pack**: clone it, open Claude Code here, and evaluate your resume locally with +your own agent — nothing leaves your machine. + +## Power-user commands + +- `/evaluate-cv [--jd ]` — score a resume against + the rubric and surface issues + unsupported claims. +- `/setup-profile` — assemble a resume if you don't have a file ready. + +## No build needed for evaluation + +The `cv-evaluation` skill (`.claude/skills/cv-evaluation/`) reads the prompts in +`packages/prompts/prompts/` and the rubric/archetypes in +`packages/intelligence/src/` straight from the repo, and returns an `EvalResult` +validated against `packages/schemas`. `pnpm install` is only needed to run the +package tests or the web app — not for the power-user flow. + +## Repo shape + +pnpm + turbo monorepo. `packages/schemas` (contract) → `packages/intelligence` +(rubric + archetypes) → `packages/prompts` (the prompt pack) → the skill ties +them together. diff --git a/apps/cli/README.md b/apps/cli/README.md new file mode 100644 index 0000000..d89fa98 --- /dev/null +++ b/apps/cli/README.md @@ -0,0 +1,60 @@ +# Power User — evaluate your resume locally + +Run the full CV evaluation on your own machine with your own +[Claude Code](https://claude.com/claude-code). Your resume never leaves your +computer — no server, no account, no API keys in this repo. + +## Quickstart + +```bash +git clone https://github.com/TechImmigrants/cv-builder.git +cd cv-builder +claude # open Claude Code at the repo root +``` + +No build or install is needed to evaluate — the skill reads the prompts and +rubric straight from the repo. (`pnpm install` is only for running the package +tests or the web app.) + +Then, inside Claude Code: + +``` +/evaluate-cv ./my-resume.pdf +``` + +Optionally add a job description as keyword context: + +``` +/evaluate-cv ./my-resume.pdf --jd ./job.md +``` + +No resume handy? Run `/setup-profile` and it'll help you assemble one. + +## What you get + +An honest, role-adaptive score (0–5) with: + +- a per-dimension breakdown across the six rubric dimensions, +- at least three specific issues, each quoting the exact line and giving a fix, +- any unsupported claims a recruiter would distrust, +- an ATS-compatibility read. + +## How it works + +The `/evaluate-cv` command drives the **cv-evaluation** skill +(`.claude/skills/cv-evaluation/`), which runs four steps against this repo: + +| Step | Source | +|---|---| +| Extract structured resume | `packages/prompts/prompts/extract.md` | +| Detect role archetype | `packages/intelligence/src/archetypes/` | +| Score against the rubric | `packages/prompts/prompts/score.md` + `packages/intelligence/src/rubric.ts` | +| Flag unsupported claims | `packages/prompts/prompts/validate-claims.md` | + +The result is validated against `EvalResultSchema` +(`packages/schemas`) before you see it. + +## Privacy + +Everything runs through your local Claude Code. This pack makes no network calls +of its own and stores nothing. diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 0000000..4ee4ad1 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,6 @@ +{ + "name": "@cv-builder/power-user", + "version": "0.1.0", + "description": "Power-user pack: evaluate your resume locally with your own Claude Code", + "private": true +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e6bd1b..8885d7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,8 @@ importers: specifier: ^5.7.0 version: 5.9.3 + apps/cli: {} + apps/web-ui: dependencies: '@cv-builder/core': From c44edf18684b4677517e23706ca392f16c7ee9d2 Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 20:11:24 +0200 Subject: [PATCH 10/39] test(eval): add golden fixtures and pnpm eval harness Five fixtures across the three archetypes (including an ATS-hostile resume) guard the deterministic layer: for each, the detected archetype and ATS read must match expectations and the resume must parse against the schema. No LLM involved. Wired as a pnpm eval task and a CI step. Closes #66 --- .github/workflows/ci.yml | 2 + package.json | 1 + packages/eval/README.md | 17 +++++ .../fixtures/data-ml-strong/expected.json | 5 ++ .../eval/fixtures/data-ml-strong/resume.md | 15 ++++ .../eval/fixtures/pm-strong/expected.json | 5 ++ packages/eval/fixtures/pm-strong/resume.md | 15 ++++ .../eval/fixtures/swe-strong/expected.json | 5 ++ packages/eval/fixtures/swe-strong/resume.md | 15 ++++ .../fixtures/swe-weak-table/expected.json | 5 ++ .../eval/fixtures/swe-weak-table/resume.md | 9 +++ .../eval/fixtures/swe-with-jd/expected.json | 5 ++ packages/eval/fixtures/swe-with-jd/jd.md | 4 ++ packages/eval/fixtures/swe-with-jd/resume.md | 15 ++++ packages/eval/package.json | 22 ++++++ packages/eval/src/__tests__/fixtures.test.ts | 69 +++++++++++++++++++ packages/eval/tsconfig.json | 9 +++ packages/eval/vitest.config.ts | 8 +++ pnpm-lock.yaml | 19 +++++ turbo.json | 3 + 20 files changed, 248 insertions(+) create mode 100644 packages/eval/README.md create mode 100644 packages/eval/fixtures/data-ml-strong/expected.json create mode 100644 packages/eval/fixtures/data-ml-strong/resume.md create mode 100644 packages/eval/fixtures/pm-strong/expected.json create mode 100644 packages/eval/fixtures/pm-strong/resume.md create mode 100644 packages/eval/fixtures/swe-strong/expected.json create mode 100644 packages/eval/fixtures/swe-strong/resume.md create mode 100644 packages/eval/fixtures/swe-weak-table/expected.json create mode 100644 packages/eval/fixtures/swe-weak-table/resume.md create mode 100644 packages/eval/fixtures/swe-with-jd/expected.json create mode 100644 packages/eval/fixtures/swe-with-jd/jd.md create mode 100644 packages/eval/fixtures/swe-with-jd/resume.md create mode 100644 packages/eval/package.json create mode 100644 packages/eval/src/__tests__/fixtures.test.ts create mode 100644 packages/eval/tsconfig.json create mode 100644 packages/eval/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b80541..5935c96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,4 +29,6 @@ jobs: - run: pnpm test + - run: pnpm eval + - run: pnpm build diff --git a/package.json b/package.json index bd0c486..d8dbb9a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "turbo dev", "lint": "turbo lint", "test": "turbo test", + "eval": "turbo eval", "format": "prettier --write \"**/*.{ts,tsx,md,json}\"", "format:check": "prettier --check \"**/*.{ts,tsx,md,json}\"" }, diff --git a/packages/eval/README.md b/packages/eval/README.md new file mode 100644 index 0000000..109036b --- /dev/null +++ b/packages/eval/README.md @@ -0,0 +1,17 @@ +# @cv-builder/eval + +Golden fixtures that guard the deterministic intelligence layer. Run with +`pnpm eval` (and in CI). + +Each fixture is a folder under `fixtures/`: + +- `resume.md` — the input resume +- `jd.md` — optional job description (keyword context) +- `expected.json` — `{ archetype, atsCompatible }` + +The harness asserts, without any LLM, that for every fixture the detected +archetype and ATS read match what's expected, and that the resume parses against +the schema. A change that breaks archetype detection or the ATS check fails here. + +Scoring *quality* (the LLM-produced `EvalResult`) is out of scope — that needs a +provider adapter, which is a separate surface. diff --git a/packages/eval/fixtures/data-ml-strong/expected.json b/packages/eval/fixtures/data-ml-strong/expected.json new file mode 100644 index 0000000..b3ab9e0 --- /dev/null +++ b/packages/eval/fixtures/data-ml-strong/expected.json @@ -0,0 +1,5 @@ +{ + "description": "Strong data & ML engineer resume", + "archetype": "data-ml-engineer", + "atsCompatible": true +} diff --git a/packages/eval/fixtures/data-ml-strong/resume.md b/packages/eval/fixtures/data-ml-strong/resume.md new file mode 100644 index 0000000..c8e873b --- /dev/null +++ b/packages/eval/fixtures/data-ml-strong/resume.md @@ -0,0 +1,15 @@ +# Dev Patel + +Data & ML Engineer — dev@example.com — github.com/devpatel + +## Experience + +### DataCo — Machine Learning Engineer (2021–2024) + +- Built data pipelines with Spark, Airflow, and dbt; productionized batch and streaming ETL. +- Trained and deployed PyTorch models with model serving and experiment tracking. +- Optimized feature engineering on Snowflake and BigQuery, cutting training time 3x. + +## Skills + +Python, SQL, Spark, Airflow, dbt, PyTorch, scikit-learn, MLOps, Snowflake, BigQuery diff --git a/packages/eval/fixtures/pm-strong/expected.json b/packages/eval/fixtures/pm-strong/expected.json new file mode 100644 index 0000000..88c4be4 --- /dev/null +++ b/packages/eval/fixtures/pm-strong/expected.json @@ -0,0 +1,5 @@ +{ + "description": "Strong product manager resume", + "archetype": "product-manager", + "atsCompatible": true +} diff --git a/packages/eval/fixtures/pm-strong/resume.md b/packages/eval/fixtures/pm-strong/resume.md new file mode 100644 index 0000000..61b843a --- /dev/null +++ b/packages/eval/fixtures/pm-strong/resume.md @@ -0,0 +1,15 @@ +# Sara Lee + +Product Manager — sara@example.com — linkedin.com/in/saralee + +## Experience + +### FinCo — Senior Product Manager (2020–2024) + +- Owned the roadmap and discovery for a B2B SaaS; grew retention 25%. +- Ran A/B testing and user research; reduced churn and lifted activation. +- Defined OKRs and KPIs; drove the onboarding funnel with the growth team. + +## Skills + +Roadmap, Discovery, Prioritization, Amplitude, Mixpanel, Stakeholder management, Agile diff --git a/packages/eval/fixtures/swe-strong/expected.json b/packages/eval/fixtures/swe-strong/expected.json new file mode 100644 index 0000000..540d774 --- /dev/null +++ b/packages/eval/fixtures/swe-strong/expected.json @@ -0,0 +1,5 @@ +{ + "description": "Strong, ATS-clean software engineer resume", + "archetype": "software-engineer", + "atsCompatible": true +} diff --git a/packages/eval/fixtures/swe-strong/resume.md b/packages/eval/fixtures/swe-strong/resume.md new file mode 100644 index 0000000..26d1a9f --- /dev/null +++ b/packages/eval/fixtures/swe-strong/resume.md @@ -0,0 +1,15 @@ +# Jane Doe + +Software Engineer — jane@example.com — github.com/janedoe + +## Experience + +### Acme — Senior Software Engineer (2021–2024) + +- Built a React and Node.js checkout serving 2M users; cut p95 latency 40%. +- Migrated Postgres to a sharded cluster, scaling writes 5x. +- Automated CI/CD with Docker and Kubernetes, halving deploy time. + +## Skills + +TypeScript, React, Node, Postgres, Redis, Kafka, Docker, Kubernetes, AWS, GraphQL diff --git a/packages/eval/fixtures/swe-weak-table/expected.json b/packages/eval/fixtures/swe-weak-table/expected.json new file mode 100644 index 0000000..e901024 --- /dev/null +++ b/packages/eval/fixtures/swe-weak-table/expected.json @@ -0,0 +1,5 @@ +{ + "description": "Software engineer skills but a table layout and no email — ATS-hostile", + "archetype": "software-engineer", + "atsCompatible": false +} diff --git a/packages/eval/fixtures/swe-weak-table/resume.md b/packages/eval/fixtures/swe-weak-table/resume.md new file mode 100644 index 0000000..7c396f2 --- /dev/null +++ b/packages/eval/fixtures/swe-weak-table/resume.md @@ -0,0 +1,9 @@ +John Smith — Developer + +| Skill | Level | +| ------ | ------ | +| react | senior | +| node | mid | +| docker | basic | + +Worked on various TypeScript projects. Familiar with Kubernetes and AWS. diff --git a/packages/eval/fixtures/swe-with-jd/expected.json b/packages/eval/fixtures/swe-with-jd/expected.json new file mode 100644 index 0000000..bde48d0 --- /dev/null +++ b/packages/eval/fixtures/swe-with-jd/expected.json @@ -0,0 +1,5 @@ +{ + "description": "Backend engineer evaluated with a matching JD as keyword context", + "archetype": "software-engineer", + "atsCompatible": true +} diff --git a/packages/eval/fixtures/swe-with-jd/jd.md b/packages/eval/fixtures/swe-with-jd/jd.md new file mode 100644 index 0000000..87b006f --- /dev/null +++ b/packages/eval/fixtures/swe-with-jd/jd.md @@ -0,0 +1,4 @@ +# Backend Engineer + +We're hiring a backend engineer to scale our payments platform. You'll work with +Go, Kubernetes, Kafka, GraphQL, and Postgres at high throughput. diff --git a/packages/eval/fixtures/swe-with-jd/resume.md b/packages/eval/fixtures/swe-with-jd/resume.md new file mode 100644 index 0000000..7200380 --- /dev/null +++ b/packages/eval/fixtures/swe-with-jd/resume.md @@ -0,0 +1,15 @@ +# Miguel Santos + +Backend Engineer — miguel@example.com — github.com/miguels + +## Experience + +### Payments Inc — Backend Engineer (2019–2024) + +- Designed Go and Node services behind a GraphQL gateway handling 10k req/s. +- Scaled Postgres and Kafka pipelines; cut order-processing time 60%. +- Ran the platform on Kubernetes with automated CI/CD. + +## Skills + +Go, Node, TypeScript, GraphQL, Postgres, Kafka, Kubernetes, Docker, AWS diff --git a/packages/eval/package.json b/packages/eval/package.json new file mode 100644 index 0000000..db5962a --- /dev/null +++ b/packages/eval/package.json @@ -0,0 +1,22 @@ +{ + "name": "@cv-builder/eval", + "version": "0.1.0", + "description": "Golden fixtures and eval harness for the deterministic intelligence layer", + "type": "module", + "private": true, + "scripts": { + "lint": "tsc --noEmit", + "eval": "vitest run", + "dev": "vitest" + }, + "dependencies": { + "@cv-builder/intelligence": "workspace:*", + "@cv-builder/schemas": "workspace:*" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "license": "MIT" +} diff --git a/packages/eval/src/__tests__/fixtures.test.ts b/packages/eval/src/__tests__/fixtures.test.ts new file mode 100644 index 0000000..32a1e17 --- /dev/null +++ b/packages/eval/src/__tests__/fixtures.test.ts @@ -0,0 +1,69 @@ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { + checkAtsCompatibility, + detectArchetype, + getArchetype, +} from "@cv-builder/intelligence"; +import { ResumeSchema } from "@cv-builder/schemas"; +import { describe, expect, it } from "vitest"; + +const fixturesDir = fileURLToPath(new URL("../../fixtures", import.meta.url)); + +interface Expected { + archetype: string; + atsCompatible: boolean; + description?: string; +} + +interface Fixture { + name: string; + resume: string; + jd?: string; + expected: Expected; +} + +function loadFixtures(): Fixture[] { + return readdirSync(fixturesDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => { + const dir = `${fixturesDir}/${d.name}`; + const jdPath = `${dir}/jd.md`; + return { + name: d.name, + resume: readFileSync(`${dir}/resume.md`, "utf-8"), + jd: existsSync(jdPath) ? readFileSync(jdPath, "utf-8") : undefined, + expected: JSON.parse(readFileSync(`${dir}/expected.json`, "utf-8")) as Expected, + }; + }); +} + +const fixtures = loadFixtures(); + +describe("golden fixtures", () => { + it("covers at least 5 fixtures across at least 3 archetypes", () => { + expect(fixtures.length).toBeGreaterThanOrEqual(5); + const archetypes = new Set(fixtures.map((f) => f.expected.archetype)); + expect(archetypes.size).toBeGreaterThanOrEqual(3); + }); + + for (const f of fixtures) { + describe(f.name, () => { + it("expects a real archetype", () => { + expect(getArchetype(f.expected.archetype), f.expected.archetype).toBeDefined(); + }); + + it("detects the expected archetype", () => { + expect(detectArchetype(f.resume).id).toBe(f.expected.archetype); + }); + + it("matches expected ATS compatibility", () => { + expect(checkAtsCompatibility(f.resume).compatible).toBe(f.expected.atsCompatible); + }); + + it("parses as a Resume", () => { + expect(ResumeSchema.safeParse({ rawText: f.resume }).success).toBe(true); + }); + }); + } +}); diff --git a/packages/eval/tsconfig.json b/packages/eval/tsconfig.json new file mode 100644 index 0000000..66d8880 --- /dev/null +++ b/packages/eval/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*"] +} diff --git a/packages/eval/vitest.config.ts b/packages/eval/vitest.config.ts new file mode 100644 index 0000000..4ed8031 --- /dev/null +++ b/packages/eval/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8885d7c..8ee5510 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,25 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/eval: + dependencies: + '@cv-builder/intelligence': + specifier: workspace:* + version: link:../intelligence + '@cv-builder/schemas': + specifier: workspace:* + version: link:../schemas + devDependencies: + '@types/node': + specifier: ^25.6.2 + version: 25.6.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0) + packages/intelligence: dependencies: '@cv-builder/schemas': diff --git a/turbo.json b/turbo.json index 56b1698..81d0046 100644 --- a/turbo.json +++ b/turbo.json @@ -14,6 +14,9 @@ }, "test": { "dependsOn": ["build"] + }, + "eval": { + "dependsOn": ["^build"] } } } From 2e07cd9bc0f352498affa4dfeb2c418e38af720b Mon Sep 17 00:00:00 2001 From: AmirBahador Bahadori Date: Sun, 7 Jun 2026 20:19:17 +0200 Subject: [PATCH 11/39] docs: add power-user quickstart and update structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README gets a 'Power User — run it with Claude Code' section (clone, open Claude Code, /evaluate-cv) linking apps/cli/README.md, and the architecture tree plus the CONTRIBUTING structure now list schemas, intelligence, prompts, eval, and apps/cli. Closes #67 --- CONTRIBUTING.md | 17 ++++++++++++----- README.md | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 278bce9..bc65a39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,11 +23,18 @@ pnpm build ``` packages/ -├── core/ # Evaluation engine — TypeScript library -├── cli/ # Command-line tool -└── web/ # Browser UI (Next.js) -research/ # Sources and data backing our rules -docs/ # Architecture and guides +├── schemas/ # Zod contract shared across surfaces +├── intelligence/ # Rubric, archetypes, validators +├── prompts/ # extract / score / validate-claims prompt pack +├── core/ # Deterministic evaluation engine +├── cli/ # Command-line tool +└── eval/ # Golden fixtures (pnpm eval) +apps/ +├── cli/ # Power-user pack — quickstart for Claude Code +└── web-ui/ # Browser UI (Next.js) +.claude/ # Claude Code skill + slash commands +research/ # Sources and data backing our rules +docs/ # Architecture and guides ``` ## Development Workflow diff --git a/README.md b/README.md index d46e084..f1266d5 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,12 @@ pnpm --filter @cv-builder/cli start evaluate ./my-cv.md --jd ./job.md pnpm --filter @cv-builder/cli start archetypes ``` +### Power User — run it with Claude Code + +Clone the repo, open [Claude Code](https://claude.com/claude-code) at the root, and run `/evaluate-cv ./your-resume.pdf`. The evaluation runs entirely on your machine with your own agent — no server, no account, nothing leaves your computer. No build needed. + +See [apps/cli/README.md](apps/cli/README.md) for the full guide. + ### Web UI (coming soon) A browser-based interface where you paste your CV and JD, get instant feedback, and export a tailored PDF. No sign-up required, no data leaves your browser. @@ -79,9 +85,16 @@ A browser-based interface where you paste your CV and JD, get instant feedback, ``` packages/ -├── core/ # Evaluation engine (the brain) -├── cli/ # Command-line interface -└── web/ # Browser UI (coming soon) +├── schemas/ # Zod contract shared by every surface +├── intelligence/ # Rubric, archetypes, validators (the brain) +├── prompts/ # extract / score / validate-claims prompt pack +├── core/ # Deterministic evaluation engine +├── cli/ # Command-line interface +└── eval/ # Golden fixtures (pnpm eval) +apps/ +├── cli/ # Power-user quickstart +└── web-ui/ # Browser UI (coming soon) +.claude/ # Claude Code skill + slash commands (power-user surface) ``` See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full technical design. @@ -106,7 +119,7 @@ pnpm test | Your skill | Start here | |------------|-----------| -| TypeScript / Backend | `packages/core/` — scoring algorithms, new archetypes | +| TypeScript / Backend | `packages/intelligence/` — rubric, new archetypes, validators | | React / Frontend | `apps/web-ui/` — build the UI from scratch | | CLI / Node.js | `packages/cli/` — new commands, output formatting | | Research / Writing | `research/` — find sources, validate rules | From ce9bef14e6b2b9e691b24ade3cb08d91270edee3 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Jun 2026 20:48:10 -0700 Subject: [PATCH 12/39] rule: add outdated technologies flag --- packages/core/src/rules/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/core/src/rules/index.ts b/packages/core/src/rules/index.ts index 0e58fe8..874e5dd 100644 --- a/packages/core/src/rules/index.ts +++ b/packages/core/src/rules/index.ts @@ -93,6 +93,22 @@ export const UNIVERSAL_RULES = { fix: "Replace with evidence: 'Mentored 3 junior engineers to promotion' or 'Presented quarterly results to 200-person all-hands'", severity: "minor", }, + { + name: "Outdated tool without modern stack", + match: "\\b(jQuery(?!.*\\b(React|Vue|Angular|Svelte)\\b)|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN(?!.*\\bGit\\b)|Heroku|CoffeeScript|Backbone\\.js|(?:Grunt|Gulp)(?!.*\\b(Webpack|Vite|Rollup|Parcel|esbuild)\\b))\\b", + why: "Listing deprecated or legacy technologies without modern frameworks makes your resume look stale and lack growth.", + fix: "Replace with modern alternatives or remove the outdated technology.", + severity: "major", + }, + { + name: "Outdated tool with modern stack", + match: + "\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b.*\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b|\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b.*\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b", + why: "Listing outdated technologies alongside modern frameworks can be confusing and may not accurately represent your skills.", + fix: "Remove outdated technologies to show focus on modern skills.", + severity: "minor", + } + ] satisfies AntiPattern[], positivePatterns: [ From 0ce17c8c608eaecf49e21ea03e53c80f5ed3358b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Jun 2026 21:15:39 -0700 Subject: [PATCH 13/39] rule: add 2 new rules for Heroku --- packages/core/src/rules/index.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/core/src/rules/index.ts b/packages/core/src/rules/index.ts index 874e5dd..531fce6 100644 --- a/packages/core/src/rules/index.ts +++ b/packages/core/src/rules/index.ts @@ -95,7 +95,7 @@ export const UNIVERSAL_RULES = { }, { name: "Outdated tool without modern stack", - match: "\\b(jQuery(?!.*\\b(React|Vue|Angular|Svelte)\\b)|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN(?!.*\\bGit\\b)|Heroku|CoffeeScript|Backbone\\.js|(?:Grunt|Gulp)(?!.*\\b(Webpack|Vite|Rollup|Parcel|esbuild)\\b))\\b", + match: "\\b(jQuery(?!.*\\b(React|Vue|Angular|Svelte)\\b)|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN(?!.*\\bGit\\b)|CoffeeScript|Backbone\\.js|(?:Grunt|Gulp)(?!.*\\b(Webpack|Vite|Rollup|Parcel|esbuild)\\b))\\b", why: "Listing deprecated or legacy technologies without modern frameworks makes your resume look stale and lack growth.", fix: "Replace with modern alternatives or remove the outdated technology.", severity: "major", @@ -103,12 +103,27 @@ export const UNIVERSAL_RULES = { { name: "Outdated tool with modern stack", match: - "\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b.*\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b|\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b.*\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b", + "\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b.*\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b|\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b.*\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b", why: "Listing outdated technologies alongside modern frameworks can be confusing and may not accurately represent your skills.", fix: "Remove outdated technologies to show focus on modern skills.", severity: "minor", - } - + }, + { + name: "Heroku deployment timeline (old experience)", + match: + "\\b(Heroku)\\b[\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b|\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b[\\s\\S]*\\b(Heroku)\\b|\\b(Heroku)\\b[\\s\\S]*\\b20(?:1\\d|202[0-4])\\b|\\b20(?:1\\d|202[0-4])\\b[\\s\\S]*\\b(Heroku)\\b", + why: "Heroku is only appropriate for older work, and older dates should be clearly shown.", + fix: "Heroku can be kept for legacy experience, but post 2024, it should be replaced with a modern deployment platform.", + severity: "minor", + }, + { + name: "Heroku deployment timeline post 2024", + match: + "^(?![\\s\\S]*\\bHeroku\\b[\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b)(?![\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b[\\s\\S]*\\bHeroku\\b)[\\s\\S]*\\bHeroku\\b", + why: "Heroku as primary deployment is outdated for modern resumes unless the experience is clearly dated before 2024.", + fix: "Remove/replace Heroku with a current deployment platform.", + severity: "major", + }, ] satisfies AntiPattern[], positivePatterns: [ From 897f9bb6027a5def320a04e5eac66305f1639d09 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Jun 2026 21:30:20 -0700 Subject: [PATCH 14/39] rule: modify Heroku regex to catch dates better --- packages/core/src/rules/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/core/src/rules/index.ts b/packages/core/src/rules/index.ts index 531fce6..977a6f2 100644 --- a/packages/core/src/rules/index.ts +++ b/packages/core/src/rules/index.ts @@ -109,19 +109,17 @@ export const UNIVERSAL_RULES = { severity: "minor", }, { - name: "Heroku deployment timeline (old experience)", - match: - "\\b(Heroku)\\b[\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b|\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b[\\s\\S]*\\b(Heroku)\\b|\\b(Heroku)\\b[\\s\\S]*\\b20(?:1\\d|202[0-4])\\b|\\b20(?:1\\d|202[0-4])\\b[\\s\\S]*\\b(Heroku)\\b", + name: "Heroku deployment timeline 2024 and below", + match: "\\bHeroku\\b.*\\b(20[0-1]\\d|202[0-4]|\\d{1,2})\\b", why: "Heroku is only appropriate for older work, and older dates should be clearly shown.", fix: "Heroku can be kept for legacy experience, but post 2024, it should be replaced with a modern deployment platform.", severity: "minor", }, { name: "Heroku deployment timeline post 2024", - match: - "^(?![\\s\\S]*\\bHeroku\\b[\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b)(?![\\s\\S]*\\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(?:201\\d|202[0-4])\\b[\\s\\S]*\\bHeroku\\b)[\\s\\S]*\\bHeroku\\b", + match: "\\bHeroku\\b", why: "Heroku as primary deployment is outdated for modern resumes unless the experience is clearly dated before 2024.", - fix: "Remove/replace Heroku with a current deployment platform.", + fix: "Add a date for Heroku (pre-2025) or remove/replace Heroku with a current deployment platform.", severity: "major", }, ] satisfies AntiPattern[], From 2bdedef4318ff404e8c3de497b9239feee84b38d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Jun 2026 21:44:00 -0700 Subject: [PATCH 15/39] test: add tests to validate new universal rules --- packages/core/src/__tests__/rules.test.ts | 210 ++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 packages/core/src/__tests__/rules.test.ts diff --git a/packages/core/src/__tests__/rules.test.ts b/packages/core/src/__tests__/rules.test.ts new file mode 100644 index 0000000..d9a7f9f --- /dev/null +++ b/packages/core/src/__tests__/rules.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from "vitest"; +import { evaluate } from "../evaluator/index.js"; +import { UNIVERSAL_RULES } from "../rules/index.js"; + +describe("UNIVERSAL_RULES", () => { + it("should have anti-patterns defined", () => { + expect(UNIVERSAL_RULES.antiPatterns).toBeDefined(); + expect(UNIVERSAL_RULES.antiPatterns.length).toBeGreaterThan(0); + }); + + it("compiles all anti-pattern regexes", () => { + for (const pattern of UNIVERSAL_RULES.antiPatterns) { + expect(() => new RegExp(pattern.match, "i")).not.toThrow(); + } + }); + + it("does not flag modern tools", async () => { + const result = await evaluate({ + cv: { + content: "Built a frontend with React, deployed via GitHub Actions with Vite.", + format: "markdown", + }, + }); + + const outdatedIssues = result.issues.filter((i) => + [ + "Outdated tool without modern stack", + "Outdated tool with modern stack", + "Heroku deployment timeline 2024 and below", + "Heroku deployment timeline post 2024", + ].includes(i.element) + ); + + expect(outdatedIssues).toHaveLength(0); + }); + + it("flags outdated tooling alone as major", async () => { + const result = await evaluate({ + cv: { + content: "Built several legacy apps using jQuery, Backbone.js, and Grunt.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool without modern stack" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("flags outdated tooling with modern stack as minor", async () => { + const result = await evaluate({ + cv: { + content: + "Built a frontend with jQuery and React, deployed via GitHub Actions with Vite.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool with modern stack" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("minor"); + }); + + it("flags AngularJS (v1)", async () => { + const result = await evaluate({ + cv: { + content: "Built a frontend with AngularJS.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool without modern stack" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("does not flag Angular (v2+)", async () => { + const result = await evaluate({ + cv: { + content: "Built a frontend with Angular.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => + i.element === "Outdated tool without modern stack" || + i.element === "Outdated tool with modern stack" + ); + expect(issue).toBeUndefined(); + }); + + it("flags Heroku post 2024 as major", async () => { + const result = await evaluate({ + cv: { + content: "Built, deployed, and monitored several apps on Heroku in 2025.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Heroku deployment timeline post 2024" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("flags Heroku 2024 and below as minor", async () => { + const result = await evaluate({ + cv: { + content: "Built, deployed, and monitored several apps on Heroku in 2024.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Heroku deployment timeline 2024 and below" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("minor"); + }); + + it("flags Heroku without date as major", async () => { + const result = await evaluate({ + cv: { + content: "Built, deployed, and monitored several apps on Heroku.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Heroku deployment timeline post 2024" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("flags SVN without Git as major", async () => { + const result = await evaluate({ + cv: { + content: "Managed codebase using SVN for version control.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool without modern stack" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("flags SVN with Git as minor", async () => { + const result = await evaluate({ + cv: { + content: "Managed codebase using SVN and Git for version control.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool with modern stack" + ); + + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("minor"); + }); + + it("flags Grunt/Gulp without modern bundler as major", async () => { + const result = await evaluate({ + cv: { + content: "Built frontend assets using Grunt and Gulp.", + format: "markdown", + }, + }); + + const issue = result.issues.find( + (i) => i.element === "Outdated tool without modern stack" + ); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("major"); + }); + + it("flags Grunt/Gulp with modern bundler as minor", async () => { + const result = await evaluate({ + cv: { + content: "Built frontend assets using Grunt and Vite.", + format: "markdown", + }, + }); + const issue = result.issues.find( + (i) => i.element === "Outdated tool with modern stack" + ); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe("minor"); + }); +}); From aa2ea6a9212011640389dbc175b193614b50c7df Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Jun 2026 21:45:08 -0700 Subject: [PATCH 16/39] refactor: ran pnpm format --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/new_archetype.md | 18 +- .github/ISSUE_TEMPLATE/new_rule.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 6 +- CONTRIBUTING.md | 3 + README.md | 35 +- apps/web-ui/AGENTS.md | 2 + apps/web-ui/README.md | 8 +- .../web-ui/src/app/components/ThemeToggle.tsx | 2 +- .../src/app/components/layout/Header.tsx | 29 +- apps/web-ui/src/app/layout.tsx | 2 +- apps/web-ui/src/app/page.tsx | 28 +- docs/ARCHITECTURE.md | 19 +- docs/PHASE-1.md | 92 ++-- docs/PROPOSAL.md | 38 +- packages/cli/src/cli.ts | 4 +- packages/core/src/__tests__/evaluator.test.ts | 12 +- packages/core/src/archetypes/index.ts | 426 ++++++++++++++---- packages/core/src/builder/index.ts | 66 ++- packages/core/src/evaluator/index.ts | 80 +++- packages/core/src/rules/index.ts | 10 +- research/sources.md | 53 ++- 23 files changed, 649 insertions(+), 290 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f684e8b..baf2873 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug Report about: Something isn't working correctly title: "[Bug] " labels: bug -assignees: '' +assignees: "" --- ## What happened? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 812e5c5..34d1975 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature Request about: Suggest a new feature or improvement title: "[Feature] " labels: enhancement -assignees: '' +assignees: "" --- ## Problem diff --git a/.github/ISSUE_TEMPLATE/new_archetype.md b/.github/ISSUE_TEMPLATE/new_archetype.md index a94d002..09b9777 100644 --- a/.github/ISSUE_TEMPLATE/new_archetype.md +++ b/.github/ISSUE_TEMPLATE/new_archetype.md @@ -3,7 +3,7 @@ name: New Role Archetype about: Add a new role type (e.g., "Mobile Engineer", "Security Engineer") title: "[Archetype] " labels: archetype, good first issue -assignees: '' +assignees: "" --- ## Role name @@ -26,14 +26,14 @@ List the key terms recruiters look for in this role: How should the 6 dimensions be weighted for this role? -| Dimension | Suggested weight | -|-----------|-----------------| -| Shipped Evidence | ? | -| Quantified Impact | ? | -| Tooling Visibility | ? | -| ATS Compatibility | ? | -| Keyword Match | ? | -| Public Proof | ? | +| Dimension | Suggested weight | +| ------------------ | ---------------- | +| Shipped Evidence | ? | +| Quantified Impact | ? | +| Tooling Visibility | ? | +| ATS Compatibility | ? | +| Keyword Match | ? | +| Public Proof | ? | ## Action verbs (10+) diff --git a/.github/ISSUE_TEMPLATE/new_rule.md b/.github/ISSUE_TEMPLATE/new_rule.md index 9787b31..87850ab 100644 --- a/.github/ISSUE_TEMPLATE/new_rule.md +++ b/.github/ISSUE_TEMPLATE/new_rule.md @@ -3,7 +3,7 @@ name: New Evaluation Rule about: Propose a new CV evaluation rule (anti-pattern or positive pattern) title: "[Rule] " labels: rules, good first issue -assignees: '' +assignees: "" --- ## Rule type diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3b3a5e3..d2c97b3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,7 @@ Brief description of the change. ## Related issue -Closes #___ +Closes #\_\_\_ ## Type of change @@ -25,5 +25,5 @@ Closes #___ ## Screenshots (if UI change) -Before | After ---- | --- +| Before | After | +| ------ | ----- | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 278bce9..9690ac3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,7 @@ git checkout -b feature/your-feature-name ``` Branch naming: + - `feature/pdf-export` — new features - `fix/scoring-bug` — bug fixes - `archetype/mobile-engineer` — new role archetypes @@ -88,6 +89,7 @@ git push origin feature/your-feature-name ``` Then open a PR on GitHub. Fill in the template — it asks: + - What does this PR do? - Related issue number - Type of change @@ -160,6 +162,7 @@ ARCHETYPES.set("mobile-engineer", { ## First Time Contributing to Open Source? Welcome! You belong here. These resources help: + - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [First Timers Only](https://www.firsttimersonly.com/) - [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) diff --git a/README.md b/README.md index d46e084..08667c6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ An open source, privacy-first CV builder for tech professionals. Built by the [T Good CV tools are expensive. Many job seekers — especially immigrants, career changers, and people in countries with restricted internet — can't access them. We're building a free, open source alternative backed by real hiring research. **What makes this different:** + - Scoring based on [250+ researched sources](research/sources.md) from FAANG recruiters, AI hiring managers, and industry experts - Role-specific evaluation (not one-size-fits-all) - Privacy-first: works offline, no data collection @@ -35,14 +36,14 @@ Good CV tools are expensive. Many job seekers — especially immigrants, career ### Scoring Dimensions -| Dimension | What it measures | -|-----------|-----------------| -| Shipped Evidence | Real work in production, not theory | -| Quantified Impact | Numbers in every bullet (%, $, users) | -| Tooling Visibility | Specific tools named, matching the JD | -| ATS Compatibility | Will it parse correctly in applicant tracking systems? | -| Keyword Match | Terminology from the JD present in your CV | -| Public Proof | GitHub, blog, portfolio links that verify claims | +| Dimension | What it measures | +| ------------------ | ------------------------------------------------------ | +| Shipped Evidence | Real work in production, not theory | +| Quantified Impact | Numbers in every bullet (%, $, users) | +| Tooling Visibility | Specific tools named, matching the JD | +| ATS Compatibility | Will it parse correctly in applicant tracking systems? | +| Keyword Match | Terminology from the JD present in your CV | +| Public Proof | GitHub, blog, portfolio links that verify claims | Weights vary by role type. An AI Engineer's CV is scored differently than a Product Manager's. @@ -104,14 +105,14 @@ pnpm test ### Where to Start -| Your skill | Start here | -|------------|-----------| +| Your skill | Start here | +| -------------------- | ----------------------------------------------------- | | TypeScript / Backend | `packages/core/` — scoring algorithms, new archetypes | -| React / Frontend | `apps/web-ui/` — build the UI from scratch | -| CLI / Node.js | `packages/cli/` — new commands, output formatting | -| Research / Writing | `research/` — find sources, validate rules | -| Design / UX | UI mockups, user flows, accessibility | -| DevOps / CI | `.github/workflows/` — testing, releases, automation | +| React / Frontend | `apps/web-ui/` — build the UI from scratch | +| CLI / Node.js | `packages/cli/` — new commands, output formatting | +| Research / Writing | `research/` — find sources, validate rules | +| Design / UX | UI mockups, user flows, accessibility | +| DevOps / CI | `.github/workflows/` — testing, releases, automation | ### Issue Labels @@ -144,6 +145,7 @@ Currently built-in: ## Roadmap ### v0.1 (Current Sprint) + - [x] Core evaluation engine with 6 dimensions - [x] 6 role archetypes - [x] Universal anti-pattern detection @@ -152,6 +154,7 @@ Currently built-in: - [ ] Web UI: paste and score screen ### v0.2 + - [ ] LLM-enhanced mode (rewrite suggestions) - [ ] PDF export - [ ] 15+ role archetypes @@ -159,6 +162,7 @@ Currently built-in: - [ ] Localization (multiple languages) ### v0.3 + - [ ] Side-by-side tailoring editor - [ ] Before/after examples library - [ ] VS Code / Cursor extension @@ -179,6 +183,7 @@ Currently built-in: ## Research Our rules are backed by data, not vibes. See: + - [research/sources.md](research/sources.md) — All 250+ sources with citations - [research/data/](research/data/) — Structured market data - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — Technical design decisions diff --git a/apps/web-ui/AGENTS.md b/apps/web-ui/AGENTS.md index 8bd0e39..c153a9b 100644 --- a/apps/web-ui/AGENTS.md +++ b/apps/web-ui/AGENTS.md @@ -1,5 +1,7 @@ + # This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/apps/web-ui/README.md b/apps/web-ui/README.md index f35ce8c..4cd6eeb 100644 --- a/apps/web-ui/README.md +++ b/apps/web-ui/README.md @@ -17,12 +17,12 @@ Then open [http://localhost:3000](http://localhost:3000). ## Scripts -| Command | Purpose | -| --- |----------------------------| -| `pnpm dev` | Start the local dev server | +| Command | Purpose | +| ------------ | -------------------------- | +| `pnpm dev` | Start the local dev server | | `pnpm build` | Build the production app | | `pnpm start` | Run the production build | -| `pnpm lint` | Run lint checks | +| `pnpm lint` | Run lint checks | ## Main files diff --git a/apps/web-ui/src/app/components/ThemeToggle.tsx b/apps/web-ui/src/app/components/ThemeToggle.tsx index b7c9c75..5bcbd5b 100644 --- a/apps/web-ui/src/app/components/ThemeToggle.tsx +++ b/apps/web-ui/src/app/components/ThemeToggle.tsx @@ -25,4 +25,4 @@ export function ThemeToggle() { {theme === "dark" ? "🌙" : "☀️"} ); -} \ No newline at end of file +} diff --git a/apps/web-ui/src/app/components/layout/Header.tsx b/apps/web-ui/src/app/components/layout/Header.tsx index 24303a0..8fc3383 100644 --- a/apps/web-ui/src/app/components/layout/Header.tsx +++ b/apps/web-ui/src/app/components/layout/Header.tsx @@ -4,19 +4,16 @@ import { ThemeToggle } from "../ThemeToggle"; import Link from "next/link"; export function Header() { - return ( -
-
-
- - CV Builder - -
- -
-
- ); -} \ No newline at end of file + return ( +
+
+
+ + CV Builder + +
+ +
+
+ ); +} diff --git a/apps/web-ui/src/app/layout.tsx b/apps/web-ui/src/app/layout.tsx index f8f9856..090c949 100644 --- a/apps/web-ui/src/app/layout.tsx +++ b/apps/web-ui/src/app/layout.tsx @@ -27,4 +27,4 @@ export default function RootLayout({ children }: { children: React.ReactNode }) ); -} \ No newline at end of file +} diff --git a/apps/web-ui/src/app/page.tsx b/apps/web-ui/src/app/page.tsx index 95e9f85..2a15a2d 100644 --- a/apps/web-ui/src/app/page.tsx +++ b/apps/web-ui/src/app/page.tsx @@ -7,16 +7,14 @@ export default function Home() {
-

- CV Builder -

+

CV Builder

Tailor your resume to the role without starting from scratch.

- Paste a job description, highlight the experience that matters, - and shape a cleaner CV for tech roles in a few quick steps. + Paste a job description, highlight the experience that matters, and shape + a cleaner CV for tech roles in a few quick steps.

@@ -25,37 +23,37 @@ export default function Home() {

1. Paste the job post

- Start with the exact role description so the app can focus the CV - around the right skills, tools, and experience. + Start with the exact role description so the app can focus the CV around + the right skills, tools, and experience.

2. Shape the story

- Refine summaries, impact statements, and project bullets to match - the role while keeping your background clear and honest. + Refine summaries, impact statements, and project bullets to match the role + while keeping your background clear and honest.

3. Export a focused CV

- End with a cleaner resume that is easier to review and more - aligned with the position you are targeting. + End with a cleaner resume that is easier to review and more aligned with + the position you are targeting.

- This starter page is intentionally simple. It gives the web package - an app-specific home screen instead of the default Next.js template - and leaves room for the real workflow to grow from here. + This starter page is intentionally simple. It gives the web package an + app-specific home screen instead of the default Next.js template and leaves + room for the real workflow to grow from here.

); -} \ No newline at end of file +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eee122c..77d7e56 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -23,56 +23,73 @@ cv-builder/ ## Packages ### `schemas` + Zod schemas for: `Resume`, `JobDescription`, `Archetype`, `RubricVersion`, `EvalResult`, `Claim`, `Issue`, `LLMCall`. Every other package imports types from here. ### `prompts` + Prompt files (`.md`) grouped by step (`extract.md`, `score.md`, `validate-claims.md`, `clarify.md`). A loader returns a rendered prompt + the Zod schema the response must satisfy. The `cli` app is a thin export of this package's files. ### `llm` + One interface: + ```ts interface LLMClient { call(req: { prompt: string; schema: ZodSchema; model?: string }): Promise; } ``` + Adapters: OpenAI, Anthropic, Ollama. Handles JSON-schema mode, retries, and parse-or-throw against the Zod schema. ### `ingestion` + Parsers and fetchers. Input: file, text, or URL. Output: typed `Resume` or `JobDescription` (best-effort, partial). + - **PDF resumes** — `unpdf`. - **JD from URL** — Playwright. Most job boards (Greenhouse, Lever, Workday, LinkedIn) render content via JS, so a static fetch returns an empty shell. Playwright loads the page, waits for content, extracts the JD text. Headless Chromium is heavy but unavoidable for this surface. - **Markdown / plain text** — direct parse. ### `intelligence` + The "brain". Contains: + - `archetypes/` — role configs (weights, keywords, anti-patterns) - `rubric/` — versioned scoring rubric - `pipeline/` — `evaluate(resume, jd, archetype)` runs the deterministic steps - `validators/` — claim validator, tool/tech hallucination detector, ATS formatter checker ### `templates` + Two layers so both web-ui and CLI can use it: + - **Manifest layer** — each template described as data (sections, fields, ordering, constraints) typed against `schemas`. Prompts in `apps/cli/` reference manifests so a power user's agent knows what to fill. - **Render layer** — React components that render a `Resume` against a manifest. Consumed by `web-ui` now; feeds PDF export later. ### `eval` + Golden fixtures (`fixtures/*.json` = JD + resume + expected findings). Vitest runner asserts: required findings present, banned hallucinations absent, score within tolerance. Runs in CI. ### `core` + Orchestrator. Exposes one function per use case (e.g., `runEvaluation`) that composes ingestion → intelligence → validators → result. Apps depend on `core`, not on `intelligence` directly. ## Apps ### `web-ui` + Next.js. Routes: template editor, JD paste, eval result view. Multilingual via Tolgee. Calls `server` over HTTP. "Bring your own key" toggle stays first-class. ### `server` + Fastify. Endpoints: `POST /eval`, `POST /parse`, `POST /telegram/webhook`. Holds the server-side LLM key (or proxies the user's). Never persists resume text. ### `telegram` + Telegram bot. v1 surface: send resume + JD as files or text, receive eval result back. Long polling in dev. ### `cli` + A folder of `.md` files. README tells users to point Claude Code / Codex / Gemini CLI at it. No npm package. ## Data flow — evaluation (v1) @@ -101,7 +118,7 @@ Deterministic. Match the resume's titles, tools, and verbs against each archetyp classify(resume) → "ai-engineer" // or "backend", "pm", "data-scientist", ... ``` -### Step 4 — `evaluate` (the only LLM step) +### Step 4 — `evaluate` (the only LLM step) We hydrate a prompt template from `packages/prompts/score.md` with the archetype + rubric + the user's data, ask for a Zod-validated `EvalResult` back, and that's it. Schematically, the call looks like: diff --git a/docs/PHASE-1.md b/docs/PHASE-1.md index dd5c6aa..bd6b19e 100644 --- a/docs/PHASE-1.md +++ b/docs/PHASE-1.md @@ -11,6 +11,7 @@ A single, focused feature: **paste your resume → get an honest, role-adaptive score with actionable feedback.** Available on three surfaces simultaneously: + 1. **Web UI**. Browser-based, no install needed. 2. **Telegram bot**. Send resume as file or text, get result back. 3. **CLI prompts**. Power users run their own LLM agent against our prompt pack. @@ -18,6 +19,7 @@ Available on three surfaces simultaneously: ## User Stories ### Non-technical job seeker (primary) + > "I'm applying to jobs and I don't know if my resume is good enough. I want honest, specific feedback, not generic advice." - Opens the web UI or sends a file to the Telegram bot @@ -27,6 +29,7 @@ Available on three surfaces simultaneously: - Takes under 30 seconds ### Developer / power user + > "I want full control. I'll use my own Claude Code / Codex / Gemini and my own API key." - Clones the repo @@ -36,20 +39,20 @@ Available on three surfaces simultaneously: ## What Ships -| Package | What's in it | Minimum viable | -|---|---|---| -| `schemas` | Zod types: Resume, JobDescription, Archetype, EvalResult, Claim, Issue | All defined and exported | -| `prompts` | Prompt templates: extract, score, validate-claims | 3 working prompts | -| `llm` | Provider-agnostic client | 1 adapter working (Anthropic) | -| `ingestion` | Resume + JD parsing | PDF + plain text working | -| `intelligence` | Rubric, archetypes, eval pipeline, validators | Fixed rubric v1, 3-5 archetypes | -| `eval` | Test harness | 5 golden fixtures passing in CI | -| `core` | Orchestrator | `runEvaluation()` end-to-end | -| `server` | HTTP API | `POST /eval`, `POST /parse` | -| `templates` | Placeholder directory for future render templates | Directory with README only (rendering is Phase 2) | -| `web-ui` | Browser interface | Paste resume + JD → see result | -| `telegram` | Bot | Send file → get result | -| `cli` | Prompt files + README | Power users can run locally | +| Package | What's in it | Minimum viable | +| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------- | +| `schemas` | Zod types: Resume, JobDescription, Archetype, EvalResult, Claim, Issue | All defined and exported | +| `prompts` | Prompt templates: extract, score, validate-claims | 3 working prompts | +| `llm` | Provider-agnostic client | 1 adapter working (Anthropic) | +| `ingestion` | Resume + JD parsing | PDF + plain text working | +| `intelligence` | Rubric, archetypes, eval pipeline, validators | Fixed rubric v1, 3-5 archetypes | +| `eval` | Test harness | 5 golden fixtures passing in CI | +| `core` | Orchestrator | `runEvaluation()` end-to-end | +| `server` | HTTP API | `POST /eval`, `POST /parse` | +| `templates` | Placeholder directory for future render templates | Directory with README only (rendering is Phase 2) | +| `web-ui` | Browser interface | Paste resume + JD → see result | +| `telegram` | Bot | Send file → get result | +| `cli` | Prompt files + README | Power users can run locally | ## What We Are NOT Building (Phase 1) @@ -90,14 +93,14 @@ Phase 1 is done when ALL of these are true: The evaluation uses 6 dimensions with role-adaptive weights: -| Dimension | What it measures | -|---|---| -| Shipped Evidence | Real work in production with named tools and outcomes | -| Quantified Impact | Numbers in every bullet (scope, speed, adoption, savings) | -| Tech/Tool Visibility | Named technologies matching the role | -| ATS Compatibility | Single column, standard headings, parseable format | -| Keyword Match | Against JD if provided, else role-family keyword set | -| Public Proof Surface | GitHub, portfolio, blog, LinkedIn visibility | +| Dimension | What it measures | +| -------------------- | --------------------------------------------------------- | +| Shipped Evidence | Real work in production with named tools and outcomes | +| Quantified Impact | Numbers in every bullet (scope, speed, adoption, savings) | +| Tech/Tool Visibility | Named technologies matching the role | +| ATS Compatibility | Single column, standard headings, parseable format | +| Keyword Match | Against JD if provided, else role-family keyword set | +| Public Proof Surface | GitHub, portfolio, blog, LinkedIn visibility | Weights shift by role family (e.g., Designers get 40% on Public Proof, SWEs get 30% on Shipped Evidence). Full weight matrix in `packages/intelligence/archetypes/`. @@ -115,30 +118,31 @@ More archetypes added in later phases based on user demand. ## Team Structure -| Module | Skills needed | Potential owners | -|---|---|---| -| `web-ui` | React, Next.js, TypeScript | Frontend contributors | -| `server` | Node.js, Fastify, TypeScript | Backend contributors | -| `intelligence` + `prompts` | AI/LLM, prompt engineering | AI contributors + Sahar | -| `telegram` | Bot API, TypeScript | Bot-experienced contributors | -| `ingestion` | PDF parsing, document handling | Any TS contributor | -| `schemas` + `core` | TypeScript, system design | ABB + senior contributors | -| `eval` | Testing, fixtures | Any contributor | +| Module | Skills needed | Potential owners | +| -------------------------- | ------------------------------ | ---------------------------- | +| `web-ui` | React, Next.js, TypeScript | Frontend contributors | +| `server` | Node.js, Fastify, TypeScript | Backend contributors | +| `intelligence` + `prompts` | AI/LLM, prompt engineering | AI contributors + Sahar | +| `telegram` | Bot API, TypeScript | Bot-experienced contributors | +| `ingestion` | PDF parsing, document handling | Any TS contributor | +| `schemas` + `core` | TypeScript, system design | ABB + senior contributors | +| `eval` | Testing, fixtures | Any contributor | Assignments finalized in team call. ## Timeline -| Week | Milestone | -|---|---| -| Week 0 | Architecture merged (done), Phase 1 plan shared, team call held | -| Week 1 | `schemas` + `prompts` + `llm` + `ingestion` + `intelligence` working locally. Golden fixtures defined. | +| Week | Milestone | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Week 0 | Architecture merged (done), Phase 1 plan shared, team call held | +| Week 1 | `schemas` + `prompts` + `llm` + `ingestion` + `intelligence` working locally. Golden fixtures defined. | | Week 2 | `core` (orchestrator, depends on Week 1) + `web-ui` + `telegram` + `server` + `cli` connected. End-to-end flow working. All can start in parallel once interfaces from Week 1 are defined. | -| Week 3 | Polish, deploy, 5 golden fixtures green in CI. Public launch. | +| Week 3 | Polish, deploy, 5 golden fixtures green in CI. Public launch. | ## Relationship to cvroast.dev [cvroast.dev](https://cvroast.dev) is our existing deployed product (Cloudflare Workers, ~15s evaluation). cv-builder Phase 1 rebuilds this capability on a proper architecture that supports: + - Multiple interfaces (web + Telegram + CLI) - Structured, testable evaluation (Zod schemas, golden fixtures) - Community contribution (modular packages anyone can work on) @@ -157,14 +161,14 @@ These need a decision before implementation starts. The team call resolves them: ## Risks -| Risk | Mitigation | -|---|---| -| Scope creep (Ali's warning) | This doc is the boundary. Nothing ships that isn't listed above. | -| Team energy drops | Ship in 3 weeks, not months. Quick wins keep momentum. | -| PDF parsing unreliable | Accept "best-effort partial" in v1. Plain text always works. | -| LLM costs for hosted mode | Rate limit (3/day free like cvroast). Evaluate cost after launch. | -| Too many packages for v1 | Packages can start as single files. The architecture is the boundary, not the file count. | +| Risk | Mitigation | +| --------------------------- | ----------------------------------------------------------------------------------------- | +| Scope creep (Ali's warning) | This doc is the boundary. Nothing ships that isn't listed above. | +| Team energy drops | Ship in 3 weeks, not months. Quick wins keep momentum. | +| PDF parsing unreliable | Accept "best-effort partial" in v1. Plain text always works. | +| LLM costs for hosted mode | Rate limit (3/day free like cvroast). Evaluate cost after launch. | +| Too many packages for v1 | Packages can start as single files. The architecture is the boundary, not the file count. | --- -*This document is the product spec for Phase 1. Technical details in [ARCHITECTURE.md](./ARCHITECTURE.md) and [V1_SCOPE.md](./V1_SCOPE.md).* +_This document is the product spec for Phase 1. Technical details in [ARCHITECTURE.md](./ARCHITECTURE.md) and [V1_SCOPE.md](./V1_SCOPE.md)._ diff --git a/docs/PROPOSAL.md b/docs/PROPOSAL.md index 94e91b7..6345d44 100644 --- a/docs/PROPOSAL.md +++ b/docs/PROPOSAL.md @@ -18,20 +18,20 @@ A TypeScript-only, monorepo CV evaluator and builder. v1 ships **evaluation**: p ## Key decisions -| Decision | Choice | Why | -|---|---|---| -| Language | TypeScript only | UI needs it; nothing in scope needs Python | -| Monorepo | pnpm + turborepo (existing) | Already in place, works | -| Schema | Zod, centralized package | Shared types across apps and prompts | -| LLM | Provider-agnostic adapter | Avoid lock-in; users may self-host | -| UI | Next.js + TanStack Query + Tolgee (i18n) | SSR-friendly, typed data layer, multilingual from day one | -| Server | Fastify on Node | Mature, typed (TypeBox/Zod plugin), great plugin ecosystem | -| CLI | Prompts + templates only (no binary) | Power users run their own Claude / Codex / Gemini against our prompt pack | -| Workflow | Deterministic pipeline | Tool calling restricted to small choices (e.g., "ask user" vs "proceed") | -| PDF parser | `unpdf` | ESM-native, runs in Node and edge, actively maintained | -| JD URL crawler | Playwright | Most JD pages are JS-rendered; static fetchers (cheerio/got) miss them | -| Telegram transport | Long polling in dev, webhook in prod | No public URL needed locally; same code path | -| Default hosted LLM | Anthropic Claude | Strongest structured-output adherence in our testing; adapter still swappable | +| Decision | Choice | Why | +| ------------------ | ---------------------------------------- | ----------------------------------------------------------------------------- | +| Language | TypeScript only | UI needs it; nothing in scope needs Python | +| Monorepo | pnpm + turborepo (existing) | Already in place, works | +| Schema | Zod, centralized package | Shared types across apps and prompts | +| LLM | Provider-agnostic adapter | Avoid lock-in; users may self-host | +| UI | Next.js + TanStack Query + Tolgee (i18n) | SSR-friendly, typed data layer, multilingual from day one | +| Server | Fastify on Node | Mature, typed (TypeBox/Zod plugin), great plugin ecosystem | +| CLI | Prompts + templates only (no binary) | Power users run their own Claude / Codex / Gemini against our prompt pack | +| Workflow | Deterministic pipeline | Tool calling restricted to small choices (e.g., "ask user" vs "proceed") | +| PDF parser | `unpdf` | ESM-native, runs in Node and edge, actively maintained | +| JD URL crawler | Playwright | Most JD pages are JS-rendered; static fetchers (cheerio/got) miss them | +| Telegram transport | Long polling in dev, webhook in prod | No public URL needed locally; same code path | +| Default hosted LLM | Anthropic Claude | Strongest structured-output adherence in our testing; adapter still swappable | ## Architecture in one picture @@ -57,10 +57,10 @@ A TypeScript-only, monorepo CV evaluator and builder. v1 ships **evaluation**: p ## Two user modes (privacy is explicit, not implied) -| Mode | Who | Data path | Privacy | -|---|---|---|---| -| **Power-user (CLI)** | Devs running their own Claude / Codex / Gemini against `apps/cli/` | Resume + JD never leave the user's machine. They use their own model and their own key. | Fully private. We store nothing. | -| **Hosted (web-ui + telegram)** | Everyone else | Resume + JD go to our backend, which calls the LLM provider with our key. | Not private by default. We do not persist resume text, but data does transit our infrastructure and the provider. This is stated up-front in the UI. | +| Mode | Who | Data path | Privacy | +| ------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Power-user (CLI)** | Devs running their own Claude / Codex / Gemini against `apps/cli/` | Resume + JD never leave the user's machine. They use their own model and their own key. | Fully private. We store nothing. | +| **Hosted (web-ui + telegram)** | Everyone else | Resume + JD go to our backend, which calls the LLM provider with our key. | Not private by default. We do not persist resume text, but data does transit our infrastructure and the provider. This is stated up-front in the UI. | The hosted mode is the trade-off for being usable by non-technical people. The CLI mode is the escape hatch for anyone who wants zero data egress. @@ -86,4 +86,4 @@ The hosted mode is the trade-off for being usable by non-technical people. The C 1. Sends resume (file or text) + JD (text or URL) to the bot. 2. Bot forwards to the same `POST /eval` endpoint on `apps/server`. -3. Server returns the result, bot formats it as a Telegram message. \ No newline at end of file +3. Server returns the result, bot formats it as a Telegram message. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 96328f1..71e557f 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -39,9 +39,7 @@ async function handleEvaluate(args: string[]) { } const cvContent = readFileSync(resolve(cvPath), "utf-8"); - const jdContent = jdPath - ? readFileSync(resolve(jdPath), "utf-8") - : undefined; + const jdContent = jdPath ? readFileSync(resolve(jdPath), "utf-8") : undefined; const result = await evaluate({ cv: { content: cvContent, format: "markdown" }, diff --git a/packages/core/src/__tests__/evaluator.test.ts b/packages/core/src/__tests__/evaluator.test.ts index e6a177e..493946f 100644 --- a/packages/core/src/__tests__/evaluator.test.ts +++ b/packages/core/src/__tests__/evaluator.test.ts @@ -66,7 +66,8 @@ describe("evaluate", () => { }); it("scores higher when CV matches JD keywords", async () => { - const jd = "Looking for a Python engineer with PostgreSQL, Redis, and Kafka experience. Must have shipped production systems at scale."; + const jd = + "Looking for a Python engineer with PostgreSQL, Redis, and Kafka experience. Must have shipped production systems at scale."; const withJD = await evaluate({ cv: { content: strongCV, format: "markdown" }, @@ -76,9 +77,10 @@ describe("evaluate", () => { cv: { content: strongCV, format: "markdown" }, }); - expect(withJD.dimensions.find((d) => d.name === "Keyword Match")!.score) - .toBeGreaterThanOrEqual( - withoutJD.dimensions.find((d) => d.name === "Keyword Match")!.score - ); + expect( + withJD.dimensions.find((d) => d.name === "Keyword Match")!.score + ).toBeGreaterThanOrEqual( + withoutJD.dimensions.find((d) => d.name === "Keyword Match")!.score + ); }); }); diff --git a/packages/core/src/archetypes/index.ts b/packages/core/src/archetypes/index.ts index f5f9d51..6d1d027 100644 --- a/packages/core/src/archetypes/index.ts +++ b/packages/core/src/archetypes/index.ts @@ -20,23 +20,56 @@ ARCHETYPES.set("ai-product-manager", { name: "AI Product Manager", description: "Product managers building LLM-powered features and AI products", keywords: [ - "llm", "gpt", "claude", "gemini", "rag", "embeddings", "vector", - "fine-tuning", "prompt engineering", "a/b testing", "product-market fit", - "user research", "roadmap", "okrs", "kpis", "retention", "activation", - "funnel", "agile", "sprint", "jira", "linear", "amplitude", "mixpanel", - "hypothesis", "experiment", "hallucination", "evaluation", "guardrails", + "llm", + "gpt", + "claude", + "gemini", + "rag", + "embeddings", + "vector", + "fine-tuning", + "prompt engineering", + "a/b testing", + "product-market fit", + "user research", + "roadmap", + "okrs", + "kpis", + "retention", + "activation", + "funnel", + "agile", + "sprint", + "jira", + "linear", + "amplitude", + "mixpanel", + "hypothesis", + "experiment", + "hallucination", + "evaluation", + "guardrails", ], evaluationWeights: { - shippedEvidence: 0.30, - quantifiedImpact: 0.20, + shippedEvidence: 0.3, + quantifiedImpact: 0.2, toolingVisibility: 0.15, atsCompatibility: 0.15, - keywordMatch: 0.10, - publicProof: 0.10, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Shipped", "Owned", "Led", "Drove", "Defined", "Prioritized", - "Launched", "Measured", "Grew", "Reduced", "Coordinated", + "Shipped", + "Owned", + "Led", + "Drove", + "Defined", + "Prioritized", + "Launched", + "Measured", + "Grew", + "Reduced", + "Coordinated", ], antiPatterns: [ "passionate about AI", @@ -51,24 +84,62 @@ ARCHETYPES.set("ai-engineer", { name: "AI Engineer", description: "Engineers building production AI systems (RAG, agents, LLM apps)", keywords: [ - "python", "typescript", "pytorch", "tensorflow", "langchain", "langgraph", - "llamaindex", "rag", "vector database", "pinecone", "weaviate", "pgvector", - "embeddings", "fine-tuning", "rlhf", "vllm", "ray", "kubernetes", "docker", - "aws bedrock", "azure ai", "gcp vertex", "openai", "anthropic", "llama", - "mistral", "prompt engineering", "function calling", "tool use", "agents", - "evaluation", "deepeval", "langsmith", "mlops", "ci/cd", "fastapi", + "python", + "typescript", + "pytorch", + "tensorflow", + "langchain", + "langgraph", + "llamaindex", + "rag", + "vector database", + "pinecone", + "weaviate", + "pgvector", + "embeddings", + "fine-tuning", + "rlhf", + "vllm", + "ray", + "kubernetes", + "docker", + "aws bedrock", + "azure ai", + "gcp vertex", + "openai", + "anthropic", + "llama", + "mistral", + "prompt engineering", + "function calling", + "tool use", + "agents", + "evaluation", + "deepeval", + "langsmith", + "mlops", + "ci/cd", + "fastapi", ], evaluationWeights: { - shippedEvidence: 0.30, + shippedEvidence: 0.3, quantifiedImpact: 0.15, toolingVisibility: 0.25, - atsCompatibility: 0.10, - keywordMatch: 0.10, - publicProof: 0.10, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Built", "Architected", "Deployed", "Optimized", "Reduced", - "Migrated", "Implemented", "Scaled", "Automated", "Designed", + "Built", + "Architected", + "Deployed", + "Optimized", + "Reduced", + "Migrated", + "Implemented", + "Scaled", + "Automated", + "Designed", ], antiPatterns: [ "familiar with machine learning", @@ -83,23 +154,56 @@ ARCHETYPES.set("backend-engineer", { name: "Backend Engineer", description: "Server-side engineers building APIs, services, and infrastructure", keywords: [ - "api", "rest", "graphql", "grpc", "microservices", "distributed systems", - "postgresql", "mysql", "mongodb", "redis", "kafka", "rabbitmq", - "kubernetes", "docker", "aws", "gcp", "azure", "terraform", - "ci/cd", "github actions", "node.js", "python", "go", "java", "rust", - "typescript", "testing", "monitoring", "observability", "datadog", + "api", + "rest", + "graphql", + "grpc", + "microservices", + "distributed systems", + "postgresql", + "mysql", + "mongodb", + "redis", + "kafka", + "rabbitmq", + "kubernetes", + "docker", + "aws", + "gcp", + "azure", + "terraform", + "ci/cd", + "github actions", + "node.js", + "python", + "go", + "java", + "rust", + "typescript", + "testing", + "monitoring", + "observability", + "datadog", ], evaluationWeights: { shippedEvidence: 0.25, - quantifiedImpact: 0.20, - toolingVisibility: 0.20, + quantifiedImpact: 0.2, + toolingVisibility: 0.2, atsCompatibility: 0.15, - keywordMatch: 0.10, - publicProof: 0.10, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Built", "Architected", "Scaled", "Optimized", "Migrated", - "Designed", "Deployed", "Reduced", "Automated", "Implemented", + "Built", + "Architected", + "Scaled", + "Optimized", + "Migrated", + "Designed", + "Deployed", + "Reduced", + "Automated", + "Implemented", ], antiPatterns: [ "team player", @@ -114,23 +218,55 @@ ARCHETYPES.set("frontend-engineer", { name: "Frontend Engineer", description: "Engineers building web UIs, design systems, and user experiences", keywords: [ - "react", "next.js", "vue", "svelte", "typescript", "javascript", - "css", "tailwind", "html", "accessibility", "a11y", "wcag", - "performance", "core web vitals", "webpack", "vite", "testing", - "jest", "playwright", "cypress", "storybook", "design system", - "responsive", "mobile", "pwa", "ssr", "ssg", "graphql", "rest", + "react", + "next.js", + "vue", + "svelte", + "typescript", + "javascript", + "css", + "tailwind", + "html", + "accessibility", + "a11y", + "wcag", + "performance", + "core web vitals", + "webpack", + "vite", + "testing", + "jest", + "playwright", + "cypress", + "storybook", + "design system", + "responsive", + "mobile", + "pwa", + "ssr", + "ssg", + "graphql", + "rest", ], evaluationWeights: { shippedEvidence: 0.25, - quantifiedImpact: 0.20, - toolingVisibility: 0.20, + quantifiedImpact: 0.2, + toolingVisibility: 0.2, atsCompatibility: 0.15, - keywordMatch: 0.10, - publicProof: 0.10, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Built", "Shipped", "Designed", "Implemented", "Optimized", - "Reduced", "Created", "Migrated", "Led", "Improved", + "Built", + "Shipped", + "Designed", + "Implemented", + "Optimized", + "Reduced", + "Created", + "Migrated", + "Led", + "Improved", ], antiPatterns: [ "pixel-perfect", @@ -143,31 +279,83 @@ ARCHETYPES.set("frontend-engineer", { ARCHETYPES.set("qa-test-engineer", { id: "qa-test-engineer", name: "QA / Test Engineer", - description: "Quality assurance and test automation engineers ensuring software reliability", + description: + "Quality assurance and test automation engineers ensuring software reliability", keywords: [ - "playwright", "cypress", "selenium", "jest", "vitest", "mocha", "jasmine", - "test automation", "e2e testing", "integration testing", "unit testing", - "ci/cd", "github actions", "gitlab ci", "jenkins", "docker", "kubernetes", - "api testing", "postman", "rest assured", "graphql testing", - "performance testing", "k6", "jmeter", "load testing", "stress testing", - "regression testing", "smoke testing", "exploratory testing", "test planning", - "bug tracking", "jira", "testrail", "qase", "allure", "reportportal", - "bdd", "cucumber", "gherkin", "tdd", "contract testing", "pact", - "accessibility testing", "a11y", "axe", "lighthouse", "visual regression", - "mobile testing", "appium", "detox", "xcuitest", "espresso", + "playwright", + "cypress", + "selenium", + "jest", + "vitest", + "mocha", + "jasmine", + "test automation", + "e2e testing", + "integration testing", + "unit testing", + "ci/cd", + "github actions", + "gitlab ci", + "jenkins", + "docker", + "kubernetes", + "api testing", + "postman", + "rest assured", + "graphql testing", + "performance testing", + "k6", + "jmeter", + "load testing", + "stress testing", + "regression testing", + "smoke testing", + "exploratory testing", + "test planning", + "bug tracking", + "jira", + "testrail", + "qase", + "allure", + "reportportal", + "bdd", + "cucumber", + "gherkin", + "tdd", + "contract testing", + "pact", + "accessibility testing", + "a11y", + "axe", + "lighthouse", + "visual regression", + "mobile testing", + "appium", + "detox", + "xcuitest", + "espresso", ], evaluationWeights: { - shippedEvidence: 0.20, + shippedEvidence: 0.2, quantifiedImpact: 0.25, toolingVisibility: 0.25, - atsCompatibility: 0.10, - keywordMatch: 0.10, - publicProof: 0.10, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Automated", "Identified", "Reduced", "Prevented", "Designed", - "Implemented", "Executed", "Validated", "Debugged", "Optimized", - "Documented", "Collaborated", + "Automated", + "Identified", + "Reduced", + "Prevented", + "Designed", + "Implemented", + "Executed", + "Validated", + "Debugged", + "Optimized", + "Documented", + "Collaborated", ], antiPatterns: [ "manually tested everything", @@ -184,23 +372,55 @@ ARCHETYPES.set("devops-sre", { name: "DevOps / SRE", description: "Site reliability engineers and DevOps specialists", keywords: [ - "kubernetes", "docker", "terraform", "ansible", "aws", "gcp", "azure", - "ci/cd", "github actions", "gitlab ci", "jenkins", "argocd", - "prometheus", "grafana", "datadog", "elk", "observability", - "incident management", "sla", "slo", "sli", "linux", "networking", - "security", "iam", "vault", "helm", "istio", "service mesh", + "kubernetes", + "docker", + "terraform", + "ansible", + "aws", + "gcp", + "azure", + "ci/cd", + "github actions", + "gitlab ci", + "jenkins", + "argocd", + "prometheus", + "grafana", + "datadog", + "elk", + "observability", + "incident management", + "sla", + "slo", + "sli", + "linux", + "networking", + "security", + "iam", + "vault", + "helm", + "istio", + "service mesh", ], evaluationWeights: { - shippedEvidence: 0.20, + shippedEvidence: 0.2, quantifiedImpact: 0.25, toolingVisibility: 0.25, - atsCompatibility: 0.10, - keywordMatch: 0.10, - publicProof: 0.10, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Automated", "Reduced", "Scaled", "Built", "Migrated", - "Improved", "Designed", "Implemented", "Monitored", "Secured", + "Automated", + "Reduced", + "Scaled", + "Built", + "Migrated", + "Improved", + "Designed", + "Implemented", + "Monitored", + "Secured", ], antiPatterns: [ "managed servers", @@ -215,23 +435,54 @@ ARCHETYPES.set("data-engineer", { name: "Data Engineer", description: "Engineers building data pipelines, warehouses, and analytics platforms", keywords: [ - "python", "sql", "spark", "airflow", "dagster", "dbt", - "snowflake", "bigquery", "redshift", "databricks", "delta lake", - "kafka", "flink", "etl", "elt", "data modeling", "dimensional", - "data warehouse", "data lake", "lakehouse", "parquet", "iceberg", - "aws", "gcp", "azure", "terraform", "docker", "kubernetes", + "python", + "sql", + "spark", + "airflow", + "dagster", + "dbt", + "snowflake", + "bigquery", + "redshift", + "databricks", + "delta lake", + "kafka", + "flink", + "etl", + "elt", + "data modeling", + "dimensional", + "data warehouse", + "data lake", + "lakehouse", + "parquet", + "iceberg", + "aws", + "gcp", + "azure", + "terraform", + "docker", + "kubernetes", ], evaluationWeights: { shippedEvidence: 0.25, quantifiedImpact: 0.25, - toolingVisibility: 0.20, - atsCompatibility: 0.10, - keywordMatch: 0.10, - publicProof: 0.10, + toolingVisibility: 0.2, + atsCompatibility: 0.1, + keywordMatch: 0.1, + publicProof: 0.1, }, actionVerbs: [ - "Built", "Designed", "Optimized", "Migrated", "Automated", - "Reduced", "Scaled", "Implemented", "Modeled", "Orchestrated", + "Built", + "Designed", + "Optimized", + "Migrated", + "Automated", + "Reduced", + "Scaled", + "Implemented", + "Modeled", + "Orchestrated", ], antiPatterns: [ "worked with data", @@ -265,10 +516,7 @@ export function registerArchetype(archetype: RoleArchetype): void { * Auto-detect the best archetype based on CV content and optional JD. * Uses keyword frequency matching to determine the closest fit. */ -export function detectArchetype( - cvContent: string, - jdContent?: string -): RoleArchetype { +export function detectArchetype(cvContent: string, jdContent?: string): RoleArchetype { const text = `${cvContent} ${jdContent || ""}`.toLowerCase(); let bestMatch: RoleArchetype | null = null; diff --git a/packages/core/src/builder/index.ts b/packages/core/src/builder/index.ts index e87f7b5..d85feb3 100644 --- a/packages/core/src/builder/index.ts +++ b/packages/core/src/builder/index.ts @@ -29,9 +29,7 @@ export async function tailor( const cvLower = options.cv.content.toLowerCase(); const matched = jdKeywords.filter((kw) => cvLower.includes(kw.toLowerCase())); - const missing = jdKeywords.filter( - (kw) => !cvLower.includes(kw.toLowerCase()) - ); + const missing = jdKeywords.filter((kw) => !cvLower.includes(kw.toLowerCase())); const changes: CVChange[] = []; const markdown = options.cv.content; @@ -48,16 +46,12 @@ export async function tailor( /** * Generates improvement suggestions without modifying the CV. */ -export function suggest( - options: TailorOptions -): Suggestion[] { +export function suggest(options: TailorOptions): Suggestion[] { const suggestions: Suggestion[] = []; const cv = options.cv.content; const jdKeywords = extractKeywords(options.jd.content); - const missing = jdKeywords.filter( - (kw) => !cv.toLowerCase().includes(kw.toLowerCase()) - ); + const missing = jdKeywords.filter((kw) => !cv.toLowerCase().includes(kw.toLowerCase())); if (missing.length > 5) { suggestions.push({ @@ -95,12 +89,54 @@ export function suggest( function extractKeywords(text: string): string[] { const stopWords = new Set([ - "the", "a", "an", "is", "are", "was", "were", "be", "been", - "being", "have", "has", "had", "do", "does", "did", "will", - "would", "could", "should", "may", "might", "shall", "can", - "and", "or", "but", "if", "in", "on", "at", "to", "for", - "of", "with", "by", "from", "as", "we", "you", "they", - "this", "that", "our", "your", "about", "who", "what", + "the", + "a", + "an", + "is", + "are", + "was", + "were", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + "may", + "might", + "shall", + "can", + "and", + "or", + "but", + "if", + "in", + "on", + "at", + "to", + "for", + "of", + "with", + "by", + "from", + "as", + "we", + "you", + "they", + "this", + "that", + "our", + "your", + "about", + "who", + "what", ]); const words = text diff --git a/packages/core/src/evaluator/index.ts b/packages/core/src/evaluator/index.ts index 8998979..ea92019 100644 --- a/packages/core/src/evaluator/index.ts +++ b/packages/core/src/evaluator/index.ts @@ -81,9 +81,7 @@ function scoreDimensions( { name: "Keyword Match", weight: weights.keywordMatch, - score: options.jd - ? scoreKeywordMatch(options.cv.content, options.jd.content) - : 3, + score: options.jd ? scoreKeywordMatch(options.cv.content, options.jd.content) : 3, maxScore: 5, feedback: "", }, @@ -121,7 +119,8 @@ function scoreShippedEvidence(cv: string): number { } function scoreQuantifiedImpact(cv: string): number { - const numberPatterns = /\d+[%xX]|\$[\d,]+|\d+\+?\s*(users|customers|teams|engineers|requests)/g; + const numberPatterns = + /\d+[%xX]|\$[\d,]+|\d+\+?\s*(users|customers|teams|engineers|requests)/g; const matches = cv.match(numberPatterns) || []; if (matches.length >= 8) return 5; if (matches.length >= 5) return 4; @@ -140,9 +139,7 @@ function scoreToolingVisibility(cv: string, archetype: RoleArchetype): number { function scoreKeywordMatch(cv: string, jd: string): number { const jdWords = extractKeywords(jd); - const matched = jdWords.filter((w) => - cv.toLowerCase().includes(w.toLowerCase()) - ); + const matched = jdWords.filter((w) => cv.toLowerCase().includes(w.toLowerCase())); const ratio = matched.length / Math.max(jdWords.length, 1); if (ratio >= 0.7) return 5; if (ratio >= 0.5) return 4; @@ -193,29 +190,74 @@ function findStrengths(cv: string, archetype: RoleArchetype): string[] { if (/\d+%/.test(cv)) strengths.push("Uses quantified metrics"); if (/github\.com/i.test(cv)) strengths.push("Links to public code"); - if (/shipped|launched|deployed/i.test(cv)) - strengths.push("Shows shipped work"); + if (/shipped|launched|deployed/i.test(cv)) strengths.push("Shows shipped work"); const keywordMatches = archetype.keywords.filter((kw) => cv.toLowerCase().includes(kw.toLowerCase()) ); if (keywordMatches.length > 3) - strengths.push( - `Strong keyword coverage (${keywordMatches.length} matches)` - ); + strengths.push(`Strong keyword coverage (${keywordMatches.length} matches)`); return strengths; } function extractKeywords(jd: string): string[] { const stopWords = new Set([ - "the", "a", "an", "is", "are", "was", "were", "be", "been", - "being", "have", "has", "had", "do", "does", "did", "will", - "would", "could", "should", "may", "might", "shall", "can", - "and", "or", "but", "if", "in", "on", "at", "to", "for", - "of", "with", "by", "from", "as", "into", "through", "during", - "before", "after", "above", "below", "between", "we", "you", - "they", "this", "that", "these", "those", "our", "your", + "the", + "a", + "an", + "is", + "are", + "was", + "were", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + "may", + "might", + "shall", + "can", + "and", + "or", + "but", + "if", + "in", + "on", + "at", + "to", + "for", + "of", + "with", + "by", + "from", + "as", + "into", + "through", + "during", + "before", + "after", + "above", + "below", + "between", + "we", + "you", + "they", + "this", + "that", + "these", + "those", + "our", + "your", ]); const words = jd diff --git a/packages/core/src/rules/index.ts b/packages/core/src/rules/index.ts index 977a6f2..b1d8e45 100644 --- a/packages/core/src/rules/index.ts +++ b/packages/core/src/rules/index.ts @@ -88,22 +88,24 @@ export const UNIVERSAL_RULES = { }, { name: "Strong/excellent skills claim", - match: "(strong|excellent|exceptional) (communication|problem[- ]solving|leadership) skills", + match: + "(strong|excellent|exceptional) (communication|problem[- ]solving|leadership) skills", why: "Self-assessed soft skills are ignored by screeners. Actions prove skills.", fix: "Replace with evidence: 'Mentored 3 junior engineers to promotion' or 'Presented quarterly results to 200-person all-hands'", severity: "minor", }, { name: "Outdated tool without modern stack", - match: "\\b(jQuery(?!.*\\b(React|Vue|Angular|Svelte)\\b)|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN(?!.*\\bGit\\b)|CoffeeScript|Backbone\\.js|(?:Grunt|Gulp)(?!.*\\b(Webpack|Vite|Rollup|Parcel|esbuild)\\b))\\b", + match: + "\\b(jQuery(?!.*\\b(React|Vue|Angular|Svelte)\\b)|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN(?!.*\\bGit\\b)|CoffeeScript|Backbone\\.js|(?:Grunt|Gulp)(?!.*\\b(Webpack|Vite|Rollup|Parcel|esbuild)\\b))\\b", why: "Listing deprecated or legacy technologies without modern frameworks makes your resume look stale and lack growth.", fix: "Replace with modern alternatives or remove the outdated technology.", severity: "major", }, { name: "Outdated tool with modern stack", - match: - "\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b.*\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b|\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b.*\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b", + match: + "\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b.*\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b|\\b(React|Vue|Angular|Svelte|Git|Webpack|Vite|Rollup|Parcel|esbuild)\\b.*\\b(jQuery|AngularJS|PHP 5(?:\\.\\d+)?|Python 2(?:\\.\\d+)?|SVN|Heroku|CoffeeScript|Backbone\\.js|Grunt|Gulp)\\b", why: "Listing outdated technologies alongside modern frameworks can be confusing and may not accurately represent your skills.", fix: "Remove outdated technologies to show focus on modern skills.", severity: "minor", diff --git a/research/sources.md b/research/sources.md index ef5e4bd..57ce720 100644 --- a/research/sources.md +++ b/research/sources.md @@ -6,60 +6,65 @@ Last updated: May 2026 ## Market Data -| Stat | Value | Source | URL | -|------|-------|--------|-----| -| AI PM posting growth | 300% since 2023 | Best PM Jobs | bestpmjobs.com | -| Open AI PM positions | 600+ globally | Best PM Jobs | bestpmjobs.com | -| Applications per AI PM role | 3x traditional PM | Institute of Product Management | institutepm.com | -| AI PM total comp range | $192K-$437K | Best PM Jobs | bestpmjobs.com | -| Employers reporting AI skills gap | 47% | Industry survey | - | -| Prefer junior+AI over senior+no AI | 71% | Industry survey | - | -| Companies using AI resume screening | 70% | GetNewResume | getnewresume.com | -| Qualified applicants rejected by ATS | 75% | TieCV | tiecv.com | -| Fortune 500 using ATS | 97-98% | TieCV | tiecv.com | -| ML resumes rejected (missing MLOps) | 68% | NeuraCV | neuracv.com | -| RAG in new AI engineering roles | 70% | MirrorCV | mirrorcv.com | +| Stat | Value | Source | URL | +| ------------------------------------ | ----------------- | ------------------------------- | ---------------- | +| AI PM posting growth | 300% since 2023 | Best PM Jobs | bestpmjobs.com | +| Open AI PM positions | 600+ globally | Best PM Jobs | bestpmjobs.com | +| Applications per AI PM role | 3x traditional PM | Institute of Product Management | institutepm.com | +| AI PM total comp range | $192K-$437K | Best PM Jobs | bestpmjobs.com | +| Employers reporting AI skills gap | 47% | Industry survey | - | +| Prefer junior+AI over senior+no AI | 71% | Industry survey | - | +| Companies using AI resume screening | 70% | GetNewResume | getnewresume.com | +| Qualified applicants rejected by ATS | 75% | TieCV | tiecv.com | +| Fortune 500 using ATS | 97-98% | TieCV | tiecv.com | +| ML resumes rejected (missing MLOps) | 68% | NeuraCV | neuracv.com | +| RAG in new AI engineering roles | 70% | MirrorCV | mirrorcv.com | ## Expert Opinions ### Andrej Karpathy (Ex-OpenAI, Tesla AI Director) + - "Intent engineering" as the new senior skill - Source: htek.dev, the-decoder.com, Medium (2025-2026) ### Marty Cagan (Silicon Valley Product Group) + - PMs more important in AI era, not less - Product sense + AI tools = rare valuable combo - Source: svpg.com (2025-2026) ### Claire Vo (LaunchDarkly, Color, Optimizely, ChatPRD founder) + - "Capital C" communication stays human - Source: lennysvault.com (2025-2026) ### Lenny Rachitsky (Lenny's Newsletter, ex-Airbnb PM) + - Four PM evaluation dimensions: product sense, analytical ability, execution, communication - Source: lennyrachitsky.wiki ## Recruiter Insights -| Insight | Source | -|---------|--------| +| Insight | Source | +| -------------------------------- | ------------------------ | | Personalization beats automation | Madison Vitug @ LinkedIn | -| Speed at Meta: apply within 24h | jobstrack.io | -| Warm intros = 10x conversion | Chris Allaire (Substack) | -| High-ownership language works | Meta hiring guides | +| Speed at Meta: apply within 24h | jobstrack.io | +| Warm intros = 10x conversion | Chris Allaire (Substack) | +| High-ownership language works | Meta hiring guides | ## ATS Research -| Finding | Source | -|---------|--------| -| Gen 3 ATS uses LLM-based screening | TieCV, Hivekit | -| Quantified impact increasingly weighted | Multiple sources | -| Keyword stuffing now penalized | TieCV, GetNewResume | -| Single column still safest format | All sources agree | +| Finding | Source | +| --------------------------------------- | ------------------- | +| Gen 3 ATS uses LLM-based screening | TieCV, Hivekit | +| Quantified impact increasingly weighted | Multiple sources | +| Keyword stuffing now penalized | TieCV, GetNewResume | +| Single column still safest format | All sources agree | ## Contributing When adding a new source: + 1. Include the full URL 2. Note the date accessed 3. Quote the specific claim with page/section reference From b99db73c60e3657011fea54a7efd35b48925b3e9 Mon Sep 17 00:00:00 2001 From: SaharPak Date: Thu, 11 Jun 2026 22:58:17 +0300 Subject: [PATCH 17/39] chore: expand CodeRabbit config with per-package review guidance Add path_instructions for core/cli/web packages and markdown docs, enable ESLint and markdownlint tools, ignore build/dependency output, disable the review poem, and turn on knowledge base learnings. --- .coderabbit.yaml | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 12049ed..604bf28 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,10 +1,12 @@ -# yaml-language-server: $schema=https://docs.coderabbit.ai/schema.json +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json language: en-US +early_access: false reviews: profile: chill request_changes_workflow: false high_level_summary: true + poem: false review_status: true auto_review: enabled: true @@ -13,6 +15,38 @@ reviews: - main path_filters: - "!**/pnpm-lock.yaml" + - "!**/dist/**" + - "!**/.turbo/**" + - "!**/node_modules/**" + - "!**/*.snap" + path_instructions: + - path: "packages/core/**" + instructions: > + This is the core CV evaluation/builder logic. Prioritize correctness of + rules, evaluator scoring, and types. Flag changes to public APIs in + src/index.ts and anything in src/types.ts that could break consumers. + New rules/archetypes/evaluator behavior should have matching vitest tests. + - path: "packages/cli/**" + instructions: > + CLI for the CV builder. Watch for unhandled errors, confusing output, + and missing input validation. Behavior changes should be covered by tests. + - path: "packages/web/**" + instructions: > + Web frontend. Check accessibility, error/loading states, and that UI + stays consistent with the core package's data model. + - path: "**/*.md" + instructions: > + Docs for an open-source project. Check clarity and that setup/commands + match the actual scripts in package.json. + tools: + eslint: + enabled: true + markdownlint: + enabled: true chat: auto_reply: true + +knowledge_base: + learnings: + scope: auto From fa014fe4982dc5baf1ee80f9469e179a4471aa56 Mon Sep 17 00:00:00 2001 From: "a.hakimi" Date: Fri, 12 Jun 2026 12:21:25 +0330 Subject: [PATCH 18/39] feat: add evaluation form and results page with file upload functionality --- .../src/app/components/EvaluateForm.tsx | 99 +++++++++++++++++++ apps/web-ui/src/app/components/FileUpload.tsx | 87 ++++++++++++++++ apps/web-ui/src/app/components/ScoreCard.tsx | 37 +++++++ apps/web-ui/src/app/components/TextStats.tsx | 16 +++ apps/web-ui/src/app/lib/evaluation-storage.ts | 41 ++++++++ apps/web-ui/src/app/page.tsx | 3 + apps/web-ui/src/app/results/page.tsx | 86 ++++++++++++++++ 7 files changed, 369 insertions(+) create mode 100644 apps/web-ui/src/app/components/EvaluateForm.tsx create mode 100644 apps/web-ui/src/app/components/FileUpload.tsx create mode 100644 apps/web-ui/src/app/components/ScoreCard.tsx create mode 100644 apps/web-ui/src/app/components/TextStats.tsx create mode 100644 apps/web-ui/src/app/lib/evaluation-storage.ts create mode 100644 apps/web-ui/src/app/results/page.tsx diff --git a/apps/web-ui/src/app/components/EvaluateForm.tsx b/apps/web-ui/src/app/components/EvaluateForm.tsx new file mode 100644 index 0000000..1b4c959 --- /dev/null +++ b/apps/web-ui/src/app/components/EvaluateForm.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { evaluate } from "@cv-builder/core"; +import { TextStats } from "./TextStats"; +import { saveEvaluationResult } from "../lib/evaluation-storage"; +import { FileUpload } from "./FileUpload"; + +export function EvaluateForm() { + const router = useRouter(); + + const [cv, setCv] = useState(""); + const [jd, setJd] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleEvaluate() { + if (!cv.trim()) { + alert("Please paste your CV."); + return; + } + + + setLoading(true); + + try { + const result = await evaluate({ + cv: { + content: cv, + format: "plaintext", + }, + jd: { + content: jd, + }, + }); + + saveEvaluationResult(result); + + router.push("/results"); + } catch (error) { + console.error(error); + alert("Evaluation failed."); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+ + +