@@ -0,0 +1,82 @@
|
||||
# Dataflows en invarianten
|
||||
|
||||
## Flow A — Publieke vacaturepagina
|
||||
|
||||
1. Scheduler selecteert alleen `allow` + `active/trial` + due.
|
||||
2. Fetcher controleert policy, URL, DNS/IP, poort en iedere redirect.
|
||||
3. Response wordt begrensd op type/grootte en als `RawDocument` met korte retentie opgeslagen.
|
||||
4. Registry kiest RSS wanneer XML/feed, anders JSON-LD en daarna generieke HTML.
|
||||
5. `ExtractedJob` wordt gesanitized en genormaliseerd.
|
||||
6. Dedupe zoekt external ID, canonieke URL, canonieke key en conservatieve fuzzy kandidaat.
|
||||
7. Pipeline schrijft werkgever, vacature, alias, provenance en versie.
|
||||
8. Alle actieve profielen krijgen een nieuwe `ScoreRun`.
|
||||
9. SourceRun registreert metrics en plant volgende run.
|
||||
|
||||
Invarianten:
|
||||
|
||||
- geen fetch zonder policy + URL-validatie;
|
||||
- één canonieke vacature per canonical key;
|
||||
- originele bronURL blijft als alias bewaard;
|
||||
- HTML in de UI is gesanitized;
|
||||
- verwerking van dezelfde broninhoud is idempotent.
|
||||
|
||||
## Flow B — Vacaturemail
|
||||
|
||||
1. IMAP-task haalt maximaal een begrensd aantal berichten op.
|
||||
2. `message_identity` gebruikt RFC Message-ID of SHA-256 fallback.
|
||||
3. Bestaand record stopt dubbele verwerking.
|
||||
4. Raw mail wordt tijdelijk opgeslagen.
|
||||
5. E-mailadapter extraheert alleen HTTP(S)-links en slaat unsubscribe/privacylinks over.
|
||||
6. Iedere link wordt als vacaturedraft verwerkt; denylistlink wordt niet automatisch gefetcht.
|
||||
7. Record bewaart linklijst, processedstatus en begrensde foutmelding.
|
||||
|
||||
Invarianten:
|
||||
|
||||
- mailboxpassword staat alleen in environment/secrets;
|
||||
- geen attachmentverwerking in MVP;
|
||||
- e-mailtekst is data, geen AI-/agentinstructie;
|
||||
- duplicate Message-ID maakt geen extra vacaturesnapshot.
|
||||
|
||||
## Flow C — Feedback en sollicitatie
|
||||
|
||||
1. Authenticated user POST met CSRF.
|
||||
2. Actie wordt tegen enum gevalideerd.
|
||||
3. `Feedback` is append-only.
|
||||
4. `applied` maakt atomair één `Application` per user/job.
|
||||
5. Dossier krijgt vacaturesnapshot en standaard opvolgdatum.
|
||||
6. Externe verzending bestaat niet.
|
||||
|
||||
Invarianten:
|
||||
|
||||
- user-scoped querysets voor dossierwijziging;
|
||||
- één dossier per user/job;
|
||||
- undo/verbergen is traceerbaar;
|
||||
- feedbackleren mag uitsluitend begrensde soft weights wijzigen.
|
||||
|
||||
## Flow D — Dagelijkse digest
|
||||
|
||||
1. Scheduler evalueert actieve profielen in hun tijdzone.
|
||||
2. Alleen binnen het 30-minutenvak rond digesttijd wordt een outbox gemaakt.
|
||||
3. Dedupe key is profiel + lokale datum.
|
||||
4. Selectie neemt laatste sterke/mogelijke score per actieve vacature, verborgen jobs uitgesloten.
|
||||
5. Template rendert tekst en HTML.
|
||||
6. Bij succes status `sent`; bij fout `failed` met begrensde foutcontext.
|
||||
|
||||
Invarianten:
|
||||
|
||||
- maximaal één outbox per profiel/dag;
|
||||
- een reeds verzonden outbox wordt niet opnieuw verzonden;
|
||||
- geen recipient betekent `skipped`, geen crash;
|
||||
- digest bevat nooit raw HTML van vacaturebron.
|
||||
|
||||
## Dataretentie
|
||||
|
||||
| Data | Standaard | Reden |
|
||||
|---|---:|---|
|
||||
| Raw publieke documenten | 7 dagen | parserdebug en provenance |
|
||||
| Raw vacaturemail | 7 dagen | idempotentie/debug; daarna verwijderen |
|
||||
| Canonieke vacature | totdat verwijderd/retentiebeleid | gebruikerswaarde en historie |
|
||||
| JobVersion | blijvend tenzij gebruiker wist | wijzigingsaudit |
|
||||
| ScoreRun | configureerbaar, initieel blijvend | uitlegbaarheid/modelvergelijking |
|
||||
| Feedback/Application | totdat gebruiker wist | persoonlijke opvolging |
|
||||
| Logs | operationeel begrensd/geroteerd | foutdiagnose zonder inhoud/secrets |
|
||||
@@ -0,0 +1,133 @@
|
||||
# Datamodel
|
||||
|
||||
## Entity-relatiemodel
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ SEARCH_PROFILE : owns
|
||||
SEARCH_PROFILE ||--o{ PROFILE_REVISION : versions
|
||||
SEARCH_PROFILE ||--o{ SCORE_RUN : evaluates
|
||||
SEARCH_PROFILE ||--o{ DIGEST_OUTBOX : receives
|
||||
|
||||
SOURCE ||--o{ SOURCE_RUN : executes
|
||||
SOURCE ||--o{ RAW_DOCUMENT : retrieves
|
||||
SOURCE_RUN ||--o{ RAW_DOCUMENT : produces
|
||||
RAW_DOCUMENT ||--o{ JOB_SOURCE_ALIAS : evidences
|
||||
RAW_DOCUMENT ||--o| EMAIL_MESSAGE_RECORD : wraps
|
||||
|
||||
EMPLOYER ||--o{ JOB_POSTING : publishes
|
||||
JOB_POSTING ||--o{ JOB_SOURCE_ALIAS : aliases
|
||||
JOB_POSTING ||--o{ FIELD_PROVENANCE : fields
|
||||
JOB_SOURCE_ALIAS ||--o{ FIELD_PROVENANCE : proves
|
||||
JOB_POSTING ||--o{ JOB_VERSION : versions
|
||||
JOB_POSTING ||--o{ SCORE_RUN : scored
|
||||
JOB_POSTING ||--o{ FEEDBACK : receives
|
||||
JOB_POSTING ||--o{ APPLICATION : tracked
|
||||
USER ||--o{ FEEDBACK : gives
|
||||
USER ||--o{ APPLICATION : owns
|
||||
```
|
||||
|
||||
## Entiteiten
|
||||
|
||||
### SearchProfile
|
||||
|
||||
Bevat harde/soft voorkeuren, locatie, drempels, digesttijd en version. De JSON-velden blijven schematisch gedocumenteerd in `docs/api/INTERNAL_CONTRACTS.md` en gevalideerd in service/formcode.
|
||||
|
||||
Belangrijke invarianten:
|
||||
|
||||
- unieke naam per user;
|
||||
- recommendation threshold ≤ top threshold;
|
||||
- gewichten numeriek, niet-negatief en met positieve som;
|
||||
- coördinaten binnen bereik;
|
||||
- precies één actief profiel per user via `activate()`-serviceflow.
|
||||
|
||||
### Source
|
||||
|
||||
Bronregister met `source_type`, `status` en onafhankelijk `policy`.
|
||||
|
||||
Status betekent operationele toestand; policy betekent toestemming:
|
||||
|
||||
- `candidate` — ontdekt, niet actief;
|
||||
- `trial` — beperkte proefcontrole;
|
||||
- `active` — normaal gepland;
|
||||
- `quarantined` — automatisch/manual geïsoleerd;
|
||||
- `paused/disabled` — niet plannen;
|
||||
- `allow/review/deny` — netwerktoegangsbesluit.
|
||||
|
||||
Een `deny`-bron mag niet `active` zijn.
|
||||
|
||||
### RawDocument
|
||||
|
||||
Tijdelijke immutable-ish fetchsnapshot. Bevat alleen begrensde tekstbody, headers op allowlist, hash, parserresultaat en retentie. Quarantaine verhindert cleanup tot inspectie.
|
||||
|
||||
### Employer
|
||||
|
||||
Genormaliseerde werkgever/recruiteridentiteit. Uniek op normalized name + domain. `is_direct_employer` en `is_recruiter` zijn confidencegedreven kenmerken, geen juridisch oordeel.
|
||||
|
||||
### JobPosting
|
||||
|
||||
Canonieke vacature met UUID, canonical key, content hash, status en genormaliseerde velden. De canonical key wordt uit external ID/URL of fallbackidentiteit afgeleid. De exacte key is uniek; fuzzy matching is alleen een kandidaatbeslissing.
|
||||
|
||||
Statusflow:
|
||||
|
||||
```text
|
||||
new -> active -> uncertain -> removed
|
||||
\-> expired
|
||||
active/uncertain -> duplicate/quarantined (beheerflow)
|
||||
```
|
||||
|
||||
### JobSourceAlias
|
||||
|
||||
Verbindt alle bronvermeldingen met één canonieke vacature. Bewaart externe ID, bronURL, parser, confidence en beperkte payload. De alias is het anker voor veldprovenance.
|
||||
|
||||
### FieldProvenance
|
||||
|
||||
Per veld: extractiemethode, confidence, evidenceexcerpt en parserversie. Evidence is begrensd en mag geen geheime mailinhoud bevatten.
|
||||
|
||||
### JobVersion
|
||||
|
||||
Snapshot per unieke contenthash. Bij inhoudswijziging wordt de vorige toestand bewaard; de huidige toestand krijgt eveneens een versie.
|
||||
|
||||
### ScoreRun
|
||||
|
||||
Append-only score voor één job + profielversie. Bevat componentbijdragen, confidence, recommendation, redenen en evidence. Meerdere runs maken algoritme- en profielvergelijking mogelijk.
|
||||
|
||||
### Feedback
|
||||
|
||||
Append-only useractie. Geen laatste-statekolom; views kunnen de laatste actie afleiden. Hierdoor blijft correctiehistorie behouden.
|
||||
|
||||
### Application
|
||||
|
||||
Eén dossier per user/job. `snapshot` bewaart wat de gebruiker zag toen hij solliciteerde. `document_manifest` bevat alleen metadata/paden, geen willekeurige externe URL-executie.
|
||||
|
||||
### DigestOutbox
|
||||
|
||||
Idempotente meldingseenheid met unieke dedupe key. Statusovergangen: pending → sent/failed; skipped bij ontbrekende recipient/configuratie.
|
||||
|
||||
## Indexstrategie
|
||||
|
||||
Bestaand:
|
||||
|
||||
- job title + region;
|
||||
- job status + valid_through;
|
||||
- alias source + external ID;
|
||||
- alias canonical URL;
|
||||
- source + raw contenthash;
|
||||
- profile + score descending + created_at;
|
||||
- bron next_run_at en healthdatums.
|
||||
|
||||
Toekomstig bij meetbare noodzaak:
|
||||
|
||||
- trigram/index voor fuzzy title/employer (PostgreSQL);
|
||||
- partial index op actieve vacatures;
|
||||
- latest score materialisatie;
|
||||
- JSON GIN uitsluitend voor werkelijk bevraagde keys.
|
||||
|
||||
## Migratiebeleid
|
||||
|
||||
- schemawijzigingen via Django migrations;
|
||||
- destructieve wijzigingen in twee fasen;
|
||||
- grote backfills chunked en restartable;
|
||||
- productieback-up vóór migratie;
|
||||
- migratiecheck in CI;
|
||||
- rollback of forward-fix expliciet in taskplan.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Matching- en scoringengine
|
||||
|
||||
## 1. Beslisvolgorde
|
||||
|
||||
```text
|
||||
normalisatie
|
||||
-> harde uitsluitingen
|
||||
-> deterministische features
|
||||
-> optionele AI-features (alleen aanvulling)
|
||||
-> gewogen score
|
||||
-> confidence
|
||||
-> recommendation
|
||||
-> append-only ScoreRun
|
||||
```
|
||||
|
||||
Harde uitsluitingen hebben absolute voorrang. Een score van 99 kan een uitgesloten verplichte skill of regio niet overrulen.
|
||||
|
||||
## 2. Harde regels
|
||||
|
||||
Ondersteund of gepland:
|
||||
|
||||
- uitgesloten titelterm;
|
||||
- uitgesloten/geen toegestane contractvorm;
|
||||
- uitgesloten regio/gemeente;
|
||||
- afstand boven maximum, behalve remote;
|
||||
- uitgesloten verplichte skill;
|
||||
- expliciete rijbewijs-/reis-/taalvereisten wanneer betrouwbaar geëxtraheerd;
|
||||
- verlopen of niet-actieve vacature buiten de ranking.
|
||||
|
||||
Iedere uitsluiting bewaart een concrete reden. `unknown_is_insufficient` kan later per regel bepalen of ontbrekende data naar review gaat; veilige standaard is onbekend niet automatisch uitsluiten.
|
||||
|
||||
## 3. Componenten
|
||||
|
||||
Standaardgewichten, totaal 100:
|
||||
|
||||
| Component | Gewicht | Kernsignalen |
|
||||
|---|---:|---|
|
||||
| Content | 25 | titelovereenkomst, taken, weinig ongewenste support |
|
||||
| Skills | 20 | gewenste skills in expliciete velden/tekst |
|
||||
| Locatie | 15 | afstand, remote/hybrid, voorkeursregio |
|
||||
| Voorwaarden | 10 | toegestane contract-/werkvorm |
|
||||
| Werkgever | 10 | directe bron versus recruiter |
|
||||
| Senioriteit | 10 | gevraagde ervaring versus profiel |
|
||||
| Voorkeuren | 10 | publieke sector en andere soft signals |
|
||||
|
||||
Bij aangepaste gewichten worden componentbijdragen genormaliseerd op de totale som.
|
||||
- AI-boost is opt-in via profielinstelling en telt alleen mee als profielgewicht > 0.
|
||||
- De AI-component is softwarematig begrensd (maximale bijdrage via `ai`-gewicht) om dominantie te vermijden.
|
||||
|
||||
## 4. Confidence
|
||||
|
||||
Confidence is niet hetzelfde als score. Een vacature kan inhoudelijk sterk lijken maar lage confidence hebben door ontbrekende werkgever, locatie of beschrijving.
|
||||
|
||||
Baselineformule in MVP:
|
||||
|
||||
```text
|
||||
confidence = 0.65 * extraction_confidence + 0.35 * field_completeness
|
||||
```
|
||||
|
||||
Uitbreidingen mogen rekening houden met:
|
||||
|
||||
- provenancekwaliteit per veld;
|
||||
- overeenstemming tussen aliassen;
|
||||
- geocodingconfidence;
|
||||
- parserspecifieke drift;
|
||||
- AI-/deterministische featureconsistentie.
|
||||
|
||||
## 5. Recommendations
|
||||
|
||||
- `strong` — score ≥ topmatchthreshold en confidence ≥ 0,65;
|
||||
- `possible` — score ≥ recommendationthreshold;
|
||||
- `weak` — lager maar niet hard uitgesloten;
|
||||
- `hidden` — minstens één harde uitsluiting.
|
||||
|
||||
Drempels zijn profielconfiguratie. De UI toont score en confidence apart.
|
||||
|
||||
## 6. Evidence en explainability
|
||||
|
||||
Een score bewaart:
|
||||
|
||||
- exacte componentbijdragen;
|
||||
- positives;
|
||||
- concerns;
|
||||
- hard exclusions;
|
||||
- afstand/evidence;
|
||||
- profielversie;
|
||||
- optionele model- en promptversie.
|
||||
- AI-status (`ok`, `disabled`, `timeout`, `invalid`, `error`) en foutcategorie;
|
||||
- gewichten (gevraagde vs. effectief toegepast, zodat cap zichtbaar is).
|
||||
|
||||
Copyregels:
|
||||
|
||||
- zeg "niet teruggevonden" in plaats van "ontbreekt" wanneer brondata onvolledig is;
|
||||
- label inference als inference;
|
||||
- toon maximaal enkele kernredenen bovenaan en volledige details uitklapbaar;
|
||||
- geef geen kanspercentage op aanwerving zonder gevalideerd model.
|
||||
|
||||
## 7. Feedbackleren
|
||||
|
||||
Feedback kan alleen begrensde soft weights aanpassen. Learning staat standaard uit tot `learning_enabled` actief is.
|
||||
|
||||
Regels:
|
||||
|
||||
- learning kan uit;
|
||||
- delta per feedbackactie klein en gelimiteerd;
|
||||
- weight binnen 0..40;
|
||||
- iedere wijziging maakt een profielrevisie;
|
||||
- harde regels en bronpolicy worden nooit geleerd;
|
||||
- een enkele actie leidt niet tot grote verschuiving (minimale sample-criteria per feature);
|
||||
- gebruiker kan resetten en verschil bekijken.
|
||||
- hidden feedback met reden en non-relevant categorieën wordt apart gelogd.
|
||||
|
||||
Feedbackregels worden als metadata op de feedback opgeslagen (`feedback.metadata["learning"]` met status, reden, feature, delta, sample count).
|
||||
|
||||
Offline evaluatie is beschikbaar via `scripts/feedback_learning_report.py` met feedbackcounts, churn en non-learning categorieën.
|
||||
|
||||
## 8. AI-rol
|
||||
|
||||
Toegestaan:
|
||||
|
||||
- support-/consultancy-/travelratio schatten;
|
||||
- senioritylabel;
|
||||
- korte Nederlandse samenvatting;
|
||||
- evidencefragmenten selecteren;
|
||||
- ambiguïteitswarnings.
|
||||
|
||||
Niet toegestaan:
|
||||
|
||||
- harde uitsluiting toevoegen zonder deterministisch verifieerbaar signaal;
|
||||
- vacature verwijderen;
|
||||
- URL bezoeken of tools aanroepen;
|
||||
- source policy wijzigen;
|
||||
- e-mail of sollicitatie verzenden;
|
||||
- profiel zelfstandig herschrijven.
|
||||
|
||||
AI-input staat tussen duidelijke datamarkers, output volgt JSON-schema, temperatuur is 0 en alle waarden worden gevalideerd/geclamped. Uitval is een normale fallback, geen pipelinefout.
|
||||
- AI-status en evidence worden altijd opgeslagen in `ScoreRun.evidence` en vallen nooit terug op hard-exclusions of sourcepolicywijzigingen.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Securityarchitectuur
|
||||
|
||||
## 1. Principes
|
||||
|
||||
- fail closed voor bronpolicy en URL-validatie;
|
||||
- least privilege voor containers en credentials;
|
||||
- externe inhoud is data;
|
||||
- menselijke bevestiging voor impactvolle externe acties;
|
||||
- korte retentie en beperkte evidence;
|
||||
- defense in depth: sanitization + CSP + outputescaping;
|
||||
- idempotentie en audit boven verborgen magie.
|
||||
|
||||
## 2. Netwerkpad
|
||||
|
||||
Alle automatische publieke HTTP(S)-requests lopen via `apps.sources.services.fetcher.fetch_url`.
|
||||
|
||||
Verplichte stappen:
|
||||
|
||||
1. `assess_url`: hostname + denylist + source policy/status;
|
||||
2. `validate_public_url`: schema, credentials, hostname, poort, DNS, alle IP's global;
|
||||
3. request zonder automatische redirects;
|
||||
4. ieder redirectdoel opnieuw door policy en URL-validatie;
|
||||
5. response status/contenttype/grootte controleren;
|
||||
6. alleen allowlisted headers bewaren.
|
||||
|
||||
Geen adapter of view mag dit pad omzeilen.
|
||||
|
||||
## 3. SSRF
|
||||
|
||||
Geblokkeerd:
|
||||
|
||||
- localhost en `.localhost`;
|
||||
- IPv4/IPv6 loopback, private, link-local, multicast, unspecified en niet-global;
|
||||
- embedded credentials;
|
||||
- non-HTTP(S);
|
||||
- niet-standaardpoorten tenzij expliciet geconfigureerd;
|
||||
- redirect naar geblokkeerd doel.
|
||||
|
||||
Resterend risico: klassieke DNS rebinding tussen resolutie en socketconnectie. Productiehardening moet connectie-IP pinning of een outbound proxy/egress firewall toevoegen. Tot die tijd hoort de container geen toegang te hebben tot gevoelige interne control planes.
|
||||
|
||||
## 4. Webbeveiliging
|
||||
|
||||
- Django authentication en user-scoped querysets;
|
||||
- CSRF-middleware op mutaties;
|
||||
- secure/HttpOnly/SameSite cookies in productie;
|
||||
- HSTS en SSL redirect achter correct geconfigureerde proxy;
|
||||
- `X-Frame-Options: DENY` en CSP `frame-ancestors 'none'`;
|
||||
- content type nosniff, strict referrer, beperkte permissions policy;
|
||||
- geen externe scripts/fonts in kerninterface;
|
||||
- vacature-HTML door Bleachallowlist vóór opslag/rendering.
|
||||
|
||||
## 5. E-mail
|
||||
|
||||
- dedicated mailbox;
|
||||
- attachments worden genegeerd;
|
||||
- message identity voorkomt dubbele verwerking;
|
||||
- links worden niet automatisch gevolgd tenzij later door bronpolicy goedgekeurd;
|
||||
- mailboxcredentials alleen environment/secret;
|
||||
- raw mail korte retentie;
|
||||
- log geen volledige onderwerp/body wanneer daarin PII kan staan.
|
||||
|
||||
## 6. AI
|
||||
|
||||
- opt-in en lokaal als defaultarchitectuur;
|
||||
- vacaturetekst expliciet onbetrouwbare data;
|
||||
- geen tools/function calling;
|
||||
- gestructureerd schema en temperatuur 0;
|
||||
- maximaal inputvolume;
|
||||
- outputvalidatie en deterministische fallback;
|
||||
- model-/promptversie opslaan;
|
||||
- AI-container hoeft geen internettoegang.
|
||||
|
||||
## 7. Secrets
|
||||
|
||||
Minimaal:
|
||||
|
||||
- Django secret key;
|
||||
- databasepassword;
|
||||
- adminpassword bootstrap;
|
||||
- IMAP/SMTPcredentials;
|
||||
- eventueel reverse-proxy/OIDC secrets.
|
||||
|
||||
Regels:
|
||||
|
||||
- `.env` is gitignored en mode 600;
|
||||
- geen secrets in Composebestand, fixtures, screenshots of docs;
|
||||
- productie gebruikt Unraid secrets/configpad met beperkte rechten;
|
||||
- rotatie na lek of restore naar onvertrouwde host;
|
||||
- back-ups van config/secrets apart versleutelen.
|
||||
|
||||
## 8. Containerhardening (productietaak)
|
||||
|
||||
Aanbevolen:
|
||||
|
||||
- non-root runtimeuser;
|
||||
- read-only root filesystem waar haalbaar;
|
||||
- `no-new-privileges`;
|
||||
- capabilities drop all;
|
||||
- tmpfs voor `/tmp` en Celery beatschedule;
|
||||
- resource limits;
|
||||
- database/Redis niet publiceren;
|
||||
- apart egressnetwerk/proxy;
|
||||
- immutable image digest;
|
||||
- dependency/image scanning.
|
||||
|
||||
## 9. Audit
|
||||
|
||||
Bronruns, vacatureversies, score runs, feedback en outbox vormen de functionele audittrail. Securityauditlogs moeten aanvullend login-/policy-/quarantainewijzigingen opnemen zonder vacaturetekst of secrets.
|
||||
@@ -0,0 +1,160 @@
|
||||
# Bron- en adapterarchitectuur
|
||||
|
||||
## 1. Adaptercontract
|
||||
|
||||
Iedere adapter implementeert conceptueel:
|
||||
|
||||
```python
|
||||
class SourceAdapter(Protocol):
|
||||
parser_key: str
|
||||
parser_version: str
|
||||
|
||||
def extract(self, content: str, *, url: str) -> ExtractionResult: ...
|
||||
```
|
||||
|
||||
`ExtractionResult` bevat:
|
||||
|
||||
- `jobs: list[ExtractedJob]`;
|
||||
- stabiele parser key/version;
|
||||
- confidence 0..1;
|
||||
- machine-/mensleesbare warnings.
|
||||
|
||||
Adapters:
|
||||
|
||||
- doen geen netwerkrequests;
|
||||
- schrijven niet naar ORM;
|
||||
- voeren geen scoring of bronpolicy uit;
|
||||
- werken deterministisch op aangeleverde fixtures;
|
||||
- bewaren alleen beperkte raw payload voor debugging;
|
||||
- leveren evidence per belangrijk veld.
|
||||
|
||||
## 2. Parservolgorde
|
||||
|
||||
Voor HTML:
|
||||
|
||||
1. JSON-LD `JobPosting`;
|
||||
2. gespecialiseerde adapter op expliciete source parser key;
|
||||
3. generieke HTML-fallback;
|
||||
4. geen vacature wanneer minimumvelden ontbreken.
|
||||
|
||||
Voor XML/feed:
|
||||
|
||||
1. RSS/Atom;
|
||||
2. sitemapdiscovery (backlog);
|
||||
3. geen generieke HTMLparser.
|
||||
|
||||
Voor e-mail:
|
||||
|
||||
1. MIME decode zonder attachments;
|
||||
2. linkextractie met unsubscribe/privacyfilter;
|
||||
3. platformalias bewaren;
|
||||
4. oorspronkelijke werkgever proberen te resolveren in een aparte begrensde taak.
|
||||
|
||||
## 3. Minimumvelden
|
||||
|
||||
Een jobdraft is verwerkbaar wanneer minstens aanwezig:
|
||||
|
||||
- titel;
|
||||
- HTTP(S)-URL of stabiele externe identiteit.
|
||||
|
||||
Employer, locatie en beschrijving mogen tijdelijk ontbreken, maar verlagen confidence. Een generieke pagina zonder herkenbare titel wordt niet als job opgeslagen.
|
||||
|
||||
## 4. Confidence
|
||||
|
||||
Richtwaarden:
|
||||
|
||||
| Methode | Baseline |
|
||||
|---|---:|
|
||||
| JSON-LD geldig individueel JobPosting | 0,95 |
|
||||
| gespecialiseerde publieke ATS-adapter | 0,90 |
|
||||
| RSS/Atom | 0,82 |
|
||||
| vacaturemailanchor | 0,65–0,75 |
|
||||
| generieke HTMLselectors | 0,45–0,75 |
|
||||
|
||||
Fieldconfidence en extractionconfidence zijn gescheiden. Een perfecte titel maakt een ontbrekende werkgever niet betrouwbaar.
|
||||
|
||||
## 5. Nieuwe adapter toevoegen
|
||||
|
||||
1. Maak `apps/sources/adapters/<naam>.py`.
|
||||
2. Geef vaste parser key en semverachtige version.
|
||||
3. Parse alleen de aangeleverde content.
|
||||
4. Maak geanonimiseerde fixture onder `fixtures/`.
|
||||
5. Test happy path, ontbrekende velden, gewijzigd markup en malafide HTML.
|
||||
6. Registreer uitsluitend voor een expliciete source/parserdetectie.
|
||||
7. Documenteer bronbeleid, rate limit en canonical URL-gedrag.
|
||||
8. Voeg traceability toe.
|
||||
|
||||
## 6. ATS-strategie
|
||||
|
||||
Gespecialiseerde adapters mogen publieke HTML of publieke, documenteerbare endpoints gebruiken wanneer:
|
||||
|
||||
- geen login/token nodig is;
|
||||
- bronpolicy `allow` is;
|
||||
- gebruiksvoorwaarden/robots zijn beoordeeld;
|
||||
- het endpoint rechtstreeks vacatures van de werkgever publiceert;
|
||||
- rate limiting en identifiers stabiel zijn;
|
||||
- fallback naar HTML mogelijk is.
|
||||
|
||||
Een endpoint mag technisch JSON zijn zonder een commerciële platform-API-integratie te vormen. De policybeslissing blijft per bron vereist.
|
||||
|
||||
## 7. Bronpromotie
|
||||
|
||||
```text
|
||||
candidate
|
||||
-> policy/robots/terms review
|
||||
-> trial (kleine frequentie, parserconfidence meten)
|
||||
-> active (voldoende succesvolle runs)
|
||||
-> quarantined (policy, security of herhaalde parsefout)
|
||||
-> trial/active na expliciet herstel
|
||||
```
|
||||
|
||||
Automatische promotie naar `active` vereist minimaal:
|
||||
|
||||
- meerdere succesvolle runs;
|
||||
- geen private/deny redirect;
|
||||
- voldoende extractieconfidence;
|
||||
- geen onverwachte volumepiek;
|
||||
- policy nog geldig.
|
||||
|
||||
Juridische voorwaarden mogen niet door een taalmodel als definitief toegestaan worden verklaard.
|
||||
|
||||
## 8. Rate limiting
|
||||
|
||||
Per eTLD+1/domein:
|
||||
|
||||
- concurrency standaard 1;
|
||||
- minimum interval standaard 30 seconden;
|
||||
- respecteer `Retry-After`;
|
||||
- conditionele GET met ETag/Last-Modified;
|
||||
- exponentiële backoff bij fout;
|
||||
- geen retry op policyblokkade;
|
||||
- adaptieve lagere frequentie bij stabiele, zelden wijzigende bron.
|
||||
|
||||
## 9. Parserdrift
|
||||
|
||||
Bronhealth detecteert:
|
||||
|
||||
- leeg resultaat waar eerder vacatures waren;
|
||||
- sterke daling in velddichtheid/confidence;
|
||||
- nieuwe foutstatus/contenttype;
|
||||
- wijziging in jobvolume;
|
||||
- ontbrekende title/URL;
|
||||
- veel nieuwe canonical keys door URLtrackingwijziging.
|
||||
|
||||
Automatisch herstel mag veilige selectorvarianten proberen, maar nieuwe logica moet eerst op opgeslagen fixture/snapshot draaien. Geen live agressieve exploratie.
|
||||
|
||||
## Publieke ATS-providers (VR-106)
|
||||
|
||||
| Provider | Hostherkenning | Parserbenadering | Opmerking | Rate limit (advies) |
|
||||
|---|---|---|---|---|
|
||||
| Greenhouse | `*.greenhouse.io`, `*.boards.greenhouse.io` | gespecialiseerd endpoint/JSON + detailstructuren | gebruik alleen publieke boardpagina's/feeds, geen private/persoonlijke endpoints | 15 requests/min met conditional request indien beschikbaar |
|
||||
| Lever | `jobs.lever.co` | gespecialiseerd JSON/HTML listing+detail | alleen publieke postings, geen recruiter login flow | 60 requests/min per domein |
|
||||
| Recruitee | `*.recruitee.com` | gespecialiseerd listing+detail parser | parse alleen door werkgever geïntendeerde publieke vacature-URL’s | 60 requests/min per domein |
|
||||
| SmartRecruiters | `*.smartrecruiters.com` | gespecialiseerd listing+detail parser | geen interne API-auth nodig; stop bij CAPTCHA/anti-bot | 30 requests/min per domein |
|
||||
| Workable | `apply.workable.com` | gespecialiseerd listing+detail parser | alleen publieke vacaturepagina/JSON; geen private endpoints | 30 requests/min per domein |
|
||||
|
||||
Voor elke provider-adapter:
|
||||
- `supports()` kijkt eerst op host/marker om andere pagina's niet te matchen;
|
||||
- listingpayloads produceren kandidaten zonder fetch van detail;
|
||||
- detailpayloads vullen een enkele vacaturestructuur;
|
||||
- elke veldafklaring krijgt veld-evidence met method-labels en confidence.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Systeemarchitectuur
|
||||
|
||||
## 1. Architectuurstijl
|
||||
|
||||
VacatureRadar gebruikt een modulaire Django-monoliet. De domeinen delen één relationele database maar hebben duidelijke model-, service-, task- en UI-grenzen. Dit houdt deployment en beheer eenvoudig op Unraid, terwijl de kernlogica testbaar en later splitsbaar blijft.
|
||||
|
||||
## 2. Context
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[Gebruiker] -->|browser| W[VacatureRadar web]
|
||||
M[Vacaturemailbox] -->|IMAP| C[Collectielaag]
|
||||
E[Publieke werkgevers- en ATS-pagina's] -->|HTTP(S)| C
|
||||
C --> P[Extractie en normalisatie]
|
||||
P --> D[(PostgreSQL)]
|
||||
D --> S[Scoring en dedupe]
|
||||
S --> W
|
||||
S --> N[Digest/outbox]
|
||||
N -->|SMTP| U
|
||||
O[Optionele Ollama] <--> S
|
||||
```
|
||||
|
||||
## 3. Containerarchitectuur
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
RP[Reverse proxy / LAN] --> WEB[web: Gunicorn + Django]
|
||||
WEB --> PG[(PostgreSQL)]
|
||||
WEB --> REDIS[(Redis)]
|
||||
WORKER[Celery worker] --> PG
|
||||
WORKER --> REDIS
|
||||
WORKER --> NET[Goedgekeurde publieke bronnen]
|
||||
BEAT[Celery beat] --> REDIS
|
||||
OLLAMA[Ollama optioneel] <--> WORKER
|
||||
MAIL[IMAP/SMTP optioneel] <--> WORKER
|
||||
```
|
||||
|
||||
Containerrollen:
|
||||
|
||||
- **web** — authenticatie, dashboard, detailpagina's, profiel- en beheerflows;
|
||||
- **worker** — bronfetches, parsing, scoring, lifecycle, mailbox en verzending;
|
||||
- **scheduler** — periodieke planning, geen bedrijfslogica;
|
||||
- **postgres** — canonieke data, historie en outbox;
|
||||
- **redis** — broker/resultbackend, geen bron van waarheid;
|
||||
- **ollama** — optioneel lokaal model, niet nodig voor kern.
|
||||
|
||||
## 4. Domeinmodules
|
||||
|
||||
### `apps/core`
|
||||
|
||||
- liveness/readiness;
|
||||
- dashboardaggregatie;
|
||||
- systeemstatus;
|
||||
- securityheaders en navigatiecontext.
|
||||
|
||||
### `apps/profiles`
|
||||
|
||||
- user-scoped zoekprofielen;
|
||||
- harde regels, soft preferences en scoregewichten;
|
||||
- profielversies en gecontroleerde feedbackdelta's.
|
||||
|
||||
### `apps/sources`
|
||||
|
||||
- bronregister en policy/status;
|
||||
- veilige fetcher;
|
||||
- adapters en source discovery;
|
||||
- raw document, source run en e-mailrecord;
|
||||
- Celery-orkestratie.
|
||||
|
||||
### `apps/jobs`
|
||||
|
||||
- canonieke vacature- en werkgevermodellen;
|
||||
- normalisatie, features, dedupe en provenance;
|
||||
- scoreberekening en scorehistorie;
|
||||
- feedback, lifecycle en sollicitatiedossier.
|
||||
|
||||
### `apps/notifications`
|
||||
|
||||
- dagelijkse payloadselectie;
|
||||
- idempotente outbox;
|
||||
- rendering en SMTP-verzending.
|
||||
|
||||
## 5. Laagregels
|
||||
|
||||
```text
|
||||
views/templates -> application services -> domain models / adapter interfaces
|
||||
Celery tasks -> application services -> domain models / adapter interfaces
|
||||
adapters -> pure extraction DTOs (geen ORM writes)
|
||||
fetcher -> policy + URL security (geen parserlogica)
|
||||
AI service -> gestructureerde optionele features (geen beslissingsbevoegdheid)
|
||||
```
|
||||
|
||||
Verboden afhankelijkheden:
|
||||
|
||||
- template/view die externe websites fetcht;
|
||||
- adapter die direct `JobPosting` schrijft;
|
||||
- AI die `Feedback`, `Application`, `Source.policy` of hard rules wijzigt;
|
||||
- task met verborgen businessregels die niet in een service testbaar zijn;
|
||||
- model save-hook die netwerkverkeer uitvoert.
|
||||
|
||||
## 6. Verwerkingsflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Beat
|
||||
participant T as Source task
|
||||
participant F as Safe fetcher
|
||||
participant A as Adapter registry
|
||||
participant P as Pipeline
|
||||
participant DB as PostgreSQL
|
||||
participant S as Scoring
|
||||
|
||||
B->>T: schedule due source
|
||||
T->>DB: create SourceRun
|
||||
T->>F: fetch approved URL
|
||||
F->>F: policy + DNS/IP + redirect checks
|
||||
F-->>T: bounded document / 304 / error
|
||||
T->>DB: save RawDocument
|
||||
T->>A: extract(document)
|
||||
A-->>P: ExtractionResult
|
||||
P->>P: normalize + sanitize + features
|
||||
P->>DB: find exact/fuzzy existing job
|
||||
P->>DB: upsert job, alias, evidence, version
|
||||
P->>S: score for active profiles
|
||||
S->>DB: append ScoreRun
|
||||
T->>DB: finish SourceRun + schedule next
|
||||
```
|
||||
|
||||
## 7. Consistentie en transacties
|
||||
|
||||
- `process_raw_document` en `persist_draft` zijn transactioneel;
|
||||
- unieke sleutels beschermen races op canonieke vacature, e-mail en outbox;
|
||||
- scorehistorie is append-only;
|
||||
- raw document en source run vormen debugcontext;
|
||||
- een mislukte individuele e-maillink wordt in het e-mailrecord vastgelegd zonder andere links te verliezen;
|
||||
- externe calls gebeuren buiten lange database-locks waar mogelijk.
|
||||
|
||||
## 8. Schaalpad
|
||||
|
||||
De eerste schaalgrens ligt niet bij HTTP-requestvolume maar bij het aantal bronjobs. Tot ongeveer tienduizenden vacatures en honderden bronnen volstaat de monoliet. Optimalisaties vóór opsplitsing:
|
||||
|
||||
1. indexes en querysetprofiling;
|
||||
2. chunked scoring;
|
||||
3. task queues per prioriteit;
|
||||
4. per-domain rate limit en distributed lock;
|
||||
5. materialized/latest scoreselectie;
|
||||
6. raw-bodycompressie/objectstorage.
|
||||
|
||||
Pas na aantoonbare bottleneck kan collection worker als aparte service worden afgesplitst. De adapter- en DTO-grenzen maken dat mogelijk zonder nu operationele complexiteit toe te voegen.
|
||||
|
||||
## 9. Deploymenttrust boundaries
|
||||
|
||||
- browser ↔ web: authenticated session + CSRF;
|
||||
- web/worker ↔ database/Redis: intern containernetwerk;
|
||||
- worker ↔ internet: alleen goedgekeurde URL's via safe fetcher;
|
||||
- worker ↔ IMAP/SMTP/Ollama: expliciete environmentconfiguratie;
|
||||
- reverse proxy ↔ internet: TLS, host filtering en toegangsbeleid buiten de app.
|
||||
|
||||
Zie `SECURITY_ARCHITECTURE.md` en `docs/quality/THREAT_MODEL.md`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-0001 — Modulaire Django-monoliet
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
De applicatie is persoonlijk, self-hosted en heeft meerdere samenhangende domeinen maar beperkte operationele schaal. Afzonderlijke frontend/backend/microservices zouden deployment, auth, transacties, upgrades en debugging op Unraid verzwaren.
|
||||
|
||||
## Besluit
|
||||
|
||||
Gebruik één Django-codebase met domeinapps, server-rendered templates, PostgreSQL en Celery/Redis voor asynchroon werk. Bedrijfslogica blijft in services en adapters zodat latere extractie mogelijk blijft.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
Positief:
|
||||
|
||||
- één image en releaseflow;
|
||||
- eenvoudige auth/CSRF;
|
||||
- transactionele pipeline;
|
||||
- lage beheerlast;
|
||||
- goede testbaarheid.
|
||||
|
||||
Negatief:
|
||||
|
||||
- frontendinteractie is minder SPA-achtig;
|
||||
- domeingrenzen moeten door conventie/tests bewaakt worden;
|
||||
- lange collectiontaken mogen nooit in webrequests draaien.
|
||||
|
||||
## Herzieningstrigger
|
||||
|
||||
Alleen herzien bij meetbare bottleneck, onafhankelijk releasevereiste of team-/tenantgrens die niet met modules/queues kan worden opgelost.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-0002 — Strikt bronbeleid en geen directe platformbots
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Grote vacatureplatformen hebben toegangsvoorwaarden, botdetectie en databank-/privacyrisico's. De gebruiker wil minimale handelingen, maar niet ten koste van blokkadeomzeiling of onbeheerbare scraping.
|
||||
|
||||
## Besluit
|
||||
|
||||
Onbekende bronnen starten als `review`. Alleen `allow`-bronnen worden automatisch gefetcht. LinkedIn, Indeed, StepStone, Jobat en VDAB staan standaard op de directe fetchdenylist. Platformvacatures komen binnen via door de gebruiker ingestelde e-mailalerts of handmatige import; de tool probeert de oorspronkelijke werkgeversbron te vinden.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- minder brede directe platformdekking;
|
||||
- veel lagere juridische/technische onderhoudslast;
|
||||
- bronaudit en policyreview worden productfunctionaliteit;
|
||||
- denylistlinks mogen als alias bestaan maar worden niet automatisch bezocht.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-0003 — Deterministische kern vóór optionele AI
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Vacatureteksten zijn onbetrouwbare externe data. Taalmodellen kunnen hallucineren, gevoelig zijn voor prompt injection en niet altijd beschikbaar zijn.
|
||||
|
||||
## Besluit
|
||||
|
||||
Normalisatie, harde uitsluitingen, dedupe, lifecycle en baselinescoring zijn deterministisch. AI is standaard uit en mag alleen gestructureerde aanvullende features, evidence en samenvatting leveren. AI-uitval verandert de kernflow niet.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- voorspelbaar en offline bruikbaar;
|
||||
- uitlegbare fallback;
|
||||
- minder semantische nuance zonder model;
|
||||
- model-/promptversie en evaluaties vereist vóór rankingimpact.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-0004 — Single-user-first, user-scoped data
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Het primaire doel is één persoonlijke Unraid-installatie. Volledige multi-tenant SaaS-architectuur zou onnodige complexiteit toevoegen, maar globale user-onafhankelijke querysets kunnen later datalekken veroorzaken.
|
||||
|
||||
## Besluit
|
||||
|
||||
Optimaliseer UX en beheer voor één gebruiker, maar koppel profielen, feedback, applications en digests altijd aan een Django-user en scope wijzigingsviews op die user. Canonieke vacatures/bronnen zijn installatiebreed gedeeld.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- eenvoudige installatie;
|
||||
- veilige basis voor een tweede lokale user;
|
||||
- geen tenantbilling, uitnodigingen of isolatie op databaseniveau;
|
||||
- beheerfuncties blijven admin-only in productie.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-0005 — Vacaturemails als platformingress
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Vacatureplatformen bieden nuttige zoekalgoritmes en alerts, terwijl directe scraping ongewenst is. E-mail is door de gebruiker geactiveerde, begrensde input.
|
||||
|
||||
## Besluit
|
||||
|
||||
Gebruik een dedicated IMAP-mailbox en idempotente MIME/linkextractie. Bewaar platformlinks als bronalias. Volg een link alleen wanneer bronpolicy dat later toestaat; probeer bij voorkeur een oorspronkelijke werkgeverspagina te vinden.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- gebruiker stelt alerts eenmalig in;
|
||||
- parser moet variërende e-mailsjablonen verdragen;
|
||||
- dedicated mailbox en retentie nodig;
|
||||
- geen platformcredentials in VacatureRadar.
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-0006 — Geen automatische sollicitaties
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Automatisch solliciteren kan onjuiste verklaringen, privacyverlies, reputatieschade en ongewenste massacommunicatie veroorzaken. Vacatureformulieren verschillen sterk en bevatten vaak gevoelige vragen.
|
||||
|
||||
## Besluit
|
||||
|
||||
VacatureRadar archiveert, scoort, maakt een dossier en kan conceptinformatie voorbereiden, maar bevat geen externe submitactie. Alleen de gebruiker verstuurt een sollicitatie via het oorspronkelijke kanaal.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- menselijke controle blijft gegarandeerd;
|
||||
- minder end-to-endautomatisering;
|
||||
- een eventuele toekomstige wijziging vereist een nieuwe expliciete ADR, security/privacyreview en per-submitconfirmatie.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-0007 — PostgreSQL als waarheid, Redis/Celery voor werk
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
Broncontroles, mailboxpolling en scoring mogen webrequests niet blokkeren. Taken moeten retrybaar zijn, terwijl Redisdata verloren mag gaan zonder canonieke data te verliezen.
|
||||
|
||||
## Besluit
|
||||
|
||||
PostgreSQL is de enige productiebron van waarheid. Redis is uitsluitend broker/resultbackend. Celery worker en beat voeren periodiek/asynchroon werk uit. SQLite en eager tasks blijven beschikbaar voor lokale tests.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- duidelijke herstelstrategie;
|
||||
- extra twee containers;
|
||||
- taken moeten idempotent zijn omdat at-least-once uitvoering mogelijk is;
|
||||
- schedulerstate mag worden herbouwd uit database/sourcevelden.
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-0008 — Server-rendered UI zonder aparte SPA
|
||||
|
||||
- Status: accepted
|
||||
- Datum: 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
De interface bestaat vooral uit lijsten, detailpagina's, formulieren en enkele POST-acties. Een React-buildketen verdubbelt contracts, auth, dependencies en deployment zonder duidelijke MVP-winst.
|
||||
|
||||
## Besluit
|
||||
|
||||
Gebruik Django templates, semantische HTML, eigen CSS en minimale vanilla JavaScript. Progressive enhancement mag; kernflows werken zonder clientframework.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
- snelle, toegankelijke en eenvoudige deployment;
|
||||
- minder rijke realtimeinteractie;
|
||||
- API blijft intern tenzij een concrete integratiebehoefte ontstaat.
|
||||
Reference in New Issue
Block a user