Initial deploy setup
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-21 14:00:00 +02:00
commit b8091e59bd
285 changed files with 27854 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
# 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 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.