305 lines
14 KiB
Markdown
305 lines
14 KiB
Markdown
# 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 **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 |
|