174 lines
6.8 KiB
Markdown
174 lines
6.8 KiB
Markdown
# 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`.
|
||
|
||
```python
|
||
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`.
|
||
|
||
```python
|
||
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`.
|
||
|
||
```python
|
||
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`.
|
||
|
||
```python
|
||
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`.
|
||
|
||
```python
|
||
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`.
|
||
|
||
```python
|
||
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.
|