feat(extractors): expand catalog, smoke coverage, and sourcing docs
CI / Linting (Biome) (push) Failing after 40s
CI / Tests (push) Successful in 5m54s
CI / Type Check (adzuna-extractor) (push) Successful in 1m8s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m11s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m8s
CI / Type Check (orchestrator) (push) Successful in 1m23s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m6s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m7s
CI / Documentation (push) Successful in 1m54s

Adds Arc.dev, BC T-Net, Eluta, iCIMS tenants, QAJobsBoard, and SmartRecruiters
manifests with registry/settings/UI wiring; registers full extractor list in
smoke-extractors and documents supplementary board access paths. Aligns Careerjet
v4 with the url query parameter and fixes strict typing in QAJobsBoard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-15 22:36:23 -04:00
co-authored by Cursor
parent 67508d56ea
commit c840f289e1
50 changed files with 2926 additions and 101 deletions
+10
View File
@@ -0,0 +1,10 @@
# qajobsboard-extractor
Loads QA-focused postings from [QAJobsBoard](https://www.qajobsboard.com) via the hosts public JSON feed:
`GET https://qajobsboard.jobboardly.com/jobs.json`
RSS is also published at `jobs.rss`; this extractor uses JSON for structured fields.
- Caps matches via `qajobsboardMaxJobsPerTerm` (default `100`).
- Filters client-side by pipeline search terms against title, categories, and description HTML.
+217
View File
@@ -0,0 +1,217 @@
/**
* QAJobsBoard (JobBoardly) — public jobs listing JSON.
*
* https://qajobsboard.jobboardly.com/jobs.json
*/
import type {
ExtractorManifest,
ExtractorRunResult,
} from "@shared/types/extractors";
import type { CreateJobInput } from "@shared/types/jobs";
const JOBS_URL = "https://qajobsboard.jobboardly.com/jobs.json";
interface JobCategory {
name?: string;
}
interface SalaryBand {
schedule?: string;
minimum?: number | null;
maximum?: number | null;
}
interface DescriptionBlock {
html?: string;
}
interface QaJobBoardlyJob {
title?: string;
arrangement?: string;
location?: string;
location_limits?: string[];
published_at?: string;
application_link?: string;
description?: DescriptionBlock;
company?: { name?: string; logo?: string };
salary?: SalaryBand;
categories?: JobCategory[];
links?: { self?: string };
}
function asString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
function decodeHtmlEntities(value: string): string {
return value
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, " ");
}
function stripHtml(html: string): string {
const noTags = html.replace(/<[^>]+>/g, " ");
return decodeHtmlEntities(noTags).replace(/\s+/g, " ").trim();
}
function salaryLabel(raw: SalaryBand | undefined): string | undefined {
if (!raw) return undefined;
const schedule = raw.schedule ? `${raw.schedule}: ` : "";
if (
typeof raw.minimum === "number" &&
typeof raw.maximum === "number" &&
Number.isFinite(raw.minimum) &&
Number.isFinite(raw.maximum)
) {
return `${schedule}${raw.minimum}${raw.maximum}`;
}
if (typeof raw.minimum === "number" && Number.isFinite(raw.minimum)) {
return `${schedule}${raw.minimum}+`;
}
if (typeof raw.maximum === "number" && Number.isFinite(raw.maximum)) {
return `${schedule}${raw.maximum}`;
}
return schedule.trim() || undefined;
}
function locationLabel(job: QaJobBoardlyJob): string {
const limits = Array.isArray(job.location_limits)
? job.location_limits.filter(
(v): v is string => typeof v === "string" && v.trim().length > 0,
)
: [];
if (limits.length > 0) return limits.join(", ");
const loc = asString(job.location);
if (loc) return loc;
return "Unknown";
}
function matchesTerm(job: QaJobBoardlyJob, term: string): boolean {
const lower = term.toLowerCase();
if (job.title?.toLowerCase().includes(lower)) return true;
const cats = Array.isArray(job.categories)
? job.categories.map((c) => c.name?.toLowerCase() ?? "").join(" ")
: "";
if (cats.includes(lower)) return true;
const html = job.description?.html ?? "";
if (stripHtml(html).toLowerCase().includes(lower)) return true;
return false;
}
function mapJob(raw: QaJobBoardlyJob): CreateJobInput | null {
const jobUrl = asString(raw.links?.self);
if (!jobUrl) return null;
const employer =
asString(raw.company?.name)
?.replace(/^[\s-]+/, "")
.trim() || "Unknown Employer";
const applicationLink = asString(raw.application_link) ?? jobUrl;
const descHtml = raw.description?.html;
const jobDescription = descHtml ? stripHtml(descHtml) : undefined;
const salary = salaryLabel(raw.salary);
const cats = Array.isArray(raw.categories)
? raw.categories
.map((c) => c?.name?.trim())
.filter((v): v is string => Boolean(v))
.join(", ")
: undefined;
return {
source: "qajobsboard",
sourceJobId: jobUrl.split("/").pop(),
title: asString(raw.title) ?? "Unknown Title",
employer,
jobUrl,
applicationLink,
location: locationLabel(raw),
isRemote: asString(raw.location)?.toLowerCase() === "remote",
datePosted: asString(raw.published_at),
jobDescription,
jobType: asString(raw.arrangement),
salary,
disciplines: cats,
companyLogo: asString(raw.company?.logo),
};
}
export const manifest: ExtractorManifest = {
id: "qajobsboard",
displayName: "QAJobsBoard",
providesSources: ["qajobsboard"],
async run(context): Promise<ExtractorRunResult> {
if (context.shouldCancel?.()) return { success: true, jobs: [] };
const maxJobs = context.settings.qajobsboardMaxJobsPerTerm
? Number.parseInt(context.settings.qajobsboardMaxJobsPerTerm, 10)
: 100;
const cap = Number.isFinite(maxJobs)
? Math.min(Math.max(maxJobs, 1), 500)
: 100;
const terms = context.searchTerms.length > 0 ? context.searchTerms : [];
context.onProgress?.({
phase: "list",
termsProcessed: 0,
termsTotal: 1,
currentUrl: JOBS_URL,
detail: "QAJobsBoard: fetching jobs.json",
});
try {
const response = await fetch(JOBS_URL, {
headers: { Accept: "application/json", "User-Agent": "JobOps/1.0" },
});
if (!response.ok) {
throw new Error(
`QAJobsBoard request failed with status ${response.status}`,
);
}
const body = (await response.json()) as unknown;
const rows = Array.isArray(body) ? body : [];
const seen = new Set<string>();
const out: CreateJobInput[] = [];
for (const row of rows as QaJobBoardlyJob[]) {
if (out.length >= cap) break;
if (terms.length > 0 && !terms.some((t) => matchesTerm(row, t)))
continue;
const mapped = mapJob(row);
if (!mapped) continue;
const key = mapped.sourceJobId || mapped.jobUrl;
if (seen.has(key)) continue;
seen.add(key);
out.push(mapped);
}
context.onProgress?.({
phase: "list",
termsProcessed: 1,
termsTotal: 1,
currentUrl: JOBS_URL,
jobPagesProcessed: out.length,
detail: `QAJobsBoard: ${out.length} matched (${rows.length} total listings)`,
});
return { success: true, jobs: out };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return { success: false, jobs: [], error: message };
}
},
};
export default manifest;
+17
View File
@@ -0,0 +1,17 @@
{
"name": "qajobsboard-extractor",
"version": "0.0.1",
"type": "module",
"description": "QAJobsBoard (JobBoardly) public jobs.json extractor",
"main": "manifest.ts",
"dependencies": {
"job-ops-shared": "^1.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "~5.9.0"
},
"scripts": {
"check:types": "tsc --noEmit"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2022",
"outDir": "dist",
"strict": true,
"noUnusedLocals": false,
"lib": ["ES2022", "DOM"],
"types": ["node"],
"baseUrl": ".",
"paths": {
"@shared/*": ["../../shared/src/*"]
}
},
"include": ["./manifest.ts", "./src/**/*"]
}