14 KiB
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/, 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 |
| 2. Intake resolution | diagrams/02-intake-resolution.svg |
| 3. Identity and duplicates | diagrams/03-identity-and-duplicates.svg |
| 4. Application lifecycle | diagrams/04-application-lifecycle.svg |
| 5. Phase cut | 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 is07-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.
flowchart LR
S1["Careers mailbox · P1"]
S2["Careers website · P2"]
S3["Manual upload · P1"]
S4["Referral · P2"]
S5["LinkedIn source · P1"]
S6["Agency, campus,<br/>walk-in · P2"]
S1 --> RAW
S2 --> RAW
S3 --> RAW
S4 --> RAW
S5 --> RAW
S6 --> RAW
RAW["<b>raw_intake</b><br/>provider msg id + mailbox<br/>= idempotency key"]
RAW --> ATT["<b>intake_attachment</b><br/>content-addressed<br/>SHA-256"]
ATT --> PARSE["<b>intake_parse_attempt</b><br/>every attempt kept,<br/>never overwritten"]
PARSE --> RESOLVE{{"Resolvable?<br/>diagram 2"}}
RESOLVE -->|"no"| QUEUE["Unassigned<br/>Applications Queue"]
QUEUE --> HR1["HR resolves in<br/>Recruitment Inbox"]
HR1 --> RESOLVE
RESOLVE -->|"yes"| IDENT["Identity resolution<br/>diagram 3"]
IDENT --> CAND["<b>candidate</b><br/>one master profile"]
CAND --> APP["<b>job_application</b><br/>pins job_version"]
APP --> SCORE["ATS scoring<br/>pins job_version +<br/>scoring_config + model"]
SCORE --> ROUTE["Recruiter routing"]
ROUTE --> REVIEW["Recruiter review<br/>score is advisory"]
REVIEW --> PIPE["Pipeline<br/>every change to history"]
PIPE --> HIRED["Hired"]
PIPE --> REJ["Rejected<br/>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.
flowchart TB
IN["Message arrives<br/>raw_intake row written first"] --> DUP{{"Provider msg id<br/>already seen?"}}
DUP -->|"yes"| STOP["Ignore — idempotent.<br/>No second row."]
DUP -->|"no"| SCAN["Validate + malware scan<br/>each attachment"]
SCAN --> BAD{{"Attachment usable?"}}
BAD -->|"corrupt / password-protected /<br/>unsupported type"| ERRQ["state = needs_review<br/>file retained, error shown"]
BAD -->|"yes"| COUNT{{"How many CVs<br/>in this message?"}}
COUNT -->|"zero"| NOCV["No CV.<br/>Could be a query, a reply,<br/>or spam."]
COUNT -->|"one"| ONE["One intake item"]
COUNT -->|"many"| MANY["One intake item PER CV.<br/>Siblings share the source message."]
NOCV --> ERRQ
ONE --> JOB
MANY --> JOB
JOB{{"Job identifiable?<br/>subject line, form field,<br/>reply-to thread"}}
JOB -->|"yes"| KNOWN["Target job known"]
JOB -->|"no — general application"| GEN["No job.<br/>Valid outcome, not an error."]
GEN --> ERRQ
KNOWN --> IDENT["Proceed to identity resolution<br/>diagram 3"]
ERRQ --> HUMAN["HR in Recruitment Inbox:<br/>link a job, create or connect a candidate,<br/>reply, or dismiss"]
HUMAN -->|"resolved"| IDENT
HUMAN -->|"not an application"| DISMISS["Dismissed<br/>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).
flowchart TB
START["Parsed CV fields available"] --> SIG["Compute match signals:<br/>normalised email, normalised phone,<br/>name, file hash, CV text similarity,<br/>LinkedIn URL, employment, education"]
SIG --> CLASS{{"Classification"}}
CLASS -->|"confirmed<br/>exact email or file hash"| LINK["Link to existing candidate"]
CLASS -->|"probable"| PAIR["candidate_duplicate_pair<br/>queued for review"]
CLASS -->|"possible"| PAIR
CLASS -->|"not duplicate"| NEW["Create new candidate"]
PAIR --> REV["HR duplicate review<br/>hr_admin only"]
REV -->|"confirm"| MERGE["Merge"]
REV -->|"reject"| DISTINCT["Recorded as confirmed_distinct<br/>never re-raised"]
DISTINCT --> NEW
LINK --> APPNEW["Create job_application"]
NEW --> APPNEW
MERGE --> OPS["candidate_merge_operation rows<br/>one per moved artefact,<br/>each with previous_value"]
OPS --> SURV["Survivor holds all applications,<br/>documents, communications,<br/>scores, notes"]
SURV --> APPNEW
SURV -.->|"reversal"| UNDO["Replay operations by<br/>reversal_rank, not by seq"]
UNDO --> SPLIT["Both candidates restored<br/>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.
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.
flowchart TB
P0["<b>Phase 0 — scope and architecture</b><br/>this package · repository review · ADRs<br/>schema design · Phase 1 backlog · risk register<br/>P0 XSS and CSP hardening"]
P1["<b>Phase 1 — core ATS and email intake</b><br/>auth, core roles, departments · jobs and job versions<br/>manual upload + careers mailbox · raw intake + Recruitment Inbox<br/>object storage · text extraction + basic parsing<br/>candidate and application · basic ATS score<br/>recruiter assignment · basic pipeline · audit history"]
P2["<b>Phase 2 — connected recruitment</b><br/>requisitions + approval · director access<br/>careers website + job publishing · communication templates<br/>duplicate review and merge · advanced routing<br/>department and region reporting · stronger access control"]
P3["<b>Phase 3 — complete workflow</b><br/>interview scheduling + feedback + scorecards<br/>assessments · offers and approval<br/>talent pool · recruiter analytics · data migration"]
P4["<b>Phase 4 — AI assistant and advanced search</b><br/>read-only chatbot · semantic search (pgvector)<br/>candidate comparison · JD assistance<br/>communication drafting · HRMS readiness"]
P0 --> P1 --> P2 --> P3 --> P4
CUT["<b>Phase 1 cut line</b><br/>everything above this point is the<br/>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 |