34 KiB
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 numberedRULING-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 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) |