6.8 KiB
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
- Adapters lezen, services beslissen. Een bronadapter maakt alleen een
ExtractionResult; hij schrijft niet naar de database en voert geen scoring uit. - Onbetrouwbare data blijft data. HTML, e-mail, JSON-LD en modeluitvoer mogen nooit instructies of code worden.
- Persistente writes zijn atomair en idempotent. Herhaalde import van dezelfde broninhoud maakt geen tweede vacature aan.
- Orchestrators bevatten geen productregels. Celery-taken plannen en registreren; domeinservices normaliseren, dedupliceren en scoren.
- 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 meerExtractedJob-objecten;parser_keyenparser_version: reproduceerbare parseridentiteit;confidence: globaal getal tussen0.0en1.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
rawopnemen; - 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
httpofhttps; - 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_keyencontent_hash; - parseert datums conservatief en timezone-aware;
- behoudt bronwaarden in
rawenevidencevoor 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:
- kies adapter via registry;
- extracteer jobs en parsermetadata;
- normaliseer iedere job;
- zoek een bestaand canoniek cluster;
- maak of werk werkgever, vacature en bronalias bij;
- leg veldherkomst en versie vast;
- 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 ofNone;reason: machineleesbare reden, bijvoorbeeldexact_canonical_key,exact_external_idoffuzzy_*;confidence: getal tussen0.0en1.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 0–100;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.