# End-to-End Flow
**Status / Scope.** A consolidated view of how a candidate moves through the platform, from any
intake source to a terminal outcome. The other documents in this package each diagram their own
concern; this one exists because none of them shows the whole spine in a single picture.
Nothing here introduces new design. Every element traces to `02-system-architecture.md` (modules),
`03-database-design.md` (entities), `04-integrations-and-processing.md` (pipeline steps) and
`05-security-rbac-ai-governance.md` (human-decision gates). Where this document and those disagree,
**they win** — raise it as a defect.
**Rendered copies.** Each diagram below is also exported as a standalone SVG in
[`diagrams/`](diagrams/), for dropping into slides or sharing with people who will not open a
markdown file. They are generated from the mermaid blocks in this document — if you change a
diagram here, re-export rather than editing the SVG.
| Diagram | SVG |
|---|---|
| 1. Master flow | [`diagrams/01-master-flow.svg`](diagrams/01-master-flow.svg) |
| 2. Intake resolution | [`diagrams/02-intake-resolution.svg`](diagrams/02-intake-resolution.svg) |
| 3. Identity and duplicates | [`diagrams/03-identity-and-duplicates.svg`](diagrams/03-identity-and-duplicates.svg) |
| 4. Application lifecycle | [`diagrams/04-application-lifecycle.svg`](diagrams/04-application-lifecycle.svg) |
| 5. Phase cut | [`diagrams/05-phase-cut.svg`](diagrams/05-phase-cut.svg) |
Two conventions used throughout:
- **Bold-bordered nodes are human decision points.** Everything else is automatic. The
"AI is advisory" requirement (assignment §3.12, §5.7) is visible as a structural property of
these diagrams: no automatic path reaches a terminal-negative outcome.
- **Phase tags** (`P1`…`P4`) mark when a step becomes available. Phase 1's cut line is
`07-implementation-plan.md` §4.2.
---
## 1. The master flow
The complete journey. Six intake sources converge on one raw-intake layer; nothing becomes a
candidate until it has been resolved.
```mermaid
flowchart LR
S1["Careers mailbox · P1"]
S2["Careers website · P2"]
S3["Manual upload · P1"]
S4["Referral · P2"]
S5["LinkedIn source · P1"]
S6["Agency, campus,
walk-in · P2"]
S1 --> RAW
S2 --> RAW
S3 --> RAW
S4 --> RAW
S5 --> RAW
S6 --> RAW
RAW["raw_intake
provider msg id + mailbox
= idempotency key"]
RAW --> ATT["intake_attachment
content-addressed
SHA-256"]
ATT --> PARSE["intake_parse_attempt
every attempt kept,
never overwritten"]
PARSE --> RESOLVE{{"Resolvable?
diagram 2"}}
RESOLVE -->|"no"| QUEUE["Unassigned
Applications Queue"]
QUEUE --> HR1["HR resolves in
Recruitment Inbox"]
HR1 --> RESOLVE
RESOLVE -->|"yes"| IDENT["Identity resolution
diagram 3"]
IDENT --> CAND["candidate
one master profile"]
CAND --> APP["job_application
pins job_version"]
APP --> SCORE["ATS scoring
pins job_version +
scoring_config + model"]
SCORE --> ROUTE["Recruiter routing"]
ROUTE --> REVIEW["Recruiter review
score is advisory"]
REVIEW --> PIPE["Pipeline
every change to history"]
PIPE --> HIRED["Hired"]
PIPE --> REJ["Rejected
with reason"]
PIPE --> WD["Withdrawn"]
PIPE --> POOL["Talent pool · P3"]
classDef human stroke-width:3px,stroke:#004d43
classDef ai stroke-dasharray:5 3
classDef terminal fill:#eafff4,stroke:#004d43
classDef src fill:#f4ffdf
class HR1,REVIEW human
class SCORE,PARSE ai
class HIRED,REJ,WD,POOL terminal
class S1,S2,S3,S4,S5,S6 src
```
The three terminal-negative outcomes (`Rejected`, `Withdrawn`, and any rejection routed to
`Talent pool`) are reachable **only** from `Recruiter review` and the pipeline stages a human drives.
No edge runs from `ATS scoring` to a terminal state.
**Reading the diagram.** Dashed nodes are the only two places a model runs. Both feed a
bold-bordered human node before anything terminal happens. `application.transition()` refuses any
terminal-negative move whose `actor_kind` is not `'user'` (RULING-01), so the "AI never auto-rejects"
guarantee is enforced at the service and database layer, not by the shape of this picture.
---
## 2. Intake resolution — the hard cases
Assignment §3.3 lists eight awkward realities of a careers mailbox. This is how each one resolves
**without** creating an incorrect candidate or application record. Every path either produces a
resolved pair or parks the item for a human; none silently drops or silently invents.
```mermaid
flowchart TB
IN["Message arrives
raw_intake row written first"] --> DUP{{"Provider msg id
already seen?"}}
DUP -->|"yes"| STOP["Ignore — idempotent.
No second row."]
DUP -->|"no"| SCAN["Validate + malware scan
each attachment"]
SCAN --> BAD{{"Attachment usable?"}}
BAD -->|"corrupt / password-protected /
unsupported type"| ERRQ["state = needs_review
file retained, error shown"]
BAD -->|"yes"| COUNT{{"How many CVs
in this message?"}}
COUNT -->|"zero"| NOCV["No CV.
Could be a query, a reply,
or spam."]
COUNT -->|"one"| ONE["One intake item"]
COUNT -->|"many"| MANY["One intake item PER CV.
Siblings share the source message."]
NOCV --> ERRQ
ONE --> JOB
MANY --> JOB
JOB{{"Job identifiable?
subject line, form field,
reply-to thread"}}
JOB -->|"yes"| KNOWN["Target job known"]
JOB -->|"no — general application"| GEN["No job.
Valid outcome, not an error."]
GEN --> ERRQ
KNOWN --> IDENT["Proceed to identity resolution
diagram 3"]
ERRQ --> HUMAN["HR in Recruitment Inbox:
link a job, create or connect a candidate,
reply, or dismiss"]
HUMAN -->|"resolved"| IDENT
HUMAN -->|"not an application"| DISMISS["Dismissed
audit record retained"]
classDef human stroke-width:3px,stroke:#004d43
classDef park fill:#fff2d9
class HUMAN human
class ERRQ,NOCV,GEN,DISMISS park
```
| Assignment §3.3 case | Path above | Why no bad record is created |
|---|---|---|
| One CV | `ONE` → identity resolution | Normal path |
| Multiple CVs | `MANY` | One intake item per CV; siblings keep the shared `source_message_id`, so the thread stays intact |
| No CV | `NOCV` → `ERRQ` | Never reaches candidate creation. A human decides whether it is an application at all |
| CV without a job title | `JOB` → `GEN` → `ERRQ` | A general application is a legitimate outcome, parked for a human to route — not a parse failure |
| General application | same as above | Resolvable later against any open job without re-parsing |
| Repeat applicant | identity resolution, diagram 3 | Matches to the existing candidate; a **new application**, not a new profile |
| Applying for multiple jobs | one `job_application` per job | The candidate row is untouched — this is the §5.2 separation working |
| Unsupported / corrupted | `BAD` → `ERRQ` | File retained, error surfaced. Never discarded |
---
## 3. Identity resolution and duplicate handling
The point at which raw intake becomes a person. Uncertain matches are **never** merged
automatically (assignment §3.6).
```mermaid
flowchart TB
START["Parsed CV fields available"] --> SIG["Compute match signals:
normalised email, normalised phone,
name, file hash, CV text similarity,
LinkedIn URL, employment, education"]
SIG --> CLASS{{"Classification"}}
CLASS -->|"confirmed
exact email or file hash"| LINK["Link to existing candidate"]
CLASS -->|"probable"| PAIR["candidate_duplicate_pair
queued for review"]
CLASS -->|"possible"| PAIR
CLASS -->|"not duplicate"| NEW["Create new candidate"]
PAIR --> REV["HR duplicate review
hr_admin only"]
REV -->|"confirm"| MERGE["Merge"]
REV -->|"reject"| DISTINCT["Recorded as confirmed_distinct
never re-raised"]
DISTINCT --> NEW
LINK --> APPNEW["Create job_application"]
NEW --> APPNEW
MERGE --> OPS["candidate_merge_operation rows
one per moved artefact,
each with previous_value"]
OPS --> SURV["Survivor holds all applications,
documents, communications,
scores, notes"]
SURV --> APPNEW
SURV -.->|"reversal"| UNDO["Replay operations by
reversal_rank, not by seq"]
UNDO --> SPLIT["Both candidates restored
nothing lost"]
classDef human stroke-width:3px,stroke:#004d43
class REV human
```
**Why `reversal_rank` and not descending `seq`.** Undoing in reverse-application order clears
`superseded_by_application_id` before re-parenting the application, which momentarily leaves two live
applications on the same candidate for the same job — exactly what the `uq_application_live` partial
unique index forbids. A partial unique index **cannot be `DEFERRABLE`**, so that violation aborts the
whole reversal transaction, and every merge that collided on a job would become permanently
unreversible. A fixed per-`op_kind` rank re-parents first and clears the supersession last.
Full treatment: `03-database-design.md` §19.5.
---
## 4. Application lifecycle
Stages are configuration, not code (`pipeline_stage` + `pipeline_transition_rule`). The states below
are the seeded default from assignment §3.10 — a different job category can define a different set
without a schema change.
```mermaid
stateDiagram-v2
[*] --> Received: application created
Received --> Processing: CV text extracted
Processing --> Screening: AI score produced (advisory)
Screening --> RecruiterReview: queued to assigned recruiter
RecruiterReview --> Shortlisted: recruiter decides
RecruiterReview --> Rejected: recruiter decides
RecruiterReview --> OnHold: recruiter decides
RecruiterReview --> TalentPool: recruiter decides
Shortlisted --> Assessment
Assessment --> InitialInterview
InitialInterview --> FinalInterview
FinalInterview --> OfferApproval
OfferApproval --> OfferSent: approvers sign off
OfferSent --> Hired: candidate accepts
OfferSent --> Rejected: candidate declines
Assessment --> Rejected
InitialInterview --> Rejected
FinalInterview --> Rejected
OnHold --> RecruiterReview: resumed
Rejected --> TalentPool: recruiter opts in
Hired --> [*]
TalentPool --> [*]
Withdrawn --> [*]
RecruiterReview --> Withdrawn: candidate withdraws
note right of Screening
The only automatic transition
into a scored state. It cannot
reach Rejected: actor_kind
must be 'user' for any
terminal-negative move.
end note
```
Every transition writes an `application_stage_history` row with actor, `actor_kind`, reason and
timestamp. The `current_stage_id` column on `job_application` is a denormalised convenience; the
history table is the source of truth (assignment §5.5).
---
## 5. Where the phases cut
The same spine, coloured by phase, so the Phase 1 cut line is legible at a glance.
```mermaid
flowchart TB
P0["Phase 0 — scope and architecture
this package · repository review · ADRs
schema design · Phase 1 backlog · risk register
P0 XSS and CSP hardening"]
P1["Phase 1 — core ATS and email intake
auth, core roles, departments · jobs and job versions
manual upload + careers mailbox · raw intake + Recruitment Inbox
object storage · text extraction + basic parsing
candidate and application · basic ATS score
recruiter assignment · basic pipeline · audit history"]
P2["Phase 2 — connected recruitment
requisitions + approval · director access
careers website + job publishing · communication templates
duplicate review and merge · advanced routing
department and region reporting · stronger access control"]
P3["Phase 3 — complete workflow
interview scheduling + feedback + scorecards
assessments · offers and approval
talent pool · recruiter analytics · data migration"]
P4["Phase 4 — AI assistant and advanced search
read-only chatbot · semantic search (pgvector)
candidate comparison · JD assistance
communication drafting · HRMS readiness"]
P0 --> P1 --> P2 --> P3 --> P4
CUT["Phase 1 cut line
everything above this point is the
demonstrable end-to-end spine"]
P1 -.-> CUT
classDef p0 fill:#ffffff,stroke:#54726c
classDef p1 fill:#eafff4,stroke:#004d43,stroke-width:3px
classDef p2 fill:#f4ffdf,stroke:#6f8f14
classDef p3 fill:#ecedff,stroke:#5b60e8
classDef p4 fill:#e4f4f9,stroke:#0d6580
classDef cut fill:#fff2d9,stroke:#8a5a00,stroke-dasharray:5 3
class P0 p0
class P1 p1
class P2 p2
class P3 p3
class P4 p4
class CUT cut
```
**Phase 1 proves the spine end to end:** a CV arrives by email or manual upload, lands in the
inbox, becomes a candidate and an application, is parsed and scored, is routed to a recruiter, and
moves through the pipeline — with every step audited. Everything else builds outward from that.
What Phase 1 deliberately excludes, and why, is `07-implementation-plan.md` §4.2. The honest
schedule for it is **24–30 weeks**, not the 14 originally carried over — see §2 of that document for
the reconciliation.
---
## 6. Traceability
| Diagram | Requirements shown | Primary source document |
|---|---|---|
| 1. Master flow | §3.2 sources, §3.5 separation, §3.7 routing, §3.10 pipeline, §3.12 advisory AI | `02` §3, `04` §4 |
| 2. Intake resolution | §3.3 all eight cases, §3.4 inbox, §5.1 raw intake first | `04` §2, `03` §18 |
| 3. Identity + duplicates | §3.6 duplicate detection, merge, reversal | `03` §13, §19 |
| 4. Lifecycle | §3.10 configurable pipeline, §5.5 current + history, §5.7 no auto-reject | `03` §15, §16 |
| 5. Phase cut | §24 phased delivery | `07` §2, §4 |