HR-ATS-Portal/docs/architecture/_open-items.md

34 KiB
Raw Blame History

Open Items Register — the single place a contradiction is settled

This file is the arbitration layer for the whole package. Before this file existed, the same cross-document contradictions were re-litigated independently in eight reconciliation sections (00 §10, 02 §15, 03 §32.1, 04 §9.1, 05 §9.1, 06 §9.1, 08 §5, 08 §7) — roughly fifty raised instances of about thirty distinct disagreements, each ending in a recommendation. An engineer could not implement from that. Every row below ends in a ruling or a named owner plus a blocking gate. Nothing here is a recommendation.

Precedence. _decisions.md §Rulings and this file are binding and rank equally — the numbered RULING- series is continuous across both. Where a ruling and any document body text disagree, the ruling wins and the document is wrong. The per-document reconciliation sections are retained as evidence and reasoning, not as decisions; each now points here.

Two id series:

Series Meaning Who can change it
RULING-nn Decided. Binding package-wide. Implement it; do not re-open it in a document. An ADR that supersedes it
OPEN-nn Not decided, and cannot be decided by an engineer — it needs a business, legal or IT answer. Carries a named owner and the artefact it blocks. The named owner

1. Rulings

RULING-01 — actor vocabulary (already binding; restated for completeness)

Stated in full in _decisions.md §Rulings. Summary: the column is actor_kind; its value set is exactly ('user','system','integration','ai_agent'); there is no human value and no actor_type column; the "AI must never auto-reject" guard is written character for character actor_kind = 'user'.

Status: CLOSED and already propagated. 02 §1/§4.4, 03 §18.1/§28.1, 04 §1, 06 §9.1 #2 and adr/0011 all carry the ruling, and 06 §9.1 #2's earlier recommendation to standardise on human is explicitly withdrawn at source. It is listed here only so a reader who finds actor_type/human in a stale draft knows which way the ruling went. Gate: already passed (migration 006 onward). Test: A-26 in 07 §11 asserts the literal string.


RULING-02 — migration tooling: plain SQL is the schema authority, Django is the runner

The most-repeated contradiction in the package. Raised six times: 00 §10 #1, 02 §15 I1, 03 §32.1 #1, 04 §9.1 #2, 05 §9.1 I-1, 06 §9.1 #4. Every instance recommended some form of hybrid and none of them decided it.

Ruling.

Aspect Decision
Schema authority Hand-written SQL. The DDL for every object lives in db/migrations/NNN_*.sql exactly as documented in 03 §33.
Ledger and runner Django's migrations app. Each numbered SQL file is wrapped by one Django migration containing a single RunSQL(sql_file.read_text(), reverse_sql=RunSQL.noop). python manage.py migrate is the only command anyone runs, and there is one ledger (django_migrations), not two.
Models Hand-written, managed = False on every model whose table is created by SQL — which is all of them. Models are read/write mappers over a schema they do not own.
The CI gate makemigrations --check is replaced, not kept. It presumes autogenerate and would fail permanently against managed = False. The gate is a schema-drift check: apply all migrations to a scratch database, introspect it with inspectdb, and assert (a) every model field maps to a real column of the right type, and (b) no model references a column that does not exist. Drift fails the build.
Why not Django-owned structure with RunSQL only for invariants Because the invariant set is not a garnish on the schema — it is the schema. Partial and expression unique indexes, CHECK regexes, DEFERRABLE constraint triggers, GiST EXCLUDE, generated columns, RANGE partitions and column-level GRANTs (03 §4.8, §28.1; 05 §5.3) carry the requirements. A schema half-owned by two tools means every invariant-bearing table is described twice and drifts, and the failure mode is a missing constraint in production. One owner, and it is the one that can express the constraints.
Cost accepted Two developers lose makemigrations convenience and gain a reviewable SQL diff. Talha reviews every migration (03 §33) — the diff being SQL is what makes that review meaningful.

Documents to amend. _decisions.md §"Backend language and framework" (drop "migrations are built in" as a justification for Django — the admin, the permission framework and the Python parsing ecosystem carry the choice on their own) and §"Testing, CI" (replace the makemigrations --check gate with the drift check). 00 §10 #1, 02 §15 I1 (§12.4 already states this position — align the wording), 03 §32.1 #1, 04 §9.1 #2, 05 §9.1 I-1, 06 §9.1 #4: replace the recommendation with a pointer to RULING-02.

ADR required — written. adr/0017-plain-sql-migrations-as-schema-authority.md. One ADR recording this, because it changes daily developer workflow. An earlier revision of this ruling directed that it be renumbered to 0013 on the basis that the package held 00010012; that basis no longer holds — the package holds 00010018, 0013 is the frontend strangler migration, and the file was written at 0017. The number in 02 §15 and §12.4 was correct as written and was not changed. The index in 02 §13 is authoritative for every ADR number in this package.

Gate: before migration 001. Nothing else in Phase 1 can start.


RULING-03 — entity naming: Part 2's table names are the database vocabulary, Part 1's are the module vocabulary

Raised five times: 00 §10 #5, 02 §15 I4, 03 §32.1 (the "smaller alignments" paragraph), 04 §9.1 #3, 05 §9.1 I-4, plus 06 §9.1 #5/#6/#11 as per-area instances.

Ruling. Part 2's snake_case table names are authoritative and are the only names that appear in SQL, migrations, ERDs, index names, constraint names and API payload field names. Part 1's CamelCase names are the Python package/module and service-facade vocabulary and appear only in module names, class names and service-method signatures. Neither is "the" name for the aggregate; each is the name in its own layer, and the mapping is now published rather than inferred.

The glossary is published as a standalone document: _glossary.md. 03 §32.1 already drafted the mapping inline, where an API or security author reading 05 or 06 would never find it. It is now its own file, extended to cover every divergence the eight reconciliation sections named, and it is the artefact a reviewer checks a name against.

Documents to amend. 00 §10 #5, 02 §15 I4, 04 §9.1 #3, 05 §9.1 I-4: replace "a glossary should be published" with a pointer to _glossary.md. 03 §32.1's inline mapping stays as the derivation but must cite _glossary.md as the published form. _decisions.md Part 1 module tables keep their CamelCase entity names — they are correct in their layer — but the four substantive Part 1 shapes that Part 2 rejects outright are corrected at source by RULING-04, RULING-05, RULING-08 and OPEN-05, not renamed.

Gate: before Phase 1 application code. Not before migration 001 — the SQL is unambiguous already. The names that leak into code are the ones this blocks.


RULING-04 — recruiter assignment: two concrete tables, never polymorphic

Raised three times: 00 §10 #2 ("Part 2 wins"), 02 §15 I7 (already corrected at source), 06 §9.1 #3. Also 03 §16 and §30.3 and 08 §2.6.

Ruling. Part 2 wins, per 00 §10 row 2. Two concrete tables — app.job_assignment and app.job_application_assignment — each with interval columns, a GiST EXCLUDE overlap constraint, and a partial unique index on the current holder of any ref.assignment_role where is_exclusive. There is no polymorphic Assignment(subject_type, subject_id, …) table and no POST /assignments endpoint that takes a subject_type discriminator. One assignment service facade and one permission surface present both tables, so the module inventory is unaffected at 25.

Reason the polymorphic shape loses: a polymorphic subject FK cannot be enforced by the database at all, and unenforceable references to jobs and applications are the specific failure this whole design exists to eliminate (_repo-findings.md §F).

Documents to amend. _decisions.md Part 1 module 12 — the stale polymorphic definition is the source that leaked into three documents; correct it there. 06 §9.1 #3 and 00 §10 #2 become pointers.

Gate: migration 006 (job_assignment) and 011 (job_application_assignment). Status: implemented. 02 §4.2 module 12, 03 §16 and 06 §2.16 already carry the two-table shape.


RULING-05 — stored_file is the single content-addressed registry; domain rows hold an FK plus per-occurrence data

Raised three times: 03 §32.1 #2 (the additive reading), 04 §9.1 #4 (proposes files.stored_object), 06 §9.1 #10 ("the underlying table is unresolved. Needs a ruling").

Ruling. 03 §8.1's additive reading is authoritative, and the table is app.stored_file (migration 005) — not files.stored_object; 04 §9.1 #4 describes the same table under a different name and is superseded, not overruled.

Concern Where it lives Rule
Content identity, blob location, size, MIME app.stored_file (sha256, storage_key, byte_size, mime_type) One row per distinct content. Five candidates sending the same file share one row.
Malware scan status app.stored_file.scan_statusand nowhere else This is the load-bearing half of the ruling. A scan status in two places is exactly how an unscanned file becomes viewable. No domain row carries a virus_scan_status column; 03 §12.3 and §14.1 are already correct on this and must stay correct.
Retention class app.stored_file.retention_class One place for the purge to look — Part 1's stated reason for a separate files module.
Per-occurrence data app.raw_intake_attachment, app.candidate_document filename_raw / original_filename, attachment_index, is_probable_cv, extraction_state, revision, layout_metadata.
The duplicated sha256 on domain rows Denormalised domain data Legitimate: it backs the identical-file duplicate-detection index (03 §12.3) without a join. It is not a second source of truth, and a mismatch between it and stored_file.sha256 is a bug, not a state.

One consequence worth stating because 06 §9.1 #10 asked it directly: yes, one stored_file row can be referenced by two subjects, and that is the point — it is why erasure, virus scanning and content dedupe each have exactly one place to happen.

Documents to amend. 04 §9.1 #4 — replace the files.stored_object proposal with a pointer here and drop the "Data-model document to confirm". 06 §9.1 #10 — resolved; the resource shape 06 chose (/documents/{id} with subject_type/subject_id) is correct under this ruling. _decisions.md §"Cross-cutting persistence patterns" / files module: state that domain rows never carry a scan status.

Gate: migration 005.


RULING-06 — one aggregate, one resource tree: /jobs. /requisitions does not exist

Raised in 06 §9.1 #1, which explicitly flags it as needing a ruling because assignment §26.7 lists "requisitions" and "jobs" as separate API groups. Related to RULING-03 but distinct: this one is about URL space, not vocabulary.

Ruling. One row, one canonical resource tree, /api/v1/jobs. /requisitions is not a resource and will not be minted. Assignment §26.7's two entries are preserved as two OpenAPI tags over one resource tree, which is what the assignment was asking for and what 06 §2.6/§2.7 already builds:

OpenAPI tag Covers Paths
requisitions The authoring, versioning and approval workflow /jobs/{id}/versions, /jobs/{id}/requirements, /jobs/{id}/approvals, /jobs/{id}/scoring-config
jobs The job record, its lifecycle, its pipeline and its assignments /jobs, /jobs/{id}, /jobs/{id}/status, /jobs/{id}/pipeline-config, /jobs/{id}/assignments

Reason two resources lose: Part 2's uniqueness rules key on job_id (uq_application_live (candidate_id, job_id), 03 §15.1). Two resource paths over one row means two PATCH surfaces onto one uniqueness domain and two sets of permission checks, and the second one will be the one that is wrong.

Documents to amend. 06 §9.1 #1 — replace "Needs a ruling" with this ruling and add the tag table to 06 §2.6. 08 §1.2 already applies the vocabulary split correctly and needs no change.

Gate: before Phase 1 application code (the OpenAPI schema is generated from the views). Status: implemented in 06; this ruling makes it a decision rather than a pick.


RULING-07 — pgvector installed at Phase 1 provisioning, used at Phase 2; PostgreSQL major pinned at 16

Raised twice: 00 §10 #4, 02 §15 I2. Both read the two positions as compatible without deciding the provisioning question they actually disagree about.

Ruling. The extension is CREATE EXTENSION-ed at Phase 1 provisioning — it is free, and installing an extension later is privileged DDL against a live database. No Phase 1 feature reads it. app.candidate_embedding and its HNSW index land in migration 028 at Phase 2, gated on DEF-14's stated entry condition (Phase 1 FTS plus trigram measured and found insufficient for a named query class), not on enthusiasm. The provisioning runbook must say in one sentence that the extension being present is not a signal that semantic search is available.

PostgreSQL major is pinned at 16 for Phase 1. app.uuidv7() comes from the migration-001 shim (03 §33). PG18's native uuidv7() is adopted only through a deliberate version-upgrade migration that swaps the shim, never by an environment drifting forward — because a UUIDv7 whose generator changed mid-life breaks the time-ordering the id strategy relies on (03 §3).

Documents to amend. 00 §10 #4, 02 §15 I2 → pointers. _decisions.md §"Deployment topology" (provision the extension, do not imply Phase 1 use) and §"Database engine" (pin 16; drop "16+ target 17" for Phase 1).

Gate: provisioning and migration 001.


RULING-08 — merge reversal has no time window

Raised in 00 §10 #3.

Ruling. Part 2 wins. There is no reversible_until column and no fixed reversal window — a window would be an arbitrary number that turns a correctness property into a deadline. Reversal is blocked by exactly two conditions, both real and both surfaced as reversal_blocked_reason: (a) stack discipline — a later merge on either candidate must be reversed first (03 §19.5), and (b) a retention purge that has already pseudonymised a re-parented row (risk R6 in 03 §32.2).

Documents to amend. _decisions.md Part 1 module 8 — drop reversible_until from the MergeOperation entity. 00 §10 #3 → pointer.

Gate: migration 013. Status: implemented (03 §19.5, 06 §2.13, ADR-0008).


RULING-09 — one approval engine serves job versions and offer versions

Raised in 03 §32.1 #4, which correctly labels it a judgement call rather than compliance.

Ruling. One engine. app.approval_route, app.approval_route_step, app.approval_request and app.approval_decision serve both job_version and offer_version through typed nullable subject columns with real FKs and a num_nonnulls = 1 CHECK — the same idiom as candidate_access_token (03 §7.6). This is not the rejected generic workflow_engine: it drives no state machine, it records route-driven sign-off, and 03 §10 already argues the distinction.

The escape hatch stays documented and stays cheap: if a reviewer disagrees, duplicating the two request/decision tables as job_version_approval* and offer_approval* is a copy-paste before migration 017 and a data migration after it.

Gate: migration 017. 03 §32.1 says "decide at 006" — that is the right moment to know, because 006 adds job_version.approval_request_id, but the tables themselves land in 017.


2. Open items — decisions an engineer cannot make

Each carries an owner who is not a developer and the artefact it blocks. These are the honest residue: nothing here is unresolved through neglect.

# Item Raised in What must be decided Owner Blocks
OPEN-01 Requisition approval phase — was stated three ways (1 / 2 / 3); engineering half now applied. The split is in: single-approver approval in Phase 1 (approval_request + approval_decision; 03 §33 splits migration 017 into 017a Phase 1 and 017b Phase 2), multi-step and parallel routes in Phase 2. 03 §5's Approval workflow row and 07 T-17b / A-16b / §4.5 criterion 13 carry it. Residual and genuinely non-engineering: 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. 08 GAP-01 (now CLOSED, residual Low), 08 §7 #1; 00 §2.4 (P1) vs 03 §5 (P2) vs 07 §4.2 (P3) Who the Phase 1 approver is — and, prior to that, whether Phase 1 requisitions require approval at all Talent Lead (OBD-12) The T-17b approver-resolution rule only. No longer blocks the migration order.
OPEN-02 Cost per Hire has no data source at Phase 2. Ruled in 08 GAP-02 as option (a) — dropped from the Phase 2 KPI set, reinstated at Phase 4. Listed here because Finance can flip it to option (b) and the table shape is ready. 08 GAP-02, 08 §6.2 Whether Finance and Talent Ops will actually maintain manual cost entry. If yes, app.hiring_cost ships in Phase 2 and the tile stays. If no or unanswered, (a) stands. Finance + Talent Lead The Phase 2 KPI set, 07 §5.1, the D2 script, and the keep/cut verdict on app.job_posting_metric
OPEN-03 Model hosting: contracted API under a DPA, or self-hosted. Already _decisions.md's top-listed risk. 04 §9.2 Q3, BRD OQ-1, REQ-DAT-01/02 Whether AI pipeline steps may call an external provider at all Legal + business ai queue sizing, GAP-11, GAP-12, the whole 04 §4 pipeline
OPEN-04 Cloud platform, and therefore object storage. _decisions.md recommends Azure Blob; the assignment asks for an S3-compatible design. 04 §6.2 resolves the contract with three adapters, but the production target is unconfirmed and Azure Blob is not S3-API-compatible. 04 §9.1 #1, 04 §9.2 Q4 Which cloud Business + Talha The storage adapter, the local emulator choice, key management, and the immutable audit archive mechanism. Cheap now, expensive after the retention and lifecycle rules exist.
OPEN-05 Access-scope dimensions: what "brand" means, and whether region exists. _decisions.md Part 1 says scope: brand/department/requisition; Part 2 has business_unit and department and no region concept. 05 §2.3 assumes eight dimensions including region; 06 §2.3 uses four. 05 §9.1 I-3, 06 §9.1 #9 (a) Confirm "brand" is business_unit and drop the alias — the likely answer, and the one consistent with the not-multi-tenant constraint. (b) Whether region becomes a grantable scope_type. Not whether the tables exist — ref.region, ref.location.region_id and access_scope.region_id are now built (03 §6, §7.3; migrations 002 and 011, per C-10), with the exclusive-arc branch and the scope_key coalesce entry in place so the three-edit collision hazard is gone in both worlds. If the answer is no, ref.region stays a reporting vocabulary and regional desks are granted several location rows; the cost is one unused nullable column and one unreachable CHECK branch. Talent Lead + Talha One line: 'region' in access_scope.scope_type's CHECK, plus the permission tests that would then exercise adr/0009's region_ids branch against non-empty input. Answer before migration 011 — that is where access_scope is created (03 §33.1); it is no longer 003.
OPEN-06 Mail provider, and whether a dedicated careers mailbox exists. 04 §9.2 Q1Q2 Provider, mailbox, and whether IT will grant application-scope Graph permissions with an ApplicationAccessPolicy Utopia Brands IT The entire 04 §2 design and the least-privilege claim. Ask in Phase 0_decisions.md flags it as a weeks-long Phase 1 critical-path risk.
OPEN-07 Minimal notifications slice in Phase 1 — ruled; engineering half applied. The split is in: Phase 1 gets outbound_message written before the provider call, Mail.Send through the MailProvider port, the idempotency guard, NDR classification and one seeded transactional template (07 T-16b, M, 46 dev-days, 07 §4.5 criterion 14); Phase 2 keeps the pipeline — template UI, retry, complaint handling, digests, notification_preference, notification centre (07 T-29 / A-31 / A-36). 03 §33 splits the schema into 011a (Phase 1) and 018 (Phase 2), numbered below 017b because a phase label cannot reorder an apply sequence (03 §33.1). Recorded as 07 §17 divergence 6 and 08 §7 finding 10; 00 DEF-07 and OBD-21 amended (OBD-21's Phase 3 answer superseded and its scope narrowed to internal notification email, which stays Phase 2). Residual and genuinely non-engineering: IT must grant Mail.Send alongside Mail.Read in the same admin-consent request (07 T-07) — this is now on the Phase 1 critical path, not Phase 2's, and it is the only part of OPEN-07 a developer cannot settle. 04 §9.1 #5 Nothing on the engineering side. Remaining: whether IT will consent to Mail.Send as an application permission scoped by ApplicationAccessPolicy, and who signs off the one seeded template's wording Utopia Brands IT (consent) + Talent Lead (template wording) T-16b's live path, and 07 §4.5 criterion 14 against a real mailbox. T-16b itself is not blocked — it is built and tested against FakeMailProvider. Without the slice at all, every parse failure is resolved by hand in Outlook, outside the audit trail.
OPEN-08 Fairness evaluation needs data the PII decision forbids storing. Resolved structurally in 05 §5.5 (Track A needs no protected data and gates activation from Phase 1; Track B is designed and blocked), so this is not a design defect — but Track B cannot start. 05 §9.1 I-2, BL-4, BRD OQ-2 Whether a protected-attribute evaluation dataset may exist at all, under what lawful basis, in a separate restricted schema Legal + business (BL-4) REQ-GOV-04/05 Track B. The Phase 3 gate must not be described to the business as a disparate-impact review until Track B exists.
OPEN-09 AI latency ceiling. OBD-03 recommends first token <2s, completion <8s. That is an assumption, not an agreed ceiling, and no acceptance criterion references it. 08 GAP-10, REQ-NFR-07 The agreed p95 ceiling Talent Lead + AI Technology The Phase 1 acceptance criterion over ai.ai_model_invocation.latency_ms
OPEN-10 Audit retention: uniform or per-jurisdiction. OBD-17 sets one uniform policy; REQ-GOV-07 implies derivation from posting jurisdiction, and no entity links an audit event to one. Recommended: apply the strictest applicable standard uniformly (OBD-04 already recommends this) and amend the requirement wording. 08 GAP-13 (a) uniform, or (b) reopen OBD-17 and build a job_posting → jurisdiction → retention-class join Legal REQ-GOV-07's wording and audit.retention_policy seeds
OPEN-11 Skill taxonomy seed source and its licence. ref.skill (~500) and ref.skill_alias (~2,000) currently cite only js/data.js:46 skillsPool, which is synthetic. 08 GAP-17 A real taxonomy source and its licence; and whether Django admin is an acceptable write path for ~2,000 aliases Talent Ops + Talha Migration 002 seed data
OPEN-12 The 27-day time-to-hire baseline may be a demo value. js/data.js:245 sets timeToHire: 27 as a constant; ASM-15 already flags it. "Measurable against the baseline" is untestable without the baseline as data. 08 GAP-03, ASM-15 Confirm or replace the figure, and its provenance Talent Ops REQ-ANL-08's acceptance criterion. Store the confirmed figure as a dated app.setting row with a source note.

3. Items closed on inspection — recorded so they are not raised a fourth time

These were raised as contradictions and are not. Each is closed; no gate, no owner.

# Raised as Raised in Why it is closed
C-01 CandidateProfileVersion exists / does not exist 03 §32.1 #3 Replaced by app.candidate_field_provenance, which answers the actual question (per-field origin and confidence) without duplicating intake_parse_attempt.parsed. _decisions.md Part 1's candidate entity list should drop it — a rename in one file, not a design question.
C-02 Module count: 26 or 25 02 §15 I3 25 modules plus one cross-cutting API layer. _decisions.md's "26 modules" prose is a stale count from before the consolidations; 02 §4 and §4.6 are consistent at 25. Cosmetic.
C-03 Search-index ownership 02 §15 I5, 03 §29.3 No conflict. candidate_search_index is a candidate-owned table because a generated tsvector column cannot read child tables. Part 1's "Postgres FTS + trigram" is the mechanism; Part 2's table is the implementation of it.
C-04 Part 1 state sets vs Part 2's 06 §9.1 #5, #6 Part 2's state sets win, and the renames are load-bearing, not cosmetic: Part 1's failed intake state is dropped because an intake whose parse failed is needs_review — which is the entire purpose of the raw layer — and failed becomes an intake_parse_attempt status. Part 1's rejected duplicate state becomes confirmed_distinct, which suppresses re-flagging forever, where rejected reads like a dismissed queue item. Recorded in _glossary.md §3.
C-05 Score entity pin set 06 §9.1 #7 Part 2 wins and 06 §2.14 makes the full pin set a mandatory response field. Part 1's shorter ApplicationScore would not satisfy its own reproducibility requirement, so there is nothing to arbitrate.
C-06 Pipeline config bound to job or job version 06 §9.1 #8 Bound to the job. Binding to a version would mint a fake job revision on every pipeline tweak — the exact failure 03 §20.3 avoids for scoring configs by keeping job_scoring_assignment orthogonal. _decisions.md Part 1's PipelineConfig(requisition_version_id) should adopt the same orthogonality; one-line correction.
C-07 Communications, assessment and talent-pool tables absent from Part 2 06 §9.1 #12, #13 A gap that has since been filled, not a conflict. 03 §22 (7 tables), §24 (4) and §26.1 (3) now model all of them, including the pinned template_version_id on every sent message. Nothing to decide.
C-08 Redis used for the HMAC replay nonce 04 §9.1 #6 No divergence. A nonce is a cache entry with a TTL, squarely inside the stated Redis use, and losing it widens the replay window to 300s rather than breaking correctness.
C-09 RLS deferred to Phase 2 02 §15 I6 A risk, not a contradiction, and already carried as R8 in 03 §32.2. Mitigation is concrete: one centralised authorization module, no repository access from views, and a test asserting every candidate-reading endpoint passes through iam.can().
C-10 Additive extensions introduced by 05 05 §9.1 I-5, I-6, I-7 and §9.2 All additive, none contradictory: audit_event.source_service, append-only ats_result_override, ats_result_criterion.match_state, a staff subject flag on pii_classification, access_grant, and the three read-only database roles. They extend _decisions.md rather than disagreeing with it. Closed 2026-07-29 — the schema half is applied, and all nine objects are now in 03: ref.region and ref.location.region_id (03 §6, migration 002ref.region created before ref.location in the same file), app.access_grant (§7.7, 011, with interview_id added by 019 and offer_id by 023), app.access_scope.region_id with its exclusive-arc branch and its scope_key coalesce entry (§7.3, 011), audit.audit_event.source_service (§28.1, 004), the pii_classification staff flag as audit.pii_classification.data_subject_kind — an enumerated column rather than a boolean (§28.2, 004), app.intake_parse_attempt.injection_signal + injection_signal_codes (§18.1, 010), app.ats_result_criterion.match_state NOT NULL with ck_ats_criterion_match_state (§20.5, 012), append-only app.ats_result_override (§20.7, 012), app.candidate_erasure_request (§28.5, 025a), and the three read-only roles provisioned NOLOGIN with zero privileges in 001, their column-scoped grants and RLS in 021a / 023 / 027a (§28.6). 03 §32.1 row 5 records the reconciliation; 03 §5 carries the four new tables and its map is derived at 159. The first clause of this row was false and is withdrawn: an earlier revision read "03 already carries match_state", and it did not — the column appeared 0 times in 03, which is exactly what 08 GAP-27 found by grep. The schema task it deferred was owned by Talha (08 GAP-27) and is done; GAP-27 is closed in 08 §5. Residue, and it is not this row's: whether region becomes a grantable scope_type — one line in access_scope's scope_type CHECK — is OPEN-05. And 05 §9.2's own withdrawal of the role_assignment scope columns for BU / department / region stands, so those are deliberately not built: role_assignment.access_scope_id already carries them (03 §7.4).
C-11 tasks has no API group 08 §7 #8 Closed. worklist is now a named group at 06 §4.5 alongside the other four surfaces assignment §26.7 omits, carrying the five task endpoints and the contract note; 06 §7 row 23 cross-references it; 02 §4.4 module 25 reads GET /api/v1/worklist/tasks; 08 §2.14 files REQ-WRK-01/02 under worklist. The earlier note that tasks were "served under the analytics area" was wrong — 06 §2.24 has no task endpoint.

4. Where each reconciliation section resolves to

The deduplication map. Any of the ~50 raised instances can be looked up here and followed to one row.

Section Row Resolves to
00 §10 1 · 2 · 3 · 4 · 5 RULING-02 · RULING-04 · RULING-08 · RULING-07 · RULING-03
02 §15 I1 · I2 · I3 · I4 · I5 · I6 · I7 RULING-02 · RULING-07 · C-02 · RULING-03 · C-03 · C-09 · RULING-04
03 §32.1 1 · 2 · 3 · 4 · 5 · smaller (PG version) · smaller (naming) RULING-02 · RULING-05 · C-01 · RULING-09 · C-10 (with the region half at OPEN-05) · RULING-07 · RULING-03
04 §9.1 1 · 2 · 3 · 4 · 5 · 6 OPEN-04 · RULING-02 · RULING-03 · RULING-05 · OPEN-07 · C-08
04 §9.2 Q1 · Q2 · Q3 · Q4 OPEN-06 · OPEN-06 · OPEN-03 · OPEN-04
05 §9.1 I-1 · I-2 · I-3 · I-4 · I-5 · I-6 · I-7 RULING-02 · OPEN-08 · OPEN-05 · RULING-03 · C-10 · C-10 · C-10
06 §9.1 1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9 · 10 · 11 · 12 · 13 RULING-06 · RULING-01 · RULING-04 · RULING-02 · C-04 · C-04 · C-05 · C-06 · OPEN-05 · RULING-05 · RULING-03 · C-07 · C-07
08 §5 GAP-01 · GAP-02 · GAP-03 · GAP-10 · GAP-13 · GAP-17 OPEN-01 · OPEN-02 · OPEN-12 · OPEN-09 · OPEN-10 · OPEN-11
08 §5 GAP-04 · GAP-05 · GAP-15 · GAP-20 · GAP-25 · GAP-26 Closed in 08 §5 itself — each is a single-document correction with a named owner and a phase, not a cross-document arbitration
08 §5 remaining GAPs Requirement-coverage gaps, correctly owned by 08 §5. Not arbitration items.
08 §7 1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 OPEN-01 · GAP-25 · GAP-05 · GAP-20 · GAP-26 · GAP-15 · GAP-04 · C-11

Count. Roughly 50 raised instances resolve to 9 rulings, 12 open items and 11 closures — 32 distinct questions. Nine are now decided, twelve have a named non-engineering owner and a blocking gate, and eleven were never really open.


5. Gates, in the order they bite

The practical reading order for anyone starting Phase 1.

Gate Must be settled first
Before any Phase 1 code or migration RULING-02 (migration tooling — and its ADR, adr/0017-plain-sql-migrations-as-schema-authority.md, still Proposed)
Provisioning / migration 001 RULING-07 (PG major pinned at 16, pgvector installed not used), OPEN-04 (cloud, therefore storage)
Migration 002 OPEN-11 (skill taxonomy seed source and licence)
Migration 005 RULING-05 (stored_file registry; scan status in one place)
Migration 006 RULING-04 (two assignment tables), RULING-09 (one approval engine — known at 006, built at 017), OPEN-01 (requisition approval phase, which decides whether 017 is Phase 1)
Migration 011 OPEN-05 (what "brand" means; whether region is a grantable scope_type) — access_scope is created in 011, not 003 (03 §33.1), so this gate bites later than earlier revisions of this table said
Migration 013 RULING-08 (no reversal window)
Before Phase 1 application code RULING-03 (_glossary.md published and used), RULING-06 (/jobs only)
Before the Phase 2 KPI set is shown to stakeholders OPEN-02 (Cost per Hire)
Phase 0, in parallel with everything OPEN-06 (mail provider and mailbox), OPEN-03 (model hosting)