-
Notifications
You must be signed in to change notification settings - Fork 45
feat(decisions): percentage-based skills scoring and AI improvements #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedFailed to post review comments WalkthroughThis PR introduces a comprehensive AI-assisted job-matching feature called "Decisions," adds extensive documentation (FAQ and docs site), establishes repository guidelines, modifies profile routing, and implements supporting database schema and API infrastructure for candidate-job evaluation and preference management. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Page as DecisionsPage
participant Jobs as JobsPane
participant Tree as DecisionTreePane
participant Resume as ResumePane
participant API as /api/decisions/*
participant AI as GPT-4o-mini
participant DB as Supabase
User->>Page: Load /[username]/decisions
Page->>API: POST /decisions/jobs (fetch candidates)
API->>DB: Query jobs from last 90 days
DB-->>API: Return jobs list
API-->>Page: Return ranked jobs
Page->>Page: useJobMatching (score jobs locally)
Page-->>Jobs: Pass ranked jobs
User->>Jobs: Select job
Jobs->>Tree: Call evaluateJob(resume, job)
Tree->>API: POST /decisions/evaluate
API->>AI: Generate text with criteria tools
AI-->>API: Tool calls (skills, experience, location...)
API-->>Tree: Return decision breakdown
Tree->>Tree: animateAIPath (highlight path)
Tree-->>Page: Update nodes/edges with colors
Page-->>Jobs: Display match analysis
User->>Jobs: Click Pass/Interested
Jobs->>API: POST /decisions/decision
API->>DB: Upsert job_decisions
DB-->>API: Return decision record
API-->>Page: Success
Page->>Page: Refetch jobs (filter decided ones)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The changes span multiple categories with significant logic density: (1) complex React component architecture with interdependent state management and hooks (DecisionTreePane, JobDetail, useDecisionTree); (2) AI integration with multiple tool definitions and result aggregation (evaluate route); (3) database schema with RLS policies and triggers (three migrations); (4) configuration modules with graph layout algorithms (decisionTree.js, designSystem.js, matchingCriteria.js); and (5) heterogeneous file types (docs, components, API routes, migrations, configs). While many changes are self-contained (e.g., documentation files, scripts), the feature core requires cross-file reasoning to verify data flow, state synchronization, and API contracts. Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Implements an interactive three-pane decision tree visualization inspired by nof1.ai that shows candidates why they match (or don't match) specific jobs. Features: - Three-pane fullscreen layout (resume, decision tree, ranked jobs) - React Flow decision tree with animated path traversal - Rules-based scoring (8 criteria, weighted to 100 points) - 3 outcomes: Strong Match, Possible Match, Not a Match - nof1.ai-inspired design system (clean, professional, data-dense) - Authenticated access (uses ResumeProvider) - Desktop-optimized (mobile in future phases) Technical: - React Flow (@xyflow/react) for tree visualization - Custom hooks (useJobMatching, useDecisionTree) - Modular config (designSystem, decisionTree, matchingCriteria) - All files <200 lines (per CLAUDE.md standards) - Supabase schema ready for Phase 2 AI caching Structure: - page.js: Main orchestrator - components/: ResumePane, DecisionTreePane, JobsPane - hooks/: useJobMatching, useDecisionTree - config/: designSystem, decisionTree, matchingCriteria Phase 2 (next): AI-enhanced matching with GPT-4o-mini, semantic skill matching, and Supabase caching. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Fixed 'Objects are not valid as a React child' error when rendering location data. Changes: - ResumePane: Safely extract city/region from location object - matchingCriteria: Remove object fallback, use nested properties only Error was caused by trying to render location object directly instead of extracting string properties. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Check if job.location is string or object before rendering - Extract city/region properties when location is object - Prevents React 'Objects are not valid as a React child' error Closes issue with location rendering in jobs list
- Handle job.location object by extracting city/region properties - Prevents location object from being interpolated into reason strings - Fixes 500 error: 'Objects are not valid as a React child' This was the final location object rendering issue causing the page crash.
- Create /api/jobs POST endpoint that accepts limit parameter - Fetch only 10 jobs from database for testing (instead of all jobs) - More efficient than fetching all jobs and slicing on client - Supports username and limit in request body - Caps limit at 1000 max for safety
- Move app/api/jobs/route.js to app/api/decisions/jobs/route.js - Update decisions page to call /api/decisions/jobs - Resolves conflict with existing pages/api/jobs.js (AI matching) - Decisions page now has its own simple jobs endpoint
…issue - Replaced dynamic location rendering with hardcoded 'Remote' - Added debug logging for non-string job.location - Page now loads successfully - confirms location was the issue - Next: implement proper location string extraction
- Created getLocationString() helper to handle string/object locations - Handles location.city, location.region, location.country - Removes debug logging - Replaces hardcoded 'Remote' with proper location rendering - Fixes: Objects are not valid as a React child error
- Move from app/[username]/decisions to app/decisions/[username] - Changes URL from /thomasdavis/decisions to /decisions/thomasdavis - Add auth checks in decisions layout (no longer inherits from parent) - Remove ProfileLayout check from parent layout (no longer needed) - Removes left sidebar navigation from decisions page - Achieves fullscreen standalone layout as originally requested
…leLayout" This reverts commit a56535e. User wants to keep top nav, just remove sidebar.
- Add 'decisions' to PUBLIC_PAGES in ProfileLayout - Bypasses ProfileCard and NavigationMenu (sidebar) - Keeps root layout with top Menu component - Add ResumeProvider to decisions layout - URL stays clean: /thomasdavis/decisions - Fullscreen three-pane layout with top nav only
- Conflict with existing pages/api/jobs.js (AI matching endpoint) - Decisions page uses /api/decisions/jobs instead
- Create /api/decisions/evaluate endpoint using GPT-4o-mini - Define 8 Zod tool schemas for decision criteria - Replace rules-based matching with AI analysis - AI evaluates full resume + job description context - Tool calls answer each decision tree question - Animate path based on AI decisions - Show loading state during AI evaluation - Display AI reasoning for each criterion Implements Phase 2 from DECISIONS_PRD.md
…nauthed access - Fixed AI SDK v5 tool definitions to use inputSchema instead of parameters - Changed from ResumeProvider to PublicResumeProvider for unauthed access - Updated page to use usePublicResume hook instead of useResume - AI evaluation now successfully calls GPT-4o-mini with Zod tool schemas - Decisions page works without authentication via /[username]/decisions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- AI SDK v5 returns tool call data in .input property, not .args - Added comprehensive debug logging to server and client - AI was calling tools correctly but we weren't reading the results - Now properly extracts all 8 decision criteria from AI tool calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Export nodeStyle function from decisionTree config - Add updateNodeColor function to change node colors dynamically - Color nodes green when criteria pass, red when they fail - Reset nodes to default state when selecting new job - Enhanced visual feedback shows decision path clearly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Integrate dagre graph layout library for automatic node positioning - Use smoothstep edges for better routing around nodes - Configure dagre with optimal spacing (nodesep: 80, ranksep: 120, edgesep: 30) - Calculate positions dynamically to minimize edge crossings - Edges now route cleanly without overlapping 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Remove auto-select useEffect that was triggering job evaluation on page load - Jobs now only evaluate when user manually clicks on them - After making a decision, job list refreshes without auto-evaluating next job - Resolves unwanted page reload behavior after decisions
- Create Supabase migration for job_evaluation_preferences table - Store user-specific criteria toggles and values - RLS policies for user data security - Unique constraint per user-criterion pair - Add PreferencesPanel UI component - Collapsible panel with sexy modern design - Toggle switches for each evaluation criterion - Dual sliders for salary and experience ranges - Real-time save to Supabase - Smooth animations and transitions - Create API routes for preferences - GET /api/decisions/preferences - fetch user preferences - POST /api/decisions/preferences - upsert preferences - Integrate preferences throughout the system - Pass preferences to decision tree visualization - Update scoring algorithm to skip disabled criteria - Re-evaluate jobs when preferences change - Decision tree nodes hide/show based on preferences - Update matching algorithm - scoreCandidateForJob now respects enabled/disabled criteria - Skip salary checks if user doesn't care about salary - Skip location/timezone if user disabled them - Dynamic scoring based on enabled criteria only Benefits: - Users can customize what matters to them - Focus on relevant criteria only - Preferences persist across sessions - Real-time updates to scoring and visualization
…eferences - Add useEffect to update node visibility when preferences change - Hide nodes when their corresponding criterion is disabled - Hide edges connected to hidden nodes - Move nodeToPreferenceMap outside component to avoid dependency issues - Nodes now instantly disappear/appear when toggling preferences Fixes issue where changing preferences didn't update the decision tree diagram
- Recalculate dagre layout when preferences change - Only position visible nodes, hidden nodes stay in place - Create bridge edges that skip over hidden nodes - Maintain connectivity from root to outcome nodes - Add smooth CSS transitions (0.3s ease-in-out) for animations - Nodes smoothly reposition when criteria are toggled New functions: - recalculateLayout() - Recalculates positions for visible nodes only - createBridgeEdges() - Creates edges that skip hidden nodes - Finds next visible downstream node recursively - Prevents duplicate bridge connections Fixes: - Decision tree now properly recalculates positions - Edges route around hidden nodes instead of breaking - Always maintains valid path from start to end - Smooth animations when toggling preferences
- Use dagre 'longest-path' ranker for better hierarchy - Pin outcome nodes (Strong/Possible/Not a Match) to 'sink' rank (bottom) - Pin root node to 'source' rank (top) - Align all outcome nodes to same Y level - Ensures clean top-to-bottom flow regardless of hidden nodes Fixes layout issue where outcome nodes would appear at random Y positions when criteria were disabled
AI Evaluation Fix: - Send COMPLETE resume JSON to AI (was only sending limited fields) - AI now has access to all work history, skills, education, etc. - Fixes issue where AI couldn't see Rails or other skills in work history Resume Display Enhancements: - Show full candidate summary/bio - Display complete work history (top 3 positions with dates, company, summary, highlights) - Show education with degrees and institutions - Display all skills (removed 12-skill limit) with better styling - Add languages section with fluency levels - Add certifications section with dates - Show phone number if available - Better visual hierarchy with emoji icons - Improved date formatting (e.g., 'Jan 2020 - Present') - Better spacing and typography - Work experience with left border accent The resume panel now shows comprehensive candidate information instead of just a brief summary, giving full context for AI matching.
- Modified useDecisionTree to include preferences in evaluation payload - Updated AI evaluation endpoint to accept preferences parameter - Built preferences context for AI prompt explaining: - Which criteria are disabled (AI should be lenient) - Custom values like salary ranges (AI should use these instead of job requirements) - AI now receives instructions to respect user preferences in matching This allows users to customize the evaluation based on what they care about, e.g. if they disable salary, AI will be lenient on salary mismatches, or if they set custom salary expectations, AI will use those instead of job's range. Closes part of preferences integration work
BREAKING CHANGE: Skills evaluation now uses flexible percentage matching instead of binary pass/fail Changes: - Updated checkRequiredSkills tool schema to use matchPercentage (0.0-1.0) - Scoring thresholds: >=0.8 excellent (green), >=0.5 acceptable (orange), <0.5 insufficient (red) - Skills score now scales with match percentage (max 40 points) - Partial match (50-80%) continues evaluation but may downgrade to possible match - Updated path animation to show orange for partial matches, green for excellent Rationale: - More realistic - most great candidates have 70-80% of required skills - Candidates can learn missing skills on the job - Binary all-or-nothing was too stringent and rejected good candidates - Matches real-world hiring practices Example: Candidate with 75% skill match (e.g., has React/Node/Postgres but missing Redis) - Old: Instant reject - New: Scores 30/40 points on skills, continues evaluation, may get possible match or strong match depending on other criteria
- Add stripEmbeddings() helper to filter out embedding fields - Update /api/decisions/jobs to strip embeddings from job lists - Update /api/jobs/[uuid] to strip embeddings from single job - Reduces payload size - embeddings are large arrays not needed on client - Only /api/job-similarity keeps embeddings (needs them for calculations) Closes user request to remove embeddings from client/server communication
- Updated checkRequiredSkills tool description to search ALL resume sections - Added explicit instructions to check work.highlights, work.summary, projects - Previously AI only checked skills array, missing Rails in work experience - Example: 'Node.js / Rails' in Blockbid highlights was being missed This fixes the issue where the AI was incorrectly reporting missing skills that were actually mentioned in work experience or project highlights.
- Removed checkBonusSkills AI tool from evaluation endpoint - Removed BONUS node from decision tree config - Updated edges to connect salary directly to strong/possible outcomes - Removed all bonusCheck logic from useDecisionTree hook - Removed BONUS from nodeToPreferenceMap - Updated max score calculation (removed 5 bonus points) The bonus skills check was redundant - required skills percentage already captures skill match quality. Simpler decision tree with 7 criteria instead of 8 makes evaluations clearer and faster.
7c16cf9 to
d22e79f
Compare
- Remove unused 'awards' variable in ResumePane - Prefix unused embedding variables with underscore - Prefix unused 'username' variable with underscore - Add animateAIPath to useCallback dependencies - Prefix unused _failedAtNode variable with underscore All ESLint errors from build failure are now fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 29
🧹 Nitpick comments (50)
apps/registry/app/api/jobs/[uuid]/route.js (1)
10-25: Consider centralizing the stripEmbeddings helper.The implementation correctly removes embedding fields to reduce payload size. However, the AI summary indicates a similar
stripEmbeddingsfunction exists inapps/registry/app/api/decisions/jobs/route.js.Consider extracting this helper to a shared utility module to avoid duplication:
// lib/jobUtils.js or similar export function stripEmbeddings(job) { if (!job) return job; const { embedding: _embedding, embedding_v2: _embedding_v2, embedding_v3: _embedding_v3, embedding_v4: _embedding_v4, embedding_v5: _embedding_v5, ...jobWithoutEmbeddings } = job; return jobWithoutEmbeddings; }Then import and use it in both route files.
apps/docs/FAQ_COVERAGE.md (1)
1-309: Documentation looks comprehensive.The FAQ coverage summary is well-organized and provides clear navigation guidance. The static analysis tool flagged a minor style issue on Line 32 ("open source" vs "open-source" when used as a compound adjective), but this is an optional stylistic improvement.
apps/docs/FAQ_QUICK_REFERENCE.md (2)
104-104: Consider adding language identifiers to fenced code blocks.For better syntax highlighting and documentation clarity, add language identifiers to the fenced code blocks.
Apply this change:
-``` +```text https://jsonresume.org/faq#validation--troubleshooting ...Also applies to: 116-116 --- `311-312`: **Use markdown link syntax for URLs.** Bare URLs should use markdown link syntax for better formatting and accessibility. Apply this change: ```diff -- GitHub Issues: https://github.com/jsonresume/jsonresume.org/issues -- Discussions: https://github.com/jsonresume/jsonresume.org/discussions +- [GitHub Issues](https://github.com/jsonresume/jsonresume.org/issues) +- [Discussions](https://github.com/jsonresume/jsonresume.org/discussions)apps/registry/scripts/jobs/checkJobsDates.js (3)
2-2: Simplify the dotenv path construction.The path
__dirname + '/./../../.env'contains a redundant./segment.Apply this change:
-require('dotenv').config({ path: __dirname + '/./../../.env' }); +require('dotenv').config({ path: __dirname + '/../../.env' });
6-8: Add credential validation before creating Supabase client.The script creates a Supabase client without validating that
supabaseKeyexists, which could lead to unclear error messages. This pattern is handled correctly incheckJobsCount.js(lines 12-17).Add validation before line 8:
const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; const supabaseKey = process.env.SUPABASE_KEY; + +if (!supabaseKey) { + console.error('Missing SUPABASE_KEY environment variable'); + process.exit(1); +} + const supabase = createClient(supabaseUrl, supabaseKey);
6-6: Consider centralizing the Supabase URL configuration.The Supabase URL
https://itxuhvvwryeuzuyihpkp.supabase.cois hardcoded across multiple files (this script, checkJobsCount.js, checkOctoberJobs.js, and various API routes). Consider extracting it to a shared configuration module or environment variable for easier maintenance.apps/registry/scripts/jobs/checkJobsCount.js (2)
2-2: Simplify the dotenv path construction.The path
__dirname + '/./../../.env'contains a redundant./segment.Apply this change:
-require('dotenv').config({ path: __dirname + '/./../../.env' }); +require('dotenv').config({ path: __dirname + '/../../.env' });
9-9: Consider centralizing the Supabase URL configuration.The Supabase URL is hardcoded here and in multiple other files (checkJobsDates.js, checkOctoberJobs.js, various API routes). Centralizing this configuration would improve maintainability.
apps/registry/scripts/jobs/checkOctoberJobs.js (3)
2-2: Simplify the dotenv path construction.The path
__dirname + '/./../../.env'contains a redundant./segment.Apply this change:
-require('dotenv').config({ path: __dirname + '/./../../.env' }); +require('dotenv').config({ path: __dirname + '/../../.env' });
6-8: Add credential validation before creating Supabase client.The script creates a Supabase client without validating that
supabaseKeyexists. Following the pattern incheckJobsCount.js, add validation for clearer error messages.Add validation before line 8:
const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; const supabaseKey = process.env.SUPABASE_KEY; + +if (!supabaseKey) { + console.error('Missing SUPABASE_KEY environment variable'); + process.exit(1); +} + const supabase = createClient(supabaseUrl, supabaseKey);
6-6: Consider centralizing the Supabase URL configuration.The Supabase URL is duplicated across this and other scripts. Centralizing it would improve maintainability.
AGENTS.md (1)
26-26: Fix word coherency: use consistent "colocate" variant throughout.Line 26 uses "Colocate" while the codebase or other docs may use "co-locate". Choose one form and apply consistently across the file and documentation.
Apply this diff to standardize:
-Colocate deterministic Vitest specs (`*.test.(ts|js)`) with the code they cover, +Co-locate deterministic Vitest specs (`*.test.(ts|js)`) with the code they cover,(Or use "colocate" throughout if that's the preferred style guide.)
apps/docs/FAQ_DOCUMENTATION_SUMMARY.md (1)
241-241: Minor: Correct redundant acronym usage."SEO Optimization" is tautological since "O" in SEO stands for "optimization". Use either "SEO" or "optimization" but not both.
Apply these diffs:
-## SEO Optimization +## SEO - - SEO optimization pass + - SEO passAlso applies to: 358-358
apps/registry/apps/docs/pages/index.mdx (1)
3-3: Use hyphenated "open-source" when used as adjective.Per standard English grammar and the LanguageTool hints, "open-source" should be hyphenated when modifying a noun (as adjective). Currently uses "open source" without hyphen in multiple places when it should be hyphenated.
Apply these diffs:
-Welcome to the official documentation for JSON Resume - the open source initiative to create a JSON-based standard for resumes. +Welcome to the official documentation for JSON Resume — the open-source initiative to create a JSON-based standard for resumes. -JSON Resume is a community-driven open source initiative to create a standardized format for resumes. +JSON Resume is a community-driven open-source initiative to create a standardized format for resumes. -JSON Resume is open source software licensed under the MIT License. +JSON Resume is open-source software licensed under the MIT License.Also note: consider using em dash (—) instead of hyphen (-) for sentence breaks (line 3: "JSON Resume - the" → "JSON Resume — the").
Also applies to: 7-7, 82-82
apps/registry/apps/docs/package.json (1)
1-25: Add Node engine and package manager metadata; optional scripts.Locks CI/dev to a compatible runtime and improves DX.
Apply:
{ "name": "docs", "version": "1.0.0", "private": true, + "engines": { + "node": ">=18.18 <23", + "pnpm": ">=8.6" + }, + "packageManager": "pnpm@8", "scripts": { "dev": "next dev --port 3001", "build": "next build", "start": "next start", - "lint": "next lint --max-warnings=0" + "lint": "next lint --max-warnings=0", + "check:types": "tsc --noEmit" },Optional: consider bumping to Next 15 / React 19 in a separate PR if the monorepo is ready. Based on learnings.
apps/registry/apps/docs/pages/api.mdx (1)
11-49: Document responses, errors, and caching.Add:
- Example JSON responses and common error bodies (404, 429, 5xx).
- Headers: Cache-Control, ETag, Vary, RateLimit headers if applicable.
- Auth/rate limits and theme list/validation note.
apps/registry/apps/docs/pages/contributing.mdx (1)
19-30: Include how to run the docs app.Add a snippet for the docs site:
# Start docs cd apps/registry/apps/docs pnpm devapps/registry/apps/docs/pages/getting-started.mdx (1)
88-105: Clarify registry publishing requirements and add schema reference.
- Explicitly state: only public gists are supported.
- Link to the JSON Resume schema and suggest validating against the exact version used by the registry.
- Optionally show
npx resume-cli@latestto avoid old global installs.apps/registry/app/[username]/decisions/components/DecisionTreePane.jsx (1)
33-44: Controls style object likely doesn’t style buttons.Controls’ style prop targets the wrapper; nested button styles aren’t applied. Use className + CSS (e.g., .react-flow__controls-button) or CSS variables per @xyflow/react docs.
Example:
<Controls className="rf-controls" showZoom showFitView showInteractive={false} />/* colocated CSS or global stylesheet */ .rf-controls .react-flow__controls-button { background-color: #fff; border: 1px solid #e2e8f0; color: #1e293b; }apps/registry/app/api/decisions/preferences/route.js (2)
58-75: Consider validating thevalueparameter.The POST handler accepts a
valueparameter without validation. While it defaults to{}if missing, malformed or excessively large values could be persisted to the database.Consider adding Zod validation:
+import { z } from 'zod'; + +const preferenceValueSchema = z.object({ + min: z.number().optional(), + max: z.number().optional(), +}).passthrough(); + export async function POST(request) { // ... try { const body = await request.json(); const { userId, criterion, enabled, value } = body; // Validation if (!userId || !criterion) { return NextResponse.json( { message: 'userId and criterion are required' }, { status: 400 } ); } if (typeof enabled !== 'boolean') { return NextResponse.json( { message: 'enabled must be a boolean' }, { status: 400 } ); } + + // Validate value structure + if (value !== undefined && value !== null) { + const validation = preferenceValueSchema.safeParse(value); + if (!validation.success) { + return NextResponse.json( + { message: 'Invalid value structure' }, + { status: 400 } + ); + } + }
39-39: Consider using structured logging instead of console.error.The error handling uses
console.errordirectly. For better observability and consistency with the codebase (which appears to use Pino logging in other areas), consider using structured logging.Example pattern:
import { logger } from '@/lib/logger'; // Instead of: console.error('Error fetching preferences:', error); // Use: logger.error({ err: error, userId }, 'Failed to fetch preferences');Also applies to: 99-99
apps/registry/app/[username]/decisions/components/ResumePane.jsx (1)
29-38: Consider extracting duplicate experience calculation logic.This experience calculation logic appears to be duplicated from
apps/registry/app/[username]/decisions/config/matchingCriteria.js(lines 276-287). Consider extracting it to a shared utility function to follow DRY principles.Create a shared utility:
// utils/resumeHelpers.js export function calculateYearsOfExperience(workHistory) { const totalMonths = workHistory.reduce((total, job) => { const startDate = job.startDate ? new Date(job.startDate) : null; const endDate = job.endDate ? new Date(job.endDate) : new Date(); if (!startDate) return total; const months = Math.round( (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30) ); return total + months; }, 0); return Math.round(totalMonths / 12); }Then import and use in both files:
+import { calculateYearsOfExperience } from '../utils/resumeHelpers'; - const totalMonths = work.reduce((total, job) => { - const startDate = job.startDate ? new Date(job.startDate) : null; - const endDate = job.endDate ? new Date(job.endDate) : new Date(); - if (!startDate) return total; - const months = Math.round( - (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30) - ); - return total + months; - }, 0); - const yearsOfExperience = Math.round(totalMonths / 12); + const yearsOfExperience = calculateYearsOfExperience(work);apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx (3)
276-304: Extract duplicate range input rendering logic.The range input rendering for "salary" (lines 276-304) and "experience" (lines 315-343) is nearly identical, violating DRY principles. Consider extracting this into a reusable component or helper function.
Create a helper component:
function RangeInput({ label, value, min, max, step, onChange, formatter }) { return ( <div className="space-y-2"> <div className="flex items-center justify-between text-xs text-slate-700"> <span>{formatter('min', value.min || 0)}</span> <span>{formatter('max', value.max || 0)}</span> </div> <div className="space-y-1"> <input type="range" min={min} max={max} step={step} value={value.min || 0} onChange={(e) => onChange({ ...value, min: parseInt(e.target.value) })} className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer accent-blue-500" /> <input type="range" min={min} max={max} step={step} value={value.max || 0} onChange={(e) => onChange({ ...value, max: parseInt(e.target.value) })} className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer accent-blue-500" /> </div> </div> ); }Then use it:
{pref.enabled && criterion.type === 'range' && ( <RangeInput value={pref.value} min="0" max="300000" step="5000" onChange={(newValue) => handleValueChange(criterion.id, newValue)} formatter={(type, val) => `${type === 'min' ? 'Min' : 'Max'}: $${val.toLocaleString()}`} /> )}Also applies to: 315-343
276-287: Consider debouncing range input changes to reduce API calls.The range inputs trigger
handleValueChangeon everyonChangeevent, which immediately callssavePreferenceand makes a POST request. As users drag the slider, this could generate dozens of rapid API calls.Add debouncing to batch updates:
import { useState, useEffect, useCallback } from 'react'; import { debounce } from 'lodash'; // or implement your own export function PreferencesPanel({ user, onChange }) { // ...existing state... // Debounced save function const debouncedSave = useCallback( debounce(async (criterionId, enabled, value) => { if (!user) return; try { const response = await fetch('/api/decisions/preferences', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: user.id, criterion: criterionId, enabled, value }), }); if (!response.ok) throw new Error('Failed to save preference'); if (onChange) onChange(criterionId, enabled, value); } catch (error) { console.error('Error saving preference:', error); } }, 500), // Wait 500ms after last change [user, onChange] ); const handleValueChange = async (criterionId, newValue) => { const current = preferences[criterionId]; // Update UI immediately setPreferences((prev) => ({ ...prev, [criterionId]: { ...current, value: newValue }, })); // Save to backend with debounce debouncedSave(criterionId, current.enabled, newValue); }; // ...rest of component... }Also applies to: 296-300, 321-325, 335-339
96-106: Consider showing error feedback to users when preferences fail to load.When preference loading fails (lines 96-106), the component falls back to defaults silently. Users won't know if their saved preferences failed to load or if they simply haven't set any yet.
Add user-visible error feedback:
const [error, setError] = useState(null); const loadPreferences = async () => { try { // ...existing fetch logic... setError(null); } catch (error) { console.error('Error loading preferences:', error); setError('Failed to load your saved preferences. Using defaults.'); // ...fallback to defaults... } finally { setLoading(false); } }; // In render: {error && ( <div className="p-2 mb-2 bg-yellow-50 border border-yellow-200 rounded text-xs text-yellow-800"> {error} </div> )}apps/registry/app/api/decisions/evaluate/route.js (2)
154-156: Remove bonusSkills from jobContext to match PR (bonus removed)The PR removes bonus skills. Keep jobContext aligned to avoid confusing prompts.
- bonusSkills: gptJob.bonusSkills || [],
145-164: Guard prompt size: truncate excessively long resume/job fieldsSending entire resume/job can blow token limits and raise cost. Consider summarizing/truncating long arrays (e.g., highlights) before JSON.stringify.
I can provide a small helper to clamp arrays/strings by length if you want.
apps/registry/app/api/decisions/decision/route.js (1)
5-5: Deduplicate supabaseUrl; move to shared configsupabaseUrl is hardcoded here and in other routes. Centralize in a single module to prevent drift.
-const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; +import { supabaseUrl } from '@/lib/supabaseConfig';apps/registry/app/api/decisions/jobs/route.js (3)
10-22: Avoid duplication: centralize stripEmbeddingsThis duplicates logic from the single‑job route. Extract to a shared util to keep fields in sync as schemas change.
-function stripEmbeddings(jobs) { - return jobs.map((job) => { - const { - embedding: _embedding, - embedding_v2: _embedding_v2, - embedding_v3: _embedding_v3, - embedding_v4: _embedding_v4, - embedding_v5: _embedding_v5, - ...jobWithoutEmbeddings - } = job; - return jobWithoutEmbeddings; - }); -} +import { stripEmbeddings } from '@/lib/jobs/stripEmbeddings';
35-52: Parse and bound “limit” defensivelylimit may arrive as a string or invalid; normalize and cap.
- const { username: _username, userId, limit = 100 } = body; + const { username: _username, userId, limit: rawLimit } = body; + const limit = Number.isFinite(Number(rawLimit)) ? Number(rawLimit) : 100; ... - if (limit) { - query = query.limit(Math.min(limit, 1000)); // Cap at 1000 max - } + if (limit) { + query = query.limit(Math.min(Math.max(1, limit), 1000)); // 1..1000 + }
82-85: Use shared logger instead of console.errorKeep logging consistent with other routes and support redaction/levels.
-import { NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; +import { logger } from '@/lib/logger'; ... - console.error('Error fetching jobs:', error); + logger.error({ err: error?.message }, 'Error fetching jobs');supabase/migrations/20251021153509_create_job_decisions_table.sql (2)
15-17: Redundant composite index.UNIQUE(user_id, job_id) already creates a btree index; idx_job_decisions_user_job duplicates it. Drop the extra index to reduce write/space overhead.
Apply:
CREATE INDEX idx_job_decisions_user_id ON job_decisions(user_id); CREATE INDEX idx_job_decisions_job_id ON job_decisions(job_id); -CREATE INDEX idx_job_decisions_user_job ON job_decisions(user_id, job_id);
5-6: Consider referential integrity for job_id.If job_id references public.jobs(id), add an FK with ON DELETE CASCADE/RESTRICT per your data model. This prevents orphaned decisions.
Example (adjust type/schema to match jobs.id):
- job_id UUID NOT NULL, + job_id UUID NOT NULL REFERENCES public.jobs(id) ON DELETE CASCADE,We’ll finalize this once the correct type is confirmed (see the follow-up migration).
apps/registry/app/[username]/decisions/config/designSystem.js (1)
126-162: Optional: freeze or type tokens to prevent accidental mutation.If you’ve had runtime style drift, consider Object.freeze or JSDoc/TS typings for tokens. Low effort, safer DX.
apps/registry/app/[username]/decisions/components/JobsPane.jsx (3)
11-20: Deduplicate getLocationString helper.Same logic exists in JobDetail.jsx. Extract to a shared util (e.g., decisions/utils/location.js) to keep behavior consistent.
88-94: Animation keyframes duplicated across components.Consider moving spin/pulse/fade keyframes into a shared CSS (globals or a module) or the design system for reuse.
112-123: Accessibility nits for list items.Add type="button" and aria-selected={isSelected} for better semantics in a selectable list.
Apply:
- <button + <button + type="button" key={job.id} onClick={() => handleSelectJob(job)} className={`relative w-full text-left p-3 rounded-xl border transition-all ${apps/registry/app/[username]/decisions/page.js (2)
38-76: Make jobs fetch abortable to prevent setting state after unmount/param change.Use AbortController and pass signal to fetch; abort in cleanup.
Apply:
- useEffect(() => { - const fetchJobs = async () => { + useEffect(() => { + const controller = new AbortController(); + const fetchJobs = async () => { try { setJobsLoading(true); setJobsError(null); const response = await fetch('/api/decisions/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, userId: user?.id, limit: 10, }), // Limit to 10 for testing + signal: controller.signal, }); if (!response.ok) { throw new Error(`Failed to fetch jobs: ${response.statusText}`); } const data = await response.json(); setJobs(Array.isArray(data) ? data : []); } catch (error) { - logger.error( + if (error.name === 'AbortError') return; + logger.error( { error: error.message, username }, 'Error fetching jobs for decisions page' ); setJobsError(error.message); setJobs([]); } finally { setJobsLoading(false); } }; if (username) { fetchJobs(); } - }, [username, user]); + return () => controller.abort(); + }, [username, user]);
102-128: Refetch duplication.refetchJobs duplicates the fetch body and error handling. Consider extracting a shared loadJobs(username, userId) to DRY. Not blocking.
supabase/migrations/20251021154610_fix_job_decisions_job_id_type.sql (1)
17-21: Redundant composite index.Remove idx_job_decisions_user_job; UNIQUE(user_id, job_id) already provides it.
Apply:
CREATE INDEX idx_job_decisions_user_id ON job_decisions(user_id); CREATE INDEX idx_job_decisions_job_id ON job_decisions(job_id); -CREATE INDEX idx_job_decisions_user_job ON job_decisions(user_id, job_id);apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (1)
500-516: Cap final score post-fix and consider 95-point max.Given weights (skills up to 40; others total 55), max is 95. Keeping Math.min(100, score) is fine but consider normalizing to 0–100 for display consistency, or show 95/95. Optional.
apps/registry/app/[username]/decisions/components/JobDetail.jsx (3)
226-244: Render explicit error state (not as “No Match”).When matchResult.outcome === 'error', this block styles like noMatch. Add a branch to show an error color/message. Example: use a neutral/error palette and skip “% match”.
423-427: Avoid “undefined” in salary display.Provide defaults/fallbacks.
- 💰 ${gptJob.salary.min?.toLocaleString()} - $ - {gptJob.salary.max?.toLocaleString()} + 💰 ${(gptJob.salary.min ?? 0).toLocaleString()} - $ + {(gptJob.salary.max ?? 0).toLocaleString()}
12-20: Deduplicate location stringify logic.getLocationString duplicates code used in JobsPane.jsx. Extract a shared helper to keep behavior consistent.
apps/registry/app/[username]/decisions/config/matchingCriteria.js (1)
58-71: Safely parse gpt_content across checks.Each check uses JSON.parse. Wrap in a tiny helper to avoid runtime crashes.
Example pattern used in coreSkills above; replicate in experience, location, timezone, workRights, availability, salary.
Also applies to: 78-118, 123-151, 156-177, 182-197, 202-229
apps/registry/app/[username]/decisions/config/decisionTree.js (2)
20-24: Remove unused BONUS node id.This constant is unused after the bonus step was removed. Cleanup to avoid confusion.
AVAIL: 'avail', SAL: 'sal', - BONUS: 'bonus', STRONG: 'strong',
104-108: Update core node label to match percentage-based scoring.Reflect the 50%/80% thresholds in the UI copy.
- data: { label: 'Has ALL required skills?' }, + data: { label: 'Skills match? (≥80% pass, 50–80% partial)' },apps/registry/app/[username]/decisions/schema.sql (2)
6-7: Ensure gen_random_uuid() is available.Requires pgcrypto extension in Postgres/Supabase. If not enabled, either enable it or switch to uuid_generate_v4() with uuid-ossp.
56-58: Outcome values differ from app outcomes.overall_fit CHECK uses ('strong','possible','weak') but UI uses strongMatch/possibleMatch/noMatch. Align or map consistently to prevent confusion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (18)
.playwright-mcp/decisions-ai-second-job.pngis excluded by!**/*.png.playwright-mcp/decisions-ai-test.pngis excluded by!**/*.png.playwright-mcp/decisions-page-state.pngis excluded by!**/*.png.playwright-mcp/job-detail-bottom.pngis excluded by!**/*.png.playwright-mcp/job-detail-fixed.pngis excluded by!**/*.png.playwright-mcp/job-detail-scrolled.pngis excluded by!**/*.png.playwright-mcp/jobs-filtered.pngis excluded by!**/*.png.playwright-mcp/jobs-list-filters.pngis excluded by!**/*.png.playwright-mcp/jobs-page-current.pngis excluded by!**/*.png.playwright-mcp/jobs-page-fixed.pngis excluded by!**/*.png.playwright-mcp/jobs-page.pngis excluded by!**/*.png.playwright-mcp/jobs-with-dates.pngis excluded by!**/*.pngsupabase/.temp/cli-latestis excluded by!**/.temp/**supabase/.temp/gotrue-versionis excluded by!**/.temp/**supabase/.temp/pooler-urlis excluded by!**/.temp/**supabase/.temp/postgres-versionis excluded by!**/.temp/**supabase/.temp/project-refis excluded by!**/.temp/**supabase/.temp/rest-versionis excluded by!**/.temp/**
📒 Files selected for processing (46)
AGENTS.md(1 hunks)apps/docs/FAQ_COVERAGE.md(1 hunks)apps/docs/FAQ_DOCUMENTATION_SUMMARY.md(1 hunks)apps/docs/FAQ_PART2_COVERAGE.md(1 hunks)apps/docs/FAQ_QUICK_REFERENCE.md(1 hunks)apps/docs/questions.md(1 hunks)apps/registry/app/[username]/ProfileLayout.js(1 hunks)apps/registry/app/[username]/decisions/DECISIONS_PRD.md(1 hunks)apps/registry/app/[username]/decisions/README.md(1 hunks)apps/registry/app/[username]/decisions/components/DecisionTreePane.jsx(1 hunks)apps/registry/app/[username]/decisions/components/JobDetail.jsx(1 hunks)apps/registry/app/[username]/decisions/components/JobsPane.jsx(1 hunks)apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx(1 hunks)apps/registry/app/[username]/decisions/components/ResumePane.jsx(1 hunks)apps/registry/app/[username]/decisions/config/decisionTree.js(1 hunks)apps/registry/app/[username]/decisions/config/designSystem.js(1 hunks)apps/registry/app/[username]/decisions/config/matchingCriteria.js(1 hunks)apps/registry/app/[username]/decisions/hooks/useDecisionTree.js(1 hunks)apps/registry/app/[username]/decisions/hooks/useJobMatching.js(1 hunks)apps/registry/app/[username]/decisions/layout.js(1 hunks)apps/registry/app/[username]/decisions/page.js(1 hunks)apps/registry/app/[username]/decisions/schema.sql(1 hunks)apps/registry/app/api/decisions/decision/route.js(1 hunks)apps/registry/app/api/decisions/evaluate/route.js(1 hunks)apps/registry/app/api/decisions/jobs/route.js(1 hunks)apps/registry/app/api/decisions/preferences/route.js(1 hunks)apps/registry/app/api/jobs/[uuid]/route.js(2 hunks)apps/registry/apps/docs/.eslintrc.json(1 hunks)apps/registry/apps/docs/.gitignore(1 hunks)apps/registry/apps/docs/next.config.js(1 hunks)apps/registry/apps/docs/package.json(1 hunks)apps/registry/apps/docs/pages/_meta.json(1 hunks)apps/registry/apps/docs/pages/api.mdx(1 hunks)apps/registry/apps/docs/pages/architecture.mdx(1 hunks)apps/registry/apps/docs/pages/contributing.mdx(1 hunks)apps/registry/apps/docs/pages/getting-started.mdx(1 hunks)apps/registry/apps/docs/pages/index.mdx(1 hunks)apps/registry/apps/docs/pages/jobs.mdx(1 hunks)apps/registry/apps/docs/theme.config.tsx(1 hunks)apps/registry/apps/docs/tsconfig.json(1 hunks)apps/registry/scripts/jobs/checkJobsCount.js(1 hunks)apps/registry/scripts/jobs/checkJobsDates.js(1 hunks)apps/registry/scripts/jobs/checkOctoberJobs.js(1 hunks)supabase/migrations/20251021153509_create_job_decisions_table.sql(1 hunks)supabase/migrations/20251021154610_fix_job_decisions_job_id_type.sql(1 hunks)supabase/migrations/20251021160000_create_job_evaluation_preferences.sql(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
apps/registry/app/[username]/decisions/components/ResumePane.jsx (2)
apps/registry/app/[username]/decisions/config/matchingCriteria.js (1)
totalMonths(277-288)apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx (1)
PreferencesPanel(56-354)
apps/registry/app/[username]/decisions/components/DecisionTreePane.jsx (2)
apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (2)
nodes(29-29)edges(30-30)apps/registry/app/[username]/decisions/config/decisionTree.js (2)
reactFlowOptions(444-458)reactFlowOptions(444-458)
apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx (2)
apps/registry/app/[username]/decisions/page.js (1)
preferences(35-35)apps/registry/app/components/MenuModule/hooks/useAuth.js (1)
user(10-10)
apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (4)
apps/registry/app/[username]/decisions/config/decisionTree.js (5)
NODE_IDS(11-24)NODE_IDS(11-24)recalculateLayout(291-366)createBridgeEdges(375-441)nodeStyle(39-93)apps/registry/app/[username]/decisions/page.js (2)
useDecisionTree(82-89)preferences(35-35)apps/registry/app/[username]/decisions/config/designSystem.js (2)
colors(6-69)colors(6-69)apps/registry/app/api/decisions/evaluate/route.js (2)
result(245-250)decisions(258-258)
apps/registry/app/[username]/decisions/components/JobsPane.jsx (4)
apps/registry/app/[username]/decisions/components/JobDetail.jsx (3)
parts(16-18)JobDetail(22-481)gptJob(25-25)apps/registry/app/[username]/decisions/page.js (3)
handleSelectJob(92-100)selectedJob(32-32)jobs(31-31)apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (1)
matchResult(31-31)apps/registry/app/components/MenuModule/hooks/useAuth.js (1)
user(10-10)
apps/registry/scripts/jobs/checkJobsCount.js (5)
apps/registry/scripts/jobs/checkJobsDates.js (4)
require(4-4)supabaseUrl(6-6)supabaseKey(7-7)supabase(8-8)apps/registry/scripts/jobs/checkOctoberJobs.js (4)
require(4-4)supabaseUrl(6-6)supabaseKey(7-7)supabase(8-8)apps/registry/app/api/decisions/jobs/route.js (2)
supabaseUrl(4-4)supabase(37-37)apps/registry/app/api/decisions/decision/route.js (5)
supabaseUrl(5-5)supabase(38-38)supabase(41-55)supabase(98-98)supabase(100-105)apps/registry/app/api/decisions/preferences/route.js (5)
supabaseUrl(4-4)supabase(28-28)supabase(30-33)supabase(77-77)supabase(80-93)
apps/registry/app/api/decisions/decision/route.js (3)
apps/registry/app/api/decisions/jobs/route.js (4)
supabaseUrl(4-4)POST(24-88)body(34-34)supabase(37-37)apps/registry/app/api/decisions/preferences/route.js (11)
supabaseUrl(4-4)POST(50-105)body(59-59)body(60-60)userId(19-19)supabase(28-28)supabase(30-33)supabase(77-77)supabase(80-93)GET(9-45)URL(18-18)apps/registry/lib/logger.js (1)
logger(14-44)
apps/registry/app/[username]/decisions/config/matchingCriteria.js (5)
apps/registry/app/api/decisions/evaluate/route.js (1)
gptJob(137-137)apps/registry/app/[username]/decisions/components/JobDetail.jsx (1)
gptJob(25-25)apps/registry/app/[username]/decisions/components/ResumePane.jsx (1)
totalMonths(29-37)apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx (1)
preferences(58-58)apps/registry/app/[username]/decisions/page.js (1)
preferences(35-35)
apps/registry/app/[username]/decisions/config/decisionTree.js (2)
apps/registry/app/[username]/decisions/config/designSystem.js (2)
colors(6-69)colors(6-69)apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (3)
nodes(29-29)edges(30-30)nodeToPreferenceMap(19-26)
apps/registry/app/[username]/decisions/page.js (7)
apps/registry/app/components/MenuModule/hooks/useAuth.js (1)
user(10-10)apps/registry/lib/logger.js (1)
logger(14-44)apps/registry/app/[username]/decisions/hooks/useJobMatching.js (2)
useJobMatching(12-47)rankedJobs(14-30)apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (5)
useDecisionTree(28-529)evaluateJob(152-214)nodes(29-29)edges(30-30)matchResult(31-31)apps/registry/app/[username]/decisions/components/JobsPane.jsx (2)
handleSelectJob(34-37)JobsPane(22-194)apps/registry/app/[username]/decisions/components/ResumePane.jsx (1)
ResumePane(12-295)apps/registry/app/[username]/decisions/components/DecisionTreePane.jsx (1)
DecisionTreePane(12-64)
apps/registry/scripts/jobs/checkOctoberJobs.js (5)
apps/registry/scripts/jobs/checkJobsCount.js (4)
require(7-7)supabaseUrl(9-9)supabaseKey(10-10)supabase(19-19)apps/registry/scripts/jobs/checkJobsDates.js (5)
require(4-4)supabaseUrl(6-6)supabaseKey(7-7)supabase(8-8)jobs(13-18)apps/registry/app/api/decisions/jobs/route.js (3)
supabaseUrl(4-4)supabase(37-37)jobs(54-54)apps/registry/app/api/decisions/decision/route.js (5)
supabaseUrl(5-5)supabase(38-38)supabase(41-55)supabase(98-98)supabase(100-105)apps/registry/app/api/decisions/preferences/route.js (5)
supabaseUrl(4-4)supabase(28-28)supabase(30-33)supabase(77-77)supabase(80-93)
apps/registry/app/api/decisions/preferences/route.js (2)
apps/registry/app/api/decisions/decision/route.js (11)
supabaseUrl(5-5)GET(78-120)URL(87-87)userId(88-88)supabase(38-38)supabase(41-55)supabase(98-98)supabase(100-105)POST(11-72)body(20-20)body(21-21)apps/registry/app/api/decisions/evaluate/route.js (2)
request(123-123)POST(121-281)
apps/registry/app/api/decisions/evaluate/route.js (4)
apps/registry/app/[username]/decisions/components/JobDetail.jsx (1)
gptJob(25-25)apps/registry/app/[username]/decisions/config/matchingCriteria.js (8)
gptJob(30-30)gptJob(60-60)gptJob(78-78)gptJob(129-129)gptJob(157-157)gptJob(185-185)gptJob(205-205)gptJob(240-240)apps/registry/app/[username]/decisions/components/PreferencesPanel.jsx (1)
preferences(58-58)apps/registry/app/[username]/decisions/page.js (1)
preferences(35-35)
apps/registry/app/api/decisions/jobs/route.js (2)
apps/registry/app/api/jobs/[uuid]/route.js (4)
supabaseUrl(5-5)job(39-43)_embedding(16-23)supabase(37-37)apps/registry/app/api/decisions/decision/route.js (9)
supabaseUrl(5-5)POST(11-72)body(20-20)body(21-21)supabase(38-38)supabase(41-55)supabase(98-98)supabase(100-105)userId(88-88)
apps/registry/app/[username]/decisions/layout.js (2)
apps/registry/app/[username]/decisions/page.js (1)
params(24-24)apps/registry/app/providers/PublicResumeProvider.js (1)
PublicResumeProvider(27-86)
apps/registry/app/[username]/decisions/components/JobDetail.jsx (5)
apps/registry/app/[username]/decisions/components/JobsPane.jsx (1)
parts(16-18)apps/registry/app/api/decisions/evaluate/route.js (1)
gptJob(137-137)apps/registry/app/[username]/decisions/config/matchingCriteria.js (8)
gptJob(30-30)gptJob(60-60)gptJob(78-78)gptJob(129-129)gptJob(157-157)gptJob(185-185)gptJob(205-205)gptJob(240-240)apps/registry/app/components/MenuModule/hooks/useAuth.js (1)
user(10-10)apps/registry/app/[username]/decisions/hooks/useDecisionTree.js (1)
matchResult(31-31)
apps/registry/app/[username]/decisions/hooks/useJobMatching.js (2)
apps/registry/app/[username]/decisions/page.js (3)
useJobMatching(79-79)jobs(31-31)preferences(35-35)apps/registry/app/[username]/decisions/config/matchingCriteria.js (3)
scoreResult(377-377)scoreCandidateForJob(297-371)determineOutcome(376-396)
apps/registry/scripts/jobs/checkJobsDates.js (5)
apps/registry/scripts/jobs/checkJobsCount.js (4)
require(7-7)supabaseUrl(9-9)supabaseKey(10-10)supabase(19-19)apps/registry/scripts/jobs/checkOctoberJobs.js (5)
require(4-4)supabaseUrl(6-6)supabaseKey(7-7)supabase(8-8)jobs(15-20)apps/registry/app/api/decisions/jobs/route.js (3)
supabaseUrl(4-4)supabase(37-37)jobs(54-54)apps/registry/app/api/decisions/decision/route.js (5)
supabaseUrl(5-5)supabase(38-38)supabase(41-55)supabase(98-98)supabase(100-105)apps/registry/app/api/decisions/preferences/route.js (5)
supabaseUrl(4-4)supabase(28-28)supabase(30-33)supabase(77-77)supabase(80-93)
🪛 Biome (2.1.2)
apps/registry/app/[username]/decisions/hooks/useDecisionTree.js
[error] 213-214: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
apps/registry/apps/docs/theme.config.tsx
[error] 17-17: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".
Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.
(lint/security/noBlankTarget)
🪛 LanguageTool
apps/docs/FAQ_DOCUMENTATION_SUMMARY.md
[style] ~241-~241: This phrase is redundant (‘O’ stands for ‘Optimization’). Use simply “SEO”.
Context: ... - Netlify - Vercel - AWS CodeBuild ## SEO Optimization Keyword Density: - "JSON Resume" ...
(ACRONYM_TAUTOLOGY)
[style] ~358-~358: This phrase is redundant (‘O’ stands for ‘optimization’). Use simply “SEO”.
Context: ...as needed - Reorganize based on usage - SEO optimization pass Annually: - Complete content...
(ACRONYM_TAUTOLOGY)
apps/docs/questions.md
[style] ~32-~32: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...should I format dates in my resume? 21. How do I represent current jobs without an ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~52-~52: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: .... How do I export my resume to PDF? 36. How do I export to HTML? 37. How do I expor...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~53-~53: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...o PDF? 36. How do I export to HTML? 37. How do I export to Markdown? 38. How can I ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~54-~54: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...L? 37. How do I export to Markdown? 38. How can I install a custom theme? 39. How c...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~55-~55: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: .... How can I install a custom theme? 39. How can I list available themes? 40. How ca...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~56-~56: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...9. How can I list available themes? 40. How can I validate my resume using the CLI?...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~81-~81: Consider using a different verb for a more formal wording.
Context: ...ny premium or paid themes? 60. How do I fix issues with a theme not rendering corre...
(FIX_RESOLVE)
[style] ~91-~91: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...vate? 64. Can I unpublish a resume? 65. Can I host multiple resumes? 66. Can I use ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~92-~92: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...e? 65. Can I host multiple resumes? 66. Can I use a custom domain? 67. Is hosting o...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~98-~98: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... password-protect my hosted resume? 72. Can I delete my hosted resume permanently? ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~99-~99: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...elete my hosted resume permanently? 73. Can I export a hosted resume back to JSON? ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~100-~100: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...xport a hosted resume back to JSON? 74. Can I link my hosted resume to LinkedIn or ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~118-~118: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...me from GitHub commits or activity? 87. Can JSON Resume be used with Zapier or Make...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~119-~119: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...me be used with Zapier or Make.com? 88. Can I use JSON Resume in mobile apps or bot...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~120-~120: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...JSON Resume in mobile apps or bots? 89. Can I feed JSON Resume data into AI or LLM ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~137-~137: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... How do I query JSON Resume data? 101. How do I parse JSON Resume in Python? 102. ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~138-~138: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...do I parse JSON Resume in Python? 102. How do I parse it in PHP or Laravel? 103. ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~139-~139: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... do I parse it in PHP or Laravel? 103. How do I render a JSON Resume in React? 104...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~140-~140: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... I render a JSON Resume in React? 104. How do I handle resume schema migrations? 1...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~149-~149: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...do I convert a PDF to JSON Resume? 108. How do I export JSON Resume to Word? 109. H...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~150-~150: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...w do I export JSON Resume to Word? 109. How do I convert JSON Resume to CSV? 110. C...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~279-~279: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... resume formats using JSON Resume? 198. Can JSON Resume help with DEI or bias reduc...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~311-~311: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ed for portfolios or research CVs? 220. Can I make resumes for multiple languages o...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~353-~353: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ow do I fix npm permission errors? 247. How do I uninstall the CLI? 248. Can I run ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
apps/registry/apps/docs/pages/getting-started.mdx
[uncategorized] ~92-~92: The official name of this software platform is spelled with a capital “H”.
Context: .... Create a new public gist at gist.github.com 2. Name t...
(GITHUB)
apps/docs/FAQ_QUICK_REFERENCE.md
[style] ~299-~299: This phrase is redundant (‘O’ stands for ‘optimization’). Use simply “SEO”.
Context: ... Major content updates - New sections - SEO optimization ### Annually - Complete audit - Restr...
(ACRONYM_TAUTOLOGY)
apps/docs/FAQ_PART2_COVERAGE.md
[style] ~215-~215: Consider an alternative to strengthen your wording.
Context: ...dance ✅ Multiple approaches - shows different solutions to problems ✅ Error handling - incl...
(DIFFERENT_ALTERNATIVE)
[style] ~224-~224: Consider a different adjective to strengthen your wording.
Context: ...de-offs:
(DEEP_PROFOUND)
apps/registry/app/[username]/decisions/DECISIONS_PRD.md
[style] ~223-~223: This phrase is redundant. Consider writing “outcome”.
Context: ...path (blue → outcome color) - Highlight final outcome node - Show reasons in left pane footer...
(FINAL_END)
[grammar] ~292-~292: Use a hyphen to join words.
Context: ...red Skills Check** - Logic: ALL job required skills must be in candidate ski...
(QB_NEW_EN_HYPHEN)
[grammar] ~621-~621: Ensure spelling is correct
Context: ...n checks - Animate edges along path (200ms per edge) - Highlight outcome node 4...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[style] ~893-~893: This phrase is redundant (‘O’ stands for ‘Optimization’). Use simply “SEO”.
Context: ... Verify color contrast ratios ### 11.4 SEO Optimization Meta Tags: ```html <title>{userna...
(ACRONYM_TAUTOLOGY)
AGENTS.md
[uncategorized] ~26-~26: Do not mix variants of the same word (‘colocate’ and ‘co-locate’) within a single text.
Context: ...s-compatible. ## Testing Expectations Colocate deterministic Vitest specs (`*.test.(ts...
(EN_WORD_COHERENCY)
apps/docs/FAQ_COVERAGE.md
[uncategorized] ~32-~32: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ions - Profile pictures - Side projects/open source showcase - Minimalist/one-page template...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
apps/registry/apps/docs/pages/index.mdx
[grammar] ~3-~3: Use a hyphen to join words.
Context: ...documentation for JSON Resume - the open source initiative to create a JSON-based...
(QB_NEW_EN_HYPHEN)
[grammar] ~7-~7: Use a hyphen to join words.
Context: ... JSON Resume is a community-driven open source initiative to create a standardiz...
(QB_NEW_EN_HYPHEN)
[grammar] ~82-~82: Use a hyphen to join words.
Context: ...e ideas ## License JSON Resume is open source software licensed under the MIT L...
(QB_NEW_EN_HYPHEN)
apps/registry/app/[username]/decisions/README.md
[grammar] ~5-~5: Use a hyphen to join words.
Context: ...feature is now live! This AI-powered job matching visualization helps candidates ...
(QB_NEW_EN_HYPHEN)
[grammar] ~30-~30: Ensure spelling is correct
Context: ...utcome nodes - Animated path traversal (200ms per edge) - Color-coded outcomes (green...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.18.1)
apps/docs/FAQ_DOCUMENTATION_SUMMARY.md
97-97: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
104-104: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
111-111: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
113-113: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
123-123: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
131-131: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
169-169: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
187-187: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
apps/docs/FAQ_QUICK_REFERENCE.md
104-104: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
116-116: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
311-311: Bare URL used
(MD034, no-bare-urls)
312-312: Bare URL used
(MD034, no-bare-urls)
apps/docs/FAQ_PART2_COVERAGE.md
230-230: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
237-237: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
244-244: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
apps/registry/app/[username]/decisions/DECISIONS_PRD.md
87-87: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
127-127: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
290-290: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
297-297: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
304-304: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
310-310: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
317-317: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
323-323: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
329-329: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
335-335: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
343-343: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
353-353: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
362-362: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
452-452: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
474-474: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
496-496: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
apps/registry/app/[username]/decisions/README.md
67-67: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
194-194: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test
| ## What Was Created | ||
|
|
||
| A comprehensive FAQ page has been created at: | ||
| **`/Users/ajaxdavis/repos/jsonresume.org/apps/docs/app/faq/page.mdx`** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove absolute user path from committed documentation.
Line 6 contains a hardcoded absolute path (/Users/ajaxdavis/repos/...) that should not be committed to version control. This may expose user directory structure and will break for other developers.
Apply this diff to use a relative path:
-**`/Users/ajaxdavis/repos/jsonresume.org/apps/docs/app/faq/page.mdx`**
+**`apps/docs/app/faq/page.mdx`**📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| **`/Users/ajaxdavis/repos/jsonresume.org/apps/docs/app/faq/page.mdx`** | |
| **`apps/docs/app/faq/page.mdx`** |
🤖 Prompt for AI Agents
In apps/docs/FAQ_DOCUMENTATION_SUMMARY.md around line 6, remove the hardcoded
absolute user path (/Users/ajaxdavis/repos/...) from the committed documentation
and replace it with a relative path or repository-root placeholder (e.g.,
./apps/docs/app/faq/page.mdx or <repo-root>/apps/docs/app/faq/page.mdx) so the
doc does not expose local directory structures and works correctly for other
developers; update any accompanying text to reference the new relative/path
placeholder and commit the change.
| const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {}; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard JSON parsing of job.gpt_content.
Invalid JSON will crash the component. Wrap in try/catch.
- const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {};
+ let gptJob = {};
+ try {
+ gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {};
+ } catch {
+ gptJob = {};
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {}; | |
| let gptJob = {}; | |
| try { | |
| gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {}; | |
| } catch { | |
| gptJob = {}; | |
| } |
🤖 Prompt for AI Agents
In apps/registry/app/[username]/decisions/components/JobDetail.jsx around lines
25-26, the JSON.parse of job.gpt_content can throw on invalid JSON; wrap the
parse in a try/catch so the component won't crash, return an empty object on
failure, and optionally log the parse error for debugging (e.g., process/console
logger with context about job id or username).
| className="w-full mt-3 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition" | ||
| onClick={() => window.open(job.url, '_blank')} | ||
| > | ||
| View Full Job Posting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent reverse‑tabnabbing with target="_blank".
Use noopener/noreferrer and null out opener.
- onClick={() => window.open(job.url, '_blank')}
+ onClick={() => {
+ const w = window.open(job.url, '_blank', 'noopener,noreferrer');
+ if (w) w.opener = null;
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className="w-full mt-3 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition" | |
| onClick={() => window.open(job.url, '_blank')} | |
| > | |
| View Full Job Posting | |
| className="w-full mt-3 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition" | |
| onClick={() => { | |
| const w = window.open(job.url, '_blank', 'noopener,noreferrer'); | |
| if (w) w.opener = null; | |
| }} | |
| > | |
| View Full Job Posting |
🤖 Prompt for AI Agents
In apps/registry/app/[username]/decisions/components/JobDetail.jsx around lines
473-476, the onClick opens job.url with target "_blank" which is vulnerable to
reverse-tabnabbing; update the handler to open the new window with
noopener/noreferrer and null out the opener: call window.open(job.url, '_blank',
'noopener,noreferrer') and immediately after, if the returned window object is
non-null set newWin.opener = null; alternatively replace the button with an
anchor element using target="_blank" and add rel="noopener noreferrer" to it.
| {jobs.map((job) => { | ||
| const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {}; | ||
| const isSelected = selectedJob?.id === job.id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard JSON.parse to avoid runtime crash on malformed gpt_content.
Untrusted/partial data can break the list. Add a safe parser.
Apply:
- const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {};
+ const gptJob = (() => {
+ if (!job?.gpt_content) return {};
+ try {
+ return JSON.parse(job.gpt_content);
+ } catch {
+ return {};
+ }
+ })();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {jobs.map((job) => { | |
| const gptJob = job.gpt_content ? JSON.parse(job.gpt_content) : {}; | |
| const isSelected = selectedJob?.id === job.id; | |
| {jobs.map((job) => { | |
| const gptJob = (() => { | |
| if (!job?.gpt_content) return {}; | |
| try { | |
| return JSON.parse(job.gpt_content); | |
| } catch { | |
| return {}; | |
| } | |
| })(); | |
| const isSelected = selectedJob?.id === job.id; |
🤖 Prompt for AI Agents
In apps/registry/app/[username]/decisions/components/JobsPane.jsx around lines
106 to 108, JSON.parse(job.gpt_content) is called directly and will throw on
malformed or non-JSON content; wrap the parse in a safe parser (either a small
try/catch that returns {} on error or call a shared safeParse utility) so
parsing failures don’t crash rendering, and use the returned object (empty
fallback) for gptJob; keep behavior identical otherwise.
| <a | ||
| href={basics.url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="text-blue-600 hover:text-blue-700 underline" | ||
| > | ||
| Website | ||
| </a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Add rel="noopener noreferrer" to external links.
External links with target="_blank" are missing proper rel attributes, creating a security vulnerability where opened pages can access window.opener.
Apply this diff:
<a
href={basics.url}
target="_blank"
- rel="noopener noreferrer"
+ rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 underline"
>
Website
</a>
)}
{basics.profiles?.map((profile, idx) => (
<a
key={idx}
href={profile.url}
target="_blank"
- rel="noopener noreferrer"
+ rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 underline"
>
{profile.network}
</a>Wait, I see the code already has rel="noopener noreferrer" on lines 267 and 278. Let me re-check... Yes, they're already there. This is a false positive from my initial scan.
Also applies to: 274-282
🤖 Prompt for AI Agents
In apps/registry/app/[username]/decisions/components/ResumePane.jsx around lines
264-271 and 274-282, the review flagged missing rel="noopener noreferrer" on
external links; verify both anchor tags that use target="_blank" include
rel="noopener noreferrer" and add it where absent (or remove target="_blank" if
the link is internal), ensuring all external blank-target links have the rel
attribute to prevent window.opener access.
| "paths": { | ||
| "@/*": ["./*"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set baseUrl; otherwise paths mapping is ignored.
TypeScript ignores "paths" without "baseUrl".
Apply:
"compilerOptions": {
+ "baseUrl": ".",
"target": "ES2020",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "paths": { | |
| "@/*": ["./*"] | |
| } | |
| { | |
| "compilerOptions": { | |
| "baseUrl": ".", | |
| "target": "ES2020", | |
| "paths": { | |
| "@/*": ["./*"] | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/registry/apps/docs/tsconfig.json around lines 22-24 the "paths" mapping
is present but TypeScript ignores it because "baseUrl" is not set; to fix, add a
"baseUrl" entry (e.g. "baseUrl": ".") inside the same "compilerOptions" object
alongside "paths" so the paths mapping is enabled, ensuring the key/value is a
valid string and the compilerOptions JSON remains well-formed.
| -- Policy: Users can update their own decisions | ||
| CREATE POLICY "Users can update their own job decisions" | ||
| ON job_decisions FOR UPDATE | ||
| USING (auth.uid() = user_id); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RLS UPDATE policy missing WITH CHECK allows ownership transfer on update.
An updater can change user_id while passing USING; add WITH CHECK to enforce post-update row still belongs to the actor.
Apply:
-CREATE POLICY "Users can update their own job decisions"
- ON job_decisions FOR UPDATE
- USING (auth.uid() = user_id);
+CREATE POLICY "Users can update their own job decisions"
+ ON job_decisions FOR UPDATE
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);🤖 Prompt for AI Agents
In supabase/migrations/20251021153509_create_job_decisions_table.sql around
lines 32 to 36, the RLS UPDATE policy "Users can update their own job decisions"
only defines USING and therefore allows an updater to change user_id during
UPDATE; add a WITH CHECK clause mirroring the USING condition so post-update row
still belongs to the actor (i.e., add WITH CHECK (auth.uid() = user_id)) to
prevent ownership transfer on update.
| -- Drop the existing table and recreate with correct job_id type | ||
| DROP TABLE IF EXISTS job_decisions; | ||
|
|
||
| -- Create job_decisions table with BIGINT job_id to match jobs table | ||
| CREATE TABLE IF NOT EXISTS job_decisions ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping and recreating the table risks irreversible data loss.
If any env applied the first migration and has data, this DROP will wipe it. Prefer an ALTER or a copy–rename migration with data preservation and a guard.
Safer pattern (sketch):
-- 1) If existing table has wrong type AND data, abort to avoid loss
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name='job_decisions' AND column_name='job_id' AND data_type <> 'bigint'
) AND EXISTS (SELECT 1 FROM job_decisions LIMIT 1) THEN
RAISE EXCEPTION 'job_decisions has data; manual migration required to BIGINT';
END IF;
END$$;
-- 2) If empty or absent, proceed to create correct schema (no DROP needed if absent)Or: create job_decisions_new with BIGINT, copy/transform data, swap names in a transaction.
| job_id BIGINT NOT NULL, | ||
| decision TEXT NOT NULL CHECK (decision IN ('interested', 'pass')), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Add FK to jobs(id) if that is the intended reference.
This enforces integrity and enables ON DELETE behavior.
Apply (align schema/table to your jobs table):
- job_id BIGINT NOT NULL,
+ job_id BIGINT NOT NULL REFERENCES public.jobs(id) ON DELETE CASCADE,Run to confirm jobs.id type so we match the FK:
🏁 Script executed:
#!/bin/bash
rg -n --type=sql -C2 -g "supabase/migrations/**/*.sql" -e 'CREATE TABLE .*jobs' -e 'jobs\s*\(' -e '\bid\b .*'Length of output: 1853
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for jobs table definition in migrations
rg -n --type=sql 'CREATE TABLE.*jobs' supabase/migrations/
rg -n --type=sql -A5 'CREATE TABLE IF NOT EXISTS jobs' supabase/migrations/
Length of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# Broader search for jobs table references
find supabase -type f -name "*.sql" | head -20
rg -n "jobs" supabase/migrations/ --type=sql -i | head -30
rg -n "FROM jobs\|INTO jobs\|TABLE jobs" supabase/ --type=sql
Length of output: 477
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for jobs table in all SQL files across entire supabase directory
find supabase -name "*.sql" -type f -exec grep -l "CREATE TABLE.*jobs" {} \;
# Also check if there are any other SQL file locations
find . -name "*.sql" -type f 2>/dev/null | grep -v node_modules | head -20
# Look for any table definitions or schema files
ls -la supabase/ 2>/dev/null
Length of output: 491
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for jobs table definition anywhere in the entire repository
rg -n "CREATE TABLE.*jobs\|CREATE TABLE IF NOT EXISTS.*jobs" --type=sql -i
# Search for references to public.jobs or just jobs table
rg -n "public\.jobs\|FROM jobs\|JOIN jobs\|REFERENCES.*jobs" --type=sql -i | head -20
# Check the schema.sql file found earlier
cat "./apps/registry/app/[username]/decisions/schema.sql" 2>/dev/null || echo "File not found"
Length of output: 3667
Fix FK constraint to match jobs.id type: use INTEGER, not BIGINT.
The schema.sql defines job_id INTEGER NOT NULL REFERENCES jobs(id) in the decisions tables, meaning jobs.id is INTEGER. The migration incorrectly uses BIGINT, creating a type mismatch. Correct the constraint to:
- job_id BIGINT NOT NULL,
+ job_id INTEGER NOT NULL REFERENCES public.jobs(id) ON DELETE CASCADE,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| job_id BIGINT NOT NULL, | |
| decision TEXT NOT NULL CHECK (decision IN ('interested', 'pass')), | |
| job_id INTEGER NOT NULL REFERENCES public.jobs(id) ON DELETE CASCADE, | |
| decision TEXT NOT NULL CHECK (decision IN ('interested', 'pass')), |
🤖 Prompt for AI Agents
In supabase/migrations/20251021154610_fix_job_decisions_job_id_type.sql around
lines 8-9 the job_id column is declared as BIGINT which mismatches jobs.id
(INTEGER); change the column definition to job_id INTEGER NOT NULL and ensure
the FK references jobs(id) so the types match (update any accompanying ALTER
TABLE/ADD CONSTRAINT statements in this migration to use INTEGER as well).
| -- Policy: Users can update their own decisions | ||
| CREATE POLICY "Users can update their own job decisions" | ||
| ON job_decisions FOR UPDATE | ||
| USING (auth.uid() = user_id); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RLS UPDATE policy again missing WITH CHECK.
Mirror INSERT’s check for post-update rows.
Apply:
-CREATE POLICY "Users can update their own job decisions"
- ON job_decisions FOR UPDATE
- USING (auth.uid() = user_id);
+CREATE POLICY "Users can update their own job decisions"
+ ON job_decisions FOR UPDATE
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);🤖 Prompt for AI Agents
In supabase/migrations/20251021154610_fix_job_decisions_job_id_type.sql around
lines 35 to 39, the RLS UPDATE policy for job_decisions is missing a WITH CHECK
clause; add a WITH CHECK expression mirroring the INSERT policy’s post-insert
condition so that post-update rows satisfy the same predicate (e.g., append WITH
CHECK (auth.uid() = user_id AND <same additional conditions as INSERT>) to the
CREATE POLICY statement).
Summary
This PR implements percentage-based skills matching and several AI evaluation improvements for the job matching decision tree system.
Major Changes
Percentage-based Skills Scoring: Changed from binary "has ALL required skills" to flexible 0.0-1.0 percentage matching
Comprehensive Skills Detection: AI now searches ENTIRE resume for skills evidence
Removed Bonus Skills Check: Eliminated redundant 8th criterion
Removed Embedding Fields: Strip embedding vectors from API responses
Technical Details
Files Modified:
app/api/decisions/evaluate/route.js- Updated AI tool schema and promptsapp/[username]/decisions/hooks/useDecisionTree.js- Percentage-based scoring logic and orange path animationapp/api/decisions/jobs/route.js- Strip embeddings from job listingsapp/api/jobs/[uuid]/route.js- Strip embeddings from single jobapp/[username]/decisions/config/decisionTree.js- Removed bonus node and edgesTest Plan
Breaking Changes
None - this is backward compatible as it only changes the internal evaluation logic
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]
Summary by CodeRabbit
Release Notes
New Features
Database
API