diff --git a/biome.json b/biome.json index 11fc24b..1fd4830 100644 --- a/biome.json +++ b/biome.json @@ -17,6 +17,66 @@ "tailwindDirectives": true } }, + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "../infra/**", + "../../infra/**", + "../../../infra/**", + "../../../../infra/**", + "../shared/**", + "../../shared/**", + "../../../shared/**", + "../../../../shared/**" + ], + "message": "Use path aliases (for example @infra/* or @shared/*) instead of parent-relative imports for shared modules." + }, + { + "group": [ + "../../api", + "../../api/**", + "../../hooks", + "../../hooks/**", + "../../components", + "../../components/**", + "../../lib", + "../../lib/**", + "../../test", + "../../test/**" + ], + "message": "Use @client/* aliases instead of ../../ imports for client modules." + }, + { + "group": [ + "../../services", + "../../services/**", + "../../repositories", + "../../repositories/**", + "../../config", + "../../config/**", + "../../utils", + "../../utils/**", + "../../pipeline", + "../../pipeline/**", + "../../db", + "../../db/**", + "../../extractors", + "../../extractors/**" + ], + "message": "Use @server/* aliases instead of ../../ imports for server modules." + } + ] + } + } + } + } + }, "overrides": [ { "includes": ["**/*.test.ts", "**/*.test.tsx", "**/test-utils.ts"], diff --git a/extractors/adzuna/src/run.ts b/extractors/adzuna/src/run.ts index 599ec29..6b117b4 100644 --- a/extractors/adzuna/src/run.ts +++ b/extractors/adzuna/src/run.ts @@ -63,13 +63,6 @@ export interface AdzunaResult { error?: string; } -export function shouldApplyStrictLocationFilter( - location: string, - countryKey: string, -): boolean { - return shouldApplyStrictCityFilter(location, countryKey); -} - function resolveTsxCliPath(): string | null { try { return require.resolve("tsx/dist/cli.mjs"); @@ -214,8 +207,7 @@ export async function runAdzuna( for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) { const location = runLocations[runIndex]; const strictLocationFilter = - location !== null && - shouldApplyStrictLocationFilter(location, countryKey); + location !== null && shouldApplyStrictCityFilter(location, countryKey); await new Promise((resolve, reject) => { const extractorEnv = { diff --git a/extractors/adzuna/tests/location.test.ts b/extractors/adzuna/tests/location.test.ts index af19f51..7a9146b 100644 --- a/extractors/adzuna/tests/location.test.ts +++ b/extractors/adzuna/tests/location.test.ts @@ -1,15 +1,13 @@ +import { shouldApplyStrictCityFilter } from "@shared/search-cities.js"; import { describe, expect, it } from "vitest"; -import { shouldApplyStrictLocationFilter } from "../src/run"; describe("adzuna location query strictness", () => { it("enables strict filtering when city differs from country", () => { - expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe( - true, - ); + expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true); }); it("disables strict filtering when location is country-level", () => { - expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false); - expect(shouldApplyStrictLocationFilter("United States", "us")).toBe(false); + expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false); + expect(shouldApplyStrictCityFilter("United States", "us")).toBe(false); }); }); diff --git a/extractors/hiringcafe/src/run.ts b/extractors/hiringcafe/src/run.ts index ff77bc2..8c6404a 100644 --- a/extractors/hiringcafe/src/run.ts +++ b/extractors/hiringcafe/src/run.ts @@ -65,13 +65,6 @@ export interface HiringCafeResult { error?: string; } -export function shouldApplyStrictLocationFilter( - location: string, - countryKey: string, -): boolean { - return shouldApplyStrictCityFilter(location, countryKey); -} - function resolveTsxCliPath(): string | null { try { return require.resolve("tsx/dist/cli.mjs"); @@ -213,8 +206,7 @@ export async function runHiringCafe( for (let runIndex = 0; runIndex < runLocations.length; runIndex += 1) { const location = runLocations[runIndex]; const strictLocationFilter = - location !== null && - shouldApplyStrictLocationFilter(location, countryKey); + location !== null && shouldApplyStrictCityFilter(location, countryKey); await clearStorageDataset(); diff --git a/extractors/hiringcafe/tests/location.test.ts b/extractors/hiringcafe/tests/location.test.ts index 91c81aa..6d436cc 100644 --- a/extractors/hiringcafe/tests/location.test.ts +++ b/extractors/hiringcafe/tests/location.test.ts @@ -1,15 +1,13 @@ +import { shouldApplyStrictCityFilter } from "@shared/search-cities.js"; import { describe, expect, it } from "vitest"; -import { shouldApplyStrictLocationFilter } from "../src/run"; describe("hiringcafe location query strictness", () => { it("enables strict filtering when city differs from country", () => { - expect(shouldApplyStrictLocationFilter("Leeds", "united kingdom")).toBe( - true, - ); + expect(shouldApplyStrictCityFilter("Leeds", "united kingdom")).toBe(true); }); it("disables strict filtering when location is country-level", () => { - expect(shouldApplyStrictLocationFilter("UK", "united kingdom")).toBe(false); - expect(shouldApplyStrictLocationFilter("United States", "us")).toBe(false); + expect(shouldApplyStrictCityFilter("UK", "united kingdom")).toBe(false); + expect(shouldApplyStrictCityFilter("United States", "us")).toBe(false); }); }); diff --git a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.test.tsx b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.test.tsx index edfdf09..c15d3bd 100644 --- a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.test.tsx +++ b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.test.tsx @@ -1,3 +1,5 @@ +import * as api from "@client/api"; +import { renderWithQueryClient } from "@client/test/renderWithQueryClient"; import { createJob } from "@shared/testing/factories.js"; import type { Job } from "@shared/types.js"; import { fireEvent, screen, waitFor } from "@testing-library/react"; @@ -5,8 +7,6 @@ import type React from "react"; import { MemoryRouter } from "react-router-dom"; import { toast } from "sonner"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as api from "../../api"; -import { renderWithQueryClient } from "../../test/renderWithQueryClient"; import { DiscoveredPanel } from "./DiscoveredPanel"; const render = (ui: Parameters[0]) => @@ -44,11 +44,11 @@ vi.mock("@/components/ui/dropdown-menu", () => { }; }); -vi.mock("../../hooks/useSettings", () => ({ +vi.mock("@client/hooks/useSettings", () => ({ useSettings: () => ({ showSponsorInfo: false }), })); -vi.mock("../../api", () => ({ +vi.mock("@client/api", () => ({ rescoreJob: vi.fn(), skipJob: vi.fn(), processJob: vi.fn(), diff --git a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx index f2fa914..dacf9d1 100644 --- a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx +++ b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx @@ -1,10 +1,10 @@ +import * as api from "@client/api"; +import { useSkipJobMutation } from "@client/hooks/queries/useJobMutations"; +import { useRescoreJob } from "@client/hooks/useRescoreJob"; import type { Job } from "@shared/types.js"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import * as api from "../../api"; -import { useSkipJobMutation } from "../../hooks/queries/useJobMutations"; -import { useRescoreJob } from "../../hooks/useRescoreJob"; import { JobDetailsEditDrawer } from "../JobDetailsEditDrawer"; import { DecideMode } from "./DecideMode"; import { EmptyState } from "./EmptyState"; diff --git a/orchestrator/src/client/components/discovered-panel/TailorMode.test.tsx b/orchestrator/src/client/components/discovered-panel/TailorMode.test.tsx index f2b1ca8..2fde6a7 100644 --- a/orchestrator/src/client/components/discovered-panel/TailorMode.test.tsx +++ b/orchestrator/src/client/components/discovered-panel/TailorMode.test.tsx @@ -1,24 +1,24 @@ +import * as api from "@client/api"; +import { useProfile } from "@client/hooks/useProfile"; +import { _resetTracerReadinessCache } from "@client/hooks/useTracerReadiness"; +import { renderWithQueryClient } from "@client/test/renderWithQueryClient"; import { createJob as createBaseJob } from "@shared/testing/factories.js"; import type { Job } from "@shared/types.js"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as api from "../../api"; -import { useProfile } from "../../hooks/useProfile"; -import { _resetTracerReadinessCache } from "../../hooks/useTracerReadiness"; -import { renderWithQueryClient } from "../../test/renderWithQueryClient"; import { TailorMode } from "./TailorMode"; const render = (ui: Parameters[0]) => renderWithQueryClient(ui); -vi.mock("../../api", () => ({ +vi.mock("@client/api", () => ({ getResumeProjectsCatalog: vi.fn().mockResolvedValue([]), updateJob: vi.fn(), summarizeJob: vi.fn(), getTracerReadiness: vi.fn(), })); -vi.mock("../../hooks/useProfile", () => ({ +vi.mock("@client/hooks/useProfile", () => ({ useProfile: vi.fn(), })); diff --git a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx index ac08672..f9aafe0 100644 --- a/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx +++ b/orchestrator/src/client/components/ghostwriter/GhostwriterPanel.tsx @@ -1,8 +1,8 @@ +import * as api from "@client/api"; import type { Job, JobChatMessage, JobChatStreamEvent } from "@shared/types"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import * as api from "../../api"; import { Composer } from "./Composer"; import { MessageList } from "./MessageList"; diff --git a/orchestrator/src/client/components/tailoring/TailoringWorkspace.tsx b/orchestrator/src/client/components/tailoring/TailoringWorkspace.tsx index 0f9cf23..30b8458 100644 --- a/orchestrator/src/client/components/tailoring/TailoringWorkspace.tsx +++ b/orchestrator/src/client/components/tailoring/TailoringWorkspace.tsx @@ -1,3 +1,6 @@ +import * as api from "@client/api"; +import { useProfile } from "@client/hooks/useProfile"; +import { useTracerReadiness } from "@client/hooks/useTracerReadiness"; import type { Job } from "@shared/types.js"; import { ArrowLeft, Check, FileText, Loader2, Sparkles } from "lucide-react"; import type React from "react"; @@ -6,9 +9,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; -import * as api from "../../api"; -import { useProfile } from "../../hooks/useProfile"; -import { useTracerReadiness } from "../../hooks/useTracerReadiness"; import { fromEditableSkillGroups, getOriginalHeadline, diff --git a/orchestrator/src/client/components/tailoring/useTailoringDraft.ts b/orchestrator/src/client/components/tailoring/useTailoringDraft.ts index 20a3b85..c3c58db 100644 --- a/orchestrator/src/client/components/tailoring/useTailoringDraft.ts +++ b/orchestrator/src/client/components/tailoring/useTailoringDraft.ts @@ -1,6 +1,6 @@ +import * as api from "@client/api"; import type { Job, ResumeProjectCatalogItem } from "@shared/types.js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import * as api from "../../api"; import { createTailoredSkillDraftId, type EditableSkillGroup, diff --git a/orchestrator/src/client/pages/job/Timeline.tsx b/orchestrator/src/client/pages/job/Timeline.tsx index a2f4610..73cbeef 100644 --- a/orchestrator/src/client/pages/job/Timeline.tsx +++ b/orchestrator/src/client/pages/job/Timeline.tsx @@ -1,3 +1,4 @@ +import { CollapsibleSection } from "@client/components/discovered-panel/CollapsibleSection"; import { type ApplicationStage, STAGE_LABELS, @@ -18,7 +19,6 @@ import { import React from "react"; import { Badge } from "@/components/ui/badge"; import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils"; -import { CollapsibleSection } from "../../components/discovered-panel/CollapsibleSection"; const stageIcons: Record = { applied: , diff --git a/orchestrator/src/client/pages/orchestrator/JobDetailPanel.test.tsx b/orchestrator/src/client/pages/orchestrator/JobDetailPanel.test.tsx index 0826ab9..6f82cdd 100644 --- a/orchestrator/src/client/pages/orchestrator/JobDetailPanel.test.tsx +++ b/orchestrator/src/client/pages/orchestrator/JobDetailPanel.test.tsx @@ -1,10 +1,10 @@ +import * as api from "@client/api"; +import { renderWithQueryClient } from "@client/test/renderWithQueryClient"; import { createJob } from "@shared/testing/factories.js"; import type { Job } from "@shared/types.js"; import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import type React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as api from "../../api"; -import { renderWithQueryClient } from "../../test/renderWithQueryClient"; import { JobDetailPanel } from "./JobDetailPanel"; const render = (ui: Parameters[0]) => @@ -42,7 +42,7 @@ vi.mock("@/components/ui/dropdown-menu", () => { }; }); -vi.mock("../../components", () => ({ +vi.mock("@client/components", () => ({ DiscoveredPanel: ({ job }: { job: Job | null }) => (
{job?.id ?? "no-job"}
), @@ -51,7 +51,7 @@ vi.mock("../../components", () => ({ TailoredSummary: () =>
, })); -vi.mock("../../components/ReadyPanel", () => ({ +vi.mock("@client/components/ReadyPanel", () => ({ ReadyPanel: ({ onEditDescription }: { onEditDescription?: () => void }) => (
@@ -62,7 +62,7 @@ vi.mock("../../components/ReadyPanel", () => ({ ), })); -vi.mock("../../components/TailoringEditor", () => ({ +vi.mock("@client/components/TailoringEditor", () => ({ TailoringEditor: ({ onDirtyChange, }: { @@ -79,7 +79,7 @@ vi.mock("../../components/TailoringEditor", () => ({ ), })); -vi.mock("../../components/JobDetailsEditDrawer", () => ({ +vi.mock("@client/components/JobDetailsEditDrawer", () => ({ JobDetailsEditDrawer: ({ open, onOpenChange, @@ -116,7 +116,7 @@ vi.mock("@/lib/utils", async (importOriginal) => { }; }); -vi.mock("../../api", () => ({ +vi.mock("@client/api", () => ({ updateJob: vi.fn(), processJob: vi.fn(), generateJobPdf: vi.fn(), diff --git a/orchestrator/src/client/pages/orchestrator/JobDetailPanel.tsx b/orchestrator/src/client/pages/orchestrator/JobDetailPanel.tsx index 54c0ef4..8f8ce9b 100644 --- a/orchestrator/src/client/pages/orchestrator/JobDetailPanel.tsx +++ b/orchestrator/src/client/pages/orchestrator/JobDetailPanel.tsx @@ -1,3 +1,18 @@ +import * as api from "@client/api"; +import { + DiscoveredPanel, + FitAssessment, + JobHeader, + TailoredSummary, +} from "@client/components"; +import { JobDetailsEditDrawer } from "@client/components/JobDetailsEditDrawer"; +import { ReadyPanel } from "@client/components/ReadyPanel"; +import { TailoringEditor } from "@client/components/TailoringEditor"; +import { + useMarkAsAppliedMutation, + useSkipJobMutation, +} from "@client/hooks/queries/useJobMutations"; +import { useProfile } from "@client/hooks/useProfile"; import type { Job, JobListItem } from "@shared/types.js"; import { CheckCircle2, @@ -32,21 +47,6 @@ import { safeFilenamePart, stripHtml, } from "@/lib/utils"; -import * as api from "../../api"; -import { - DiscoveredPanel, - FitAssessment, - JobHeader, - TailoredSummary, -} from "../../components"; -import { JobDetailsEditDrawer } from "../../components/JobDetailsEditDrawer"; -import { ReadyPanel } from "../../components/ReadyPanel"; -import { TailoringEditor } from "../../components/TailoringEditor"; -import { - useMarkAsAppliedMutation, - useSkipJobMutation, -} from "../../hooks/queries/useJobMutations"; -import { useProfile } from "../../hooks/useProfile"; import type { FilterTab } from "./constants"; interface JobDetailPanelProps { diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorSummary.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorSummary.tsx index 6861912..0919ea9 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorSummary.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorSummary.tsx @@ -1,6 +1,6 @@ +import { PipelineProgress } from "@client/components"; import type { JobStatus } from "@shared/types.js"; import type React from "react"; -import { PipelineProgress } from "../../components"; interface OrchestratorSummaryProps { stats: Record; diff --git a/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.test.ts b/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.test.ts index 665580f..d32a056 100644 --- a/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.test.ts +++ b/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.test.ts @@ -1,12 +1,12 @@ +import * as api from "@client/api"; import { createJob } from "@shared/testing/factories.js"; import type { JobActionResponse, JobActionStreamEvent } from "@shared/types.js"; import { act, renderHook, waitFor } from "@testing-library/react"; import { toast } from "sonner"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as api from "../../api"; import { useJobSelectionActions } from "./useJobSelectionActions"; -vi.mock("../../api", () => ({ +vi.mock("@client/api", () => ({ streamJobAction: vi.fn(), })); diff --git a/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.tsx b/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.tsx index a63a133..6ea5ef4 100644 --- a/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.tsx +++ b/orchestrator/src/client/pages/orchestrator/useJobSelectionActions.tsx @@ -1,3 +1,4 @@ +import * as api from "@client/api"; import type { JobAction, JobActionResponse, @@ -5,7 +6,6 @@ import type { } from "@shared/types.js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import * as api from "../../api"; import type { FilterTab } from "./constants"; import { JobActionProgressToast } from "./JobActionProgressToast"; import { diff --git a/orchestrator/src/client/pages/orchestrator/useOrchestratorData.test.ts b/orchestrator/src/client/pages/orchestrator/useOrchestratorData.test.ts index d0f4068..6141f4b 100644 --- a/orchestrator/src/client/pages/orchestrator/useOrchestratorData.test.ts +++ b/orchestrator/src/client/pages/orchestrator/useOrchestratorData.test.ts @@ -1,13 +1,13 @@ +import * as api from "@client/api"; +import { renderHookWithQueryClient } from "@client/test/renderWithQueryClient"; import { act, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as api from "../../api"; -import { renderHookWithQueryClient } from "../../test/renderWithQueryClient"; import { useOrchestratorData } from "./useOrchestratorData"; const renderHook = (callback: () => ReturnType) => renderHookWithQueryClient(callback); -vi.mock("../../api", () => ({ +vi.mock("@client/api", () => ({ getJobs: vi.fn(), getJobsRevision: vi.fn(), getJob: vi.fn(), diff --git a/orchestrator/src/client/pages/orchestrator/useOrchestratorData.ts b/orchestrator/src/client/pages/orchestrator/useOrchestratorData.ts index 87ec964..efaa98f 100644 --- a/orchestrator/src/client/pages/orchestrator/useOrchestratorData.ts +++ b/orchestrator/src/client/pages/orchestrator/useOrchestratorData.ts @@ -1,10 +1,10 @@ +import * as api from "@client/api"; +import { subscribeToEventSource } from "@client/lib/sse"; import type { Job, JobListItem, JobStatus } from "@shared/types"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { queryKeys } from "@/client/lib/queryKeys"; -import * as api from "../../api"; -import { subscribeToEventSource } from "../../lib/sse"; const initialStats: Record = { discovered: 0, diff --git a/orchestrator/src/lib/utils.ts b/orchestrator/src/lib/utils.ts index 149b465..203b50b 100644 --- a/orchestrator/src/lib/utils.ts +++ b/orchestrator/src/lib/utils.ts @@ -3,6 +3,7 @@ import { sourceLabel as getExtractorSourceLabel, } from "@shared/extractors"; import type { Job } from "@shared/types"; +import { stripHtmlTags } from "@shared/utils/string"; import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -101,11 +102,7 @@ export async function copyTextToClipboard(text: string) { } // --- Text Processing --- -export const stripHtml = (value: string) => - value - .replace(/<[^>]*>/g, " ") - .replace(/\s+/g, " ") - .trim(); +export const stripHtml = (value: string) => stripHtmlTags(value); export const safeFilenamePart = (value: string) => { const cleaned = value.replace(/[^a-z0-9]/gi, "_"); diff --git a/orchestrator/src/server/api/routes/backup.ts b/orchestrator/src/server/api/routes/backup.ts index 91290fc..dc7b114 100644 --- a/orchestrator/src/server/api/routes/backup.ts +++ b/orchestrator/src/server/api/routes/backup.ts @@ -1,4 +1,7 @@ +import { notFound } from "@infra/errors"; +import { fail } from "@infra/http"; import { logger } from "@infra/logger"; +import { isDemoMode, sendDemoBlocked } from "@server/config/demo"; import { createBackup, deleteBackup, @@ -6,7 +9,6 @@ import { listBackups, } from "@server/services/backup/index"; import { type Request, type Response, Router } from "express"; -import { isDemoMode, sendDemoBlocked } from "../../config/demo"; export const backupRouter = Router(); @@ -104,7 +106,7 @@ backupRouter.delete("/:filename", async (req: Request, res: Response) => { }); if (message.includes("not found")) { - res.status(404).json({ success: false, error: message }); + return fail(res, notFound(message)); } else if (message.includes("Invalid")) { res.status(400).json({ success: false, error: message }); } else { diff --git a/orchestrator/src/server/api/routes/database.test.ts b/orchestrator/src/server/api/routes/database.test.ts index b9402d7..4222354 100644 --- a/orchestrator/src/server/api/routes/database.test.ts +++ b/orchestrator/src/server/api/routes/database.test.ts @@ -17,7 +17,7 @@ describe.sequential("Database API routes", () => { }); it("clears jobs and pipeline runs", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); await createJob({ source: "manual", title: "Cleanup Role", diff --git a/orchestrator/src/server/api/routes/database.ts b/orchestrator/src/server/api/routes/database.ts index cdbd57f..80ecead 100644 --- a/orchestrator/src/server/api/routes/database.ts +++ b/orchestrator/src/server/api/routes/database.ts @@ -1,6 +1,6 @@ +import { isDemoMode, sendDemoBlocked } from "@server/config/demo"; +import { clearDatabase } from "@server/db/clear"; import { type Request, type Response, Router } from "express"; -import { isDemoMode, sendDemoBlocked } from "../../config/demo"; -import { clearDatabase } from "../../db/clear"; export const databaseRouter = Router(); diff --git a/orchestrator/src/server/api/routes/demo.ts b/orchestrator/src/server/api/routes/demo.ts index 0f0fb27..11b0705 100644 --- a/orchestrator/src/server/api/routes/demo.ts +++ b/orchestrator/src/server/api/routes/demo.ts @@ -1,6 +1,6 @@ import { ok } from "@infra/http"; +import { getDemoInfo } from "@server/config/demo"; import { type Request, type Response, Router } from "express"; -import { getDemoInfo } from "../../config/demo"; export const demoRouter = Router(); diff --git a/orchestrator/src/server/api/routes/ghostwriter.test.ts b/orchestrator/src/server/api/routes/ghostwriter.test.ts index 25fe210..7b9ca81 100644 --- a/orchestrator/src/server/api/routes/ghostwriter.test.ts +++ b/orchestrator/src/server/api/routes/ghostwriter.test.ts @@ -2,7 +2,7 @@ import type { Server } from "node:http"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { startServer, stopServer } from "./test-utils"; -vi.mock("../../services/ghostwriter", () => ({ +vi.mock("@server/services/ghostwriter", () => ({ listThreads: vi.fn(async () => [ { id: "thread-1", diff --git a/orchestrator/src/server/api/routes/ghostwriter.ts b/orchestrator/src/server/api/routes/ghostwriter.ts index 9d6ba42..43f0aba 100644 --- a/orchestrator/src/server/api/routes/ghostwriter.ts +++ b/orchestrator/src/server/api/routes/ghostwriter.ts @@ -2,9 +2,9 @@ import { asyncRoute, fail, ok } from "@infra/http"; import { runWithRequestContext } from "@infra/request-context"; import { setupSse, writeSseData } from "@infra/sse"; import { badRequest, toAppError } from "@server/infra/errors"; +import * as ghostwriterService from "@server/services/ghostwriter"; import { type Request, Router } from "express"; import { z } from "zod"; -import * as ghostwriterService from "../../services/ghostwriter"; export const ghostwriterRouter = Router({ mergeParams: true }); diff --git a/orchestrator/src/server/api/routes/jobs.test.ts b/orchestrator/src/server/api/routes/jobs.test.ts index be871cf..0aec8b5 100644 --- a/orchestrator/src/server/api/routes/jobs.test.ts +++ b/orchestrator/src/server/api/routes/jobs.test.ts @@ -17,7 +17,7 @@ describe.sequential("Jobs API routes", () => { }); it("lists jobs and supports status filtering", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Test Role", @@ -40,7 +40,7 @@ describe.sequential("Jobs API routes", () => { }); it("supports lightweight and full jobs list views", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); await createJob({ source: "manual", title: "List View Role", @@ -76,7 +76,7 @@ describe.sequential("Jobs API routes", () => { }); it("returns jobs revision and supports status filtering", async () => { - const { createJob, updateJob } = await import("../../repositories/jobs"); + const { createJob, updateJob } = await import("@server/repositories/jobs"); const readyJob = await createJob({ source: "manual", title: "Ready Role", @@ -133,7 +133,7 @@ describe.sequential("Jobs API routes", () => { }); it("updates core job detail fields", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Original Title", @@ -176,7 +176,7 @@ describe.sequential("Jobs API routes", () => { }); it("blocks enabling tracer links when readiness check fails", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Tracer Blocked", @@ -221,8 +221,8 @@ describe.sequential("Jobs API routes", () => { }); it("allows updates for already-enabled tracer links without re-gating", async () => { - const { createJob } = await import("../../repositories/jobs"); - const { updateJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); + const { updateJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Tracer Already On", @@ -288,8 +288,8 @@ describe.sequential("Jobs API routes", () => { }); it("prefers JOBOPS_PUBLIC_BASE_URL over forwarded headers for generate-pdf origin", async () => { - const { createJob } = await import("../../repositories/jobs"); - const { generateFinalPdf } = await import("../../pipeline/index"); + const { createJob } = await import("@server/repositories/jobs"); + const { generateFinalPdf } = await import("@server/pipeline/index"); const job = await createJob({ source: "manual", title: "Origin Test", @@ -324,7 +324,7 @@ describe.sequential("Jobs API routes", () => { }); it("returns 409 when patching to a duplicate job URL", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const first = await createJob({ source: "manual", title: "First", @@ -354,7 +354,7 @@ describe.sequential("Jobs API routes", () => { }); it("validates job updates and supports skip/delete flow", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Test Role", @@ -414,7 +414,7 @@ describe.sequential("Jobs API routes", () => { }); it("runs skip action with partial failures", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const discovered = await createJob({ source: "manual", title: "Discovered Role", @@ -436,7 +436,7 @@ describe.sequential("Jobs API routes", () => { jobUrl: "https://example.com/job/action-applied", jobDescription: "Test description", }); - const { updateJob } = await import("../../repositories/jobs"); + const { updateJob } = await import("@server/repositories/jobs"); await updateJob(ready.id, { status: "ready" }); await updateJob(applied.id, { status: "applied" }); @@ -465,7 +465,7 @@ describe.sequential("Jobs API routes", () => { }); it("runs move_to_ready action and rejects ineligible statuses", async () => { - const { createJob, updateJob } = await import("../../repositories/jobs"); + const { createJob, updateJob } = await import("@server/repositories/jobs"); const discovered = await createJob({ source: "manual", title: "New Role", @@ -481,7 +481,7 @@ describe.sequential("Jobs API routes", () => { jobDescription: "Test description", }); await updateJob(ready.id, { status: "ready" }); - const { processJob } = await import("../../pipeline/index"); + const { processJob } = await import("@server/pipeline/index"); const previousBaseUrl = process.env.JOBOPS_PUBLIC_BASE_URL; process.env.JOBOPS_PUBLIC_BASE_URL = "https://canonical.jobops.example"; @@ -516,8 +516,8 @@ describe.sequential("Jobs API routes", () => { }); it("supports legacy move_to_ready endpoint", async () => { - const { createJob } = await import("../../repositories/jobs"); - const { processJob } = await import("../../pipeline/index"); + const { createJob } = await import("@server/repositories/jobs"); + const { processJob } = await import("@server/pipeline/index"); const job = await createJob({ source: "manual", title: "Legacy Ready Route", @@ -550,9 +550,9 @@ describe.sequential("Jobs API routes", () => { }); it("runs rescore action with partial failures", async () => { - const { createJob, updateJob } = await import("../../repositories/jobs"); - const { scoreJobSuitability } = await import("../../services/scorer"); - const { getProfile } = await import("../../services/profile"); + const { createJob, updateJob } = await import("@server/repositories/jobs"); + const { scoreJobSuitability } = await import("@server/services/scorer"); + const { getProfile } = await import("@server/services/profile"); vi.mocked(getProfile).mockResolvedValue({}); vi.mocked(scoreJobSuitability).mockResolvedValue({ @@ -618,7 +618,7 @@ describe.sequential("Jobs API routes", () => { }); it("streams job action progress with done counters", async () => { - const { createJob, updateJob } = await import("../../repositories/jobs"); + const { createJob, updateJob } = await import("@server/repositories/jobs"); const discovered = await createJob({ source: "manual", title: "Discovered Role", @@ -727,7 +727,7 @@ describe.sequential("Jobs API routes", () => { }); it("applies a job", async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Test Role", @@ -746,9 +746,9 @@ describe.sequential("Jobs API routes", () => { }); it("rescoring a job updates the suitability fields", async () => { - const { createJob } = await import("../../repositories/jobs"); - const { scoreJobSuitability } = await import("../../services/scorer"); - const { getProfile } = await import("../../services/profile"); + const { createJob } = await import("@server/repositories/jobs"); + const { scoreJobSuitability } = await import("@server/services/scorer"); + const { getProfile } = await import("@server/services/profile"); vi.mocked(getProfile).mockResolvedValue({}); vi.mocked(scoreJobSuitability).mockResolvedValue({ @@ -764,7 +764,7 @@ describe.sequential("Jobs API routes", () => { jobDescription: "Test description", }); - const { updateJob } = await import("../../repositories/jobs"); + const { updateJob } = await import("@server/repositories/jobs"); await updateJob(job.id, { suitabilityScore: 55, suitabilityReason: "Old fit", @@ -785,7 +785,7 @@ describe.sequential("Jobs API routes", () => { }); it("deletes jobs below a score threshold (excluding applied)", async () => { - const { createJob, updateJob } = await import("../../repositories/jobs"); + const { createJob, updateJob } = await import("@server/repositories/jobs"); // Create jobs with different scores and statuses const lowScoreJob = await createJob({ @@ -883,7 +883,7 @@ describe.sequential("Jobs API routes", () => { it("checks visa sponsor status for a job", async () => { const { searchSponsors } = await import( - "../../services/visa-sponsors/index" + "@server/services/visa-sponsors/index" ); vi.mocked(searchSponsors).mockReturnValue([ { @@ -893,7 +893,7 @@ describe.sequential("Jobs API routes", () => { }, ]); - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Sponsored Dev", @@ -915,7 +915,7 @@ describe.sequential("Jobs API routes", () => { let jobId: string; beforeEach(async () => { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const job = await createJob({ source: "manual", title: "Tracking Test", @@ -986,7 +986,7 @@ describe.sequential("Jobs API routes", () => { }); it("manages application tasks", async () => { - const { db, schema } = await import("../../db/index"); + const { db, schema } = await import("@server/db/index"); const { eq } = await import("drizzle-orm"); const { tasks } = schema; diff --git a/orchestrator/src/server/api/routes/jobs.ts b/orchestrator/src/server/api/routes/jobs.ts index f923fd9..aaa616d 100644 --- a/orchestrator/src/server/api/routes/jobs.ts +++ b/orchestrator/src/server/api/routes/jobs.ts @@ -1,7 +1,42 @@ +import { + AppError, + type AppErrorCode, + badRequest, + conflict, + notFound, +} from "@infra/errors"; import { fail, ok, okWithMeta } from "@infra/http"; import { logger } from "@infra/logger"; import { sanitizeWebhookPayload } from "@infra/sanitize"; import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse"; +import { isDemoMode, sendDemoBlocked } from "@server/config/demo"; +import { + generateFinalPdf, + processJob, + summarizeJob, +} from "@server/pipeline/index"; +import * as jobsRepo from "@server/repositories/jobs"; +import * as settingsRepo from "@server/repositories/settings"; +import { + deleteStageEvent, + getStageEvents, + getTasks, + stageEventMetadataSchema, + transitionStage, + updateStageEvent, +} from "@server/services/applicationTracking"; +import { + simulateApplyJob, + simulateGeneratePdf, + simulateProcessJob, + simulateRescoreJob, + simulateSummarizeJob, +} from "@server/services/demo-simulator"; +import { getProfile } from "@server/services/profile"; +import { scoreJobSuitability } from "@server/services/scorer"; +import { getTracerReadiness } from "@server/services/tracer-links"; +import * as visaSponsors from "@server/services/visa-sponsors/index"; +import { asyncPool } from "@server/utils/async-pool"; import { APPLICATION_OUTCOMES, APPLICATION_STAGES, @@ -17,40 +52,6 @@ import { } from "@shared/types"; import { type Request, type Response, Router } from "express"; import { z } from "zod"; -import { isDemoMode, sendDemoBlocked } from "../../config/demo"; -import { - AppError, - type AppErrorCode, - badRequest, - conflict, -} from "../../infra/errors"; -import { - generateFinalPdf, - processJob, - summarizeJob, -} from "../../pipeline/index"; -import * as jobsRepo from "../../repositories/jobs"; -import * as settingsRepo from "../../repositories/settings"; -import { - deleteStageEvent, - getStageEvents, - getTasks, - stageEventMetadataSchema, - transitionStage, - updateStageEvent, -} from "../../services/applicationTracking"; -import { - simulateApplyJob, - simulateGeneratePdf, - simulateProcessJob, - simulateRescoreJob, - simulateSummarizeJob, -} from "../../services/demo-simulator"; -import { getProfile } from "../../services/profile"; -import { scoreJobSuitability } from "../../services/scorer"; -import { getTracerReadiness } from "../../services/tracer-links"; -import * as visaSponsors from "../../services/visa-sponsors/index"; -import { asyncPool } from "../../utils/async-pool"; export const jobsRouter = Router(); const JOB_ACTION_CONCURRENCY = 4; @@ -884,7 +885,7 @@ jobsRouter.get("/:id", async (req: Request, res: Response) => { try { const job = await jobsRepo.getJobById(req.params.id); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } res.json({ success: true, data: job }); } catch (error) { @@ -996,7 +997,7 @@ jobsRouter.patch("/:id/outcome", async (req: Request, res: Response) => { }); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } res.json({ success: true, data: job }); @@ -1126,7 +1127,7 @@ jobsRouter.post("/:id/summarize", async (req: Request, res: Response) => { } const job = await jobsRepo.getJobById(req.params.id); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } return okWithMeta(res, job, { simulated: true }); } @@ -1153,7 +1154,7 @@ jobsRouter.post("/:id/check-sponsor", async (req: Request, res: Response) => { const job = await jobsRepo.getJobById(req.params.id); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } if (!job.employer) { @@ -1203,7 +1204,7 @@ jobsRouter.post("/:id/generate-pdf", async (req: Request, res: Response) => { } const job = await jobsRepo.getJobById(req.params.id); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } return okWithMeta(res, job, { simulated: true }); } @@ -1237,7 +1238,7 @@ jobsRouter.post("/:id/apply", async (req: Request, res: Response) => { const job = await jobsRepo.getJobById(req.params.id); if (!job) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } const appliedAtDate = new Date(); @@ -1266,7 +1267,7 @@ jobsRouter.post("/:id/apply", async (req: Request, res: Response) => { } if (!updatedJob) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } res.json({ success: true, data: updatedJob }); diff --git a/orchestrator/src/server/api/routes/manual-jobs.test.ts b/orchestrator/src/server/api/routes/manual-jobs.test.ts index 866c1d5..5c8b70a 100644 --- a/orchestrator/src/server/api/routes/manual-jobs.test.ts +++ b/orchestrator/src/server/api/routes/manual-jobs.test.ts @@ -46,7 +46,9 @@ describe.sequential("Manual jobs API routes", () => { }); expect(badRes.status).toBe(400); - const { inferManualJobDetails } = await import("../../services/manualJob"); + const { inferManualJobDetails } = await import( + "@server/services/manualJob" + ); vi.mocked(inferManualJobDetails).mockResolvedValue({ job: { title: "Backend Engineer", employer: "Acme" }, warning: null, @@ -63,8 +65,8 @@ describe.sequential("Manual jobs API routes", () => { }); it("imports manual jobs and generates a fallback URL", async () => { - const { processJob } = await import("../../pipeline/index"); - const { scoreJobSuitability } = await import("../../services/scorer"); + const { processJob } = await import("@server/pipeline/index"); + const { scoreJobSuitability } = await import("@server/services/scorer"); vi.mocked(scoreJobSuitability).mockResolvedValue({ score: 88, reason: "Strong fit", diff --git a/orchestrator/src/server/api/routes/manual-jobs.ts b/orchestrator/src/server/api/routes/manual-jobs.ts index 644ba9c..26bc009 100644 --- a/orchestrator/src/server/api/routes/manual-jobs.ts +++ b/orchestrator/src/server/api/routes/manual-jobs.ts @@ -1,5 +1,12 @@ import { randomUUID } from "node:crypto"; +import { notFound } from "@infra/errors"; +import { fail } from "@infra/http"; import { logger } from "@infra/logger"; +import { processJob } from "@server/pipeline/index"; +import * as jobsRepo from "@server/repositories/jobs"; +import { inferManualJobDetails } from "@server/services/manualJob"; +import { getProfile } from "@server/services/profile"; +import { scoreJobSuitability } from "@server/services/scorer"; import type { ApiResponse, ManualJobFetchResponse, @@ -8,11 +15,6 @@ import type { import { type Request, type Response, Router } from "express"; import { JSDOM } from "jsdom"; import { z } from "zod"; -import { processJob } from "../../pipeline/index"; -import * as jobsRepo from "../../repositories/jobs"; -import { inferManualJobDetails } from "../../services/manualJob"; -import { getProfile } from "../../services/profile"; -import { scoreJobSuitability } from "../../services/scorer"; export const manualJobsRouter = Router(); @@ -252,7 +254,7 @@ manualJobsRouter.post("/import", async (req: Request, res: Response) => { const processedJob = await jobsRepo.getJobById(createdJob.id); if (!processedJob) { - return res.status(404).json({ success: false, error: "Job not found" }); + return fail(res, notFound("Job not found")); } // Score asynchronously so the import returns immediately. diff --git a/orchestrator/src/server/api/routes/onboarding.ts b/orchestrator/src/server/api/routes/onboarding.ts index de2cd4a..aa1e61f 100644 --- a/orchestrator/src/server/api/routes/onboarding.ts +++ b/orchestrator/src/server/api/routes/onboarding.ts @@ -1,5 +1,6 @@ import { okWithMeta } from "@infra/http"; import { logger } from "@infra/logger"; +import { isDemoMode } from "@server/config/demo"; import { getSetting } from "@server/repositories/settings"; import { LlmService } from "@server/services/llm/service"; import { RxResumeClient } from "@server/services/rxresume-client"; @@ -9,7 +10,6 @@ import { } from "@server/services/rxresume-v4"; import { resumeDataSchema } from "@shared/rxresume-schema"; import { type Request, type Response, Router } from "express"; -import { isDemoMode } from "../../config/demo"; export const onboardingRouter = Router(); diff --git a/orchestrator/src/server/api/routes/pipeline.test.ts b/orchestrator/src/server/api/routes/pipeline.test.ts index 0939b30..4b5efbb 100644 --- a/orchestrator/src/server/api/routes/pipeline.test.ts +++ b/orchestrator/src/server/api/routes/pipeline.test.ts @@ -32,7 +32,7 @@ describe.sequential("Pipeline API routes", () => { }); expect(badRun.status).toBe(400); - const { runPipeline } = await import("../../pipeline/index"); + const { runPipeline } = await import("@server/pipeline/index"); const runRes = await fetch(`${baseUrl}/api/pipeline/run`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -81,7 +81,7 @@ describe.sequential("Pipeline API routes", () => { }); it("accepts cancellation when pipeline is running", async () => { - const { requestPipelineCancel } = await import("../../pipeline/index"); + const { requestPipelineCancel } = await import("@server/pipeline/index"); vi.mocked(requestPipelineCancel).mockReturnValue({ accepted: true, pipelineRunId: "run-1", diff --git a/orchestrator/src/server/api/routes/pipeline.ts b/orchestrator/src/server/api/routes/pipeline.ts index 13a9fee..21ab4cf 100644 --- a/orchestrator/src/server/api/routes/pipeline.ts +++ b/orchestrator/src/server/api/routes/pipeline.ts @@ -9,23 +9,23 @@ import { fail, ok, okWithMeta } from "@infra/http"; import { logger } from "@infra/logger"; import { runWithRequestContext } from "@infra/request-context"; import { setupSse, startSseHeartbeat, writeSseData } from "@infra/sse"; -import { PIPELINE_EXTRACTOR_SOURCE_IDS } from "@shared/extractors"; -import type { PipelineStatusResponse } from "@shared/types"; -import { type Request, type Response, Router } from "express"; -import { z } from "zod"; -import { isDemoMode } from "../../config/demo"; +import { isDemoMode } from "@server/config/demo"; import { type ExtractorRegistry, getExtractorRegistry, -} from "../../extractors/registry"; +} from "@server/extractors/registry"; import { getPipelineStatus, requestPipelineCancel, runPipeline, subscribeToProgress, -} from "../../pipeline/index"; -import * as pipelineRepo from "../../repositories/pipeline"; -import { simulatePipelineRun } from "../../services/demo-simulator"; +} from "@server/pipeline/index"; +import * as pipelineRepo from "@server/repositories/pipeline"; +import { simulatePipelineRun } from "@server/services/demo-simulator"; +import { PIPELINE_EXTRACTOR_SOURCE_IDS } from "@shared/extractors"; +import type { PipelineStatusResponse } from "@shared/types"; +import { type Request, type Response, Router } from "express"; +import { z } from "zod"; export const pipelineRouter = Router(); diff --git a/orchestrator/src/server/api/routes/post-application-providers.test.ts b/orchestrator/src/server/api/routes/post-application-providers.test.ts index 6538c17..5fab7db 100644 --- a/orchestrator/src/server/api/routes/post-application-providers.test.ts +++ b/orchestrator/src/server/api/routes/post-application-providers.test.ts @@ -2,7 +2,7 @@ import type { Server } from "node:http"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { startServer, stopServer } from "./test-utils"; -vi.mock("../../services/post-application/providers", () => ({ +vi.mock("@server/services/post-application/providers", () => ({ executePostApplicationProviderAction: vi.fn(), })); @@ -36,7 +36,7 @@ describe.sequential("Post-Application Provider actions API", () => { it("dispatches provider status action and returns unified success contract", async () => { const { executePostApplicationProviderAction } = await import( - "../../services/post-application/providers" + "@server/services/post-application/providers" ); vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({ provider: "gmail", @@ -92,7 +92,7 @@ describe.sequential("Post-Application Provider actions API", () => { it("defaults to account key 'default' when omitted", async () => { const { executePostApplicationProviderAction } = await import( - "../../services/post-application/providers" + "@server/services/post-application/providers" ); vi.mocked(executePostApplicationProviderAction).mockResolvedValueOnce({ provider: "gmail", @@ -155,7 +155,7 @@ describe.sequential("Post-Application Provider actions API", () => { it("maps provider service errors to standardized error responses", async () => { const { executePostApplicationProviderAction } = await import( - "../../services/post-application/providers" + "@server/services/post-application/providers" ); const { AppError } = await import("@infra/errors"); vi.mocked(executePostApplicationProviderAction).mockRejectedValueOnce( diff --git a/orchestrator/src/server/api/routes/post-application-providers.ts b/orchestrator/src/server/api/routes/post-application-providers.ts index df5806f..041be9a 100644 --- a/orchestrator/src/server/api/routes/post-application-providers.ts +++ b/orchestrator/src/server/api/routes/post-application-providers.ts @@ -2,13 +2,13 @@ import { randomUUID } from "node:crypto"; import { badRequest, serviceUnavailable, upstreamError } from "@infra/errors"; import { asyncRoute, fail, ok } from "@infra/http"; import { logger } from "@infra/logger"; +import { executePostApplicationProviderAction } from "@server/services/post-application/providers"; import { POST_APPLICATION_PROVIDER_ACTIONS, POST_APPLICATION_PROVIDERS, } from "@shared/types"; import { type Request, type Response, Router } from "express"; import { z } from "zod"; -import { executePostApplicationProviderAction } from "../../services/post-application/providers"; const providerActionParamsSchema = z.object({ provider: z.enum(POST_APPLICATION_PROVIDERS), diff --git a/orchestrator/src/server/api/routes/post-application-review.test.ts b/orchestrator/src/server/api/routes/post-application-review.test.ts index d7764f5..bebe8fe 100644 --- a/orchestrator/src/server/api/routes/post-application-review.test.ts +++ b/orchestrator/src/server/api/routes/post-application-review.test.ts @@ -29,9 +29,9 @@ describe.sequential("Post-Application Review Workflow API", () => { message: PostApplicationMessage; jobId: string; }> { - const { createJob } = await import("../../repositories/jobs"); + const { createJob } = await import("@server/repositories/jobs"); const { upsertPostApplicationMessage } = await import( - "../../repositories/post-application-messages" + "@server/repositories/post-application-messages" ); const job = await createJob({ @@ -93,7 +93,7 @@ describe.sequential("Post-Application Review Workflow API", () => { it("approves an inbox item and writes stage event", async () => { const { message, jobId } = await seedPendingMessage(); - const { db, schema } = await import("../../db"); + const { db, schema } = await import("@server/db"); const res = await fetch( `${baseUrl}/api/post-application/inbox/${message.id}/approve`, @@ -121,7 +121,7 @@ describe.sequential("Post-Application Review Workflow API", () => { it("returns conflict on second approve and increments sync-run approval once", async () => { const { startPostApplicationSyncRun, getPostApplicationSyncRunById } = - await import("../../repositories/post-application-sync-runs"); + await import("@server/repositories/post-application-sync-runs"); const run = await startPostApplicationSyncRun({ provider: "gmail", accountKey: "default", @@ -218,7 +218,7 @@ describe.sequential("Post-Application Review Workflow API", () => { it("lists messages for a sync run", async () => { const { startPostApplicationSyncRun } = await import( - "../../repositories/post-application-sync-runs" + "@server/repositories/post-application-sync-runs" ); const run = await startPostApplicationSyncRun({ provider: "gmail", @@ -243,7 +243,7 @@ describe.sequential("Post-Application Review Workflow API", () => { const { message, jobId } = await seedPendingMessage({ stageTarget: "rejected", }); - const { db, schema } = await import("../../db"); + const { db, schema } = await import("@server/db"); const res = await fetch( `${baseUrl}/api/post-application/inbox/${message.id}/approve`, @@ -274,7 +274,7 @@ describe.sequential("Post-Application Review Workflow API", () => { const { message, jobId } = await seedPendingMessage({ stageTarget: "withdrawn", }); - const { db, schema } = await import("../../db"); + const { db, schema } = await import("@server/db"); const res = await fetch( `${baseUrl}/api/post-application/inbox/${message.id}/approve`, diff --git a/orchestrator/src/server/api/routes/post-application-review.ts b/orchestrator/src/server/api/routes/post-application-review.ts index 0894388..fd53fb3 100644 --- a/orchestrator/src/server/api/routes/post-application-review.ts +++ b/orchestrator/src/server/api/routes/post-application-review.ts @@ -1,12 +1,5 @@ import { badRequest } from "@infra/errors"; import { asyncRoute, fail, ok } from "@infra/http"; -import { - APPLICATION_STAGES, - POST_APPLICATION_PROVIDERS, - POST_APPLICATION_ROUTER_STAGE_TARGETS, -} from "@shared/types"; -import { type Request, type Response, Router } from "express"; -import { z } from "zod"; import { approvePostApplicationInboxItem, denyPostApplicationInboxItem, @@ -14,7 +7,14 @@ import { listPostApplicationReviewRuns, listPostApplicationRunMessages, runPostApplicationInboxAction, -} from "../../services/post-application/review"; +} from "@server/services/post-application/review"; +import { + APPLICATION_STAGES, + POST_APPLICATION_PROVIDERS, + POST_APPLICATION_ROUTER_STAGE_TARGETS, +} from "@shared/types"; +import { type Request, type Response, Router } from "express"; +import { z } from "zod"; const listQuerySchema = z.object({ provider: z.enum(POST_APPLICATION_PROVIDERS).default("gmail"), diff --git a/orchestrator/src/server/api/routes/profile.test.ts b/orchestrator/src/server/api/routes/profile.test.ts index 8545e5b..604ea08 100644 --- a/orchestrator/src/server/api/routes/profile.test.ts +++ b/orchestrator/src/server/api/routes/profile.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { startServer, stopServer } from "./test-utils"; // Mock the rxresume-v4 service -vi.mock("../../services/rxresume-v4", () => ({ +vi.mock("@server/services/rxresume-v4", () => ({ getResume: vi.fn(), listResumes: vi.fn(), RxResumeCredentialsError: class RxResumeCredentialsError extends Error { @@ -15,13 +15,13 @@ vi.mock("../../services/rxresume-v4", () => ({ })); // Mock the profile service -vi.mock("../../services/profile", () => ({ +vi.mock("@server/services/profile", () => ({ getProfile: vi.fn(), clearProfileCache: vi.fn(), })); // Mock the settings repository -vi.mock("../../repositories/settings", async (importOriginal) => { +vi.mock("@server/repositories/settings", async (importOriginal) => { const original = (await importOriginal()) as Record; return { ...original, @@ -29,12 +29,12 @@ vi.mock("../../repositories/settings", async (importOriginal) => { }; }); -import { getSetting } from "../../repositories/settings"; -import { getProfile } from "../../services/profile"; +import { getSetting } from "@server/repositories/settings"; +import { getProfile } from "@server/services/profile"; import { getResume, RxResumeCredentialsError, -} from "../../services/rxresume-v4"; +} from "@server/services/rxresume-v4"; describe.sequential("Profile API routes", () => { let server: Server; diff --git a/orchestrator/src/server/api/routes/profile.ts b/orchestrator/src/server/api/routes/profile.ts index 2aa563f..28263a5 100644 --- a/orchestrator/src/server/api/routes/profile.ts +++ b/orchestrator/src/server/api/routes/profile.ts @@ -1,13 +1,13 @@ -import { type Request, type Response, Router } from "express"; -import { isDemoMode } from "../../config/demo"; -import { DEMO_PROJECT_CATALOG } from "../../config/demo-defaults"; -import { getSetting } from "../../repositories/settings"; -import { clearProfileCache, getProfile } from "../../services/profile"; -import { extractProjectsFromProfile } from "../../services/resumeProjects"; +import { isDemoMode } from "@server/config/demo"; +import { DEMO_PROJECT_CATALOG } from "@server/config/demo-defaults"; +import { getSetting } from "@server/repositories/settings"; +import { clearProfileCache, getProfile } from "@server/services/profile"; +import { extractProjectsFromProfile } from "@server/services/resumeProjects"; import { getResume, RxResumeCredentialsError, -} from "../../services/rxresume-v4"; +} from "@server/services/rxresume-v4"; +import { type Request, type Response, Router } from "express"; export const profileRouter = Router(); diff --git a/orchestrator/src/server/api/routes/settings.ts b/orchestrator/src/server/api/routes/settings.ts index 49c7ddb..27edb18 100644 --- a/orchestrator/src/server/api/routes/settings.ts +++ b/orchestrator/src/server/api/routes/settings.ts @@ -1,4 +1,5 @@ import { logger } from "@infra/logger"; +import { isDemoMode, sendDemoBlocked } from "@server/config/demo"; import { setBackupSettings } from "@server/services/backup/index"; import { extractProjectsFromProfile } from "@server/services/resumeProjects"; import { @@ -10,7 +11,6 @@ import { getEffectiveSettings } from "@server/services/settings"; import { applySettingsUpdates } from "@server/services/settings-update"; import { updateSettingsSchema } from "@shared/settings-schema"; import { type Request, type Response, Router } from "express"; -import { isDemoMode, sendDemoBlocked } from "../../config/demo"; export const settingsRouter = Router(); diff --git a/orchestrator/src/server/api/routes/test-utils.ts b/orchestrator/src/server/api/routes/test-utils.ts index b43cf26..245c46e 100644 --- a/orchestrator/src/server/api/routes/test-utils.ts +++ b/orchestrator/src/server/api/routes/test-utils.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { vi } from "vitest"; -vi.mock("../../pipeline/index", () => { +vi.mock("@server/pipeline/index", () => { const progress = { step: "idle", message: "Ready", @@ -51,19 +51,19 @@ vi.mock("../../pipeline/index", () => { }; }); -vi.mock("../../services/manualJob", () => ({ +vi.mock("@server/services/manualJob", () => ({ inferManualJobDetails: vi.fn(), })); -vi.mock("../../services/scorer", () => ({ +vi.mock("@server/services/scorer", () => ({ scoreJobSuitability: vi.fn(), })); -vi.mock("../../services/profile", () => ({ +vi.mock("@server/services/profile", () => ({ getProfile: vi.fn().mockResolvedValue({}), })); -vi.mock("../../services/visa-sponsors/index", () => ({ +vi.mock("@server/services/visa-sponsors/index", () => ({ getStatus: vi.fn(), searchSponsors: vi.fn(), getOrganizationDetails: vi.fn(), @@ -102,13 +102,13 @@ export async function startServer(options?: { ...envOverrides, }; - await import("../../db/migrate"); + await import("@server/db/migrate"); const { applyStoredEnvOverrides } = await import( - "../../services/envSettings" + "@server/services/envSettings" ); const { createApp } = await import("../../app"); - const { closeDb } = await import("../../db/index"); - const { getPipelineStatus } = await import("../../pipeline/index"); + const { closeDb } = await import("@server/db/index"); + const { getPipelineStatus } = await import("@server/pipeline/index"); vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false }); await applyStoredEnvOverrides(); diff --git a/orchestrator/src/server/api/routes/tracer-links.test.ts b/orchestrator/src/server/api/routes/tracer-links.test.ts index 792ffe9..80dd164 100644 --- a/orchestrator/src/server/api/routes/tracer-links.test.ts +++ b/orchestrator/src/server/api/routes/tracer-links.test.ts @@ -19,7 +19,7 @@ describe.sequential("Tracer links routes", () => { }); async function seedTracerFixtures() { - const { db, schema } = await import("../../db"); + const { db, schema } = await import("@server/db"); const now = new Date().toISOString(); const jobId = "job-tracer-fixture"; diff --git a/orchestrator/src/server/api/routes/tracer-links.ts b/orchestrator/src/server/api/routes/tracer-links.ts index afeb88f..0bdadc9 100644 --- a/orchestrator/src/server/api/routes/tracer-links.ts +++ b/orchestrator/src/server/api/routes/tracer-links.ts @@ -1,13 +1,13 @@ import { badRequest, notFound } from "@infra/errors"; import { asyncRoute, fail, ok } from "@infra/http"; -import { type Request, type Response, Router } from "express"; -import { z } from "zod"; -import * as jobsRepo from "../../repositories/jobs"; +import * as jobsRepo from "@server/repositories/jobs"; import { getJobTracerLinksAnalytics, getTracerAnalytics, getTracerReadiness, -} from "../../services/tracer-links"; +} from "@server/services/tracer-links"; +import { type Request, type Response, Router } from "express"; +import { z } from "zod"; export const tracerLinksRouter = Router(); diff --git a/orchestrator/src/server/api/routes/visa-sponsors.test.ts b/orchestrator/src/server/api/routes/visa-sponsors.test.ts index 19c2b20..af9fdba 100644 --- a/orchestrator/src/server/api/routes/visa-sponsors.test.ts +++ b/orchestrator/src/server/api/routes/visa-sponsors.test.ts @@ -18,7 +18,7 @@ describe.sequential("Visa sponsors API routes", () => { it("returns status and surfaces update errors", async () => { const { getStatus, downloadLatestCsv } = await import( - "../../services/visa-sponsors/index" + "@server/services/visa-sponsors/index" ); vi.mocked(getStatus).mockReturnValue({ lastUpdated: null, @@ -46,7 +46,7 @@ describe.sequential("Visa sponsors API routes", () => { it("validates search payloads and handles missing organizations", async () => { const { searchSponsors, getOrganizationDetails } = await import( - "../../services/visa-sponsors/index" + "@server/services/visa-sponsors/index" ); vi.mocked(searchSponsors).mockReturnValue([ { diff --git a/orchestrator/src/server/api/routes/visa-sponsors.ts b/orchestrator/src/server/api/routes/visa-sponsors.ts index 5817b70..1e1f01d 100644 --- a/orchestrator/src/server/api/routes/visa-sponsors.ts +++ b/orchestrator/src/server/api/routes/visa-sponsors.ts @@ -1,3 +1,6 @@ +import { notFound } from "@infra/errors"; +import { fail } from "@infra/http"; +import * as visaSponsors from "@server/services/visa-sponsors/index"; import type { ApiResponse, VisaSponsorSearchResponse, @@ -6,8 +9,6 @@ import type { import { type Request, type Response, Router } from "express"; import { z } from "zod"; -import * as visaSponsors from "../../services/visa-sponsors/index"; - export const visaSponsorsRouter = Router(); /** @@ -74,9 +75,7 @@ visaSponsorsRouter.get( const entries = visaSponsors.getOrganizationDetails(name); if (entries.length === 0) { - return res - .status(404) - .json({ success: false, error: "Organization not found" }); + return fail(res, notFound("Organization not found")); } res.json({ diff --git a/orchestrator/src/server/api/routes/webhook.ts b/orchestrator/src/server/api/routes/webhook.ts index 404476e..80bf3b7 100644 --- a/orchestrator/src/server/api/routes/webhook.ts +++ b/orchestrator/src/server/api/routes/webhook.ts @@ -2,10 +2,10 @@ import { unauthorized } from "@infra/errors"; import { fail, okWithMeta } from "@infra/http"; import { logger } from "@infra/logger"; import { runWithRequestContext } from "@infra/request-context"; +import { isDemoMode } from "@server/config/demo"; +import { runPipeline } from "@server/pipeline/index"; +import { simulatePipelineRun } from "@server/services/demo-simulator"; import { type Request, type Response, Router } from "express"; -import { isDemoMode } from "../../config/demo"; -import { runPipeline } from "../../pipeline/index"; -import { simulatePipelineRun } from "../../services/demo-simulator"; export const webhookRouter = Router(); diff --git a/orchestrator/src/server/pipeline/steps/discover-jobs.test.ts b/orchestrator/src/server/pipeline/steps/discover-jobs.test.ts index 7143f54..a0724c1 100644 --- a/orchestrator/src/server/pipeline/steps/discover-jobs.test.ts +++ b/orchestrator/src/server/pipeline/steps/discover-jobs.test.ts @@ -3,15 +3,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { getProgress, resetProgress } from "../progress"; import { discoverJobsStep } from "./discover-jobs"; -vi.mock("../../repositories/settings", () => ({ +vi.mock("@server/repositories/settings", () => ({ getAllSettings: vi.fn(), })); -vi.mock("../../repositories/jobs", () => ({ +vi.mock("@server/repositories/jobs", () => ({ getAllJobUrls: vi.fn().mockResolvedValue([]), })); -vi.mock("../../extractors/registry", () => ({ +vi.mock("@server/extractors/registry", () => ({ getExtractorRegistry: vi.fn(), })); @@ -33,8 +33,8 @@ describe("discoverJobsStep", () => { }); it("aggregates source errors for enabled sources", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); const jobspyManifest = { id: "jobspy", @@ -93,8 +93,8 @@ describe("discoverJobsStep", () => { }); it("throws when all enabled sources fail", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); const ukvisaManifest = { id: "ukvisajobs", @@ -130,8 +130,8 @@ describe("discoverJobsStep", () => { }); it("throws when all requested sources are incompatible for country", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({ searchTerms: JSON.stringify(["engineer"]), @@ -157,8 +157,8 @@ describe("discoverJobsStep", () => { }); it("does not throw when no sources are requested", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); vi.mocked(settingsRepo.getAllSettings).mockResolvedValue({ searchTerms: JSON.stringify(["engineer"]), @@ -183,8 +183,8 @@ describe("discoverJobsStep", () => { }); it("drops discovered jobs when employer matches blocked company keywords", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); const jobspyManifest = { id: "jobspy", @@ -236,8 +236,8 @@ describe("discoverJobsStep", () => { }); it("applies shared city filtering for sources without native city filtering", async () => { - const settingsRepo = await import("../../repositories/settings"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const registryModule = await import("@server/extractors/registry"); const gradcrackerManifest = { id: "gradcracker", @@ -313,9 +313,9 @@ describe("discoverJobsStep", () => { }); it("tracks source completion counters across source transitions", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); - const registryModule = await import("../../extractors/registry"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); + const registryModule = await import("@server/extractors/registry"); const jobspyManifest = { id: "jobspy", diff --git a/orchestrator/src/server/pipeline/steps/discover-jobs.ts b/orchestrator/src/server/pipeline/steps/discover-jobs.ts index fbe7e12..0d8eb5b 100644 --- a/orchestrator/src/server/pipeline/steps/discover-jobs.ts +++ b/orchestrator/src/server/pipeline/steps/discover-jobs.ts @@ -1,5 +1,9 @@ import { logger } from "@infra/logger"; import { sanitizeUnknown } from "@infra/sanitize"; +import { getExtractorRegistry } from "@server/extractors/registry"; +import { getAllJobUrls } from "@server/repositories/jobs"; +import * as settingsRepo from "@server/repositories/settings"; +import { asyncPool } from "@server/utils/async-pool"; import { formatCountryLabel, isSourceAllowedForCountry, @@ -12,10 +16,6 @@ import { shouldApplyStrictCityFilter, } from "@shared/search-cities.js"; import type { CreateJobInput, PipelineConfig } from "@shared/types"; -import { getExtractorRegistry } from "../../extractors/registry"; -import { getAllJobUrls } from "../../repositories/jobs"; -import * as settingsRepo from "../../repositories/settings"; -import { asyncPool } from "../../utils/async-pool"; import { type CrawlSource, progressHelpers, updateProgress } from "../progress"; const DISCOVERY_CONCURRENCY = 3; diff --git a/orchestrator/src/server/pipeline/steps/import-jobs.ts b/orchestrator/src/server/pipeline/steps/import-jobs.ts index 9a93868..c88f8c1 100644 --- a/orchestrator/src/server/pipeline/steps/import-jobs.ts +++ b/orchestrator/src/server/pipeline/steps/import-jobs.ts @@ -1,6 +1,6 @@ import { logger } from "@infra/logger"; +import * as jobsRepo from "@server/repositories/jobs"; import type { CreateJobInput } from "@shared/types"; -import * as jobsRepo from "../../repositories/jobs"; import { progressHelpers } from "../progress"; export async function importJobsStep(args: { diff --git a/orchestrator/src/server/pipeline/steps/load-profile.ts b/orchestrator/src/server/pipeline/steps/load-profile.ts index fba6a0a..dea02a4 100644 --- a/orchestrator/src/server/pipeline/steps/load-profile.ts +++ b/orchestrator/src/server/pipeline/steps/load-profile.ts @@ -1,5 +1,5 @@ import { logger } from "@infra/logger"; -import { getProfile } from "../../services/profile"; +import { getProfile } from "@server/services/profile"; export async function loadProfileStep(): Promise> { logger.info("Loading profile"); diff --git a/orchestrator/src/server/pipeline/steps/notify-webhook.ts b/orchestrator/src/server/pipeline/steps/notify-webhook.ts index 478ed90..0e3ad4e 100644 --- a/orchestrator/src/server/pipeline/steps/notify-webhook.ts +++ b/orchestrator/src/server/pipeline/steps/notify-webhook.ts @@ -1,6 +1,6 @@ import { logger } from "@infra/logger"; import { sanitizeWebhookPayload } from "@infra/sanitize"; -import * as settingsRepo from "../../repositories/settings"; +import * as settingsRepo from "@server/repositories/settings"; export async function notifyPipelineWebhookStep( event: "pipeline.completed" | "pipeline.failed", diff --git a/orchestrator/src/server/pipeline/steps/process-jobs.ts b/orchestrator/src/server/pipeline/steps/process-jobs.ts index b3c9e53..3178ad7 100644 --- a/orchestrator/src/server/pipeline/steps/process-jobs.ts +++ b/orchestrator/src/server/pipeline/steps/process-jobs.ts @@ -1,5 +1,5 @@ import { logger } from "@infra/logger"; -import { asyncPool } from "../../utils/async-pool"; +import { asyncPool } from "@server/utils/async-pool"; import { progressHelpers, updateProgress } from "../progress"; import type { ScoredJob } from "./types"; diff --git a/orchestrator/src/server/pipeline/steps/score-jobs.test.ts b/orchestrator/src/server/pipeline/steps/score-jobs.test.ts index 9c1c5c9..1c3bd56 100644 --- a/orchestrator/src/server/pipeline/steps/score-jobs.test.ts +++ b/orchestrator/src/server/pipeline/steps/score-jobs.test.ts @@ -10,20 +10,20 @@ vi.mock("@infra/logger", () => ({ }, })); -vi.mock("../../repositories/jobs", () => ({ +vi.mock("@server/repositories/jobs", () => ({ getUnscoredDiscoveredJobs: vi.fn(), updateJob: vi.fn(), })); -vi.mock("../../repositories/settings", () => ({ +vi.mock("@server/repositories/settings", () => ({ getSetting: vi.fn(), })); -vi.mock("../../services/scorer", () => ({ +vi.mock("@server/services/scorer", () => ({ scoreJobSuitability: vi.fn(), })); -vi.mock("../../services/visa-sponsors/index", () => ({ +vi.mock("@server/services/visa-sponsors/index", () => ({ searchSponsors: vi.fn(), calculateSponsorMatchSummary: vi.fn(), })); @@ -40,10 +40,10 @@ describe("scoreJobsStep auto-skip behavior", () => { beforeEach(async () => { vi.clearAllMocks(); - const jobsRepo = await import("../../repositories/jobs"); - const settingsRepo = await import("../../repositories/settings"); - const scorer = await import("../../services/scorer"); - const visaSponsors = await import("../../services/visa-sponsors/index"); + const jobsRepo = await import("@server/repositories/jobs"); + const settingsRepo = await import("@server/repositories/settings"); + const scorer = await import("@server/services/scorer"); + const visaSponsors = await import("@server/services/visa-sponsors/index"); vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([ createJob({ @@ -68,8 +68,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("auto-skips jobs when score is below threshold", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); const { logger } = await import("@infra/logger"); vi.mocked(settingsRepo.getSetting).mockResolvedValue("50"); @@ -94,9 +94,9 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("does not auto-skip jobs when score equals threshold", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); - const scorer = await import("../../services/scorer"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); + const scorer = await import("@server/services/scorer"); const { logger } = await import("@infra/logger"); vi.mocked(settingsRepo.getSetting).mockResolvedValue("50"); @@ -124,8 +124,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("does not auto-skip when threshold setting is null", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); vi.mocked(settingsRepo.getSetting).mockResolvedValue(null); @@ -138,8 +138,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("does not auto-skip when threshold setting is NaN", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); vi.mocked(settingsRepo.getSetting).mockResolvedValue("not-a-number"); @@ -152,8 +152,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("never auto-skips applied jobs even when score is below threshold", async () => { - const settingsRepo = await import("../../repositories/settings"); - const jobsRepo = await import("../../repositories/jobs"); + const settingsRepo = await import("@server/repositories/settings"); + const jobsRepo = await import("@server/repositories/jobs"); const { logger } = await import("@infra/logger"); vi.mocked(settingsRepo.getSetting).mockResolvedValue("50"); @@ -185,8 +185,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("scores multiple jobs and reports completion progress", async () => { - const jobsRepo = await import("../../repositories/jobs"); - const scorer = await import("../../services/scorer"); + const jobsRepo = await import("@server/repositories/jobs"); + const scorer = await import("@server/services/scorer"); const { progressHelpers } = await import("../progress"); vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([ @@ -217,8 +217,8 @@ describe("scoreJobsStep auto-skip behavior", () => { }); it("stops before processing when cancellation is requested", async () => { - const jobsRepo = await import("../../repositories/jobs"); - const scorer = await import("../../services/scorer"); + const jobsRepo = await import("@server/repositories/jobs"); + const scorer = await import("@server/services/scorer"); vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([ createJob({ diff --git a/orchestrator/src/server/pipeline/steps/score-jobs.ts b/orchestrator/src/server/pipeline/steps/score-jobs.ts index a372232..d74302f 100644 --- a/orchestrator/src/server/pipeline/steps/score-jobs.ts +++ b/orchestrator/src/server/pipeline/steps/score-jobs.ts @@ -1,10 +1,10 @@ import { logger } from "@infra/logger"; +import * as jobsRepo from "@server/repositories/jobs"; +import * as settingsRepo from "@server/repositories/settings"; +import { scoreJobSuitability } from "@server/services/scorer"; +import * as visaSponsors from "@server/services/visa-sponsors/index"; +import { asyncPool } from "@server/utils/async-pool"; import type { Job } from "@shared/types"; -import * as jobsRepo from "../../repositories/jobs"; -import * as settingsRepo from "../../repositories/settings"; -import { scoreJobSuitability } from "../../services/scorer"; -import * as visaSponsors from "../../services/visa-sponsors/index"; -import { asyncPool } from "../../utils/async-pool"; import { progressHelpers, updateProgress } from "../progress"; import type { ScoredJob } from "./types"; diff --git a/orchestrator/src/server/services/backup/index.test.ts b/orchestrator/src/server/services/backup/index.test.ts index b11796b..bb9ad7b 100644 --- a/orchestrator/src/server/services/backup/index.test.ts +++ b/orchestrator/src/server/services/backup/index.test.ts @@ -6,11 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as backup from "./index"; // Mock the dataDir module -vi.mock("../../config/dataDir", () => ({ +vi.mock("@server/config/dataDir", () => ({ getDataDir: vi.fn(), })); -import { getDataDir } from "../../config/dataDir"; +import { getDataDir } from "@server/config/dataDir"; describe("Backup Service", () => { let tempDir: string; diff --git a/orchestrator/src/server/services/backup/index.ts b/orchestrator/src/server/services/backup/index.ts index 428583c..fd59bce 100644 --- a/orchestrator/src/server/services/backup/index.ts +++ b/orchestrator/src/server/services/backup/index.ts @@ -8,10 +8,10 @@ import fs from "node:fs"; import type { FileHandle } from "node:fs/promises"; import path from "node:path"; +import { getDataDir } from "@server/config/dataDir"; +import { createScheduler } from "@server/utils/scheduler"; import type { BackupInfo } from "@shared/types"; import Database from "better-sqlite3"; -import { getDataDir } from "../../config/dataDir"; -import { createScheduler } from "../../utils/scheduler"; const DB_FILENAME = "jobs.db"; const AUTO_BACKUP_PREFIX = "jobs_"; diff --git a/orchestrator/src/server/services/ghostwriter-context.test.ts b/orchestrator/src/server/services/ghostwriter-context.test.ts index eb75d42..592afe2 100644 --- a/orchestrator/src/server/services/ghostwriter-context.test.ts +++ b/orchestrator/src/server/services/ghostwriter-context.test.ts @@ -1,6 +1,6 @@ +import type { AppError } from "@infra/errors"; import { createJob } from "@shared/testing/factories"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { AppError } from "../infra/errors"; import { buildJobChatPromptContext } from "./ghostwriter-context"; vi.mock("../repositories/jobs", () => ({ diff --git a/orchestrator/src/server/services/ghostwriter-context.ts b/orchestrator/src/server/services/ghostwriter-context.ts index c099ca8..cca8369 100644 --- a/orchestrator/src/server/services/ghostwriter-context.ts +++ b/orchestrator/src/server/services/ghostwriter-context.ts @@ -1,7 +1,7 @@ +import { badRequest, notFound } from "@infra/errors"; import { logger } from "@infra/logger"; import { sanitizeUnknown } from "@infra/sanitize"; import type { Job, ResumeProfile } from "@shared/types"; -import { badRequest, notFound } from "../infra/errors"; import * as jobsRepo from "../repositories/jobs"; import { getProfile } from "./profile"; import { getEffectiveSettings } from "./settings"; diff --git a/orchestrator/src/server/services/ghostwriter.ts b/orchestrator/src/server/services/ghostwriter.ts index 13e24b0..03b0711 100644 --- a/orchestrator/src/server/services/ghostwriter.ts +++ b/orchestrator/src/server/services/ghostwriter.ts @@ -1,13 +1,13 @@ -import { logger } from "@infra/logger"; -import { getRequestId } from "@infra/request-context"; -import type { JobChatMessage, JobChatRun } from "@shared/types"; import { badRequest, conflict, notFound, requestTimeout, upstreamError, -} from "../infra/errors"; +} from "@infra/errors"; +import { logger } from "@infra/logger"; +import { getRequestId } from "@infra/request-context"; +import type { JobChatMessage, JobChatRun } from "@shared/types"; import * as jobChatRepo from "../repositories/ghostwriter"; import * as settingsRepo from "../repositories/settings"; import { buildJobChatPromptContext } from "./ghostwriter-context"; diff --git a/orchestrator/src/server/services/llm/utils/json.ts b/orchestrator/src/server/services/llm/utils/json.ts index 5e7bb9b..6f55f59 100644 --- a/orchestrator/src/server/services/llm/utils/json.ts +++ b/orchestrator/src/server/services/llm/utils/json.ts @@ -1,12 +1,14 @@ import { logger } from "@infra/logger"; -export function parseJsonContent(content: string, jobId?: string): T { - let candidate = content.trim(); - - candidate = candidate +export function stripMarkdownCodeFences(content: string): string { + return content .replace(/```(?:json|JSON)?\s*/g, "") .replace(/```/g, "") .trim(); +} + +export function parseJsonContent(content: string, jobId?: string): T { + let candidate = stripMarkdownCodeFences(content); const firstBrace = candidate.indexOf("{"); const lastBrace = candidate.lastIndexOf("}"); diff --git a/orchestrator/src/server/services/post-application/ingestion/email-router.ts b/orchestrator/src/server/services/post-application/ingestion/email-router.ts index b74679d..a1bea66 100644 --- a/orchestrator/src/server/services/post-application/ingestion/email-router.ts +++ b/orchestrator/src/server/services/post-application/ingestion/email-router.ts @@ -11,6 +11,7 @@ import type { PostApplicationRouterStageTarget, } from "@shared/types"; import { POST_APPLICATION_ROUTER_STAGE_TARGETS } from "@shared/types"; +import { normalizeWhitespace } from "@shared/utils/string"; export const ROUTER_EMAIL_CHAR_LIMIT = 12_000; @@ -91,7 +92,7 @@ export function minifyActiveJobs(jobs: Job[]): Array<{ } function sanitizeJobPromptValue(value: string): string { - return value.replace(/\s+/g, " ").trim(); + return normalizeWhitespace(value); } export function buildIndexedActiveJobs( diff --git a/orchestrator/src/server/services/post-application/ingestion/gmail-api.ts b/orchestrator/src/server/services/post-application/ingestion/gmail-api.ts index 6286e19..ff5a526 100644 --- a/orchestrator/src/server/services/post-application/ingestion/gmail-api.ts +++ b/orchestrator/src/server/services/post-application/ingestion/gmail-api.ts @@ -1,4 +1,5 @@ import { requestTimeout } from "@infra/errors"; +import { normalizeWhitespace } from "@shared/utils/string"; import { convert } from "html-to-text"; export const GMAIL_HTTP_TIMEOUT_MS = 15_000; @@ -313,7 +314,7 @@ function cleanEmailHtmlForLlm(htmlContent: string): string { } function normalizeChunkForDedup(value: string): string { - return value.replace(/\s+/g, " ").trim().toLowerCase(); + return normalizeWhitespace(value).toLowerCase(); } function decodeBase64Url(value: string): string { diff --git a/orchestrator/src/server/services/resumeProjects.ts b/orchestrator/src/server/services/resumeProjects.ts index eb20c32..99718f1 100644 --- a/orchestrator/src/server/services/resumeProjects.ts +++ b/orchestrator/src/server/services/resumeProjects.ts @@ -3,6 +3,7 @@ import type { ResumeProjectCatalogItem, ResumeProjectsSettings, } from "@shared/types"; +import { stripHtmlTags } from "@shared/utils/string"; type ResumeProjectSelectionItem = ResumeProjectCatalogItem & { summaryText: string; @@ -156,8 +157,7 @@ export function resolveResumeProjectsSettings(args: { } export function stripHtml(input: string): string { - const withoutTags = input.replace(/<[^>]*>/g, " "); - return withoutTags.replace(/\s+/g, " ").trim(); + return stripHtmlTags(input); } function uniqueStrings(values: string[]): string[] { diff --git a/orchestrator/src/server/services/rxresume-client.ts b/orchestrator/src/server/services/rxresume-client.ts index 3034be2..40b559f 100644 --- a/orchestrator/src/server/services/rxresume-client.ts +++ b/orchestrator/src/server/services/rxresume-client.ts @@ -5,6 +5,7 @@ // - The v5 client should be a drop-in replacement in the future. import type { ResumeData } from "@shared/rxresume-schema"; +import { normalizeWhitespace } from "@shared/utils/string"; type AnyObj = Record; const MAX_ERROR_SNIPPET = 300; @@ -457,6 +458,6 @@ export class RxResumeClient { function sanitizeResponseSnippet(text: string): string { if (!text) return ""; - const compact = text.replace(/\s+/g, " ").trim(); + const compact = normalizeWhitespace(text); return compact.slice(0, MAX_ERROR_SNIPPET); } diff --git a/orchestrator/src/server/services/scorer.ts b/orchestrator/src/server/services/scorer.ts index b9baa21..acaadb4 100644 --- a/orchestrator/src/server/services/scorer.ts +++ b/orchestrator/src/server/services/scorer.ts @@ -7,6 +7,7 @@ import type { Job } from "@shared/types"; import { getSetting } from "../repositories/settings"; import { LlmService } from "./llm/service"; import type { JsonSchemaDefinition } from "./llm/types"; +import { stripMarkdownCodeFences } from "./llm/utils/json"; import { getEffectiveSettings } from "./settings"; interface SuitabilityResult { @@ -162,10 +163,7 @@ export function parseJsonFromContent( let candidate = content.trim(); // Step 1: Remove markdown code fences (with or without language specifier) - candidate = candidate - .replace(/```(?:json|JSON)?\s*/g, "") - .replace(/```/g, "") - .trim(); + candidate = stripMarkdownCodeFences(candidate); // Step 2: Try to extract JSON object if there's surrounding text const jsonMatch = candidate.match(/\{[\s\S]*\}/); diff --git a/orchestrator/src/server/services/visa-sponsors/index.ts b/orchestrator/src/server/services/visa-sponsors/index.ts index 2e90f28..6a62683 100644 --- a/orchestrator/src/server/services/visa-sponsors/index.ts +++ b/orchestrator/src/server/services/visa-sponsors/index.ts @@ -6,8 +6,14 @@ import fs from "node:fs"; import path from "node:path"; -import { getDataDir } from "../../config/dataDir"; -import { createScheduler } from "../../utils/scheduler"; +import { getDataDir } from "@server/config/dataDir"; +import { createScheduler } from "@server/utils/scheduler"; +import type { + VisaSponsor, + VisaSponsorSearchResult, + VisaSponsorStatusResponse, +} from "@shared/types"; +import { normalizeWhitespace } from "@shared/utils/string"; const DATA_DIR = path.join(getDataDir(), "visa-sponsors"); @@ -16,28 +22,8 @@ if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } -export interface VisaSponsor { - organisationName: string; - townCity: string; - county: string; - typeRating: string; - route: string; -} - -export interface VisaSponsorSearchResult { - sponsor: VisaSponsor; - score: number; - matchedName: string; -} - -export interface VisaSponsorStatus { - lastUpdated: string | null; - csvPath: string | null; - totalSponsors: number; - isUpdating: boolean; - nextScheduledUpdate: string | null; - error: string | null; -} +export type { VisaSponsor, VisaSponsorSearchResult }; +export type VisaSponsorStatus = VisaSponsorStatusResponse; // Common company suffixes to strip during comparison const COMPANY_SUFFIXES = [ @@ -86,7 +72,7 @@ export function normalizeCompanyName(name: string): string { } // Collapse whitespace - normalized = normalized.replace(/\s+/g, " ").trim(); + normalized = normalizeWhitespace(normalized); return normalized; } diff --git a/shared/src/utils/string.ts b/shared/src/utils/string.ts new file mode 100644 index 0000000..d1a2f4d --- /dev/null +++ b/shared/src/utils/string.ts @@ -0,0 +1,7 @@ +export function normalizeWhitespace(value: string): string { + return value.replace(/\s+/g, " ").trim(); +} + +export function stripHtmlTags(value: string): string { + return normalizeWhitespace(value.replace(/<[^>]*>/g, " ")); +}