HR-ATS-Portal/docs/architecture/08-requirements-traceabilit...

117 KiB
Raw Permalink Blame History

08 — Requirements Traceability

Status / Scope of this document

This is the closure check on the package. Documents 0007 and ADRs 00010012 each argued a slice of the design; this document asks one question of all of them together: is every confirmed requirement actually attached to a module, a table, an endpoint group, a phase and a piece of evidence that would demonstrate it — and is every designed thing attached to a requirement?

It contains no new design. Where a cell here disagrees with the document that owns the subject, the owning document wins and the disagreement is recorded in §5 as a gap rather than resolved silently. Finding real gaps is the point; a matrix with no gaps in a package this size would mean the matrix was not checked.

Two companion files this document depends on and does not duplicate. _open-items.md is the package's single arbitration register: every cross-document contradiction resolves there to a numbered ruling, a named non-engineering owner with a blocking gate, or a closure. _glossary.md is the published Part 1 ↔ Part 2 name mapping this matrix applies in §1.2. A gap in §5 below that is a contradiction between documents rather than a hole in coverage carries its register id, so there is exactly one place to look for the answer.

Sources. 00-scope-classification.md (the REQ- set — authoritative for ids, sources and phase), 01-repository-assessment.md, 02-system-architecture.md (modules), 03-database-design.md (tables), 04-integrations-and-processing.md, 05-security-rbac-ai-governance.md, 06-api-boundaries.md (API groups), 07-implementation-plan.md (acceptance criteria and demonstration milestones), adr/0001adr/0018 (indexed in 02 §13, which is authoritative for ADR numbers), and _repo-findings.md for repository facts. No meeting transcript exists (findings §B); the assignment prompt is the requirements source.


1. Conventions used in the matrix

1.1 Requirement ids

The REQ- ids are taken unchanged from 00-scope-classification.md §2. That document established them as stable and never reused, and this matrix adopts them verbatim so the two interlock — 158 confirmed requirements across 22 families. No id was renamed, merged or invented here.

1.2 Module names versus table names

00-scope-classification.md §10 row 5 recorded a naming split inside _decisions.md and resolved it: Part 2's table names are the database vocabulary, Part 1's module names are the code vocabulary. This matrix applies that resolution literally, which is why the Module column says requisition while the Database Entity column says app.job / app.job_version. They are the same aggregate under two vocabularies, not two things.

1.3 The five columns

Column Means Source of truth
Module The owning logical module from the 25-module catalogue. A second module means a genuine shared responsibility, not a hand-off. 02 §4
Database Entity The table(s) that carry the state the requirement is about. means the requirement is a behaviour, a policy or a rendering property with no state of its own; that is legitimate but it is also where verification gets hard, so every was checked against §5. 03
API Group One of the 25 groups in 06 §2, or one of the five groups in 06 §4 that assignment §26.7 omits (audit, async jobs, reference data, health, worklist). internal means no HTTP surface — the requirement is enforced below the API. 06 §2, §4
Phase First phase in which the requirement is satisfied, per 00 §2. A range means genuinely staged delivery. 00 §2
Acceptance Evidence The specific artefact that would demonstrate it. Citation form: AC0.5 = 07 §3.5 item 5; AC1.n = 07 §4.5 item n; AC2.n = 07 §5.5 item n; AC3 = 07 §6.4 acceptance prose; AC4 = 07 §7 acceptance prose. D0D4 are the demonstration milestones. Test layers are 07 §11.1§11.6. 07, 05

1.4 The traceability spine

Every row in §2 is an instance of one chain. When a row cannot complete the chain, it is a gap.

flowchart LR
  A["Assignment §3 / §5<br/>confirmed requirement"] --> B["REQ- id<br/>doc 00 §2"]
  B --> C["Module<br/>doc 02 §4"]
  C --> D["Table(s)<br/>doc 03"]
  D --> E["API group<br/>doc 06 §2"]
  E --> F["Phase<br/>doc 00 §2 / doc 07"]
  F --> G["Acceptance criterion<br/>doc 07 §3.5-§7"]
  G --> H{"Chain complete?"}
  H -- yes --> I["Traceable - 129 of 158"]
  H -- no --> J["GAP - §5, 29 of 158"]

2. The traceability matrix

2.1 Raw intake and inbound channels (12)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-INT-01 intake app.raw_intake (append-only); app.candidate.created_from_raw_intake_id NOT NULL, non-deferrable FK recruitment intake 1 AC1.3 — inserting a candidate with no raw_intake fails at the database; 06 §2.11 has no POST /candidates at all; D1
REQ-INT-02 integrations_inbound, intake app.intake_channel, ref.source_channel (11 seed rows), app.ingestion_run, app.ingestion_dead_letter recruitment intake 1 (3 channels) / 3 (4 boards) / 4 (4 forms) AC1.1 for the Outlook path; 04 §2.10 diagram; D1. Partial — GAP-08
REQ-INT-03 document_parsing app.intake_parse_attempt (append-only), app.raw_intake_attachment recruitment intake, documents 1 07 §11.4 malformed-CV pipeline tests; 04 §4.2 17-step pipeline; D1
REQ-INT-04 intake app.raw_intake.state, app.intake_parse_attempt, app.parse_issue recruitment inbox 1 AC1.2 — a document that cannot be parsed reaches a terminal state, is visible and retryable, nothing dropped; PROP-02; OBD-06
REQ-INT-05 intake app.intake_parse_attempt.status, app.raw_intake.state recruitment intake 1 07 §11.4; the parse-review panel in 07 §4.1 sub-slice 1A; D1
REQ-INT-06 intake app.raw_intake_read, app.raw_intake.source_channel_id recruitment inbox 1 06 §2.10 inbox endpoints incl. claim/release/snooze; D1
REQ-INT-07 document_parsing app.intake_parse_attempt.parsed per-field confidence; app.candidate_field_provenance recruitment intake 1 D1 — "one field deliberately left empty because confidence was low"; PROP-20; ASM-08
REQ-INT-08 intake app.raw_intake unique on (channel, external_message_id) and on (channel, payload_sha256) recruitment intake 1 AC1.1 — redelivery produces no second row; 04 §2.6
REQ-INT-09 intake app.raw_intake.state in (rejected_unusable, quarantined); app.intake_resolution recruitment inbox 1 AC1.2 — terminal state with no candidate and no job_application row
REQ-INT-10 intake, integrations_inbound app.intake_channel mandatory manual_ui row (migration 008) recruitment intake 1 03 §33 migration 008 seeds manual_ui; Playwright journey (manual upload); D1
REQ-INT-11 files app.stored_file (sha256, scan_status, retention_class), app.raw_intake_attachment documents 1 04 §6.5 malware gating; 05 §10 Phase 1 "file validation and malware gating; signed URLs"
REQ-INT-12 intake, document_parsing app.intake_parse_attempt (append-only, parser_name/parser_version) recruitment intake 1 AC1.2 retry path; 03 §18.1 — a replay appends, the arrival row is never mutated

2.2 Candidate identity (11)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-CAN-01 candidate app.candidate (no job_id, no stage, no aiScore) + app.job_application candidates, applications 1 AC1.4 — two applications from one person produce one candidate and two independent ats_result rows; D1
REQ-CAN-02 candidate app.candidate + 13 child tables, app.candidate_search_index candidates 1 06 §2.11 filter/sort/-relevance params; GET/POST /candidates/search. Partial — GAP-06 (no bulk-action endpoint)
REQ-CAN-03 candidate, intake app.candidate.created_from_raw_intake_id NOT NULL; app.intake_resolution recruitment intake 1 AC1.3; 06 §2.11 "POST /candidates Does not exist"; 03 §12.5 five invariant layers
REQ-CAN-04 candidate app.candidate_email, app.candidate_phone; deferrable contactability trigger candidates 1 AC1.3 — no email and no phone fails at COMMIT; a malformed address fails the CHECK
REQ-CAN-05 candidate app.candidate_email global partial unique index on normalised address candidates 1 06 §2.11 → 409 email_already_identifies_candidate; PROP-10 / OBD-09 non-identifying address list
REQ-CAN-06 candidate app.candidate, _email, _phone, _employment, _education, _skill, _link, _tag, _consent candidates 1 06 §2.11 response field list; D1 candidate profile screen
REQ-CAN-07 candidate, files app.candidate_documentapp.stored_file, with raw_intake_id provenance documents, candidates 1 03 §14.1; 06 §2.11 GET /candidates/{id}/documents; D1
REQ-CAN-08 candidate, config ref.skill, ref.skill_alias, app.candidate_skill (raw_label retained) candidates, reference data 1 06 §2.11 "unmapped parser output is stored and reviewable, never dropped". Partial — GAP-17
REQ-CAN-09 candidate, identity app.candidate.public_id (UUIDv7) + reference_code; app.candidate_access_token candidates 1 03 §3.2 "URL-guessability is not authorization"; 05 §2.11 permission tests
REQ-CAN-10 talent_pool app.talent_pool, app.talent_pool_member, app.candidate_job_match talent pool 3 AC3 / D3 scope; DEF-06 entry condition (candidate/application split live, scoring stable)
REQ-CAN-11 candidate app.candidate.total_experience_months candidates 1 06 §2.11 "months, not years — the prototype stores integer years (js/data.js:121)"

2.3 Duplicate detection and merge (8)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-DUP-01 duplicate_review app.duplicate_candidate_pair, app.matching_config_version duplicate review 1 07 §4.1 sub-slice 1C "duplicate detection with the six signals and the canonical-pair constraint"; ADR-0008
REQ-DUP-02 duplicate_review app.duplicate_candidate_pair.state in (open, confirmed_duplicate, confirmed_distinct, merged) duplicate review 1 AC2.1 — nothing is deleted; 03 §19.2 state CHECK plus ck_pair_reviewed; 03 §31.3 forbids soft delete on the merge tables
REQ-DUP-03 duplicate_review app.duplicate_candidate_pair.signals, app.candidate_link.url_normalised partial unique duplicate review 1 03 §19.2 six-signal table (incl. identical normalised LinkedIn URL); D2
REQ-DUP-04 duplicate_review app.candidate_merge (performed_by_user_id NOT NULL, reason NOT NULL) duplicate review 2 AC2.1; OBD-10 (HR Administrator and above); 00 §7 rejects automatic merge permanently
REQ-DUP-05 duplicate_review app.candidate_merge_operation (op_kind, table, previous_value, seq) duplicate review 2 AC2.2 — unmerge restores every re-parented row; AC2.3 — out-of-order unmerge refused, naming the blocking merge; D2 live in front of the audience
REQ-DUP-06 duplicate_review app.duplicate_candidate_pair.state = 'confirmed_distinct' on the canonical pair duplicate review 1 06 §2.15 POST /duplicate-pairs/{id}/confirm-distinct; 03 §19.2 canonical-pair constraint plus ix_pair_distinct as the detector's skip list; suppression asserted by the detection rescan test
REQ-DUP-07 duplicate_review app.duplicate_candidate_pair.signals (per-signal, not only composite) duplicate review 2 PROP-11 unmerge confirmation screen; 03 §19.2; D2
REQ-DUP-08 duplicate_review, analytics suppressed_by_merge_id on candidate_email/_phone/_link; app.v_candidate_live duplicate review, analytics 2 AC2.1 — "the funnel report count is unchanged (no double counting)"; loser public_id still resolves via 301

2.4 Requisitions, requirements and versioning (8)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-JOB-01 requisition app.job, app.job_version; ref.department, ref.business_unit, ref.location, ref.employment_type, ref.grade requisitions, jobs 1 06 §2.6/§2.7; D1 requisition screens
REQ-JOB-02 requisition app.job_versionINSERT+SELECT-only grants plus immutability trigger jobs 1 AC1.7 — UPDATE against job_version raises an exception, asserted by tests; AC1.6; ADR-0007
REQ-JOB-03 requisition app.job_requirement (version-scoped, weighted, deferrable weight-sum trigger) jobs 1 AC1.6 — editing a requirement mints a new job_version; AC1.7; D1
REQ-JOB-04 scoring, requisition app.scoring_config, app.scoring_config_version, app.job_scoring_assignment (the binding is itself historical) ATS scoring, jobs 1 03 §20.3 — config binding orthogonal to job versioning, so a weight tweak does not fabricate a fake requisition revision; AC1.7
REQ-JOB-05 requisition, config ref.lifecycle_status domain job; app.job_status_history jobs, reference data 1 03 §4.7 consolidation; 03 §9.5
REQ-JOB-06 requisition, integrations_outbound app.job_postingjob_version_id (03 migration 006a, Phase 1) job publications 1 (record) / 4 (external) 07 §4.1 sub-slice 1B includes job_posting; 03 §11 states the Phase 1 / Phase 4 split. GAP-25 CLOSED
REQ-JOB-07 requisition app.approval_request, app.approval_decision (Phase 1); app.approval_route, _route_step (Phase 2) requisitions 1 — single-approver, per 00 §2.4 GAP-01 CLOSED. Implementing tasks 07 T-17b (schema + publish_version() gate) and 07 A-16b (approve/reject action); acceptance criterion 07 §4.5 #13, asserted as a forbidden-write test in A-26; 03 §5 and migration 017a/017b split to match. OBD-12 still sets the multi-step chain deferred to Phase 2
REQ-JOB-08 config, pipeline ref.pipeline_stage, ref.lifecycle_status, ref.vocabulary_value, app.pipeline_config_stage reference data, pipeline 1 03 §4.7 enum-vs-lookup policy; 00 §7 rejects native Postgres enums; PROP-06 admin surface

2.5 Applications and pipeline (7)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-APP-01 application app.job_application (candidate_id, job_version_id, raw_intake_id NOT NULL); app.ats_result has no candidate_id applications 1 AC1.4; 05 §5.2 — "a score is reachable only through job_application, so scores are per-application by construction"
REQ-APP-02 pipeline, application ref.pipeline_stage, app.job_application.current_stage_id, app.job_application_stage_history pipeline, applications 2 AC2 board demo; 07 §4.2 defers the board deliberately — "a stage transition is demonstrable from a list plus a drawer"
REQ-APP-03 application app.job_application_stage_history, app.job_application_status_history (actor, actor_kind, reason, non-overlapping intervals) applications 1 AC1.11 — two stage moves show actor, reason and non-overlapping intervals; D1
REQ-APP-04 application, intake app.job_application.raw_intake_id NOT NULL, source_channel_id, job_posting_id applications 1 03 §15.2 source attribution; 03 §30.3 (multi-touch attribution deliberately not built)
REQ-APP-05 application app.job_application partial unique on live application + attempt_no applications 1 AC1.5 — a second live application is refused by the partial unique index; reapplication after terminal outcome plus cooling-off succeeds with attempt_no = 2; PROP-07 / OBD-08
REQ-APP-06 application app.job_application_status_history.actor_kind; transition() service guard applications 1 AC1.9 — no code path can reach a terminal-negative status without a human actor, asserted by tests; 05 §5.3 three independent layers
REQ-APP-07 application, analytics app.job_application_stage_history interval columns; analytics read views applications, analytics 1 (queryable) / 2 (reported) AC1.11 intervals; AC2.7 — dashboard KPIs match a hand-written SQL query over the same window

2.6 Recruiter assignment (4)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-ASG-01 assignment app.job_assignment, app.job_application_assignment; ref.assignment_role (6 roles) recruiter assignments 1 00 §10 row 2 — two concrete tables, not a polymorphic one (unenforceable FK); 06 §2.16
REQ-ASG-02 assignment Interval columns (valid_from, valid_to NULL) on app.job_assignment and app.job_application_assignment, with ck_assignment_interval and the GiST EXCLUDE on tstzrange(valid_from, valid_to) (03 §16) recruiter assignments 1 07 §4.1 sub-slice 1B "assignment interval tables with EXCLUDE constraints"; owners_at(subject, ts) answers "who owned this in March". Corrected — an earlier revision of this cell named from_ts / to_ts, which are not the column names 03 §16 uses
REQ-ASG-03 assignment Partial unique index on the current primary recruiter (ref.assignment_role.is_exclusive) recruiter assignments 1 07 §4.1 sub-slice 1B — the partial unique index on the current primary recruiter
REQ-ASG-04 assignment, analytics Assignment intervals + analytics read views analytics, recruiter assignments 2 AC2.7; 02 §4.4 module 24 recruiter_performance(); 06 §2.24

2.7 ATS scoring and explainability (12)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-SCR-01 scoring app.ats_result keyed on job_application_id, no candidate_id column ATS scoring 1 AC1.4 — two independent ats_result rows; 05 §5.2
REQ-SCR-02 scoring app.ats_result.overall_score numeric(6,3) CHECK BETWEEN 0 AND 100 ATS scoring 1 05 §5.2 explainability table row 1
REQ-SCR-03 scoring app.ats_result_skill; app.ats_result_criterion one row per job_requirement_id (03 §20.5) + app.ats_result_criterion.match_state text NOT NULL with ck_ats_criterion_match_state CHECK (match_state IN ('matched','partial','missing','not_assessed')), ck_ats_criterion_unassessed and ix_ats_criterion_match03 §20.5, migration 012. GAP-27 CLOSED ATS scoring 1 AC1.8 score panel; 05 §5.2 — missing requirements are a queryable fact, not an inference from a null, which now holds: the column exists, has no DEFAULT, and ck_ats_criterion_unassessed ties not_assessed to a null job_requirement_id
REQ-SCR-04 scoring, config app.ats_result.band text NOT NULL (03 §20.4); thresholds on app.scoring_config_version.band_thresholds jsonb (03 §20.1) ATS scoring 1 OBD-19 — four BRD bands as versioned reference data with configurable thresholds; band pinned per score row. Corrected — an earlier revision of this cell named app.ats_result.band_id, a column 03 does not have; the band is a config-driven text label, which is why ats_result_override.overridden_band carries no CHECK either (03 §20.7)
REQ-SCR-05 scoring app.ats_result.input_fingerprint ATS scoring 1 AC1.6 — the previously computed score is unchanged after a requirement edit; 07 §4.1 sub-slice 1C names input_fingerprint
REQ-SCR-06 scoring, ai_orchestration app.ats_result.ai_model_id, ai_model_version, algorithm_code_version; ai.ai_model_config_version ATS scoring 1 AC1.8 — the panel shows the pinned versions; ADR-0011
REQ-SCR-07 scoring app.ats_result_criterion with stored weight_applied and contribution, matched_evidence with character offsets ATS scoring 1 AC1.8; 05 §5.2 — "an explanation is never generated by a second model call"; PROP-13 panel; D1
REQ-SCR-08 scoring app.ats_result.is_current + superseded_by_id, append-only grants ATS scoring 1 AC1.7 — UPDATE against ats_result outside the granted column list raises; AC1.6
REQ-SCR-09 candidate, scoring app.candidate_search_index + the versioned relevance-weight config (03 §29.4) candidates 1 03 §29.4 deterministic formula, now backed by app.relevance_config_version's four numeric(6,4) weight columns. GAP-04 CLOSED
REQ-SCR-10 scoring, ai_orchestration scoring_config_version excluded-attribute list; no special_category columns exist ATS scoring 1 05 §5.4 four enforcement mechanisms — input redaction, output schema constraint, excluded-attribute list, proxy audit as a release gate
REQ-SCR-11 scoring, candidate app.candidate_search_index GIN/trigram indexes; app.ats_result is_current index candidates, ATS scoring 1 07 §11.6 puts performance checks in Phase 3 — GAP-09
REQ-SCR-12 scoring app.ats_result pins job_version_id, scoring_config_version_id, algorithm_code_version, candidate_document_id, parse_attempt_id ATS scoring 1 AC1.8 — all five pinned inputs shown; 05 §5.2 "five independent things can change the number on screen"

2.8 AI capabilities (15)

ai.ai_capability.availability is the single row per capability that makes honest per-capability status a query rather than a claim. All fifteen are already visible in the prototype's AI Studio (js/data.js:448-464), which is exactly why REQ-GOV-10 and PROP-18 exist.

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-AIC-01 scoring app.ats_result, _criterion, _skill ATS scoring 1 AC1.8; D1. Ranking as ranking is gated on the Phase 3 fairness pass (07 §4.2)
REQ-AIC-02 scoring, talent_pool app.candidate_job_match (Phase 1); app.talent_pool* (Phase 3) ATS scoring, talent pool 1 03 §20.6 — matching without an application. Pool half is Phase 3 — GAP-15
REQ-AIC-03 ai_orchestration ai.ai_model_invocation, ai.ai_suggestion chatbot, candidates 1 07 §4.1 sub-slice 1C ai_orchestration framework; 06 §2.25
REQ-AIC-04 scoring app.ats_result_criterion.match_state (03 §20.5, migration 012GAP-27 CLOSED), app.ats_result_skill ATS scoring 2 Same rows as REQ-SCR-03 aggregated per pipeline; AC2 dashboard parity. A skill-gap aggregate over missing / partial is not expressible without match_state, and ix_ats_criterion_match (match_state, job_requirement_id) is the index it reads
REQ-AIC-05 assistant, candidate ai.query_intent, ai.conversation_tool_invocation chatbot, candidates 2 AC2.6 — the assistant answers a recruiter and refuses an interviewer, both in the access audit log; ADR-0010
REQ-AIC-06 ai_orchestration, requisition ai.prompt_template_version, ai.ai_suggestion requisitions, chatbot 2 REQ-GOV-12 — output is a suggestion a human accepts, never a direct write
REQ-AIC-07 ai_orchestration, notifications app.message_template_version, app.outbound_message (immutable snapshot) communications 3 03 §30.1 — a template must change, a sent message must not. Phase 3 despite the send path existing from Phase 1 (DEF-07 minimal slice): AI drafting additionally requires 03 §22.1 ck_outbound_ai_human, so no AI-drafted message leaves without a named human sender or reviewer
REQ-AIC-08 ai_orchestration, interview app.scorecard_template_version, ai.ai_suggestion interviews, feedback 3 AC3 scope
REQ-AIC-09 assistant, analytics ai.query_intent (analytics intents), analytics read views analytics, chatbot 3 AC2.7 parity rule extended to NL answers; 05 §6.4 per-tool specification
REQ-AIC-10 assistant ai.conversation, ai.conversation_message, ai.conversation_tool_invocation chatbot 2 (read) / 4 (full) AC2.6 for read; AC4 — a write action requires confirmation and produces an audit event with actor_kind='ai_agent' and on_behalf_of_user_id set; DEF-05 entry conditions
REQ-AIC-11 scoring app.ats_result_criterion across applications on one job_version ATS scoring 3 Comparison reads the stored criterion rows; no new entity
REQ-AIC-12 talent_pool, worklist app.task.origin='ai_suggestion', app.candidate_job_match, ai.ai_suggestion talent pool, analytics 4 AC4; see GAP-14 — the column lands in Phase 2, the producer in Phase 4
REQ-AIC-13 ai_orchestration, offer app.offer_version, ai.ai_suggestion, app.stored_file (letter) offers 3 AC3 — "an offer cannot be issued by a background job"; D3 letter produced
REQ-AIC-14 analytics Analytics read views over job_application_stage_history analytics 4 AC4 scope. Depends on OBD-02 (historic outcome data) for any validated forecast
REQ-AIC-15 analytics, notifications app.notification, analytics read views analytics 4 AC4 scope; DEF-13

2.9 AI governance, fairness and audit (12)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-GOV-01 application, scoring app.job_application_status_history.actor_kind; app.ats_result CHECK (review_outcome IS NULL OR reviewed_by_user_id IS NOT NULL) applications 1 AC1.9; 05 §5.3 three independent layers — dependency graph, service guard, database CHECK; plus two absences (no bulk-reject endpoint; no rule may emit a terminal-negative transition)
REQ-GOV-02 offer app.offer_version, app.approval_decision; issue() requires human confirmation offers 3 AC3 — an offer cannot be issued by a background job; D3
REQ-GOV-03 frontend, ai_orchestration ai.ai_model_invocation id surfaced as provenance on every AI-derived value all AI-bearing groups 1 PROP-13 provenance badges (Ahmed's workstream, 05 §5.2 last row); 05 §10 Phase 1
REQ-GOV-04 fairness_evaluation app.evaluation_run, app.evaluation_metric, app.evaluation_dataset ATS scoring 3 AC3 — activating a scoring config without a passing evaluation is refused; PROP-04 gate; OBD-20 threshold
REQ-GOV-05 fairness_evaluation app.evaluation_run, app.evaluation_metric ATS scoring 3 D3 — a fairness evaluation run shown to a non-engineering audience with a pass/fail verdict
REQ-GOV-06 audit, ai_orchestration audit.audit_event (actor_kind, on_behalf_of_user_id, ai_run_id, source_service), ai.ai_model_invocation audit 1 AC0.4 — every write to a classified table produces an audit row with a correct actor; 05 §7.3 audited events
REQ-GOV-07 audit audit.audit_event partitions, audit.retention_policy audit 1 OBD-17 — 13 months hot, 7 years archived. Per-jurisdiction derivation absent — GAP-13
REQ-GOV-08 ai_orchestration ai.ai_model_invocation (written before its result is usable), ai.ai_review chatbot, audit 1 03 §27.2; 06 §4.1 GET /ai/runs; ADR-0011
REQ-GOV-09 assistant, identity ai.query_intent.required_permission_key NOT NULL, ai.conversation_tool_invocation.acting_user_id NOT NULL + authorization_passed chatbot 2 AC2.6 — the same question refused for an interviewer, both attempts in the access audit log; ADR-0010; 00 §7 rejects text-to-SQL and a chatbot service account
REQ-GOV-10 ai_orchestration ai.ai_capability.availability in (unavailable,internal_alpha,beta,general) chatbot 1 AC4 — AI Studio shows true per-capability availability rather than a coming-soon grid; PROP-18
REQ-GOV-11 all ai.ai_model_invocation.status; circuit-breaker state (no table) all 1 02 §9.2 degradation matrix; 04 §8.2 degradation ladder. No acceptance criterion in any phase — GAP-21
REQ-GOV-12 ai_orchestration, application ai.ai_suggestion + accept_suggestion(suggestion, actor) applications, chatbot 1 02 §4 diagram 2 — the only edge from intelligence into domain state is accept_suggestion with a human actor, enforced by import-linter contracts in CI

2.10 Interviews (6)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-ITV-01 interview, config app.interview, ref.vocabulary_value (7 types, 3 modes), ref.lifecycle_status domain interview (4 states), app.interview_status_history interviews 2 03 §23.1; AC2 demo
REQ-ITV-02 interview app.scorecard_template(+_version, +_criterion), app.scorecard, app.scorecard_criterion_score feedback 2 AC2 D2 — scorecard submitted and locked; 05 §2.9 interviewer row
REQ-ITV-03 interview app.interview.starts_at (UTC) + scheduling_timezone (IANA) + local_start_wall interviews 2 AC2.5 — a reschedule across a DST boundary preserves the organiser's wall-clock intent; 00 §7 rejects UTC-only and numeric offsets
REQ-ITV-04 interview app.interview_participant GiST EXCLUDE constraint interviews 2 AC2.4 — the database refuses it, not the UI; PROP-12
REQ-ITV-05 interview (view) No table — calendar deliberately not a module (02 §4.6) interviews 2 02 §4.6 rationale. No acceptance criterion — GAP-18
REQ-ITV-06 interview app.scorecard.submitted_at + locked, append-only after submit feedback 2 AC2 D2 — "scorecard submitted and locked"

2.11 Assessments and offers (5)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-ASM-01 assessment app.assessment_template(+_version), app.assessment_assignment, app.assessment_result assessments 3 AC3 scope; 03 §24
REQ-ASM-02 assessment app.assessment_result.breakdown jsonb, time_taken_minutes assessments 3 Three named dimensions are not queryable dimensions — GAP-07
REQ-OFR-01 offer, config app.offer, ref.lifecycle_status domain offer (6 states), app.offer_status_history offers 3 AC3; 03 §25
REQ-OFR-02 offer app.offer_version (immutable) + its own approval_request; app.offer_response offers 3 AC3 — mismatched-currency salary range refused; JPY with decimals refused by the rounding trigger; D3 two-step chain
REQ-OFR-03 offer app.offer_version.issued_by_user_id; issue() guard offers 3 AC3 — an offer cannot be issued by a background job; D3 "issued by a human with confirmation"

2.12 Search (4)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-SRC-01 candidate app.candidate + child tables; app.candidate_search_index candidates 1 06 §2.11 filter set; 03 §29.6 expected query patterns
REQ-SRC-02 assistant, candidate ai.query_intent (typed, parameterised); app.candidate_search_index chatbot, candidates 2 AC2.6; ADR-0006; DEF-14 keeps pgvector out of Phase 1 with a stated entry condition
REQ-SRC-03 candidate pg_trgm indexes on candidate.name_normalised, current_employer_name; ref.skill.name_normalised candidates 1 03 §29.3 exact DDL; the same signals feed duplicate detection — one mechanism, tuned once
REQ-SRC-04 candidate, config app.relevance_config, app.relevance_config_version (03 §29.4, migration 014) candidates 1 03 §29.4 formula plus the immutable versioned weights and the sum-to-1.0 trigger. GAP-04 CLOSED

2.13 Analytics, reporting and dashboard (9)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-ANL-01 analytics app.v_kpi_time_to_hire, app.v_kpi_time_to_fill (03 §9.6, migration 021), app.job.close_reason_id, app.job_vacancy, plus read-only views over job_application*, job*, offer* (7 KPIs, Phase 2); app.job_posting_metric.spend_amount (Cost per Hire, Phase 4) analytics 2 (7 KPIs) / 4 (Cost per Hire) AC2.7 covers the seven Phase 2 KPIs. Cost per Hire ruled out of Phase 2 and into Phase 4 — GAP-02, OPEN-02. Time to Fill had no fill instant, no close reason and no written formula — GAP-28, now closed in 03 §9.1/§9.6
REQ-ANL-02 analytics Analytics read views; rendered by the retained js/charts.js analytics 2 AC2.7; REQ-NFR-11 (no charting dependency added)
REQ-ANL-03 analytics app.saved_report, app.report_run analytics 3 AC3 scope; DEF-08 entry condition (KPIs stable, read models materialised)
REQ-ANL-04 analytics Analytics read views over raw_intake, job_application*, offer*, job_assignment analytics 2 AC2.7 — each series matches a hand-written SQL query over the same window
REQ-ANL-05 assistant, analytics ai.query_intent analytics intents analytics, chatbot 3 05 §6.4; AC2.7 parity rule applies to NL answers
REQ-ANL-06 analytics ref.business_unit as a column, not a database boundary analytics 2 OBD-23 — one master data model with brand as an attribute; 00 §6 row 17 rejects per-region databases
REQ-ANL-07 analytics, identity app.access_scope, app.role_assignment, app.v_user_effective_scope analytics 2 05 §2.3 access-scope model; 05 §2.11 permission tests; AC1.10 extended to analytics endpoints
REQ-ANL-08 analytics No entity holds the baseline analytics 2 ASM-15 flags the 27-day figure as possibly a demo value — it is the scalar constant timeToHire: 27 at js/data.js:245 (the arrays at js/data.js:232-233 are the trend series, not the baseline). GAP-03
REQ-ANL-09 analytics raw_intake.source_channel_id (volume); app.job_posting_metric (spend, Phase 4) analytics 3 Volume side testable at Phase 3. Cost side unavailable until Phase 4 — GAP-24

2.14 Tasks, worklist and notifications (3)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-WRK-01 worklist app.task — typed nullable subject columns (candidate_id, job_application_id, job_id, interview_id, offer_id, raw_intake_id) under ck num_nonnulls(subjects) <= 1, plus assignee_user_id, due_at, status (03 §28.4) worklist (06 §4.5) 2 02 §4.4 module 25; GET /worklist/tasks/counts is the navigation open-item count. Corrected — an earlier revision of this cell named a subject_ref column and a bare assignee. Neither exists, and subject_ref would have described the (subject_type, subject_id) shape 03 §5 P7 forbids
REQ-WRK-02 worklist, ai_orchestration app.task.origin in (manual,rule,ai_suggestion) worklist (06 §4.5, ?origin= filter) 2 (column) / 4 (producer) REQ-GOV-03 badge rule. Producer lands Phase 4 — GAP-14
REQ-WRK-03 notifications app.notification, app.notification_preference communications 2 Phase cell stays 2 under T-ASM-03: "with unread counts" is first satisfied when the count surface exists (07 A-36), not when Phase 1 starts writing rows. What was wrong here was the phase rationale, now corrected: 07 §4.2 "Phase 1 writes an in-app notification row only"; internal notification email is Phase 2, not Phase 3 (OBD-21 revised — §7 finding 10); notification_preference is Phase 2 with it. The minimal Phase 1 candidate-facing send slice (app.outbound_message, 07 T-16b) satisfies no part of REQ-WRK-03 and is traced under §2.1 intake failure handling

2.15 Identity, access control and application security (9)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-SEC-01 identity app.app_user, app.user_session authentication 0 AC0.3 — a deny-by-default endpoint with no permission declared returns 403, not 200; findings §D is the baseline this replaces
REQ-SEC-02 identity app.role (7), app.permission (250 = 25 × 10), app.role_permission (~265 grants); ref.app_module (25), ref.permission_action (10) roles and permissions 1 AC1.10 — the permission matrix test covers every endpoint × every role; 05 §2.9 matrix (7 × 25 × 10 = 1,750 triples walked per 05 §2.11); 05 §2.11 four test layers; 03 §7.2
REQ-SEC-03 identity app.access_scope (incl. region_id, 03 §7.3), app.access_grant (03 §7.7), app.v_user_effective_scope branches 14 (03 §7.5) internal (can()) 1 AC0.3 — iam.can() is the only authorization implementation; 05 §2.4/§2.8; ADR-0009. GAP-27 CLOSED — both scope sources ADR-0009 resolves against now have schema: app.access_grant in migration 011 (branch 4 of the view, with interview_id added by 019 and offer_id by 023) and ref.region + ref.location.region_id + access_scope.region_id in 002/011. The region branch is empty by construction until OPEN-05 admits 'region' to access_scope.scope_type's CHECK — one line — and 03 §7.3 states that explicitly rather than leaving the ADR pointing at nothing
REQ-SEC-04 identity, interview app.interview_participant → scope branch 3 of v_user_effective_scope (migration 019) candidates, interviews 2 D1 — "log in as an interviewer and show the same candidate is not visible"; 06 §2.11 interviewer field list
REQ-SEC-05 identity, scoring app.setting (03 §28.4, Phase 1 migration 012, seeded with the score-visibility keys) roles and permissions, ATS scoring 1 06 §1.15 scope-filtering rule; 06 §8 "configurable role setting with field omission"; A-22 asserts a flip takes effect on the next request with no deploy. GAP-05 CLOSED
REQ-SEC-06 frontend — (rendering-layer property) health (CSP headers) 0 AC0.2 — CI is red for a new unescaped ${ in a template literal or a dangerouslySetInnerHTML; AC0.5 — all 34 sites escaped, zero inline handlers, CSP without unsafe-inline, a stored-XSS payload renders as literal text in both themes; D0
REQ-SEC-07 identity app.candidate_access_token (token_hash bytea, never the token) authentication 3 PROP-03; OBD-07 (token-gated status page only, no portal); 03 §7.6
REQ-SEC-08 identity app.app_user, app.user_session, app.setting authentication, users 1 05 §10 Phase 1. Password policy has nothing to enforce under OBD-15 — GAP-16
REQ-SEC-09 identity app.role_permission read by can() roles and permissions 1 AC1.10; findings §D (js/rbac.js:78,111-112 — no can() exists today) is the explicit baseline

2.16 Data protection and retention (6)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-DAT-01 platform 1 No engineering artefact — contractual. GAP-11. ADR-0012 topology; OBD-01 / OBD-14; ASM-06
REQ-DAT-02 ai_orchestration ai.ai_model_config_version.retains_data, data_processing_agreement_ref chatbot 1 03 §27.1 — "columns, not documentation", making "which candidate data went to a retaining provider" a query. Recorded, not enforced — GAP-12
REQ-DAT-03 files, candidate audit.retention_policy, audit.retention_hold, audit.retention_action, app.candidate_embedding; app.candidate_erasure_request (03 §28.5, migration 025a — the request itself as a record, with FKs to retention_action and retention_hold that make execution checkable). GAP-27 CLOSED candidates, documents 2 AC3 — a retention purge pseudonymises a candidate while leaving ats_result and stage history intact; a subject-access export returns everything pii_classification says it should
REQ-DAT-04 platform audit.pii_classification (~900 rows) — (CI gate) 1 PROP-05 CI completeness check; 05 §10 Phase 1 "pii_classification registry + CI completeness check"
REQ-DAT-05 all Absence of any special_category column, backed by audit.pii_classification.ck_no_special_category CHECK (class <> 'special_category') — (CI gate) 1 05 §5.4 — "the strongest control is that most of these attributes have no column to be written to"; 03 §31.1 makes it a constraint rather than a promise. Nothing flags a migration that DROPS the CHECK — GAP-23 (Low)
REQ-DAT-06 platform audit.audit_event_redaction (two-person rule); app.candidate_erasure_request (03 §28.5, migration 025a) as the workflow record — GAP-27 CLOSED; pseudonymisation, not deletion candidates, audit 2 PROP-15; AC3 purge test; 00 §7 rejects hard delete on retention expiry

2.17 Current state, history and audit (6)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-HIS-01 all domain Six typed history tables: job_status_history, job_application_stage_history, job_application_status_history, candidate_status_history, interview_status_history, offer_status_history per owning group 1 AC1.11; 00 §7 rejects one generic history table and rejects event sourcing, with reasons
REQ-HIS-02 all domain actor_user_id, actor_kind, reason_id on every history row per owning group 1 AC1.11 — actor and reason shown; PROP-19 actor_unknown data-quality signal for rows written without an attributable actor
REQ-HIS-03 audit audit.audit_event (RANGE-partitioned, hash-chained, incl. source_service with ck_audit_source03 §28.1, migration 004). GAP-27 CLOSED audit 1 AC0.4; 05 §7.1; 06 §4.1 GET /api/v1/audit queryable by actor and by candidate
REQ-HIS-04 audit audit.audit_event — grants revoked, trigger raises, hash chain verifies audit 1 AC0.4 nightly hash-chain verifier passes; PROP-08 off-box immutable partition export as the independent check; 00 §7 rejects a separate audit database / SIEM / blockchain
REQ-HIS-05 audit audit.audit_event with read actions, attributed to an originating service by audit.audit_event.source_service (03 §28.1, migration 004GAP-27 CLOSED); ai.conversation_tool_invocation audit 1 05 §7.3; 06 §1.13 the audit rule (profile detail view, timeline, notes, export, erasure are audited reads; list/search are not)
REQ-HIS-06 requisition, scoring app.job_version, app.job_requirement, app.scoring_config_version, app.pipeline_config_version, app.matching_config_version, app.offer_version jobs, ATS scoring 1 AC1.7 — UPDATE raises on all four Phase 1 version tables, asserted by tests

2.18 Outbound publishing (2)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-PUB-01 integrations_outbound app.job_posting, app.job_posting_metric; ref.publish_platform (8 rows, cost_band_id) job publications 4 AC4 — publishing and unpublishing reconciles state; D4 — applications arrive back through raw_intake
REQ-PUB-02 integrations_outbound, identity app.role_permission (publication.publish restricted) job publications 4 OBD-13 — central Talent Ops accounts, publish restricted to HR Administrator; DEF-04 entry condition

2.19 API contract (5)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-API-01 API layer all 1 06 §1.1 versioning; 07 §4.1 sub-slice 1C — OpenAPI schema by drf-spectacular plus a generated TypeScript client, so the contract is written once
REQ-API-02 API layer, ai_orchestration ai.ai_model_config_version (version is data, not a route) chatbot 1 ADR-0011; 06 §8 — if legal requires self-hosting the /ai/* contracts survive
REQ-API-03 API layer — (circuit breaker) all 1 06 §1.7 error envelope; 02 §9.2 degradation matrix. Shares GAP-21's missing test
REQ-API-04 API layer Queue tables owned by procrastinate in schema queue async jobs 1 06 §1.14 job-handle convention; 06 §5 complete async register; ADR-0004
REQ-API-05 API layer all 1 06 §1.11 covers rate limits. Expected latency is not published anywhere in the contract — GAP-19

2.20 Non-functional and experience requirements (11)

These are met by the prototype today and verified (findings §G). They are in the matrix because the migration must not lose them — every one is a regression target, not a build target.

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-NFR-01 frontend 0 onward AC4 — the accessibility sweep passes WCAG 2.1 AA across every migrated route in both themes; baseline is 23 routes × 2 themes, 8,459 text nodes, 0 failures (findings §G)
REQ-NFR-02 frontend 0 onward AC4 sweep; css/styles.css responsive 320px→ultrawide retained verbatim
REQ-NFR-03 frontend 0 onward AC4 sweep; 44px touch targets in the retained design system
REQ-NFR-04 frontend 0 onward D0 — the React shell renders the same design tokens; js/app.js:64,193,198 theme behaviour preserved
REQ-NFR-05 frontend 0 onward AC0.2 — CI is red for a CSS rule using a raw colour instead of a var(--…) token
REQ-NFR-06 frontend 0 onward AC4 sweep; safe-area and dvh handling in the retained CSS
REQ-NFR-07 ai_orchestration ai.ai_model_invocation.latency_ms chatbot 1 Ceiling unresolved (OBD-03), no acceptance criterion — GAP-10
REQ-NFR-08 frontend, ai_orchestration all AI-bearing groups 1 PROP-13 in-screen panels and badges. Not measurably stated — GAP-22
REQ-NFR-09 frontend 0 onward 06 §7 maps all 23 prototype routes to API contract targets; AC4 — no prototype screen remains reachable at the end
REQ-NFR-10 frontend 0 onward AC0.2 token gate; 00 §7 rejects rebuilding the CSS with a stated reason
REQ-NFR-11 frontend analytics 1 onward D0 — "one chart from the retained charts.js"; 00 §6 row 6 cost statement

2.21 Money and time (3)

Requirement Module Database Entity API Group Phase Acceptance Evidence
REQ-MON-01 platform Paired *_amount numeric + *_currency_code columns with an FK to ref.currency, on job_version (salary_min_* / salary_max_*), offer_version (base_salary_*), candidate (desired_salary_*), ref.grade (band_min_* / band_max_*) and the ai.* cost columns jobs, offers, candidates 1 AC3 — an amount in JPY with decimal places is refused by the rounding trigger; a salary range with mismatched currencies is refused; 00 §7 rejects float, money and integer minor units with reasons
REQ-MON-02 platform app.fx_rate (Phase 3, migration 015); ref.currency (Phase 1, migration 002) analytics, offers 3 PROP-09; AC3. Phase mismatch resolved — the 03 §5 row is split per table (GAP-20 closed)
REQ-MON-03 platform timestamptz throughout; date for calendar-only values all 1 03 §4.3; 00 §7 rejects timestamp without time zone plus a UTC convention; AC2.5 DST test

3. Domain-modelling principles (assignment §5)

3.1 A numbering discrepancy, stated rather than papered over

00-scope-classification.md §1 cites the data-model principles as §5.1§5.6. 05-security-rbac-ai-governance.md §5 is headed "AI governance (§5.6, §5.7, §12)". So the package as written does not agree on how many principles §5 contains. Reading both documents together, the only consistent assignment is that §5.7 is the AI-governance principle (output explainable, versioned, reviewable; advisory only) and §5.6 is the scoring principle, which is why 05 cites the two together — an ATS score is both.

Separately, the principle "duplicate detection must include manual review and reversible merge" is cited throughout 00 §2.3 as a bare Constraint with no section number at all. It is fully traced below as PRN-DUP so that nothing is lost regardless of where it actually sits in §5. 00 §1 should be corrected to read §5.1§5.7. Recorded as GAP-26.

3.2 Principle traceability

Principle REQ ids that carry it Structural mechanism (not a policy) Primary entities Phase Acceptance Evidence
§5.1 Raw intake must exist before candidate creation REQ-INT-01, INT-09, INT-12, CAN-03, APP-04 candidate.created_from_raw_intake_id NOT NULL against a non-deferrable FK, plus the deliberate absence of POST /candidates app.raw_intake, app.intake_resolution, app.candidate 1 AC1.3 (insert fails at the database), AC1.2 (terminal state with no candidate), 03 §12.5 five layers, D1
§5.2 Candidate identity separate from applications REQ-CAN-01, CAN-05, APP-01, APP-05, SCR-01 Two tables with no denormalised job columns on candidate; ats_result has no candidate_id app.candidate, app.job_application, app.ats_result 1 AC1.4, AC1.5, 05 §5.2 closing note, D1
§5.3 Jobs, requirements and scoring configs versioned REQ-JOB-02, JOB-03, JOB-04, JOB-06, SCR-12, SRC-04, HIS-06, OFR-02, MON-02 INSERT+SELECT-only grants plus immutability triggers on six *_version tables; config binding is its own historical table app.job_version, app.job_requirement, app.scoring_config_version, app.job_scoring_assignment, app.pipeline_config_version, app.matching_config_version, app.offer_version 1 AC1.6, AC1.7 (UPDATE raises, asserted by tests), ADR-0007, D1
§5.4 Recruiter assignment flexible and historical REQ-ASG-01, ASG-02, ASG-03, ASG-04 Two concrete interval tables (never polymorphic — an unenforceable FK), GiST EXCLUDE for overlap, partial unique index for the single current primary app.job_assignment, app.job_application_assignment, ref.assignment_role 1 07 §4.1 sub-slice 1B; 00 §10 row 2 resolution; owners_at(subject, ts)
§5.5 Current state and history both exist REQ-APP-03, HIS-01, HIS-02, HIS-03, DUP-08, DAT-06, OFR-02 Six typed per-entity history tables with EXCLUDE-enforced non-overlapping intervals, plus one partitioned append-only audit log — deliberately not one generic event table six *_history tables + audit.audit_event 1 AC1.11, AC0.4, 00 §7 (generic history table and event sourcing both rejected with reasons)
§5.6 ATS score is per application, explainable and reproducible REQ-SCR-01 … SCR-12, AIC-01 ats_result reachable only through job_application; weight_applied and contribution stored not recomputed; five pinned inputs; is_current + superseded_by_id so a rescore inserts; a human override is a new append-only row, never a mutation app.ats_result, _criterion, _skill, app.ats_result_override, app.scoring_config_version 1 AC1.4, AC1.6, AC1.7, AC1.8, 05 §5.2 full record, D1. The two parts of the record that were [additive] and unbuilt are now built — _criterion.match_state (03 §20.5) and append-only app.ats_result_override with tg_ats_result_override_immutable, uq_ats_result_override_current and tg_ats_override_target_current (03 §20.7), both in migration 012: GAP-27 CLOSED
§5.7 AI output explainable, versioned, reviewable; advisory only REQ-GOV-01, GOV-03, GOV-06, GOV-08, GOV-11, GOV-12, SCR-06, SCR-07, SCR-10 Three independent layers: import-linter contracts (intelligence cannot import domain), the transition() service guard on actor_kind, and the ats_result CHECK. Plus two absences: no bulk-reject endpoint, no rule may emit a terminal-negative transition ai.ai_model_invocation, ai.ai_suggestion, ai.ai_review, ai.prompt_template_version, ai.ai_model_config_version 1 AC1.9, AC0.2 (boundary-violating import fails CI), 05 §5.3, ADR-0011, D0/D1
§5-DUP Duplicate detection with manual review and reversible merge REQ-DUP-01 … DUP-08 Marked never deleted; merge requires a human decider; reversal is a mechanical replay of candidate_merge_operation rows in reverse seq under stack discipline app.duplicate_candidate_pair, app.candidate_merge, app.candidate_merge_operation 1 detect / 2 merge AC2.1, AC2.2, AC2.3, D2 (merged then unmerged live), ADR-0008

All eight principles are traceable to an enforcing database or CI mechanism, not to a policy statement. That is the single most important line in this document, because every one of the eight was violated by the prototype (findings §F) and a policy-level fix would not have survived contact with two developers and a schedule.


4. Coverage summary

4.1 By phase

Phase Confirmed requirements first satisfied Share Cumulative
0 10 6% 10 (6%)
1 94 59% 104 (66%)
2 30 19% 134 (85%)
3 19 12% 153 (97%)
4 5 3% 158 (100%)

158 confirmed requirements. 104 (66%) land in Phase 01; 54 (34%) are deferred to Phase 2 or later. Phase 1 carrying 59% of the requirement count is not a claim that Phase 1 is 59% of the work — it reflects that intake, identity, versioning, scoring, history and authorization are requirement-dense and screen-sparse, which is exactly why 07 prices Phase 1 at 85119 dev-days against Phase 2's 6085 for less than a third of the requirement count.

4.2 By traceability quality

State Count Share
Fully traceable — module, entity, API group, phase and at least one named acceptance criterion 129 82%
Carries a gap (listed in §5) 29 18%

Twenty-nine requirements, twenty-eight gaps — the two are different counts and always will be. Three gaps span two requirements each (GAP-04 → REQ-SRC-04 + REQ-SCR-09; GAP-21 → REQ-GOV-11 + REQ-API-03; GAP-27 → REQ-SCR-03 + REQ-AIC-04) and one spans none (GAP-26 is a numbering defect in 00 §1, not a requirement gap). Of the remaining 24 single-requirement gaps, two land on the same requirement — GAP-02 and GAP-28 both gap REQ-ANL-01 — so they contribute 23 distinct requirements: 23 + 3 × 2 + 0 = 29. §4.3's Gapped column and this table count requirements; §5's heading counts gaps. Do not reconcile them by changing one to match the other. Corrected — this note read "twenty-seven gaps" and "23 single-requirement gaps" after GAP-28 was raised, which left the gap total one below §5's own row count and made the 23 look like a gap count rather than a requirement count.

How many are still open. Per the disposition rule in §5, the counts above are the measurement as raised and do not move on closure. Tracked separately: eight of the twenty-eight gaps are closed or ruled — GAP-01, GAP-04, GAP-05, GAP-20, GAP-25, GAP-27 and GAP-28 closed, GAP-02 ruled — leaving twenty live, with GAP-23 corrected and downgraded to Low rather than closed. Four of the twenty live gaps have non-engineering owners held in _open-items.md (GAP-03 → OPEN-12, GAP-10 → OPEN-09, GAP-13 → OPEN-10, GAP-17 → OPEN-11), and two of the eight closed ones carry a residual business question there (GAP-01 → OPEN-01, GAP-02 → OPEN-02).

4.3 By family

Family Count P0 P1 P2 P3 P4 Gapped
INT — raw intake 12 12 1
CAN — candidate identity 11 10 1 2
DUP — duplicates and merge 8 4 4 0
JOB — requisitions and versioning 8 8 2
APP — applications and pipeline 7 6 1 0
ASG — assignment 4 3 1 0
SCR — scoring and explainability 12 12 3
AIC — AI capabilities 15 3 4 5 3 2
GOV — AI governance and audit 12 8 1 3 2
ITV — interviews 6 6 1
ASM / OFR — assessments and offers 5 5 1
SRC — search 4 3 1 1
ANL — analytics 9 6 3 3
WRK — tasks and notifications 3 3 1
SEC — identity and security 9 2 5 1 1 2
DAT — data protection 6 4 2 3
HIS — history and audit 6 6 0
PUB — outbound publishing 2 2 0
API — API contract 5 5 2
NFR — non-functional 11 8 3 2
MON — money and time 3 2 1 1
Total 158 10 94 30 19 5 29

Two arithmetic corrections are folded into the Gapped column, stated rather than absorbed silently. API is 2, not 1 — REQ-API-05 carries GAP-19 and REQ-API-03 carries GAP-21, and only the first was counted. SCR is 3 and AIC is 2 — REQ-SCR-03 and REQ-AIC-04 both trace to ats_result_criterion.match_state, which GAP-27 showed did not exist in 03 when this audit ran. The total therefore moves 25 → 29: two newly gapped requirements plus the one miscount, and the previous total was itself one below its own column sum. The match_state column now exists (03 §20.5, migration 012) and GAP-27 is closed, but the Gapped column stays at 3 / 2 and the total at 29 — this column is the as-raised measurement, per §4.2 and the §5 disposition rule.

4.4 Coverage of the two id sets that support the confirmed set

  • 20 PROP- items (00 §3): every one supports at least one REQ-, and every REQ- cited by a PROP- appears in §2 above. No orphan PROP-.
  • 21 DEF- items (00 §4): 18 map to a deferred REQ-. Three map to no confirmed requirement at all — DEF-14 (pgvector), DEF-19 (PWA) and DEF-21 (hiring-manager and calendar modules) — which is correct, because they are deferrals of ideas, not of requirements. DEF-21 in particular is a consolidation decision recorded as a deferral so it is not re-litigated.
  • 25 OBD- items (00 §8): 22 are referenced by at least one row in §2. Three are not — OBD-22 (career-portal content ownership), OBD-24 (never point the prototype at real data) and OBD-25 (mandatory screening questions). All three are genuine business decisions with no requirement to attach to; OBD-24 in particular is a prohibition, and its acceptance evidence is the absence of a real-data connection to the prototype, which no test can assert. Noted, not gapped.

5. GAP TABLE

Twenty-eight gaps, of which eight are closed or ruled and twenty are live (§4.2). Each states which part is missing, what would close it, and who should own it. A gap here is not automatically a defect in the design — several are honest consequences of an unanswered business question — but every one is something a reviewer would otherwise have to discover by reading four documents side by side.

Severity: High = a requirement will not be satisfied when its phase completes, or two documents actively contradict. Medium = satisfiable but unverifiable as written, or partially covered. Low = bookkeeping, but it will cause confusion at implementation time.

Dispositions, and why the counts in §4 do not move when a gap closes. §4 counts gaps as raised by this audit — that is the measurement, and re-deriving it every time a row closes would make the coverage percentages unciteable. Closure is tracked per row instead. Current state: GAP-02 is RULED (Cost per Hire out of Phase 2, into Phase 4 — see also _open-items.md OPEN-02), GAP-20 is CLOSED (the 03 §5 Currency and FX row is split), and GAP-23 is corrected and downgraded to Low (the control it said was missing already exists as a database CHECK; the real residual risk is narrower). Six rows are cross-document contradictions with non-engineering owners and are tracked in the register: GAP-01 → OPEN-01, GAP-02 → OPEN-02, GAP-03 → OPEN-12, GAP-10 → OPEN-09, GAP-13 → OPEN-10, GAP-17 → OPEN-11.

Closed in the 03 review pass, all four with the recommended fix applied and an acceptance criterion added: GAP-04 (app.relevance_config + app.relevance_config_version in migration 014, weights as typed columns with a sum-to-1.0 trigger, relevance_config_version_id pinned on candidate_job_match), GAP-05 (app.setting moved to Phase 1 migration 012, seeded with the score-visibility keys, with the A-22 no-deploy criterion), GAP-25 (app.job_posting split into Phase 1 migration 006a; the row's citation of "migration 013" corrected to 011/027), and GAP-28newly raised in the same pass, because Time to Fill was a named headline KPI that this list had treated as covered. §4's counts do not move for any of these, per the rule above; GAP-28 is the twenty-eighth gap id and is counted only in §5.

Closed in the Close pass: GAP-27, this list's only High finding. All nine additive objects 05 §9.2 declares are now in 03 with a named migration each — four tables (ref.region §6/002, app.access_grant §7.7/011, app.ats_result_override §20.7/012, app.candidate_erasure_request §28.5/025a), four columns (ats_result_criterion.match_state §20.5/012, audit_event.source_service §28.1/004, intake_parse_attempt.injection_signal + _codes §18.1/010, pii_classification.data_subject_kind §28.2/004), plus access_scope.region_id and ref.location.region_id (§6, §7.3) and the three read-only roles as roles with GRANTs (§28.6, provisioned 001, policy 021a/023/027a). 03 §32.1 gains reconciliation row 5 and 03 §5 records the four new tables, its map derived at 159. The GAP-27 annotations in §2.7, §2.8, §2.15 and §3.2 above are removed and those cells now name real entities. Two parts of the remediation text below did not land, both deliberately and both stated at their tables in 03: the role_assignment scope columns, which 05 §9.2 itself withdraws (role_assignment.access_scope_id already carries them, 03 §7.4), and 'region' in access_scope.scope_type's CHECK, which is OPEN-05 — the column, the exclusive-arc branch and the scope_key coalesce entry are all in place, so adopting it is one line rather than the three coordinated edits whose third-edit omission this row flagged as a silent authorization defect. The migration placements also differ from the recommendation below, because the recommendation forward-referenced: ref.region is 002 not 003, and access_grant + access_scope.region_id are 011 not 003, since 003 cannot see ref.location, app.job or app.job_application (03 §33.1). §4's counts do not move, per the rule above; the closure is recorded here and in §4.2's open/closed tally. Closed 2026-07-29; _open-items.md C-10 carries the same closure from the arbitration side.

Gap Requirement What is missing Severity Closure Owner
GAP-01 REQ-JOB-07 CLOSED — engineering half applied; one business question remains. The gap as found: 00 §2.4 said Phase 1; 03 §5 put the four approval_* tables in Phase 2; 07 §4.2 excluded "approval chains" from Phase 1 and listed them under Phase 3; no acceptance criterion existed at any phase; and so publishing a requisition version was unapproved in Phase 1 — the exact control OBD-12 and the job-board cost bands (OBD-13) assume. The recommendation below has now been applied: 07 gains T-17b (approval_request + approval_decision, single-approver route, publish_version() refuses an unapproved version) and A-16b (approve/reject action, job.approve-gated), 07 §4.5 gains criterion 13 asserted as a forbidden-write test in A-26, 07 §4.2 now excludes only multi-step chains, 03 §5 shows Approval workflow as 12, and 03 §33 splits migration 017 into 017a (Phase 1, Talha) and 017b (Phase 2, Ahmed). Residual: OBD-12 must still confirm who the single Phase 1 approver is — T-17b hardcodes "the requisition's hiring manager, else department head" and stores the resolved approver on the request, so a different answer changes one resolution rule, not the schema. HighLow (residual) Applied as recommended: single-approver in Phase 1, multi-step in Phase 2. Remaining action is OBD-12's answer, tracked as OPEN-01. Talha (done) + Talent Lead (OBD-12, residual)
GAP-02 REQ-ANL-01 Cost per Hire has no data source. RULED — option (a). One of the eight headline KPIs in REQ-ANL-01 is Phase 2, and no entity in the design holds recruiting cost. The only candidate source, app.job_posting_metric.spend_amount, is Phase 4 and covers job-board spend only — not agency fees, referral bonuses or recruiter time. The prototype's figure is the literal constant costPerHire: 4280 at js/data.js:247. So Phase 2 as scoped would ship a KPI tile two phases before even its partial source exists. High Ruled: Cost per Hire is dropped from the Phase 2 KPI set and reinstated at Phase 4 alongside app.job_posting_metric, scoped honestly as cost per hire from measurable channel spend with agency fees, referral bonuses and recruiter time named as out of scope. Phase 2 ships seven KPIs and AC2.7 covers all seven. Amend: 07 §5.1 and the D2 script (done), 03 §11 to name job_posting_metric as the Phase 4 source of REQ-ANL-01's cost dimension and of REQ-ANL-09's, and §6.2 below (done — this also settles that table's keep/cut verdict). Why (a) and not (b): option (b)'s app.hiring_cost table is cheap, but the number it produces is only as good as recruiter and Finance entry compliance, and nobody has committed to it. A tile that under-reports cost by the majority of real spend is worse than a tile that is honestly absent, and reinstating it later is one migration. app.hiring_cost does not exist and is not designed — read every mention of it below as the shape option (b) would take, not as an entity in 03. The ruling chose (a), so REQ-ANL-01's cost dimension points at app.job_posting_metric.spend_amount at Phase 4 (03 §11) and at nothing before that; app.hiring_cost appears in no 03 section, no 03 §5 map row and none of the migrations in 03 §33, deliberately, and it is the only table named anywhere in this document that 03 does not define. Reversible by Finance: if Finance confirms they will maintain manual entry, the table would ship in a Phase 2 migration with a recruiter entry screen and the tile would stay. Its sketched shape — job_id or job_application_id as P7 typed nullable FKs with CHECK (num_nonnulls(...) = 1), cost_type_id FK to a ref vocabulary covering posting spend / agency fee / referral bonus / other, an amount numeric + currency_code ISO-4217 FK pair per REQ-MON-01, incurred_on date, entered_by_user_id — is a proposal for that migration to specify, not a design this package has reviewed. Tracked as OPEN-02 in _open-items.md. Finance + Talent Lead (ruling stands unless they flip it)
GAP-03 REQ-ANL-08 The 27-day time-to-hire baseline is not stored anywhere. It exists only as the prototype constant timeToHire: 27 at js/data.js:245, and ASM-15 already flags that the figure may be a demo value rather than a measurement. "Measurable against the baseline" is untestable without the baseline as data. Medium Store the baseline as a dated app.setting row with a source note, or as a ref-style baseline table if more than one metric gets one. Confirm provenance with Talent Ops before Phase 2 (ASM-15). Talent Ops
GAP-04 REQ-SRC-04, REQ-SCR-09 The relevance-blend configuration has no table — CLOSED. 03 §29.4 stated the four weights and half_life_days "live in a versioned config row, not in code" and gave the formula, but named no table, and no such table appeared in the 03 §5 table map or the §33 migration sequence. REQ-SRC-04 explicitly requires the blend be attributable to a versioned configuration and REQ-SCR-09 depends on the same thing, so as written the requirement was unsatisfiable and the weights would have become constants in the search service — precisely the failure mode the versioning principle (03 §5.3, P3) exists to prevent, and the one that makes "why did this candidate rank third in March" unanswerable. HighClosed Applied. 03 §29.4 now defines app.relevance_config (identity: key, name, owner_user_id, current_version_id) and app.relevance_config_version (immutable: version_no, the four weights as numeric(6,4) columns, not JSON, half_life_days, published_at, created_by_user_id, config_hash), with the same INSERT/SELECT-only grants plus BEFORE UPDATE OR DELETE immutability trigger used on scoring_config_version, and a DEFERRABLE sum-to-1.0 check (the blend is treated as normalised, stated as such). Both appear in 03 §5's table map ("Search ranking configuration", §29.4, 2 tables, Phase 1) and in 03 §33 migration 014 — the same migration that creates candidate_search_index. app.candidate_job_match gains relevance_config_version_id, pinned exactly as it already pins scoring_config_version_id, so any stored ranked result is reproducible. Phase 1 acceptance criterion added: changing a weight mints a new version rather than mutating the row, asserted by a test that attempts an UPDATE on a published version and expects the immutability exception. Talha (done)
GAP-05 REQ-SEC-05 The score-visibility setting is Phase 1 but its storage was Phase 2 — CLOSED. app.setting was created in migration 020 (Phase 2, 03 §33), while REQ-SEC-05 requires match-score visibility to be a configuration setting in Phase 1 and 05 §2.10 rule 5 commits to it concretely ("ATS score visibility is a configurable role setting, defaulting to hr_admin + recruiter + hiring_manager in Phase 1 and off for interviewer permanently"). Without Phase 1 storage that rule becomes the hardcoded constant the requirement forbids — and it is not cosmetic: OBD-05 is an open business decision, so the answer WILL change after Phase 1 ships, and changing it would then be a code deploy rather than a configuration change. HighClosed Applied as recommended. app.setting moves forward to Phase 1 migration 012, seeded with the score-visibility keys only; migration 020 (renamed 020_worklist.sql) keeps task, saved_search, saved_report and report_run, and the rest of the workspace-settings surface plus the Settings screen stay in Phase 4. No UI work is added — 00 PROP-06 already makes the Django admin the Phase 1 administration surface, so registering the table there is the whole administrative story. 03 §33's migration order and 03 §5's table map are amended together (Operational surfaces now reads Phase 12, 5 tables). The Phase 1 acceptance criterion this gap lacked is added: flipping the ats.score_visible_to_roles setting changes what a hiring_manager sees on the next request with no deploy, asserted in A-22's field-policy tests — and asserted negatively for interviewer, whose exclusion is not configurable. Talha (done)
GAP-06 REQ-CAN-02 No bulk-action endpoint on candidates. REQ-CAN-02 requires "filter, sort, rank, bulk actions and a full profile view". 06 §2.11 delivers filter, sort, rank and profile, but bulk operations exist only for inbox, applications, assignments and messages — never candidates. 06 §1.16 correctly forbids a generic POST /batch, so this must be a named endpoint. Medium Add the named bulk operations recruiters actually need — POST /candidates/bulk-tag and POST /candidates/bulk-add-to-pool (≤50, all-or-nothing, idempotency key, one audit event per candidate plus one batch event, per the §2.10/§2.12 pattern). Phase 1 for tagging. Ahmed
GAP-07 REQ-ASM-02 The three named assessment dimensions are not queryable. REQ-ASM-02 requires assessments to report Code Quality, Problem Solving and Time Management. 03 §24 stores per-section detail in assessment_result.breakdown jsonb and §30.3 justifies not creating assessment_result_section on the grounds that "no report dimension needs them yet" — but this requirement is that report dimension. Medium Since the three dimensions are named and fixed, promote them: either three typed nullable columns on assessment_result, or assessment_result_section as §30.3 already anticipates. Decide before the Phase 3 migration, not after data exists in breakdown. Ahmed, Talha review
GAP-08 REQ-INT-02 3 of 11 channels at Phase 1. Outlook, careers portal and manual upload land in Phase 1; four job boards in Phase 3 (DEF-12, gated on OBD-13 and ASM-14); four intake forms in Phase 4 (DEF-11, gated on OBD-11). The requirement — "all eleven normalise into one candidate record structure" — is only fully met in Phase 4. Medium No engineering fix; this is honest phasing. But the normalisation half should be proven early: seed all 11 ref.source_channel rows in Phase 1 and add a Phase 1 test asserting that a synthetic submission on each of the 11 channel types produces a structurally identical raw_intake row. That converts a Phase 4 promise into a Phase 1 assertion. Talha
GAP-09 REQ-SCR-11 A Phase 1 requirement verified in Phase 3. "Ranking across the full candidate base does not degrade the interface" is Phase 1, but 07 §11.6 schedules performance checks in Phase 3 (A-43), and none of the 12 Phase 1 acceptance criteria mentions latency or dataset size. Medium Add a Phase 1 criterion with a number: ranked candidate list p95 under a stated threshold against a seeded dataset at the ASM-03 volume (10⁴10⁵ rows). Without a seeded dataset the criterion is untestable, so the seed script is part of the fix. Ahmed, Talha review
GAP-10 REQ-NFR-07 No latency threshold, so no criterion. OBD-03 recommends first token under 2s and completion under 8s, but that is an assumption, not an agreed ceiling, and no Phase 1 acceptance criterion references it. ai.ai_model_invocation.latency_ms records the number; nothing asserts it. Medium Get OBD-03 answered, then add the criterion as a p95 over ai_model_invocation.latency_ms with a stated window, plus the async-fallback rule from REQ-API-04 for anything above it. Talent Lead + AI Technology
GAP-11 REQ-DAT-01 No engineering artefact at all. Processing only within Utopia-controlled infrastructure or under a DPA is traceable to no module, entity, API group or test. It is discharged by ADR-0012 (topology), OBD-01/OBD-14 (hosting) and a signed DPA — all outside the codebase. Medium Accept that this is contractual, and make the acceptance evidence explicit rather than absent: a named DPA reference recorded on every ai.ai_model_config_version (the column already exists) plus an ADR stating the infrastructure boundary. Then the requirement is verifiable by document review, which is the honest standard. Legal + Talha
GAP-12 REQ-DAT-02 Recorded, not enforced. ai.ai_model_config_version.retains_data makes "which candidate data went to a retaining provider" a query, which is genuinely valuable — but nothing prevents activating a version with retains_data = true. Medium Add the guard the design already implies: refuse activation of any ai_model_config_version with retains_data = true unless an explicit, named override with a legal reference exists. One CHECK plus one service guard, Phase 1. Talha
GAP-13 REQ-GOV-07 Per-jurisdiction audit retention is not derivable. The requirement ties audit retention to "the jurisdictions in which the requisition was posted". OBD-17 sets a single uniform policy (13 months hot, 7 years archived), and no entity links an audit.audit_event to a posting jurisdiction. Medium Two honest options. (a) State that the strictest applicable standard is applied uniformly — which OBD-04 already recommends — and amend the requirement wording so it stops implying per-jurisdiction derivation. (b) If a jurisdiction genuinely needs longer, that needs a job_posting → jurisdiction → retention-class join, and OBD-17 must be reopened. (a) is recommended. Legal
GAP-14 REQ-WRK-02 Column in Phase 2, producer in Phase 4. task.origin = 'ai_suggestion' exists from Phase 2, but the capability that produces AI next-best-action suggestions (REQ-AIC-12, AI-12) is Phase 4. For two phases the requirement is structurally satisfied and functionally empty. Low Say so in the Phase 2 demo rather than letting the empty state read as a bug: the origin dimension and the badge are proven in Phase 2 with rule-originated tasks; AI-originated tasks arrive in Phase 4. Alternatively pull one narrow AI suggestion type forward. Talent Lead
GAP-15 REQ-AIC-02 Half the requirement is three phases away. Candidate Matching is Phase 1 with modules scoring + talent_pool, but talent_pool and its three tables are Phase 3 (DEF-06). The Phase 1 half is real — candidate_job_match (03 §20.6) answers "best-fit open roles across all Utopia brands" without a pool. Low Restate REQ-AIC-02's phase as "1 (ad-hoc match) / 3 (pool-scoped rematch)" in 00 §2.8, matching how REQ-AIC-10 is already split. No engineering change. Talha
GAP-16 REQ-SEC-08 One of the four named settings has nothing to enforce. REQ-SEC-08 requires SSO, 2FA, session timeout and password policy to be enforced rather than display chrome. Under OBD-15 (SSO only, no local password store) there is no password to have a policy about, and 2FA is enforced by Entra ID, not TalentFlow. Low Reword to what is actually enforced and where: session timeout and idle expiry in app.user_session (TalentFlow), SSO and 2FA at the IdP with TalentFlow asserting the claim, password policy not applicable under OBD-15. State the IdP dependency so nobody reads the settings screen as a control it isn't. Talha
GAP-17 REQ-CAN-08 No seed source, no write path, no test for the reviewable half. ref.skill (~500) and ref.skill_alias (~2,000) exist, but the only cited source is js/data.js:46 skillsPool, which is synthetic demo data (findings §C). 06 exposes GET /ref/{vocab} only — administration is Django admin (PROP-06) with no API. And no acceptance criterion tests that unmapped parser labels remain storable and reviewable. Medium Three small items: name a real seed taxonomy source and its licence; add a Phase 1 criterion asserting an unmapped raw_label survives parse, is visible in the profile, and is promotable to a ref.skill by an admin; confirm Django admin is an acceptable write path for ~2,000 aliases before Phase 1 closes. Ahmed, Talha review
GAP-18 REQ-ITV-05 No acceptance criterion for the month view. Having no entity is correct — 02 §4.6 rightly makes calendar a read view over interview + worklist rather than a table. But none of the seven Phase 2 criteria mentions it, so a requirement with no state and no criterion has nothing holding it. Low Add a Phase 2 criterion: the month view renders scheduled interviews in the viewer's timezone with the organiser's wall-clock intent preserved across a DST boundary (reusing the AC2.5 fixture), and shows nothing the viewer's scope forbids. Ahmed
GAP-19 REQ-API-05 Rate limits are published; expected latency is not. 06 §1.11 defines rate limiting with headers. Nothing in 06 publishes per-endpoint expected latency, and the SLOs in 02 §11.1 are operational targets, not part of the contract the interface reads. Low Put a latency budget in the OpenAPI schema as a per-operation extension, generated from the same source as the SLO table so the two cannot drift. Then "published" is literally true. Talha
GAP-20 REQ-MON-02 CLOSED. 03 §5 listed Currency and FX as one row at Phase 1 while REQ-MON-02 and PROP-09 are Phase 3, so app.fx_rate had no Phase 1 consumer — and the same row claimed one table where §26.2 documents two objects. Low Done. The 03 §5 row is split: ref.currency Phase 1 (REQ-MON-01 depends on it, migration 002, counted in the §6 reference-data area) and app.fx_rate Phase 3 (migration 015). The phase column is now honest per table and the area count no longer double-counts ref.currency. Talha — closed
GAP-21 REQ-GOV-11, REQ-API-03 "Fully operable with AI unavailable" is never tested. 02 §9.2 has a degradation matrix and 04 §8.2 a degradation ladder, but no phase acceptance list contains a criterion that exercises them. This is the requirement most likely to be quietly false, because the failure only appears when the provider is down. Medium Add a Phase 1 criterion: with the AI provider forced to fail, the five Playwright journeys still pass, every AI-derived surface renders an explicit unavailable state, and no request returns 5xx because of it. That is a fault-injection fixture, not a chaos platform. Ahmed, Talha review
GAP-22 REQ-NFR-08 Not measurably stated. "Recruiters act on AI output from the screens where they already work" has no entity, no endpoint and no criterion, and no definition of what would falsify it. Low Convert it into something checkable: every AI-derived value in the migrated screens carries a provenance badge and an in-place accept/dismiss action, with no separate AI screen required to act on it — asserted by the Playwright journeys. Then it is a UI contract rather than an aspiration. Ahmed
GAP-23 REQ-DAT-05 A database-level control exists; nothing flags its removal. Corrected — an earlier revision of this row claimed "a future migration could add such a column and nothing would fail", which understates a control the design already has. audit.pii_classification.ck_no_special_category CHECK (class <> 'special_category') (03 §31.1, §28.2) is the strongest available control and it is a constraint, not a promise: introducing special-category data requires a deliberate migration that drops the CHECK, which forces the access-control, aggregate-only-read and lawful-basis decisions to be made explicitly. The genuine residual risk is much narrower than "nothing would fail" — it is that a migration can drop the CHECK, and no gate notices when it does. Low One CI assertion, not a new control: after migrations run against the scratch database, assert that ck_no_special_category is still present on audit.pii_classification (query pg_constraint by name). That turns removing it into a deliberate, reviewable act instead of a silent one, and it composes with the RULING-02 drift check that already stands up a migrated scratch database. Optionally pair it with the name denylist as a warning, not a gate. Do not build a column-absence checker03 §31.1 already documents the control; an implementer sent to build one would be duplicating the CHECK in Python. Ahmed
GAP-24 REQ-ANL-09 Cost side of channel comparability unavailable until Phase 4. REQ-ANL-09 is Phase 3 and requires the eleven inbound channels be comparable. Volume comes from raw_intake.source_channel_id (Phase 1). Cost per channel needs job_posting_metric.spend_amount, which is Phase 4, and four of the eleven channels do not exist until Phase 4 either (GAP-08). Low Scope REQ-ANL-09 explicitly at Phase 3 to volume, conversion and quality by channel — which is the comparison recruiters actually use — and phase the cost dimension with REQ-PUB-01 at Phase 4. Interacts with GAP-02. Talent Lead
GAP-25 REQ-JOB-06 job_posting phase conflict — CLOSED. 03 §5 listed Job postings and publishing at Phase 4; 07 §4.1 sub-slice 1B includes job_posting in Phase 1; and job_application.job_posting_id is a Phase 1 column created in 03 §33 migration 011 while app.job_posting was created in migration 027 (Phase 4). Citation corrected: an earlier revision of this row cited "migration 013", which creates matching_config, duplicate_candidate_pair, candidate_merge and candidate_merge_operation and has nothing to do with postings — the FK's owner is 011 and the referenced table was 027. That is not merely a wrong citation; it is a hard violation of 03 §33's own stated rule that "a migration may only reference objects created in a lower-numbered file", and 03 §33.1 now records it as one of six such violations with a CI gate that parses each migration for referenced relations and fails on any forward reference. Medium → Closed Applied as recommended. 03 splits app.job_posting out of 027 into a new Phase 1 migration 006a (careers-portal postings only, one row per published version), leaving job_posting_metric and all external-platform state in 027 (renamed 027_publishing_external.sql). 03 §5's table-map row now reads "1 (job_posting, migration 006a) / 4 (job_posting_metric)" and 03 §11 states the split and its forcing reason. Talha (done)
GAP-28 REQ-ANL-01 Time to Fill is a named headline KPI with no fill instant, no close reason and no written formula — CLOSED. This gap list previously flagged only Cost per Hire (GAP-02) and treated time-to-fill as covered. It was not. 03 §9.1 gave job.opened_at ("first transition to Open"), job.closed_at and job.vacancies_filled int (a trigger-maintained counter with no timestamp), which produces three separate wrong answers: (a) closed_at conflates "filled" with "cancelled / withdrawn / headcount pulled" — there was no close-reason column — so the metric could not exclude requisitions that were never filled, inflating or deflating it arbitrarily; (b) job_version.vacancies can exceed 1 and vacancies_filled is a bare counter, so for a 3-vacancy requisition "the fill instant" was undefined; (c) opened_at is the first Open transition, so a requisition closed and reopened spans its dormant period. Time to Hire was fine in principle (job_application.applied_atterminal_at where the status is hired, exactly recoverable from job_application_status_history) but neither formula was written down anywhere as SQL, so every report would have re-derived it differently. MediumClosed Applied — three additions to 03. (1) app.job.close_reason_id bigint NULL FK ref.vocabulary_value (vocabulary job_close_reason: filled / cancelled / headcount_withdrawn / merged) with ck_job_close_reason CHECK ((closed_at IS NULL) = (close_reason_id IS NULL)), so time-to-fill restricts to genuinely filled requisitions. (2) The fill instant is defined as the hired transition read from job_application_status_historymin(valid_from) for a single vacancy, the Nth such transition for N — and app.job_vacancy (job_id, seq, filled_by_application_id, filled_at, closed_without_fill_at) gives a per-slot timestamp, with a unique index preventing one hire filling two slots. (3) Both formulas are written as SQL views in migration 021 and nowhere elseapp.v_kpi_time_to_hire and app.v_kpi_time_to_fill, the latter taking its origin from the latest open interval in job_status_history rather than job.opened_at, and filtering on close_reason = 'filled'. Report-level aggregation sits on top; the definition is not repeated. 03 §5's Job requisitions row goes 6 → 7 tables. Talha (done)
GAP-26 Assignment §5 numbering 00 §1 cites the principles as §5.1§5.6; 05 §5 cites §5.6 and §5.7. And the duplicate-detection principle is cited throughout 00 §2.3 as a bare Constraint with no section number. Low Correct 00 §1 to §5.1§5.7 and give the duplicate-detection principle its section number. §3.2 above traces all eight regardless, so nothing is lost — but two documents disagreeing about how many principles exist is exactly the kind of thing a reviewer notices first. Whoever owns 00
GAP-27 REQ-SCR-03, REQ-AIC-04 (counted); REQ-SEC-03, REQ-DAT-03/06, REQ-HIS-03/05 (affected, not counted) The nine additive schema objects 05 §9.2 declares as security and explainability controls existed in no table and no migration in 03 — CLOSED 2026-07-29. The finding as raised follows, unedited, because it is the evidence; what was done about it is in the Closure cell. access_grant, ref.region + ref.location.region_id, role_assignment scope columns for BU / department / region, audit_event.source_service, ats_result_override, ats_result_criterion.match_state, intake_parse_attempt.injection_signal, candidate_erasure_request, the staff subject flag on pii_classification, and the three read-only database roles (ats_ai_reader, ats_report_reader, ats_support_readonly) appear only in 05 and in adr/0009adr/0011. 03 §7.3 constrains access_scope.scope_type to (global, business_unit, department, location, job, job_application, talent_pool) — no region, no grant table; 03 §32.1 lists four reconciliation points and none of these; none of the 29 migrations in 03 §33 creates them. Three consequences, in order of severity: (a) adr/0009 is Accepted and its enforcement strategy resolves scope from access_grant and job__current_version__location__region_id, so an accepted ADR depends on objects the schema does not define; (b) REQ-SCR-03 and REQ-AIC-04 name match_state as their Database Entity, which is a dangling reference, and 05 §5.2's claim that "missing requirements are a queryable fact, not an inference from a null" is unimplementable against 03 as written — a missing requirement is an absent criterion row, indistinguishable from one never assessed; (c) REQ-SEC-03, REQ-DAT-03/06 and REQ-HIS-03/05 stay traceable through objects that do exist (access_scope, v_user_effective_scope, retention_*, audit_event) but lose the specific control 05 designed for them. HighClosed Applied — all nine are in 03, each in a named migration, and every cell above now names a real entity. Where each landed: ref.region + ref.location.region_id03 §6, migration 002 (ref.region created before ref.location in the same file so the FK is a same-file backward reference); audit.audit_event.source_service with ck_audit_source and audit.pii_classification.data_subject_kind with ck_pii_subject_kind — the staff subject flag, as an enumerated column rather than a boolean → §28.1 / §28.2, migration 004; app.intake_parse_attempt.injection_signal + injection_signal_codes + ck_parse_injection, both inside the closing-column grant → §18.1, migration 010; app.access_grant (P7 typed nullable subject FKs, not 05 §2.3's (subject_table, subject_id); ck_access_grant_window/_ceiling/_no_self/_revoked/_subject, the revocation-column grant and tg_access_grant_immutable) and app.access_scope.region_id with its exclusive-arc branch and its scope_key coalesce entry → §7.7 / §7.3, migration 011, with access_grant.interview_id added by 019 and offer_id by 023, each re-declaring ck_access_grant_subject, and v_user_effective_scope gaining branch 4 in the same files; app.ats_result_criterion.match_state text NOT NULL with ck_ats_criterion_match_state, ck_ats_criterion_unassessed and ix_ats_criterion_match, and append-only app.ats_result_override → §20.5 / §20.7, migration 012; app.candidate_erasure_request → §28.5, migration 025a; the three read-only roles → §28.6, provisioned NOLOGIN with zero privileges in 001, column-scoped grants and RLS in 021a / 023 / 027a. 03 §32.1 carries them as reconciliation row 5 and 03 §5 records the four new tables with its map derived at 159 from its own column sum. Two parts of the recommendation deliberately did not land, and both are stated at their tables in 03 rather than dropped silently: the role_assignment scope columns, which 05 §9.2 itself withdrawsrole_assignment.access_scope_idaccess_scope already carries the same enum (03 §7.4), and parallel columns would give it two homes, so the recommendation text above is stale on that single point; and 'region' in access_scope.scope_type's CHECK, which is OPEN-05 and not an engineering decision — the other two of the three coordinated edits are done, so adopting it is one DROP/ADD CONSTRAINT pair and the collision hazard is gone in both worlds. The recommended migration numbers were also wrong and were corrected in the applying: 003 cannot create access_grant, ref.region or access_scope.region_id, because it cannot see ref.location, app.job or app.job_application (03 §33.1's forward-reference gate) — hence 002 and 011. Each of the four new tables traces to a confirmed requirement (REQ-SEC-03, REQ-SEC-03, REQ-SCR-08 / REQ-GOV-01, REQ-DAT-03 / REQ-DAT-06), so §6's reverse check is unaffected in direction; T-ASM-05's figure moved with them. Talha (done)

6. Reverse check — designed things with no confirmed requirement

The matrix above runs requirement → design. This section runs design → requirement, which is where over-engineering shows up. All 25 modules and every table defined in 03 §§628 were checked — 142 tables at the baseline this check was run against, derived below rather than inherited. 03 §5's headline has since moved to 159, derived from its own column sum: the 03 review pass added seven tables (ref.retention_subject, ref.non_identifying_contact, app.job_vacancy, app.relevance_config + _version, app.communication_suppression, app.api_idempotency_record, audit.partition_seal) and fixed the headline-vs-column arithmetic this section flagged, and the Close pass added GAP-27's four (ref.region, app.access_grant, app.ats_result_override, app.candidate_erasure_request). The 142-table derivation below is the baseline of this audit and is not re-run; the eleven new tables each trace to a confirmed requirement (REQ-GOV-07, REQ-CAN-01, REQ-ANL-01, REQ-SRC-04/REQ-SCR-09, REQ-COM-04, REQ-API-02, REQ-GOV-05; then REQ-SEC-03 ×2, REQ-SCR-08/REQ-GOV-01, REQ-DAT-03/REQ-DAT-06), so §6's conclusion is unaffected in direction.

The table count was recounted, because the figure this check rested on did not add up. 03 §5 opened "144 tables" while its 25 area rows summed to 147, and the Currency and FX row claimed one table where §26.2 documents two objects. With the headline three below its own column sum, nothing in §6 could be shown to be exhaustive in either direction, and T-ASM-05 rested on the figure.

Recounting two independent ways — §5's areas against the section bodies §§628, and the 29 migrations in 03 §33, which are the only place every object is enumerated one by one — both give 142: 19 ref + 104 app + 13 ai + 6 audit, with the five app.v_* views excluded because views own no state. Seven cells in 03 §5 do not survive the recount, and its owner must correct them there — this document cannot fix the source of truth it audits:

03 §5 row States Should be Why
Reference data and vocabularies 20 19 ref.consent_purpose is explicitly "folded into ref.vocabulary_value" in the §6 roll-up — a documented row, not a table
Recruitment intake and inbox 10 8 §12.1§12.4 document eight: intake_channel, raw_intake, raw_intake_attachment, raw_intake_read, ingestion_run, ingestion_dead_letter, integration_webhook_event, intake_resolution. intake_parse_attempt and parse_issue are counted at §18, as §12.3 says
Candidates 12 14 §13.2 documents thirteen child tables plus candidate
Candidate documents 2 1 §14 documents only app.candidate_document; the blob registry is app.stored_file at §8
Applications 4 3 §15.1§15.2 document three; job_application_assignment is counted at §16
Interviews, feedback and scorecards 11 9 user_availability_rule / _exception are documented and counted at §7.6, which says so
Currency and FX 1 split the row ref.currency Phase 1 (migration 002, counted in the §6 reference-data area) and app.fx_rate Phase 3 (migration 015). One row cannot carry two phases honestly — this closes GAP-20

The figure is a floor and it has moved. GAP-01, GAP-04 and GAP-27 are now all adopted, and 03 §5's headline reads 159 against a column sum of 159 — the arithmetic defect this section flagged did not survive the additions. Re-derive the total from the migration sequence whenever §5 changes; do not inherit it, and do not inherit it from here either. The per-area corrections above are independent of the additions and stand regardless. See T-ASM-05.

6.1 Result

  • All 25 modules trace to at least one confirmed requirement. 02 §4.6 additionally lists eleven tempting modules that were deliberately not created (search, embeddings, workflow_engine, calendar, managers, recruiterhub, aistudio, settings, help, reporting_warehouse, tenant/region), each with a stated reason. That is the reverse check already having been done once at module level, and it holds.
  • 121 of the 142 tables at the checked baseline trace directly to a confirmed requirement. The 16 entries below — 21 tables plus five views, which own no state — do not, or do so only weakly. Verdicts follow. Tables added to 03 §5 after this baseline (GAP-04's two, GAP-27's four) each trace to a named confirmed requirement, so they enter the traceable side and do not lengthen this list; re-run §6.2 if anything else is added.

6.2 The sixteen

Table(s) Phase Nearest confirmed requirement Judgement
app.user_availability_rule, app.user_availability_exception 2 REQ-ITV-04 (no double-booking) Not justified as scoped. REQ-ITV-04 needs only the interview_participant EXCLUDE constraint — recurring working hours, timezones and leave periods are a scheduling-assistant feature nobody confirmed. They also add a personal-class PII surface (03 §7.6 notes leave is inferable) for no confirmed requirement. Recommend cutting from Phase 2 and reintroducing as a PROP- if interviewers actually complain about being offered impossible slots. Two tables, ~12,000 rows, and a data-protection surface, for a nice-to-have.
app.job_posting_metric 4 REQ-PUB-01, REQ-ANL-01 (cost dimension), REQ-ANL-09 Justified — the condition is now met. Keep. At ~1.1M rows it was the largest unrequired table in the design, and REQ-ANL-09's volume dimension is served by raw_intake.source_channel_id without it. GAP-02's ruling settles it: Cost per Hire moves out of Phase 2 and into Phase 4 with this table named as its source, so the requirement and the table now land in the same phase and the table is required rather than speculative. 03 §11 must state that link explicitly — it is the whole justification. Scope honestly: this yields cost per hire from measurable channel spend, not from agency fees, referral bonuses or recruiter time.
app.integration_webhook_event 1 REQ-INT-02 (Graph subscription lifecycle) Justified, built one phase early. Its three stated consumers — delivery receipts (Phase 2), posting state changes (Phase 4), assessment completions (Phase 3) — none exist in Phase 1. The Graph subscription-renewal path (04 §2.7) does, and that alone warrants the table. Keep; state the single Phase 1 consumer in 03 §12.3 so nobody builds Phase 2 handlers early to justify it.
app.approval_request, app.approval_decision 1 REQ-JOB-07, REQ-OFR-02, REQ-OFR-03 Justified, and the phase is now correct. Two requirements need approval and 03 §30.1 correctly consolidates them into one engine rather than two shapes. The phase disagreement with REQ-JOB-07 (GAP-01) is resolved: these two tables are Phase 1 in their single-approver form (07 T-17b), with the offer subject arriving in Phase 3 via the same typed nullable subject column. 03 §10 already flags the "is this a workflow engine?" tension honestly; the answer — typed nullable subject columns, real FKs, no generic engine — is the right one.
app.approval_route, app.approval_route_step 2 REQ-JOB-07 (multi-step), REQ-OFR-02, REQ-OFR-03 Justified as Phase 2. Phase 1 needs one approver, not a routing engine: T-17b hardcodes the resolution rule and stores the resolved approver on the request, so migration 017b can introduce routes and backfill without rewriting Phase 1 decisions. Deferring the route tables is what makes the Phase 1 gate small enough to actually land.
app.saved_search 2 REQ-SRC-01 (weakly), REQ-NFR-09 Justified on two grounds, neither of which is the obvious one. Not because search needs saving, but because the prototype has four saved searches (js/data.js:410-415) and REQ-NFR-09 makes the 23-route IA the screen backlog; and because 03 §26.1 makes it a compliance surface — a saved filter can encode a discriminatory query, so they are readable by compliance. 1,000 rows. Keep.
app.saved_report, app.report_run 2 REQ-ANL-03 (Phase 3) Justified, phase-early by one phase. Migration 020 is Phase 2; the requirement is Phase 3. Harmless (the tables are empty until used) but it makes 07's Phase 2 look larger than the requirements demand. Move to the Phase 3 migration or note why not.
app.candidate_tag, ref.tag 1 REQ-CAN-02 (filter/sort), REQ-NFR-09 Justified. No requirement names tags, but the prototype has them, REQ-CAN-02 requires filtering, and ~100 tag rows plus a join table is the cheapest thing in the schema. Also the target of the GAP-06 bulk-tag endpoint.
app.candidate_embedding 2 REQ-SRC-02 Correctly deferred, correctly placed. DEF-14 gives a specific entry condition (Phase 1 FTS plus trigram measured and found insufficient for a named query class), and ADR-0006 chose pgvector in the same database precisely so candidate erasure does not have to reach into a second system (REQ-DAT-03). Keep as a Phase 2 migration; do not enable it early to look modern.
app.fx_rate 3 REQ-MON-02 (Phase 3) Justified, and the phase is now correct. Was listed Phase 1 by an unsplit 03 §5 row; the row is now split (ref.currency Phase 1, app.fx_rate Phase 3) and GAP-20 is closed.
app.candidate_access_token 1 (migration 003) REQ-SEC-07 (Phase 3) Justified, deliberately early. Five other tables carry candidate_access_token_id FKs (assessment_assignment, offer_response, and the status-page/upload/interview-confirm paths), so creating it with identity in migration 003 avoids three later ALTERs. Phasing note, not over-engineering.
ai.query_intent 2 REQ-GOV-09, REQ-SRC-02 Justified, and load-bearing. ~40 rows that are the entire Phase 12 chatbot capability surface. ADR-0010 and 00 §7 both reject text-to-SQL and a chatbot service account; this table is the mechanism that makes the rejection structural.
audit.audit_event_redaction 1 REQ-DAT-06 vs REQ-HIS-04 Justified — it is the only honest reconciliation. REQ-HIS-04 says the application has no update or delete path into the audit log; REQ-DAT-06 says identifying data must be erasable. One of the two has to bend, and a two-person-rule, append-only, reasoned exception path is the correct bend. ~200 rows.
app.pipeline_transition_rule 1 REQ-JOB-08 (weakly), DEF-01 (Phase 3) Thin at Phase 1. With one default pipeline config, "which moves are legal" is a constant. The versioning of pipeline_config is justified (03 §30.2 — a config edit must not retro-change which moves were legal, which is §5.3 reasoning), but the rule table itself has no Phase 1 requirement. Keep the versioned config; consider deferring pipeline_transition_rule to Phase 3 with DEF-01.
app.message_thread 2 REQ-INT-01, REQ-INT-06 Justified. Without it a candidate's reply lands as an unrelated raw_intake row and the recruiter re-triages a conversation they are already having (04 §2.8). It is what makes per-source attribution survive a reply.
app.candidate_job_match 1 REQ-AIC-02, REQ-CAN-10 Justified, and a good consolidation. 03 §30.1 folds the pool-rematch result table into it because the question is identical and both answers must be pinned and explainable the same way — one explainability implementation instead of two.
app.v_activity_feed, app.v_candidate_live, app.v_job_live, app.v_job_application_live, app.v_user_effective_scope (views) 12 REQ-HIS-03, REQ-DUP-08, REQ-SEC-03 Justified. 03 §30.3 explicitly rejects an activity_event table in favour of a 30-day whitelisted view because a table would duplicate and drift from audit.audit_event. v_user_effective_scope is the single scope resolution REQ-SEC-03 demands. Views own no state and cost nothing.

6.3 The one thing this check found that matters

Of sixteen candidates, one is genuinely unrequired (user_availability_rule / _exception), one became required when GAP-02 was ruled (job_posting_metric — Cost per Hire now lands in Phase 4 with this table named as its source, so the conditional verdict resolved to keep), one is thin (pipeline_transition_rule), and the remaining thirteen are justified — several of them by reasoning the owning document already wrote down. Two tables out of 142 is a low over-engineering rate for a design of this size, and it is low for a specific reason worth stating: 03 §30 and 02 §4.6 already ran this check at the design stage and recorded twenty-one deliberate non-creations. The discipline that produced those two sections is what kept this section short.


7. Cross-document consistency findings

Where cross-document contradictions are settled: _open-items.md. That register is the single arbitration layer for the whole package. It deduplicates the ~50 instances raised across 00 §10, 02 §15, 03 §32.1, 04 §9.1, 05 §9.1, 06 §9.1 and this section into ~32 distinct questions, and it ends each one in a ruling (RULING-01…RULING-09), a named non-engineering owner plus a blocking gate (OPEN-01…OPEN-12), or a closure (C-01…C-11). _open-items.md §4 is the lookup table: any row of any reconciliation section maps to one register id. §5 lists the gates in the order they bite. Do not resolve a contradiction locally in a document again — record the evidence where it arose and take the ruling from the register.

The nine per-document instances this section previously re-litigated now resolve as: row 1 → OPEN-01, row 4 → GAP-20 (closed), row 8 → C-11, and rows 2/3/5/6/7 stay here because each is a single-document phase or numbering correction with a named owner rather than an arbitration.

Recorded here because building the matrix is what surfaced them. These are additional to the five inconsistencies 00 §10 recorded inside _decisions.md, and to the per-document reconciliation sections in 03 §32, 04 §9.1, 05 §9.1 and 06 §9.1 — all of which now point at _open-items.md for the ruling.

# Finding Documents Disposition
1 Requisition approval phase stated three ways (1 / 2 / 3) 00 §2.4, 03 §5, 07 §4.2 GAP-01 — closed. The three documents now agree: single-approver in Phase 1, multi-step in Phase 2. 07 gains T-17b, A-16b and §4.5 criterion 13; 07 §4.2 excludes only multi-step chains; 03 §5 reads 12; migration 017 is split into 017a (Phase 1) and 017b (Phase 2) — 017a depends only on 006, so nothing reorders. Residual OPEN-01 is narrowed to OBD-12's business question (who approves), which changes one resolution rule rather than any phase or table.
2 job_posting phase stated two ways (1 / 4) 03 §5, 07 §4.1 GAP-25 — closed. 03 splits app.job_posting into Phase 1 migration 006a and leaves job_posting_metric plus external-platform state in 027; the 03 §5 table-map row and 03 §11 both state the split. The row's citation of "migration 013" is corrected to 011 (the FK's owner) and 027 (the former target).
3 app.setting needed in Phase 1, delivered in Phase 2 00 §2.15, 03 §33 GAP-05 — closed. app.setting moves to Phase 1 migration 012, seeded with the score-visibility keys; 020 is renamed 020_worklist.sql. Registered in the Django admin, which 00 PROP-06 already makes the Phase 1 administration surface, so no UI work is added.
4 Currency and FX phased as one row when the two halves differ 03 §5, 00 §2.21 GAP-20 — closed. The row is split in 03 §5; see §6 above.
5 Assignment §5 principle count stated two ways (§5.1§5.6 / §5.6, §5.7) 00 §1, 05 §5 GAP-26. Correct 00 §1.
6 talent_pool named as a Phase 1 module for REQ-AIC-02 while the module and its tables are Phase 3 00 §2.8, 02 §4.2, 03 §5 GAP-15. Split the phase cell as REQ-AIC-10 already is.
7 A versioned relevance config is asserted but never given a table 03 §29.4, 00 §2.12 GAP-04 — closed. app.relevance_config and app.relevance_config_version are defined in 03 §29.4, listed in 03 §5 and created in migration 014; weights are four numeric(6,4) columns with a deferred sum-to-1.0 trigger, and candidate_job_match pins relevance_config_version_id.
8 00 §2.14 assigns the worklist module to REQ-WRK-01/02, but the group was named in neither 06 §2 (25 groups, no tasks group) nor 06 §4 (which listed four omitted surfaces and not this one) — its five endpoints existed only inside 06 §7 row 23, a prototype-route mapping table. 02 §4.4 module 25 compounded it by declaring GET /api/v1/tasks, a path no other document uses 00 §2.14, 06 §2 / §4 / §7, 02 §4.4 Resolved — C-11. 06 §4.5 now defines the worklist group (five endpoints, permission mapping to 05 §2.9 row 25, contract note); 06 §7 row 23 is a cross-reference; 02 §4.4 module 25 reads GET /api/v1/worklist/tasks. The endpoints were never in analytics06 §2.24's eighteen endpoints contain no task endpoint, so an earlier reading of this finding that filed them there was wrong.
9 The nine additive objects 05 §9.2 declares were in no 03 table, no 03 §5 map row, no 03 §32.1 reconciliation point and none of the 29 migrations. access_grant and ref.region appeared only in 05 and adr/0009; match_state only in 05, this document and adr/0011; source_service, injection_signal and candidate_erasure_request only in 05 05 §9.2, 03 §5 / §7.3 / §32.1 / §33, adr/0009 §"Consequences", adr/0011 §4§5, this document §2.7 / §2.8 / §2.15 GAP-27 — was the only High finding in this table; now RESOLVED. 03 adopted all nine per migration (002, 004, 010, 011, 012, 025a, plus the roles in 001/021a/023/027a), 03 §32.1 carries reconciliation row 5, 03 §5 records the four new tables at a derived 159, and the annotations in §2.7 / §2.8 / §2.15 / §3.2 above are out — those cells name real entities. adr/0009 no longer rests on undefined schema. Two exceptions, both deliberate and both stated at their tables in 03: the role_assignment scope columns are withdrawn by 05 §9.2 itself, and 'region' as a grantable scope_type is OPEN-05.

| 10 | Outbound email delivery phased four different ways, and this matrix did not flag it. 07 §5.1 and 03 §5 / §33 migration 018 put the whole of Communications at Phase 2; 00 DEF-07 said 23; 00 OBD-21 said in-app Phase 12 with email at Phase 3; 04 §9.1 row 5 argued a minimal outbound_message slice must be pulled into Phase 1 because "every parse failure is a dead end a recruiter must resolve in Outlook by hand, outside the audit trail". Same class as findings 1 and 2 and it appeared in neither §5 nor §7. | 00 §4 DEF-07 / §5 OBD-21, 03 §5 / §22 / §33, 04 §9.1 row 5, 07 §4.1 / §4.2 / §5.1, 02 §4.1 module 5, this document §2.14 | Resolved, not open — no GAP row. Ruled: Phase 1 takes 04's minimal slice (the app.outbound_message row, Mail.Send through the same MailProvider port as inbound, the send idempotency guard, NDR classification, one seeded transactional template — 46 dev-days, now 07 T-16b); Phase 2 takes 07/03's delivery pipeline (templates UI, retry, bounce and complaint handling, digests, notification_preference, notification centre — 07 T-29 / A-31 / A-36). 04 wins on Phase 1 because an NDR with no outbound_message row to attach to cannot be classified at all, which makes this a Phase 1 correctness hole rather than a deferred feature; 07/03 win on Phase 2 because the pipeline's costly parts (sender domain, template approval, bounce policy) are separable from one Mail.Send call. OBD-21's Phase 3 answer is superseded and its scope narrowed to internal notification email, which stays Phase 2. Amended in all six places listed. |

Finding 9 is the one exception to the sentence below: an Accepted ADR resting on schema that does not exist is a design error, not a bookkeeping one, and it is the only finding here that can silently ship as a missing control rather than as a visible inconsistency.

Findings 18 and 10 are not design errors. They are the predictable cost of eight documents written against one decision file, and all nine are cheap to fix now and expensive to discover during the Phase 1 migration sequence. Finding 10 carries no GAP row because it was ruled rather than left open; findings 18 keep theirs because they still need someone's decision. Finding 8 is already closed, which is the shape the other seven should end in: the group is named in one place, every other document points at it, and no endpoint path exists in two spellings.


8. Assumptions made by this document

Every item is an assumption, labelled as such, and each states how it would be falsified.

# Assumption How it would be falsified
T-ASM-01 The assignment's §5 contains exactly seven numbered principles, with §5.7 being the AI-governance principle, inferred from 05 §5's heading. Reading assignment §5 directly. This is the weakest assumption here and the reason §3.1 exists rather than a silent renumbering.
T-ASM-02 "Confirmed requirement" means exactly the 158 REQ- rows in 00 §2 — no more, no fewer. PROP-, DEF- and OBD- items are not requirements and are not counted in the coverage totals. A stakeholder treating a PROP- item as confirmed scope, which would raise the denominator and change every percentage in §4.
T-ASM-03 A requirement's Phase is the phase in which it is first satisfied, not the phase in which it is fully complete for every sub-case. REQ-INT-02 and REQ-AIC-10 are counted at their first phase. A stakeholder reading "66% by Phase 1" as "66% of the platform by Phase 1". §4.1 states the distinction explicitly for exactly this reason.
T-ASM-04 The acceptance criteria in 07 §3.5§7 are the complete acceptance set. Where a requirement has no criterion there, it has none anywhere. A separate test plan existing that 07 does not reference. None exists in the repository (findings §B — no test file, runner or CI configuration of any kind).
T-ASM-05 Withdrawn as an assumption and replaced by a derivation. The old form — "the 144-table figure and the per-area counts in 03 §5 are complete, so the reverse check in §6 is exhaustive" — was not safe to assume: §5's headline read 144 while its own 25 area rows summed to 147, so the premise was internally false and the reverse check's exhaustiveness rested on it. §6 now derives the figure two independent ways (§5's areas against the section bodies §§628, and the 29 migrations in §33) and reconciles them at 142 for the baseline it checked, and it lists the seven 03 §5 cells that do not survive the recount. What remains assumed is narrower: that a table documented in a 03 section body is also created by some migration in §33, and vice versa — checked in both directions and reconciled table by table. A table in a section body that no migration creates, or a migration creating a table no section documents. Also: the total is a floor and it has moved. GAP-04's two tables and GAP-27's four are now adopted, and 03 §5's headline reads 159 against a column sum of 159 — the arithmetic defect did not survive the additions, and §5 now states outright that its headline is derived from the column. Re-derive from §33 whenever §5 changes rather than inheriting either number. The per-area corrections in §6 are independent of the additions and hold regardless.