334 lines
34 KiB
Markdown
334 lines
34 KiB
Markdown
# 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 `GRANT`s (`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 `0001`–`0012`; that
|
||
basis no longer holds — the package holds `0001`–`0018`, `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_status` — **and 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 Q1–Q2 | 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, 4–6 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 `002` — `ref.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) |
|