# 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 |