Files
VacatureRadar/docs/api/INTERNAL_CONTRACTS.md
T
Jens b8091e59bd
deploy / deploy (push) Canceled after 0s
Initial deploy setup
2026-07-21 14:00:00 +02:00

6.8 KiB
Raw Blame History

Interne contracten

Dit document beschrijft de stabiele grenzen tussen domeinmodules. Het is geen belofte van een externe REST-API. De eerste release gebruikt Django-views, formulieren en servicefuncties; daardoor blijft de onderhoudslast laag en kan later zonder herschrijven een JSON-laag boven dezelfde services worden geplaatst.

Contractprincipes

  1. Adapters lezen, services beslissen. Een bronadapter maakt alleen een ExtractionResult; hij schrijft niet naar de database en voert geen scoring uit.
  2. Onbetrouwbare data blijft data. HTML, e-mail, JSON-LD en modeluitvoer mogen nooit instructies of code worden.
  3. Persistente writes zijn atomair en idempotent. Herhaalde import van dezelfde broninhoud maakt geen tweede vacature aan.
  4. Orchestrators bevatten geen productregels. Celery-taken plannen en registreren; domeinservices normaliseren, dedupliceren en scoren.
  5. AI is optioneel. Geen core-contract mag een taalmodel vereisen om geldige output te leveren.

Bronadapter

Implementatie: apps/sources/adapters/base.py.

class SourceAdapter(Protocol):
    parser_key: str
    parser_version: str

    def extract(self, content: str, *, url: str) -> ExtractionResult: ...

Invoer

  • content: reeds opgehaalde Unicode-inhoud; netwerktoegang vindt vóór de adapter plaats.
  • url: gevalideerde bron- of document-URL, uitsluitend voor context en relatieve URL-resolutie.

Uitvoer

ExtractionResult bevat:

  • jobs: nul of meer ExtractedJob-objecten;
  • parser_key en parser_version: reproduceerbare parseridentiteit;
  • confidence: globaal getal tussen 0.0 en 1.0;
  • warnings: niet-fatale, korte, machineveilige waarschuwingen.

Een ExtractedJob gebruikt zo veel mogelijk brongetrouwe waarden. Normalisatie gebeurt pas in normalize_extracted_job. Een adapter moet:

  • ontbrekende velden leeg laten in plaats van ze te verzinnen;
  • iedere URL absoluut maken;
  • beschrijving als bron-HTML én/of platte tekst leveren;
  • veldbewijs toevoegen wanneer een waarde heuristisch is gevonden;
  • nooit secrets, cookies, headers of volledige mailmetadata in raw opnemen;
  • geen exception gebruiken voor een geldig document met nul vacatures.

Fetcher

Implementatie: apps/sources/services/fetcher.py en url_security.py.

fetch_url(
    url: str,
    *,
    source: Source | None = None,
    conditional_headers: dict[str, str] | None = None,
) -> FetchedDocument

Voorwaarden vóór ieder request en na iedere redirect:

  • schema is http of https;
  • host is niet leeg, localhost, link-local, multicast, private of anderszins speciaal;
  • poort is 80/443, tenzij expliciet toegestaan;
  • domein is niet op de centrale platformdenylist;
  • bronstatus en bronbeleid laten toegang toe;
  • response blijft binnen tijd-, redirect- en bytegrenzen.

FetchedDocument bevat de aangevraagde en uiteindelijke URL, status, gefilterde headers, bytes, tekst en SHA-256. Callers mogen de responsebody niet loggen.

Machineleesbare foutcategorieën:

  • PolicyBlockedError: bron/domein is niet toegestaan; niet blind retrien;
  • ContentRejectedError: type, grootte of response is onbruikbaar;
  • FetchError: tijdelijke netwerk- of HTTP-fout die begrensd mag worden herhaald;
  • UnsafeUrlError: URL faalt SSRF-validatie vóór netwerktoegang.

Normalisatie

Implementatie: apps/jobs/services/normalization.py.

normalize_extracted_job(extracted: ExtractedJob) -> CanonicalJobDraft

De functie:

  • saneert HTML vóór opslag/rendering;
  • normaliseert titel, contracttypen, werkplek en taal;
  • canonicaliseert de URL en verwijdert gekende trackingparameters;
  • berekent een stabiele canonical_key en content_hash;
  • parseert datums conservatief en timezone-aware;
  • behoudt bronwaarden in raw en evidence voor uitlegbaarheid.

Een CanonicalJobDraft is een immutable overdrachtsobject. Het kent geen database-ID en bevat geen score.

Persistente pipeline

Implementatie: apps/jobs/services/pipeline.py.

process_raw_document(document: RawDocument) -> dict[str, int | str | list[str]]

Transactionele volgorde:

  1. kies adapter via registry;
  2. extracteer jobs en parsermetadata;
  3. normaliseer iedere job;
  4. zoek een bestaand canoniek cluster;
  5. maak of werk werkgever, vacature en bronalias bij;
  6. leg veldherkomst en versie vast;
  7. score tegen ieder actief zoekprofiel.

De resultaatmapping bevat minimaal extracted, created, updated, duplicates, parser en warnings. De tellers moeten deterministisch blijven bij een replay van hetzelfde document.

Deduplicatie

Implementatie: apps/jobs/services/dedupe.py.

find_existing_job(draft: CanonicalJobDraft) -> DedupeDecision

DedupeDecision bevat:

  • job: bestaand canoniek object of None;
  • reason: machineleesbare reden, bijvoorbeeld exact_canonical_key, exact_external_id of fuzzy_*;
  • confidence: getal tussen 0.0 en 1.0.

Een fuzzy match mag alleen plaatsvinden wanneer werkgever, titel, locatie en periode voldoende bewijs leveren. Een rechtstreekse werkgeversbron krijgt canonieke voorkeur boven een platform- of recruiterlink; bronaliassen blijven behouden.

Scoring

Implementatie: apps/jobs/services/scoring.py.

score_job(job: JobPosting, profile: SearchProfile) -> ScoreResult
score_and_save(job: JobPosting, profile: SearchProfile) -> ScoreRun

ScoreResult levert:

  • score: begrensd op 0100;
  • hard_exclusions: stabiele regelcodes;
  • explanation: korte positieve en negatieve factoren;
  • feature_values: auditbare, niet-persoonsgevoelige featurewaarden;
  • model_version: versie van regels/gewichten.

Harde uitsluitingen worden nooit door een hoge zachte score opgeheven. AI-output mag hoogstens aanvullende kenmerken leveren nadat schema- en confidencevalidatie zijn geslaagd.

Feedback

Implementatie: apps/jobs/services/feedback.py.

Toegestane acties zijn interesting, save, hide en applied. Iedere mutatie is user-scoped en CSRF-beschermd. Feedback mag gewichten slechts binnen de in het profiel vastgelegde grenzen aanpassen. De gebruiker moet de wijziging kunnen resetten en een vacature handmatig terugvinden, ook wanneer ze is verborgen.

Notificaties

Implementatie: apps/notifications/services.py.

Een digest wordt eerst als DigestOutbox aangemaakt en daarna verzonden. Uniciteit per profiel en digestdatum voorkomt dubbele mails. Verzending verandert alleen de outboxstatus; het opnieuw genereren van de selectie moet reproduceerbaar zijn uit opgeslagen ScoreRun-records.

Nieuwe service toevoegen

Een nieuwe interne service is pas een stabiel contract wanneer:

  • publieke types en foutcategorieën zijn gedocumenteerd;
  • unit- en integratietests succes, lege invoer en negatieve paden dekken;
  • netwerk- of secretvereisten injecteerbaar zijn;
  • er geen databasewrite in parser- of pure normalisatielogica zit;
  • traceability en backlog zijn bijgewerkt.