diff --git a/apps/web/src/lib/job-dump.ts b/apps/web/src/lib/job-dump.ts index bac440b..f08213f 100644 --- a/apps/web/src/lib/job-dump.ts +++ b/apps/web/src/lib/job-dump.ts @@ -1,4 +1,4 @@ -import { isFreshJob } from "@openintern/db"; +import { isDelisted, isFreshJob } from "@openintern/db"; const DEFAULT_DUMP_URL = "https://github.com/dnexdev/openintern/releases/download/dump-latest/jobs.json"; @@ -61,6 +61,7 @@ export async function loadJobsDump(): Promise { /** Same gates as {@link publicJobSql} — dump rows are pre-filtered but re-check on read. */ export function isPublicDumpJob(job: DumpJob, now = new Date()): boolean { + if (isDelisted(job.id)) return false; const roles = job.roles ?? []; if (!Array.isArray(roles) || roles.length === 0) return false; return isFreshJob( diff --git a/packages/db/src/delisted.ts b/packages/db/src/delisted.ts new file mode 100644 index 0000000..84a7442 --- /dev/null +++ b/packages/db/src/delisted.ts @@ -0,0 +1,14 @@ +/** + * Listings pulled by hand: reported closed, wrong, or unsafe. They are hidden + * from the board, API, sitemap and dumps on the next deploy, even while ingest + * can't run. Remove an entry once ingest has retired the job. + * + * Key: job id (lowercase UUID). Value: issue and reason, for the audit trail. + */ +export const DELISTED_JOBS: ReadonlyMap = new Map([ + ["cbb76a8d-0726-4a2a-b84d-8f359be23a90", "#24 Khan Academy SWE Intern Summer 2027: closed (Greenhouse 404), 2026-10-24"], +]); + +export function isDelisted(jobId: string): boolean { + return DELISTED_JOBS.has(jobId.toLowerCase()); +} diff --git a/packages/db/src/freshness.ts b/packages/db/src/freshness.ts index a51b40e..b285f1c 100644 --- a/packages/db/src/freshness.ts +++ b/packages/db/src/freshness.ts @@ -1,4 +1,5 @@ -import { sql, type SQL } from "drizzle-orm"; +import { notInArray, sql, type SQL } from "drizzle-orm"; +import { DELISTED_JOBS } from "./delisted"; import { jobs } from "./schema"; /** Max age for jobs without an explicit term/year. */ @@ -51,13 +52,15 @@ export function freshnessSql(): SQL { } /** - * Public corpus rows must be fresh and have at least one canonical role tag. - * This is a defensive gate for legacy data while stricter ingest retires noise. + * Public corpus rows must be fresh, have at least one canonical role tag, and + * not be delisted. This is a defensive gate for legacy data while stricter + * ingest retires noise. */ export function publicJobSql(): SQL { return sql`( ${freshnessSql()} AND jsonb_typeof(coalesce(${jobs.roles}, '[]'::jsonb)) = 'array' AND jsonb_array_length(coalesce(${jobs.roles}, '[]'::jsonb)) > 0 + AND ${DELISTED_JOBS.size > 0 ? notInArray(jobs.id, [...DELISTED_JOBS.keys()]) : sql`true`} )`; } diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index a542cc5..6717f16 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -29,3 +29,4 @@ export type SqlClient = ReturnType; export * from "./schema"; export * from "./freshness"; +export * from "./delisted"; diff --git a/packages/ingest/src/freshness.test.ts b/packages/ingest/src/freshness.test.ts index 0d7bf30..5022f2d 100644 --- a/packages/ingest/src/freshness.test.ts +++ b/packages/ingest/src/freshness.test.ts @@ -1,4 +1,5 @@ -import { isFreshJob, MAX_AGE_DAYS_NO_TERM } from "@openintern/db"; +import { PgDialect } from "drizzle-orm/pg-core"; +import { DELISTED_JOBS, isDelisted, isFreshJob, MAX_AGE_DAYS_NO_TERM, publicJobSql } from "@openintern/db"; function assert(condition: boolean, message: string) { if (!condition) throw new Error(message); @@ -50,4 +51,16 @@ assert( "old termless posting is stale", ); +for (const id of DELISTED_JOBS.keys()) { + assert(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id), `delisted id ${id} is a lowercase UUID`); + assert(isDelisted(id.toUpperCase()), "delisting ignores case"); +} +assert(!isDelisted("00000000-0000-0000-0000-000000000000"), "unlisted job is not delisted"); +const publicSql = new PgDialect().sqlToQuery(publicJobSql()); +assert(/not in/i.test(publicSql.sql), "public SQL excludes delisted jobs"); +assert( + [...DELISTED_JOBS.keys()].every((id) => publicSql.params.includes(id)), + "every delisted id is a bound parameter", +); + console.log("freshness policy ok");