HR-ATS-Portal/docs/architecture/09-end-to-end-flow.md

305 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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,<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.
```mermaid
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).
```mermaid
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.
```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["<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 **2430 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 |