From adcd3097f83f92df0a8f3dd7bbfd66b7b320d401 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Wed, 9 Sep 2026 14:04:01 +0500 Subject: [PATCH] add abtcht additon --- backend/global_cities.py | 2 - backend/org_settings/app.py | 74 + ...ntFlow-ATS-Business-Requirements-v1.0.docx | Bin 26526 -> 0 bytes docs/architecture/00-scope-classification.md | 584 -- docs/architecture/01-repository-assessment.md | 620 -- docs/architecture/02-system-architecture.md | 1257 ---- docs/architecture/03-database-design.md | 6431 ----------------- .../04-integrations-and-processing.md | 1461 ---- .../05-security-rbac-ai-governance.md | 814 --- docs/architecture/06-api-boundaries.md | 2723 ------- docs/architecture/07-implementation-plan.md | 1566 ---- .../08-requirements-traceability.md | 713 -- docs/architecture/09-end-to-end-flow.md | 304 - docs/architecture/README.md | 91 - docs/architecture/_decisions.md | 482 -- docs/architecture/_glossary.md | 144 - docs/architecture/_open-items.md | 333 - docs/architecture/_repo-findings.md | 148 - ...1-modular-monolith-versus-microservices.md | 237 - .../adr/0002-primary-relational-database.md | 266 - .../adr/0003-object-storage-strategy.md | 254 - .../adr/0004-background-job-queue.md | 302 - .../adr/0005-email-integration-method.md | 257 - .../adr/0006-candidate-search-strategy.md | 277 - .../adr/0007-job-and-scoring-versioning.md | 338 - ...candidate-duplicate-resolution-strategy.md | 306 - .../0009-permission-enforcement-strategy.md | 359 - ...0-chatbot-controlled-query-architecture.md | 331 - ...-ai-provider-abstraction-and-versioning.md | 410 -- .../adr/0012-deployment-topology.md | 297 - .../adr/0013-frontend-strangler-migration.md | 203 - .../adr/0014-phase-0-xss-csp-hardening.md | 234 - .../adr/0015-module-boundary-enforcement.md | 289 - .../adr/0016-real-postgres-in-ci.md | 271 - ...lain-sql-migrations-as-schema-authority.md | 295 - .../0018-backend-language-and-framework.md | 212 - docs/architecture/diagrams/01-master-flow.svg | 1 - .../diagrams/02-intake-resolution.svg | 1 - .../diagrams/03-identity-and-duplicates.svg | 1 - .../diagrams/04-application-lifecycle.svg | 1 - docs/architecture/diagrams/05-phase-cut.svg | 1 - docs/aws-production-cost-estimate.md | 678 -- .../buffer/Buffer-API.postman_collection.json | 1976 ----- .../Buffer-API.postman_environment.json | 61 - docs/integrations/buffer/README.md | 185 - frontend/src/api/orgSettings.js | 14 + frontend/src/screens/Settings.jsx | 127 +- 47 files changed, 205 insertions(+), 25726 deletions(-) delete mode 100644 docs/TalentFlow-ATS-Business-Requirements-v1.0.docx delete mode 100644 docs/architecture/00-scope-classification.md delete mode 100644 docs/architecture/01-repository-assessment.md delete mode 100644 docs/architecture/02-system-architecture.md delete mode 100644 docs/architecture/03-database-design.md delete mode 100644 docs/architecture/04-integrations-and-processing.md delete mode 100644 docs/architecture/05-security-rbac-ai-governance.md delete mode 100644 docs/architecture/06-api-boundaries.md delete mode 100644 docs/architecture/07-implementation-plan.md delete mode 100644 docs/architecture/08-requirements-traceability.md delete mode 100644 docs/architecture/09-end-to-end-flow.md delete mode 100644 docs/architecture/README.md delete mode 100644 docs/architecture/_decisions.md delete mode 100644 docs/architecture/_glossary.md delete mode 100644 docs/architecture/_open-items.md delete mode 100644 docs/architecture/_repo-findings.md delete mode 100644 docs/architecture/adr/0001-modular-monolith-versus-microservices.md delete mode 100644 docs/architecture/adr/0002-primary-relational-database.md delete mode 100644 docs/architecture/adr/0003-object-storage-strategy.md delete mode 100644 docs/architecture/adr/0004-background-job-queue.md delete mode 100644 docs/architecture/adr/0005-email-integration-method.md delete mode 100644 docs/architecture/adr/0006-candidate-search-strategy.md delete mode 100644 docs/architecture/adr/0007-job-and-scoring-versioning.md delete mode 100644 docs/architecture/adr/0008-candidate-duplicate-resolution-strategy.md delete mode 100644 docs/architecture/adr/0009-permission-enforcement-strategy.md delete mode 100644 docs/architecture/adr/0010-chatbot-controlled-query-architecture.md delete mode 100644 docs/architecture/adr/0011-ai-provider-abstraction-and-versioning.md delete mode 100644 docs/architecture/adr/0012-deployment-topology.md delete mode 100644 docs/architecture/adr/0013-frontend-strangler-migration.md delete mode 100644 docs/architecture/adr/0014-phase-0-xss-csp-hardening.md delete mode 100644 docs/architecture/adr/0015-module-boundary-enforcement.md delete mode 100644 docs/architecture/adr/0016-real-postgres-in-ci.md delete mode 100644 docs/architecture/adr/0017-plain-sql-migrations-as-schema-authority.md delete mode 100644 docs/architecture/adr/0018-backend-language-and-framework.md delete mode 100644 docs/architecture/diagrams/01-master-flow.svg delete mode 100644 docs/architecture/diagrams/02-intake-resolution.svg delete mode 100644 docs/architecture/diagrams/03-identity-and-duplicates.svg delete mode 100644 docs/architecture/diagrams/04-application-lifecycle.svg delete mode 100644 docs/architecture/diagrams/05-phase-cut.svg delete mode 100644 docs/aws-production-cost-estimate.md delete mode 100644 docs/integrations/buffer/Buffer-API.postman_collection.json delete mode 100644 docs/integrations/buffer/Buffer-API.postman_environment.json delete mode 100644 docs/integrations/buffer/README.md diff --git a/backend/global_cities.py b/backend/global_cities.py index 5922764..afa8da1 100644 --- a/backend/global_cities.py +++ b/backend/global_cities.py @@ -4,9 +4,7 @@ Pakistan is one country in this map, not a special case. The employment-agent prompt receives `countries_prompt_block()` so the model can map a messy locality to exactly one city name. `canonical_city` uses the same index. """ - from __future__ import annotations - import re Countries={ diff --git a/backend/org_settings/app.py b/backend/org_settings/app.py index 381bbdb..7965647 100644 --- a/backend/org_settings/app.py +++ b/backend/org_settings/app.py @@ -72,6 +72,14 @@ class ExcludeCompanyUpdate(BaseModel): linkedin_url: str | None = None +class ExcludeUniversityBatchCreate(BaseModel): + universities: list[ExcludeUniversityCreate] + + +class ExcludeCompanyBatchCreate(BaseModel): + companies: list[ExcludeCompanyCreate] + + @router.get("/org-settings/exclude-university/fetch") async def fetch_exclude_universities( current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_VIEW)), @@ -103,6 +111,39 @@ async def create_exclude_university( raise HTTPException(status_code=500, detail=str(e)) +@router.post("/org-settings/exclude-university/create-batch") +async def create_exclude_universities_batch( + payload: ExcludeUniversityBatchCreate, + current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_CONFIGURE)), + session: AsyncSession = Depends(get_session), +): + try: + if not payload.universities: + raise HTTPException(status_code=400, detail="universities is required") + service = Exclusion(session=session) + data = [] + failed = [] + for item in payload.universities: + try: + row = await service.create_university( + item.model_dump(exclude_unset=True), current_user + ) + data.append(row) + except HTTPException as e: + failed.append({ + "name": (item.name or "").strip(), + "detail": e.detail if isinstance(e.detail, str) else str(e.detail), + "status_code": e.status_code, + }) + return JSONResponse( + content={"data": data, "total": len(data), "failed": failed, "status_code": 200} + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.patch("/org-settings/exclude-university/update") async def update_exclude_university( payload: ExcludeUniversityUpdate, @@ -169,6 +210,39 @@ async def create_exclude_company( raise HTTPException(status_code=500, detail=str(e)) +@router.post("/org-settings/exclude-company/create-batch") +async def create_exclude_companies_batch( + payload: ExcludeCompanyBatchCreate, + current_user: dict = Depends(require_permission(PermissionTag.SETTINGS_CONFIGURE)), + session: AsyncSession = Depends(get_session), +): + try: + if not payload.companies: + raise HTTPException(status_code=400, detail="companies is required") + service = Exclusion(session=session) + data = [] + failed = [] + for item in payload.companies: + try: + row = await service.create_company( + item.model_dump(exclude_unset=True), current_user + ) + data.append(row) + except HTTPException as e: + failed.append({ + "name": (item.name or "").strip(), + "detail": e.detail if isinstance(e.detail, str) else str(e.detail), + "status_code": e.status_code, + }) + return JSONResponse( + content={"data": data, "total": len(data), "failed": failed, "status_code": 200} + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.patch("/org-settings/exclude-company/update") async def update_exclude_company( payload: ExcludeCompanyUpdate, diff --git a/docs/TalentFlow-ATS-Business-Requirements-v1.0.docx b/docs/TalentFlow-ATS-Business-Requirements-v1.0.docx deleted file mode 100644 index eaaaaa71c491ef2ef99f71ba68540d00c2744a5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26526 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 B{"Stated in assignment §3
or §5.1-§5.6?"} - B -- yes --> C["CONFIRMED - assign REQ- id"] - B -- no --> D{"Required to satisfy
a confirmed REQ?"} - D -- yes --> E["PROPOSED - assign PROP- id,
needs owner sign-off, not re-debate"] - D -- no --> F{"Wanted, but not needed
for the phase in hand?"} - F -- yes --> G["DEFERRED - assign DEF- id
with phase and entry condition"] - F -- no --> H{"Would it break a constraint
or add unearned cost?"} - H -- yes --> I["REJECTED - record reason
and the condition that reopens it"] - H -- no --> J["OPEN BUSINESS DECISION -
assign OBD- id and proceed
on the recommended assumption"] -``` - ---- - -## 2. Confirmed requirements - -Rules for this table. One line per requirement. `REQ-` ids are **stable and never reused** — -if a requirement is dropped, its id is retired, not recycled. `Source` cites the assignment -constraint or the BRD handle. `Module` is the owning module from `_decisions.md`. `Phase` -follows the `_decisions.md` phasing (0 = hardening/foundations, 1 = vertical slice, 2–4 as -listed there). A requirement being confirmed says nothing about *when* it lands — see the -Phase column, and §5 for what that means in practice. - -### 2.1 Raw intake and inbound channels - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-INT-01 | Every inbound submission is recorded as an immutable raw-intake row before any candidate or application can exist. | Constraint §5.1 | `intake` | 1 | -| REQ-INT-02 | All eleven inbound channels normalise into one candidate record structure. | BO-4, BRD §8.1 | `integrations_inbound` | 1 / 3 / 4 | -| REQ-INT-03 | Parsing is attempted for every inbound document regardless of channel. | BRD §6.3 | `document_parsing` | 1 | -| REQ-INT-04 | Parse failures are surfaced to the recruiter with the document retained, never silently dropped. | BRD §6.3, FR-7 | `intake` | 1 | -| REQ-INT-05 | Each document reports a per-document state of Parsed, Pending, Parsing or Failed. | FR-7, BRD §9.2 | `intake` | 1 | -| REQ-INT-06 | The inbox is a single triage queue with read/unread state and per-source attribution. | FR-2 | `intake` | 1 | -| REQ-INT-07 | Where the parser cannot determine a field with confidence, the field is left empty rather than guessed. | BRD §6.3 | `document_parsing` | 1 | -| REQ-INT-08 | Redelivery of the same channel message is idempotent and never produces a second intake row. | BRD §11 (cross-channel dedupe) | `intake` | 1 | -| REQ-INT-09 | A submission that can never become a candidate has a terminal representable state carrying no candidate. | Constraint §5.1; findings §F | `intake` | 1 | -| REQ-INT-10 | Manual recruiter CV upload is a channel through the same intake path, not an exception route. | FR-7 | `intake` | 1 | -| REQ-INT-11 | Attachments are held in object storage with checksum and virus-scan state, addressed from the intake row. | BRD §7.4 | `files` | 1 | -| REQ-INT-12 | A parse may be retried, and a parser-version change replayed, without mutating the arrival record. | Constraint §5.1 | `intake` | 1 | - -### 2.2 Candidate identity - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-CAN-01 | Candidate identity is a separate entity from job applications; one candidate may hold many applications over time. | Constraint §5.2; findings §F (`js/data.js:117-127`) | `candidate` | 1 | -| REQ-CAN-02 | The candidate master record supports filter, sort, rank, bulk actions and a full profile view. | FR-4 | `candidate` | 1 | -| REQ-CAN-03 | A candidate cannot be created without a prior raw-intake row, including for manual entry. | Constraint §5.1 | `candidate` | 1 | -| REQ-CAN-04 | A candidate must carry at least one usable contact channel; a malformed email cannot produce a candidate. | Constraint §5.1 | `candidate` | 1 | -| REQ-CAN-05 | One normalised email address resolves to at most one live candidate identity. | Constraint §5.2 | `candidate` | 1 | -| REQ-CAN-06 | The candidate record carries the BRD §9 field groups: identity, experience, education, skills and ownership. | BRD §9 | `candidate` | 1 | -| REQ-CAN-07 | CV revisions are retained as candidate documents, each traceable to the intake it arrived on. | BRD §6.3, §9 | `candidate` | 1 | -| REQ-CAN-08 | Skills resolve against a controlled vocabulary with aliases; unmapped parser labels remain storable and reviewable. | BRD §9.2 | `candidate`, `config` | 1 | -| REQ-CAN-09 | Candidate-facing identifiers are non-enumerable. | Constraint (data protection) | `candidate` | 1 | -| REQ-CAN-10 | Previously sourced candidates not hired for their original role can be retained and re-surfaced. | FR-5 | `talent_pool` | 3 | -| REQ-CAN-11 | Experience is stored at a precision that preserves ordering, not rounded to whole years. | findings §F (`js/data.js:121`) | `candidate` | 1 | - -### 2.3 Duplicate detection and merge - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-DUP-01 | Duplicate detection runs against the existing candidate base for every new candidate. | BRD §6.3 | `duplicate_review` | 1 | -| REQ-DUP-02 | Suspected duplicates are **marked**, never deleted. | BRD §6.3 | `duplicate_review` | 1 | -| REQ-DUP-03 | Duplicates arriving through different channels are detected and marked for recruiter review. | BRD §11 | `duplicate_review` | 1 | -| REQ-DUP-04 | Merge requires manual human review; nothing merges automatically, ever. | Constraint | `duplicate_review` | 2 | -| REQ-DUP-05 | Merge is reversible, with enough recorded detail that reversal is a mechanical replay. | Constraint | `duplicate_review` | 2 | -| REQ-DUP-06 | A "not a duplicate" decision persists and suppresses re-flagging of that pair. | Constraint (reviewability) | `duplicate_review` | 1 | -| REQ-DUP-07 | The reviewer sees per-signal evidence, not only a composite similarity score. | BRD §7.3 (explainability) | `duplicate_review` | 2 | -| REQ-DUP-08 | Merge never deletes rows and never double-counts history in reports. | Constraint §5.5 | `duplicate_review` | 2 | - -### 2.4 Requisitions, requirements and versioning - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-JOB-01 | Requisitions are managed across department, business unit, location, employment type, grade and status. | FR-3, BRD §9.1 | `requisition` | 1 | -| REQ-JOB-02 | Jobs are versioned; a published version is immutable. | Constraint §5.3; findings §F (`js/data.js:85-108`) | `requisition` | 1 | -| REQ-JOB-03 | Requirements are version-scoped and weighted; they can never be edited in place on the job. | Constraint §5.3 | `requisition` | 1 | -| REQ-JOB-04 | Scoring configuration is versioned, and its binding to a job is itself historical. | Constraint §5.3 | `scoring` | 1 | -| REQ-JOB-05 | Job statuses are Open, On Hold, Closed, Draft. | FR-3, BRD §9.2 | `requisition` | 1 | -| REQ-JOB-06 | A posting records the exact requisition version text an applicant read. | Constraint §5.3 (defensibility) | `requisition` | 1 | -| REQ-JOB-07 | Publishing a requisition version requires approval by a Hiring Manager or Department Head. | BRD §4 (Approve level); chain scope is OBD-12 | `requisition` | 1 | -| REQ-JOB-08 | Pipeline stages, statuses and vocabularies are editable reference data, not code constants. | BRD §9.2; findings §F | `config`, `pipeline` | 1 | - -### 2.5 Applications and pipeline - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-APP-01 | An application is the join between a candidate and a requisition version, and is the unit ATS scores attach to. | Constraint §5.2, §5.6 | `application` | 1 | -| REQ-APP-02 | The pipeline is a kanban board across the seven stages with drag-and-drop progression. | FR-6, BRD §9.2 | `pipeline` | 2 | -| REQ-APP-03 | Every stage and status change is recorded with actor, actor kind, reason and timestamp. | Constraint §5.5 | `application` | 1 | -| REQ-APP-04 | Each application retains source attribution back to the intake it originated from. | BO-4, FR-2 | `application` | 1 | -| REQ-APP-05 | At most one live application per candidate per job; reapplication is a distinct, ordered attempt. | Constraint §5.2 | `application` | 1 | -| REQ-APP-06 | A transition to a terminal-negative state requires a human actor. | BRD §7.1 | `application` | 1 | -| REQ-APP-07 | Time-in-stage is directly queryable for funnel and SLA reporting. | FR-9, FR-17 | `application`, `analytics` | 1 | - -### 2.6 Recruiter assignment - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-ASG-01 | Assignment is flexible: several people may be attached to one requisition or application in distinct roles. | Constraint §5.4; findings §F (`js/data.js:96,123`) | `assignment` | 1 | -| REQ-ASG-02 | Assignment is historical — "who owned this in March" is answerable from stored data. | Constraint §5.4 | `assignment` | 1 | -| REQ-ASG-03 | Exactly one current primary recruiter exists per requisition at any instant. | Constraint §5.4 | `assignment` | 1 | -| REQ-ASG-04 | Per-recruiter workload, efficiency, SLA state and hiring trend are reportable. | FR-9 | `assignment`, `analytics` | 2 | - -### 2.7 ATS scoring and explainability - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-SCR-01 | The ATS score is per application, never a property of the candidate. | Constraint §5.6; findings §F (`js/data.js:123`) | `scoring` | 1 | -| REQ-SCR-02 | The score is expressed on a 0–100 scale. | BRD §6.2 | `scoring` | 1 | -| REQ-SCR-03 | Matched skills and missing skills are exposed alongside the score. | BRD §6.2, §11 | `scoring` | 1 | -| REQ-SCR-04 | A recommendation band is derivable from the score and exposed with it. | BRD §6.2 | `scoring` | 1 | -| REQ-SCR-05 | The score is reproducible: the same candidate and role yield the same score absent a model or data change. | BRD §6.2 | `scoring` | 1 | -| REQ-SCR-06 | A score change caused by a model version change is traceable to that version. | BRD §6.2 | `scoring` | 1 | -| REQ-SCR-07 | Every score carries the factors that produced it, at a level a recruiter can restate to a hiring manager. | BRD §7.3 | `scoring` | 1 | -| REQ-SCR-08 | Historical scores are never mutated; a rescore appends a new result and supersedes the old one. | Constraint §5.6 | `scoring` | 1 | -| REQ-SCR-09 | Sorting the candidate list by AI Relevance returns a stable, reproducible order. | BRD §11 | `scoring` | 1 | -| REQ-SCR-10 | Protected characteristics — name, age, gender, nationality, photograph — are not ranking features. | BRD §7.2 | `scoring` | 1 | -| REQ-SCR-11 | Ranking across the full candidate base does not degrade the interface. | NFR-9 | `scoring` | 1 | -| REQ-SCR-12 | A score pins every input that could change it: requisition version, scoring config version, scorer code version, document and parse. | Constraint §5.3, §5.6; BRD §6.2 | `scoring` | 1 | - -### 2.8 AI capabilities - -All fifteen are confirmed requirements. Priority is the BRD's; Phase is `_decisions.md`. -Today all fifteen are interface preview only, with "Model endpoint · Not connected" -(BRD §6) — nothing in the repository performs inference (findings §B). - -| REQ | Capability | Priority | Module | Phase | -|---|---|---|---|---| -| REQ-AIC-01 | Resume Ranking — ordered shortlist per job by fit (AI-1). | P0 | `scoring` | 1 | -| REQ-AIC-02 | Candidate Matching — best-fit open roles across all Utopia brands (AI-2). | P0 | `scoring`, `talent_pool` | 1 | -| REQ-AIC-03 | Resume Summary — structured one-click summary for reviewer hand-off (AI-3). | P0 | `ai_orchestration` | 1 | -| REQ-AIC-04 | Skill Gap Analysis — skills present and missing, per candidate and per pipeline (AI-4). | P1 | `scoring` | 2 | -| REQ-AIC-05 | Natural Language Search over the candidate base (AI-5). | P1 | `assistant`, `candidate` | 2 | -| REQ-AIC-06 | JD Generator from a short requisition brief (AI-6). | P1 | `ai_orchestration`, `requisition` | 2 | -| REQ-AIC-07 | Email Generator — candidate correspondence in Utopia brand voice (AI-7). | P1 | `ai_orchestration`, `notifications` | 3 | -| REQ-AIC-08 | Interview Question Generator — role-specific question banks (AI-8). | P1 | `ai_orchestration`, `interview` | 3 | -| REQ-AIC-09 | Recruitment Analytics — natural-language questions across the funnel (AI-9). | P1 | `assistant`, `analytics` | 3 | -| REQ-AIC-10 | Recruiter Copilot — assistant in every workflow with current-screen context (AI-10). | P1 | `assistant` | 2 (read) / 4 (full) | -| REQ-AIC-11 | Candidate Comparison — side-by-side against role criteria (AI-11). | P2 | `scoring` | 3 | -| REQ-AIC-12 | Candidate Recommendation — proactive suggestions of who to contact (AI-12). | P2 | `talent_pool`, `worklist` | 4 | -| REQ-AIC-13 | Offer Letter Generator from offer parameters (AI-13). | P2 | `ai_orchestration`, `offer` | 3 | -| REQ-AIC-14 | Hiring Forecast — time-to-hire prediction and pipeline health flags (AI-14). | P2 | `analytics` | 4 | -| REQ-AIC-15 | Hiring Insights — weekly generated digest of notable movements (AI-15). | P2 | `analytics`, `notifications` | 4 | - -### 2.9 AI governance, fairness and audit - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-GOV-01 | No candidate may be rejected solely by an automated decision. | BRD §7.1; Constraint | `application` | 1 | -| REQ-GOV-02 | No offer is issued without explicit human confirmation. | BRD §7.1 | `offer` | 3 | -| REQ-GOV-03 | The interface identifies machine-generated content before a recruiter acts on it. | BRD §7.1 | frontend, `ai_orchestration` | 1 | -| REQ-GOV-04 | Disparate-impact evaluation is completed before production release and repeated on every model version change. | BRD §7.2 | `fairness_evaluation` | 3 | -| REQ-GOV-05 | Evaluation results are recorded and readable by the business, not held only in engineering. | BRD §7.2, §11 | `fairness_evaluation` | 3 | -| REQ-GOV-06 | Every AI-influenced decision is logged with model version, input reference, output and timestamp. | BRD §7.3 | `audit`, `ai_orchestration` | 1 | -| REQ-GOV-07 | Audit records are retained for the period required by the jurisdictions in which the requisition was posted. | BRD §7.3; jurisdictions per OBD-04 | `audit` | 1 | -| REQ-GOV-08 | Every AI invocation is individually addressable, versioned and reviewable after the fact. | Constraint | `ai_orchestration` | 1 | -| REQ-GOV-09 | The chatbot never bypasses access controls; every AI data access carries the asking human's identity. | Constraint | `assistant`, `identity` | 2 | -| REQ-GOV-10 | AI Studio reflects true per-capability availability; the "not connected" state clears only when a capability is live. | FR-19, BRD §11 | `ai_orchestration` | 1 | -| REQ-GOV-11 | TalentFlow remains fully operable with the AI service unavailable. | NFR-7, BRD §8.3, §11 | all | 1 | -| REQ-GOV-12 | AI output is a suggestion record that a human accepts; it is never a direct domain write. | Constraint | `ai_orchestration` | 1 | - -### 2.10 Interviews - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-ITV-01 | Seven interview types across three meeting modes, with four lifecycle states. | FR-12, BRD §9.2 | `interview` | 2 | -| REQ-ITV-02 | Structured scorecards; an interviewer sees and scores only their own interviews. | FR-12, BRD §4 | `interview` | 2 | -| REQ-ITV-03 | Interview instants are stored in UTC alongside the organiser's wall-clock intent and IANA zone. | Constraint (findings §F: no tz discipline, `js/data.js:237`) | `interview` | 2 | -| REQ-ITV-04 | A participant cannot be double-booked across overlapping scheduled interviews. | Constraint (correctness) | `interview` | 2 | -| REQ-ITV-05 | A month view of scheduled interviews and hiring events exists. | FR-16 | `interview` (view) | 2 | -| REQ-ITV-06 | A submitted scorecard locks. | FR-12, BRD §7.3 | `interview` | 2 | - -### 2.11 Assessments and offers - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-ASM-01 | Six assessment types can be assigned and scored. | FR-13, BRD §9.2 | `assessment` | 3 | -| REQ-ASM-02 | Assessments report Code Quality, Problem Solving and Time Management. | FR-13 | `assessment` | 3 | -| REQ-OFR-01 | Offers move through Draft, Sent, Negotiating, Accepted, Declined, Expired. | FR-14, BRD §9.2 | `offer` | 3 | -| REQ-OFR-02 | An offer revision is a new immutable offer version with its own approval, not a field edit. | Constraint §5.3, §5.5 | `offer` | 3 | -| REQ-OFR-03 | Issuing an offer requires an explicit human confirmation step that cannot be automated. | BRD §7.1 | `offer` | 3 | - -### 2.12 Search - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-SRC-01 | The candidate base is filterable, sortable and rankable on structured attributes. | FR-4 | `candidate` | 1 | -| REQ-SRC-02 | The candidate base is searchable in plain English. | AI-5 | `assistant`, `candidate` | 2 | -| REQ-SRC-03 | Name and employer search is fuzzy and typo-tolerant. | BRD §6.3 (dedupe signals reuse) | `candidate` | 1 | -| REQ-SRC-04 | Relevance blending is attributable to a versioned configuration, not hardcoded. | Constraint §5.3; findings §F (`js/candidates.js:18`) | `candidate` | 1 | - -### 2.13 Analytics, reporting and dashboard - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-ANL-01 | Eight headline KPIs: Open Jobs, Total Candidates, Interviews Today, Offers Accepted, Time to Hire, Time to Fill, Cost per Hire, Closed Jobs. | FR-1 | `analytics` | 2 | -| REQ-ANL-02 | Hiring-trend and pipeline visualisations on the dashboard. | FR-1 | `analytics` | 2 | -| REQ-ANL-03 | Hiring funnel, time-to-hire versus time-to-fill, department performance, and a saved report library. | FR-17 | `analytics` | 3 | -| REQ-ANL-04 | Analytics set: hiring trend, applications received, source breakdown, offer acceptance, pipeline distribution, applications by department, recruiter performance. | FR-18 | `analytics` | 2 | -| REQ-ANL-05 | Natural-language report and analytics generation. | AI-9, FR-17, FR-18 | `assistant`, `analytics` | 3 | -| REQ-ANL-06 | Leadership has a single view across all Utopia brands and departments. | BO-6 | `analytics` | 2 | -| REQ-ANL-07 | Analytics results are scoped to the viewer's role — a recruiter sees own, leadership sees aggregate. | BRD §4, FR-21 | `analytics`, `identity` | 2 | -| REQ-ANL-08 | Time to hire is measurable against the current 27-day baseline. | BO-2 | `analytics` | 2 | -| REQ-ANL-09 | Source and channel performance is comparable across the eleven inbound channels. | FR-8, FR-18, BO-4 | `analytics` | 3 | - -### 2.14 Tasks, worklist and notifications - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-WRK-01 | Recruiter actions are assignable and trackable, with open-item counts surfaced in navigation. | FR-10 | `worklist` | 2 | -| REQ-WRK-02 | AI next-best-action suggestions appear as tasks, visibly marked as AI-originated. | FR-10, AI-12, BRD §7.1 | `worklist` | 2 | -| REQ-WRK-03 | System and hiring-event notifications exist with unread counts. | FR-20 | `notifications` | 2 | - -### 2.15 Identity, access control and application security - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-SEC-01 | Every request is attributable to an authenticated user; there is no anonymous internal access. | findings §D (no auth of any kind) | `identity` | 0 | -| REQ-SEC-02 | RBAC is enforced server-side over a seeded catalogue of roles, permission-controlled modules and permission verbs. The catalogue is **configuration, not structure** — Phase 1 seeds 7 roles × 25 modules × 10 verbs (`05` §2.1/§2.2/§2.9); adding a role or module is an INSERT plus grants. The prototype's 8 roles / 13 modules / 8 permission types (`js/data.js:425-446`) are demo data derived from a `level` cutoff index (findings §D) and are **not** the requirement; BRD §4 is a 66-seat allocation, not a permission catalogue. | FR-21, BRD §3, §4 | `identity` | 1 | -| REQ-SEC-03 | There is exactly one authorization decision point, used by the UI, the API and the assistant alike. | Constraint (chatbot access control) | `identity` | 1 | -| REQ-SEC-04 | Interviewers can see only the candidates attached to their assigned interviews. | BRD §4 | `identity`, `interview` | 2 | -| REQ-SEC-05 | Match-score visibility per role is a configuration setting, not a hardcoded rule. | OQ-5 → OBD-05 | `identity`, `scoring` | 1 | -| REQ-SEC-06 | No data-derived value is rendered without output escaping, and a CSP without `unsafe-inline` for scripts is in force. | findings §E (34 unescaped `innerHTML` sites, `js/candidates.js:68,121`) | frontend | 0 | -| REQ-SEC-07 | Candidate-facing links are scoped, expiring, revocable tokens — never a bare entity identifier. | Constraint (data protection) | `identity` | 3 | -| REQ-SEC-08 | Security settings — SSO, 2FA, session timeout, password policy — are enforced, not display chrome. | findings §D (`js/settings.js:148-154`), FR-22 | `identity` | 1 | -| REQ-SEC-09 | The permission matrix governs behaviour; it is not a display widget. | findings §D (`js/rbac.js:78,111-112` — no `can()` exists) | `identity` | 1 | - -### 2.16 Data protection and retention - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-DAT-01 | Candidate data is processed only within Utopia-controlled infrastructure or by a processor under a data-processing agreement. | BRD §7.4, §3.3 | platform | 1 | -| REQ-DAT-02 | Candidate data must not be used to train third-party foundation models. | BRD §7.4 | `ai_orchestration` | 1 | -| REQ-DAT-03 | Retention and deletion honour candidate rights requests, including within derived embeddings and indexes. | BRD §7.4 | `files`, `candidate` | 2 | -| REQ-DAT-04 | Every column on a candidate-touching table carries a machine-readable PII classification. | BRD §7.4 (enforceability) | platform | 1 | -| REQ-DAT-05 | No special-category data (diversity, health, accommodation) is stored in Phase 1. | BRD §7.2 scope decision | all | 1 | -| REQ-DAT-06 | Erasure and permanent history coexist: identifying data is removable without tearing holes in history or reporting. | Constraint §5.5 + BRD §7.4 | platform | 2 | - -### 2.17 Current state, history and audit - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-HIS-01 | Current state and full history both exist, as separate stored things. | Constraint §5.5; findings §F (no history tables) | all domain | 1 | -| REQ-HIS-02 | Every history row carries actor, actor kind and reason. | Constraint §5.5 | all domain | 1 | -| REQ-HIS-03 | A single append-only audit log spans all modules and is queryable by actor and by candidate. | BRD §7.3, §11 | `audit` | 1 | -| REQ-HIS-04 | The application has no update or delete path into the audit log. | BRD §7.3 | `audit` | 1 | -| REQ-HIS-05 | Access events — profile viewed, export run, chatbot answer returned — are audited, not only data changes. | BRD §7.3, §7.4 | `audit` | 1 | -| REQ-HIS-06 | Requisition, requirement and scoring-config versions are immutable once published. | Constraint §5.3 | `requisition`, `scoring` | 1 | - -### 2.18 Outbound publishing - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-PUB-01 | Requisition versions publish to eight external platforms with per-platform connection state and cost banding. | FR-8, BRD §8.2 | `integrations_outbound` | 4 | -| REQ-PUB-02 | Publishing is an authorised action with a cost implication, restricted above recruiter level. | BRD §8.2 (cost bands), §4 | `integrations_outbound` | 4 | - -### 2.19 API contract - -| REQ | Requirement | Source | Module | Phase | -|---|---|---|---|---| -| REQ-API-01 | The service is exposed over a documented, versioned HTTP JSON API. | BRD §8.3 | API layer | 1 | -| REQ-API-02 | Adopting a new model version requires no interface change. | BRD §8.3 | API layer | 1 | -| REQ-API-03 | The API degrades gracefully — AI absent rather than request failed. | BRD §8.3, NFR-7 | API layer | 1 | -| REQ-API-04 | Long-running operations (bulk ranking, batch parsing) are asynchronous with retrievable job status. | BRD §8.3 | API layer | 1 | -| REQ-API-05 | Rate limits and expected latency are published so the interface can set user expectations. | BRD §8.3 | API layer | 1 | - -### 2.20 Non-functional and experience requirements (must not regress) - -These are already met by the prototype and verified (findings §G). They are confirmed -requirements precisely because the migration must not lose them. - -| REQ | Requirement | Source | Phase | -|---|---|---|---| -| REQ-NFR-01 | WCAG 2.1 AA contrast across all views in both themes. | NFR-1; verified 23 routes × 2 themes (findings §G) | 0 onward | -| REQ-NFR-02 | Usable from 320px to ultrawide without horizontal overflow. | NFR-2 | 0 onward | -| REQ-NFR-03 | Minimum 44×44px touch targets. | NFR-3 | 0 onward | -| REQ-NFR-04 | Light and dark themes following OS preference until the user chooses. | NFR-4 (`js/app.js:64,193,198`) | 0 onward | -| REQ-NFR-05 | Utopia Brands palette and typeface hierarchy per the brand guideline. | NFR-5 | 0 onward | -| REQ-NFR-06 | Works on iOS, Android, macOS and Windows browsers, with safe-area handling. | NFR-6 | 0 onward | -| REQ-NFR-07 | Interactive AI responses land within a latency threshold that does not interrupt recruiter flow. | NFR-8; ceiling is OBD-03 | 1 | -| REQ-NFR-08 | Recruiters act on AI output from the screens where they already work. | BRD §2.3 | 1 | -| REQ-NFR-09 | The existing 23-route information architecture is preserved as the screen backlog. | findings §G (`js/app.js:7-16`) | 0 onward | -| REQ-NFR-10 | The design system in `css/styles.css` is retained as the design contract, not rebuilt. | findings §G; `_decisions.md` | 0 onward | -| REQ-NFR-11 | The dependency-free canvas chart engine `js/charts.js` is retained rather than replaced by a charting dependency. | findings §G (`js/charts.js:339`) | 1 onward | - -### 2.21 Money and time - -| REQ | Requirement | Source | Phase | -|---|---|---|---| -| REQ-MON-01 | Every monetary value carries an explicit ISO-4217 currency; an amount without a currency is not storable. | findings §F (`js/data.js:126`, `js/offers.js:129` — bare integers, no currency) | 1 | -| REQ-MON-02 | The original amount and currency are immutable; any conversion is stored alongside the rate it used. | Constraint §5.3 (pinning) | 3 | -| REQ-MON-03 | All instants are stored in UTC; calendar-only values are stored as dates. | findings §F (hardcoded "today", `js/data.js:237`) | 1 | - ---- - -## 3. Proposed requirements - -Not stated in assignment §3, but **required** to satisfy something that is. Each needs an -owner's acknowledgement, not a re-debate — if a `PROP-` item is rejected, the confirmed -requirement it supports becomes unsatisfiable and that must be stated explicitly. - -| PROP | Proposed requirement | Supports | Why it is proposed rather than confirmed | Owner | Phase | -|---|---|---|---|---|---| -| PROP-01 | Phase 0 escaping + CSP patch of the existing prototype, with a CI gate against new unescaped interpolation. | REQ-SEC-06 | §3 does not mention the prototype's XSS exposure; findings §E makes it P0 the moment real CV or mail data lands. 2–3 developer-days. | Talha Ahmed | 0 | -| PROP-02 | A recruiter-facing intake triage queue with explicit `needs_review`, `rejected_unusable` and `quarantined` outcomes. | REQ-INT-04, REQ-INT-09 | §3 requires failures be visible; it does not specify the surface. This is where OBD-06 lands operationally. | Talent Ops | 1 | -| PROP-03 | Candidate-facing access tokens (hashed, scoped, expiring, revocable) for status pages, upload links and interview confirmations. | REQ-SEC-07, REQ-CAN-09 | Emailed links leak by forwarding and archives; an unguessable id can never expire or be revoked. | Talha Ahmed | 3 | -| PROP-04 | A fairness-evaluation **gate**: no scoring configuration version becomes active without a passing evaluation reference. | REQ-GOV-04, REQ-GOV-05 | §3 requires evaluation before release; a gate is what makes that enforceable rather than procedural. | Legal + Talent Lead | 3 | -| PROP-05 | A machine-readable PII classification registry with a CI completeness check. | REQ-DAT-03, REQ-DAT-04 | Three jobs must read the classification (purge, subject-access export, non-production anonymisation); prose cannot be read by a job. | Ahmed Mujtaba | 1 | -| PROP-06 | Controlled vocabularies and templates administered through a back-office admin rather than a bespoke Settings UI in Phase 1. | REQ-JOB-08 | Removes FR-22 from the Phase 1 critical path without blocking anyone. | Talent Ops | 1 | -| PROP-07 | A reapplication cooling-off period with an audited override. | REQ-APP-05 | §3 requires reapplication support but sets no interval. A hard block invites recruiters to create duplicate candidates to evade it. | Talent Lead (OBD-08) | 1 | -| PROP-08 | Audit hash chaining plus a daily export of closed audit partitions to write-once storage. | REQ-HIS-03, REQ-HIS-04 | Grants and triggers prevent application tampering; only an off-box immutable copy provides an independent check. Stated as tamper *evidence*, not prevention. | Talha Ahmed | 2 | -| PROP-09 | An FX rate table plus a per-row pinned reporting currency for cross-jurisdiction compensation reporting. | REQ-MON-02, REQ-ANL-06 | Six posting jurisdictions and a single leadership view require conversion; converting at read time makes reports drift. | Finance + Talent Lead | 3 | -| PROP-10 | A non-identifying address list (agency mailboxes, `info@`, shared family addresses) excluded from the one-email-one-identity rule. | REQ-CAN-05 | Real agency and referral submissions will otherwise fail resolution and silently back up the review queue. | Talent Ops | 1 | -| PROP-11 | An unmerge confirmation screen that lists exactly which rows will remain with the survivor before the recruiter confirms. | REQ-DUP-05 | Rows created after a merge have no defensible pre-merge owner; showing the list converts an invisible surprise into an informed decision. | Ahmed Mujtaba | 2 | -| PROP-12 | Database-level prevention of interviewer double-booking. | REQ-ITV-04 | Application-level checks race under concurrent scheduling. | Talha Ahmed | 2 | -| PROP-13 | A score-explanation panel plus AI-provenance badges on every AI-derived value in the UI. | REQ-SCR-07, REQ-GOV-03 | §3 requires explainability and content labelling; neither exists as a UI surface today. | Ahmed Mujtaba | 1 | -| PROP-14 | A dedicated recruiting mailbox plus an Entra ID app registration with admin-consented mail scopes. | REQ-INT-02 | Inbound channel #1 is Outlook (BRD §8.1) and this dependency sits with corporate IT, outside the team's control. Start in Phase 0. | Corporate IT | 0 | -| PROP-15 | Retention purge implemented as pseudonymisation with skeleton-row retention, not row deletion. | REQ-DAT-06 | The only mechanism that lets permanent history and erasure coexist. | Legal | 2 | -| PROP-16 | Five end-to-end smoke journeys in CI plus module-facade test coverage. | REQ-GOV-11, all | There are no tests, no test runner and no CI anywhere today (findings §B). | Ahmed Mujtaba | 0 | -| PROP-17 | An intake-to-candidate resolution record with its own actor, timestamp, reason and evidence. | REQ-INT-01, REQ-INT-09 | §3 requires raw intake before candidate creation; the *decision* to promote is the reviewable artefact and needs to be a stored thing. | Talha Ahmed | 1 | -| PROP-18 | A stated per-capability availability model behind AI Studio, driven by real capability status. | REQ-GOV-10 | All fifteen capabilities are already visible in the UI, which creates the expectation that they are nearly done. | Talha Ahmed | 1 | -| PROP-19 | An `actor_unknown` data-quality signal wherever a history row was written without an attributable actor. | REQ-HIS-02 | Background jobs, imports and ad-hoc SQL fixes will bypass actor propagation; a visible gap is far better than a wrong attribution. | Talha Ahmed | 1 | -| PROP-20 | A parse-confidence threshold below which a field is left empty and flagged for review, per field. | REQ-INT-07 | BRD §11 asks for population "without recruiter re-keying"; real mixed-quality PDFs will not reach that unqualified standard, so the review step must be designed in rather than discovered. | Talent Ops | 1 | - ---- - -## 4. Deferred requirements - -Confirmed or accepted in principle, deliberately **not** in the phase in hand. Each carries -the phase it is expected in and the condition that must hold before it starts. - -| DEF | Deferred item | Target phase | Entry condition | Consequence of deferring | -|---|---|---|---|---| -| DEF-01 | Per-requisition custom pipeline configuration (beyond the seven default stages). | 3 | A second job family demonstrably needs different stages. | Phase 1–2 uses one default pipeline; FR-6 still satisfied. | -| DEF-02 | Assessments module (FR-13, REQ-ASM-01/02). | 3 | Interviews and scorecards are live. | Assessment data stays outside the platform until Phase 3. | -| DEF-03 | Offers with approval chain (FR-14, REQ-OFR-*). | 3 | Requisition approval chain proven in Phase 1. | Offers continue outside the platform; a real gap for Talent Ops, stated. | -| DEF-04 | Outbound publishing to eight platforms (FR-8, REQ-PUB-*). | 4 | Job-board credentials and cost approval exist (OBD-13). | Requisitions are posted manually until then. | -| DEF-05 | Full tool-using assistant (AI-10, REQ-AIC-10 full). | 4 | RBAC enforced and audited; PROP-03 and REQ-GOV-09 verified. | Phase 2 assistant is read-only — see §6 row 15. | -| DEF-06 | Talent pool re-surfacing and rematch (FR-5, REQ-CAN-10). | 3 | Candidate/application split live and scoring stable. | Rejected candidates are not systematically re-surfaced until Phase 3. | -| DEF-07 | Outbound email delivery **pipeline** — templates UI, retry with backoff, bounce and complaint handling, digests, per-user preferences (FR-20 email, AI-7). | **2 (full). A minimal send slice is Phase 1, not deferred** — see the note below this table. | Notification templates approved; sender domain configured. | Phase 1 sends only the transactional replies the intake failure paths need; internal notifications are in-app rows until Phase 2. | -| DEF-08 | Saved report library (FR-17). | 3 | Dashboard KPIs stable and read models materialised. | Phase 2 ships fixed KPIs, not ad-hoc reports. | -| DEF-09 | Bespoke Settings UI (FR-22). | 4 | Admin back-office proves insufficient. | Covered in Phase 1 by PROP-06 and `identity`. | -| DEF-10 | Help / knowledge base (FR-23). | 4 | — | Lowest value; static docs suffice. | -| DEF-11 | Referral, agency, campus and walk-in intake forms (4 of 11 channels). | 4 | Form ownership and field sets agreed (OBD-11). | Those channels arrive via Outlook or manual upload in the meantime. | -| DEF-12 | Job-board inbound ingestion — LinkedIn, Indeed, Rozee, Mustakbil (4 of 11 channels). | 3 | Per-board API entitlement or a stable email format (OBD-13, and §6 row 2). | Board applications arrive through the Outlook channel. | -| DEF-13 | AI capabilities AI-4 through AI-15 (REQ-AIC-04..15). | 2–4 by BRD priority | AI-1/2/3 live and evaluated. | AI Studio must show honest availability (PROP-18) or the team is judged against the mockup. | -| DEF-14 | pgvector semantic retrieval and hybrid search. | 2 | Phase 1 FTS + trigram search measured and found insufficient for a named query class. | Phase 1 search is lexical and fuzzy, not semantic. | -| DEF-15 | Row-level security for the AI query path. | 2 | The application authorization layer is live and tested. | Phase 1 candidate PII protection rests entirely on the new application layer — a stated risk. | -| DEF-16 | A separately isolated untrusted-parsing worker queue. | 2 | — | Phase 1 mitigations (timeouts, memory caps, restricted OS user, no outbound network) are weaker than a sandbox. Stated risk. | -| DEF-17 | Candidate-facing self-service portal. | Out of scope this phase | Product decision (OQ-7 → OBD-07). | The RBAC role exists with zero provisioned seats (BRD §4). | -| DEF-18 | Special-category / diversity data capture. | Not planned | Separate lawful basis, aggregate-only reads and distinct access control agreed with Legal. | Fairness evaluation must run on proxies or externally supplied cohorts (OBD-02). | -| DEF-19 | PWA installability and offline shell. | 4 | A data-protection position on caching candidate PII on personal devices. | Responsive web already meets NFR-2/3/6. | -| DEF-20 | Read replica dedicated to search. | On trigger | p95 search latency exceeds 500 ms after index and query tuning. | Not expected at the assumed volume (ASM-03). | -| DEF-21 | Hiring-manager and calendar modules as standalone stores. | Never as modules | — | Managers are users with roles and assignments; calendar is a view over interviews. Deliberate consolidation, not an omission. | - -**DEF-07 — the outbound-mail split, ruled once.** Four documents previously phased outbound -email four different ways (this table at 2–3, OBD-21 at Phase 3, `03` §5 / §33 migration 018 and -`07` §5.1 at Phase 2, `04` §9.1 row 5 arguing for Phase 1). The binding split, recorded here and -mirrored in `03` §22 / §33, `04` §9.1 row 5, `07` §4.1 / §4.2 / §5.1 and `08` §2.14 / §7 finding 10: - -| Phase | What lands | Why it cannot sit on the other side of the line | -|---|---|---| -| **1 — minimal slice** (~4–6 dev-days, `07` T-16b) | One `app.outbound_message` row per send; `Mail.Send` through the **same** `MailProvider` port the inbound Graph adapter already uses; the send idempotency guard; NDR/bounce **classification** on the inbound side; one seeded transactional template ("send an unprotected copy of your CV"). Plus the in-app `app.notification` row `07` §4.2 already promised. | Phase 1 owns inbound email and CV parsing. Without a send path, **every parse failure is a dead end a recruiter resolves in Outlook by hand, outside the audit trail** — and an NDR arriving with no `outbound_message` row to attach to cannot be classified at all. This is a correctness hole in Phase 1, not a missing Phase 2 feature. | -| **2 — full pipeline** (`07` T-29, A-31, A-36) | Template authoring UI and versioning workflow, retry with backoff, complaint handling, digest sends, per-user `notification_preference`, the in-app notification centre. | None of it is needed to make a Phase 1 failure path actionable, and all of it needs sender-domain configuration and template approval, which are other people's work items. | - -The distinction that makes this coherent: Phase 1 sends **transactional candidate-facing replies -the intake pipeline itself generates**. Phase 1 does **not** email internal users — that is -OBD-21's subject and it stays in-app until Phase 2. - ---- - -## 5. One-month expectation — stated plainly - -One month with two developers delivers **Phase 0 plus the first end-to-end thread of -Phase 1**: one channel ingesting real mail, a CV parsed into a candidate, one application -created against a versioned requisition, one score with visible components, on one hardened -screen. It does not deliver a platform, and any plan that says otherwise is wrong. - -| Phase | Scope | Range | Confidence | -|---|---|---|---| -| 0 | Prototype XSS/CSP hardening; repo, CI, container, Postgres, migration skeleton; `identity` + `audit` + `config`; ADRs; frontend shell scaffold | 2–3 weeks | High | -| 1 | Vertical slice: intake → parsing → candidate → versioned requisition → application → assignment → scoring v1 with explanations; three inbound channels; enforced RBAC; five screens | 10–14 weeks | Medium | -| 2 | Pipeline board; interviews + scorecards; duplicate review and reversible merge UI; dashboard KPIs; notifications; worklist; read-only assistant | 7–10 weeks | Medium-low | -| 3 | Assessments; offers with approvals; report library; fairness-evaluation gate; talent pool; job-board inbound | 7–10 weeks | Low | -| 4 | Outbound publishing; full tool-using assistant; remaining P2 AI capabilities; Settings and Help | 5–8 weeks | Low | - -Confidence degrades after Phase 2 because Phase 3 depends on unresolved business decisions — -OBD-01 (model hosting), OBD-02 (historic outcome data) and OBD-04 (jurisdictions). - ---- - -## 6. Verdict on every unconfirmed idea (assignment §4) - -Verdicts use exactly one of: **Recommended now** / **Recommended later** / **Not -recommended** / **Dependent on existing stack** / **Dependent on business confirmation**. - -| # | Idea | Verdict | Reason | Condition that changes the verdict | -|---|---|---|---|---| -| 1 | Mandatory public screening form before CV submission | **Dependent on business confirmation** | Changes the intake mix and the candidate experience, and knock-out questions are a regulated screening decision in several of the six posting jurisdictions. The career portal already exists as channel #2 (BRD §8.1); making a structured form *mandatory* is a policy choice, not an engineering one. Recommended shape if approved: optional per requisition, structured answers stored on the application, never used as an automatic reject. | Talent Lead confirms it may be mandatory per requisition **and** Legal approves the question set as non-discriminatory. Then: Phase 2, small. | -| 2 | Direct LinkedIn API integration | **Dependent on business confirmation** | There is no general-purpose LinkedIn ATS ingestion API outside its Talent Solutions partner programme; access requires a commercial entitlement Utopia may or may not hold, and scraping is prohibited by LinkedIn's terms. This is a contract question before it is a build question. Interim path costs nothing: LinkedIn application notifications arrive as email and are ingested through the Outlook channel (REQ-INT-02). | Utopia produces a Recruiter System Connect / Talent Hub entitlement and API credentials. Then: DEF-12, Phase 3. | -| 3 | Automatic HRMS sync | **Dependent on existing stack** | HRIS is explicitly out of scope this phase (BRD §3.2) and no HRMS is named anywhere in the assignment or the repository. A bidirectional sync also creates a second source of truth for a person, which is the failure mode already rejected for the `managers` entity. | An HRMS is named, has a documented API, and Talent Ops confirms direction. Recommended shape: **one-way export of hired candidates only**, Phase 4. Bidirectional sync stays not recommended. | -| 4 | Native mobile app (iOS/Android) | **Not recommended** | 66 named seats, of which 24 are interviewers who consume only assigned-interview data (BRD §4). The responsive web app already meets 320px, 44px touch targets and safe-area handling, verified (NFR-2/3/6, findings §G). Two app stores, two release trains and a third codebase for two developers is indefensible. | Interviewer scorecard completion measurably fails on mobile web, **or** push notification delivery becomes a hard requirement that web push cannot meet. Even then, evaluate DEF-19 first. | -| 5 | Progressive Web App (installability, offline shell) | **Recommended later** | Cheap once the React shell exists — a manifest, an app-shell service worker and an install prompt. Not now for one substantive reason: caching candidate PII on personal devices needs a data-protection position under BRD §7.4, and there is no Phase 1 offline data story worth having. | Nothing blocks it technically. Phase 4 (DEF-19), after Legal accepts a cache policy that excludes candidate PII. | -| 6 | Vue / React / another frontend framework | **Recommended now** — React 18 + Vite + TypeScript, progressive migration | Decided in `_decisions.md`: the CSS is the valuable verified asset (1269 lines, 93 design tokens across 159 custom-property declarations and 470 `var(--…)` references (measured, `01` §12), dual themes, WCAG AA across 23 routes × 2 themes) and is preserved verbatim; the *rendering layer* is the liability — 34 unescaped `innerHTML` sites (findings §E), ~115 form-control sites across 15 files, 22 ordered `