HR-ATS-Portal/docs/architecture/_glossary.md

145 lines
12 KiB
Markdown

# Glossary — Part 1 module vocabulary ↔ Part 2 database vocabulary
> Published as a standalone document per **RULING-03** (`_open-items.md` §1). The mapping was
> drafted inline in `03-database-design.md` §32.1, where an API or security author reading `05` or
> `06` would never find it. Five reconciliation sections (`00` §10 #5, `02` §15 I4, `03` §32.1,
> `04` §9.1 #3, `05` §9.1 I-4) each independently asked for "a glossary published before Phase 1
> code". This is it.
## 1. The rule
| Layer | Vocabulary | Where it appears |
|---|---|---|
| **Database** | Part 2's `snake_case` table names. **Authoritative.** | SQL, migrations, ERDs, index names, constraint names, trigger names, JSON field names in API payloads, the `03` table map |
| **Code** | Part 1's `CamelCase` module and entity names | Python package names, module names, service-facade class and method names, `import-linter` contract names, the `02` §4 module catalogue |
Neither is "the" name for the aggregate. Each is the name in its own layer, and a document that
mixes them in one sentence should say which layer it means. `08` §1.2 states the convention and the
matrix applies it literally — the Module column says `requisition`, the Database Entity column says
`app.job` / `app.job_version`, and they are the same aggregate under two vocabularies.
**The one name most likely to cause real confusion is `requisition` versus `job`**, because Part 2's
uniqueness rules key on `job_id` (`uq_application_live (candidate_id, job_id)`, `03` §15.1) while
the module, the screens and the business all say "requisition". See **RULING-06**: the URL space is
`/jobs` only, with `requisitions` surviving as an OpenAPI tag over the workflow sub-resources.
---
## 2. Entity mapping
Alphabetical by Part 1 name. Rows marked **†** are not renames — Part 2 rejected Part 1's *shape*,
and the ruling that settled it is cited. Do not read those as vocabulary differences.
| Part 1 (module / code) | Part 2 (table) | Note |
|---|---|---|
| `AiRun` | `ai.ai_model_invocation` | The run ledger. Written *before* its result is usable (`03` §27.2) |
| `Application` | `app.job_application` | The candidate ↔ `job_version` join. Owns stage and status; the only thing an `ats_result` attaches to |
| `ApplicationScore` | `app.ats_result` | **†** Part 1's pin set was short by four columns. Part 2's full pin set is mandatory — RULING via `06` §9.1 #7 / C-05 |
| `ApplicationStageHistory` | `app.job_application_stage_history` | Transition rows, not diffs |
| `Assignment(subject_type, subject_id)` | `app.job_assignment` **+** `app.job_application_assignment` | **†** One polymorphic table becomes two concrete ones — **RULING-04**. There is no `subject_type` discriminator anywhere |
| `AssessmentAssignment` | `app.assessment_assignment` | |
| `AssessmentResult` | `app.assessment_result` | |
| `AssessmentTemplateVersion` | `app.assessment_template_version` | Parent `app.assessment_template` |
| `AuditEvent` | `audit.audit_event` | RANGE-partitioned, hash-chained, composite PK `(id, occurred_at)` |
| `CandidateProfileVersion` | *(does not exist)* | **†** Replaced by `app.candidate_field_provenance` — C-01. A whole-profile version row would duplicate `intake_parse_attempt.parsed` |
| `CandidateSearchIndex` | `app.candidate_search_index` | A table, not a generated column — a `tsvector` column cannot read child tables (C-03) |
| `DuplicateCandidateLink` | `app.duplicate_candidate_pair` | State set differs — see §3 |
| `InboundSubmission` | `app.raw_intake` | The raw layer. State set differs — see §3 |
| `MergeOperation` | `app.candidate_merge_operation` | **†** Part 1's `reversible_until` does not exist — **RULING-08** |
| `Notification` | `app.notification` | |
| `NotificationPreference` | `app.notification_preference` | |
| `OutboundMessage` | `app.outbound_message` | Immutable body/recipient snapshot; only delivery-state columns are updatable |
| `PipelineConfig(requisition_version_id)` | `app.pipeline_config` **+** `app.pipeline_config_version` **+** `app.job_pipeline_assignment` | **†** Bound to the **job**, not the job version — C-06. Binding to a version would mint a fake job revision on every pipeline tweak |
| `Pool` | `app.talent_pool` | |
| `ProcessingAttempt` | `app.intake_parse_attempt` | Append-only; carries `parser_name`/`parser_version` |
| `RefValue` | `ref.vocabulary_value` **+** `ref.lifecycle_status` | Split deliberately: statuses carry `is_terminal`/`is_negative`/`requires_reason`, pure labels do not (`03` §4.7) |
| `Requisition` | `app.job` | |
| `RequisitionRequirement` | `app.job_requirement` | Version-scoped and weighted |
| `RequisitionVersion` | `app.job_version` | Immutable: `INSERT`+`SELECT` grants plus an immutability trigger |
| `RoleAssignment` | `app.role_assignment` | **†** Scope dimensions unresolved — **OPEN-05**. Part 1's `brand` is almost certainly `ref.business_unit`; `region` may not exist |
| `ScoreComponent` | `app.ats_result_criterion` | Stores `weight_applied` and `contribution`, never recomputes them |
| `SkillMatch` | `app.ats_result_skill` | |
| `StoredFile` | `app.stored_file` | **†** Registry **and** denormalised domain columns, additively — **RULING-05**. Scan status lives only here. Not `files.stored_object` (the `04` §9.1 #4 name is superseded) |
| `SubmissionAttachment` | `app.raw_intake_attachment` | |
| `Task` | `app.task` | |
| `TalentPoolMembership` | `app.talent_pool_member` | |
## 2.1 Column-name divergences worth naming
Semantics agree in every row; only the spelling differed, and Part 2's spelling wins.
| Part 1 | Part 2 | Why Part 2's is better |
|---|---|---|
| `actor_type``human`/`system`/`ai` | `actor_kind``user`/`system`/`integration`/`ai_agent` | **RULING-01.** There is no `human` value. `integration` (a service principal, e.g. the careers-form endpoint) is distinguishable from `system` (a timer or unattributed trigger write) |
| `starts_at_utc`, `tz`, `local_time` | `starts_at`, `ends_at`, `scheduling_timezone`, `local_start_wall` | `03` §4.3: `timestamptz` already *is* the instant, so the `_utc` suffix is noise; `local_start_wall` names what it actually preserves — the organiser's wall-clock intent across a DST boundary |
| `object_store_key` on domain rows | `app.stored_file.storage_key` | **RULING-05.** One place for the purge to look |
| `virus_scan_status` on domain rows | `app.stored_file.scan_status` | **RULING-05.** A scan status in two places is how an unscanned file becomes viewable |
| `brand` (scope dimension) | `ref.business_unit` | **OPEN-05.** Also: "brand" reads as tenancy, and this platform is explicitly not multi-tenant |
---
## 3. State-set divergences — these are not renames
`06` §9.1 #5/#6 established that Part 2's state sets replace Part 1's, and the differences carry
meaning. Recorded here so nobody restores a Part 1 value thinking it is a synonym.
### `raw_intake.state`
| Part 1 | Part 2 | What changed |
|---|---|---|
| `received` | `received` | — |
| `parsing` | `parsing` | — |
| `parsed` | `parsed` | — |
| `failed` | *(dropped)* | **Deliberate.** An intake whose parse failed is `needs_review` — that is the entire purpose of the raw layer. `failed` becomes an `intake_parse_attempt.status`, so the *attempt* failed and the *intake* is still live and retryable |
| `needs_review` | `needs_review` | — |
| `discarded` | `rejected_unusable` | Names the reason rather than the gesture |
| — | `resolved_new_candidate`, `resolved_existing_candidate`, `quarantined` | Added. The two `resolved_*` states are what make "raw intake exists before candidate creation" a queryable fact; `quarantined` is the malware-gating terminal |
### `duplicate_candidate_pair.status`
| Part 1 | Part 2 | What changed |
|---|---|---|
| `suspected` | `suspected` | — |
| `confirmed` | `confirmed` | — |
| `rejected` | `rejected` (semantically **confirmed distinct**) | The value name is retained in `03` §19.2 but its meaning is load-bearing: a `rejected` pair suppresses re-flagging of that canonical pair **forever**, via the canonical-pair constraint. It is not a dismissed queue item. Where prose is ambiguous, say "confirmed distinct" |
---
## 4. Terms that mean one specific thing in this package
| Term | Means | Does **not** mean |
|---|---|---|
| **Requisition** | The hiring request and its versioning/approval workflow — the `requisition` module, and the `requisitions` OpenAPI tag over `/jobs/{id}/versions`, `/requirements`, `/approvals` | A second table or a `/requisitions` resource path (**RULING-06**) |
| **Job** | The `app.job` row: the persistent identity a candidate applies to, and the key every uniqueness rule uses | Only the published advert — that is `app.job_posting` |
| **Score** | `app.ats_result` — per **application**, never per candidate. `ats_result` has no `candidate_id` column, by construction | A candidate-level attribute. The prototype's `aiScore` on the candidate (`js/data.js:123`) is exactly the shape this design rejects |
| **Current** | A time predicate over an interval, e.g. `tstzrange(valid_from, valid_to) @> now()` | `valid_to IS NULL`. `03` §7.4 calls this the single most important sentence in §7: a grant with an end date has `valid_to IS NOT NULL` from creation, and spelling "current" as `valid_to IS NULL` silently revokes it |
| **Brand** | A value of `ref.business_unit`, carried as a **column** | A tenant, a database boundary, or a deployment. `00` §6 row 17 rejects per-region databases; OBD-23 is one master data model with brand as an attribute |
| **Region** | Unresolved — see **OPEN-05** | An existing table. `ref.location.timezone` and `ref.location.region` (text) exist; `ref.region` does not |
| **Special category** | The `audit.pii_classification.class` value, guarded by `ck_no_special_category CHECK (class <> 'special_category')` in Phase 1 | "sensitive data" loosely. Compensation and CV text are `sensitive_personal`, which *is* stored |
| **Version** (of a job, scoring config, offer, pipeline config, matching config, message template, scorecard template, assessment template) | An immutable row with `INSERT`+`SELECT`-only grants and an immutability trigger. `UPDATE` raises | A mutable record with a `version_no` column |
| **Raw intake** | `app.raw_intake` — the append-only arrival record. Exists before, and independently of, any candidate | An inbox row. The inbox is a *view* over raw intake plus `raw_intake_read` |
| **Module** | One of the 25 logical Python packages with a public service facade (`02` §4) | A deployable service. Boundaries are enforced by `import-linter`, not by network hops (ADR-0001) |
---
## 5. Naming conventions, so a new name does not need a ruling
From `03` §4.1, restated here because this is the file someone checks before inventing a name.
| Kind | Pattern | Example |
|---|---|---|
| Table | `snake_case`, singular | `app.job_application` |
| History table | `<entity>_status_history` / `<entity>_stage_history` | `app.offer_status_history` |
| Version table | `<entity>_version` | `app.scoring_config_version` |
| Join / assignment table | `<subject>_<role>` | `app.job_scoring_assignment` |
| Reference table | `ref.<noun>`, with `key` + `label` | `ref.rejection_reason` |
| View | `app.v_<noun>` | `app.v_candidate_live` |
| Unique index | `uq_<table>_<discriminator>` | `uq_application_live` |
| Partial/expression index | `ix_<table>_<purpose>` | `ix_candidate_name_trgm` |
| CHECK | `ck_<table>_<rule>` | `ck_no_special_category` |
| EXCLUDE | `ex_<table>_<rule>` | `ex_role_assignment_overlap` |
| Schema | `ref` (vocabularies) · `app` (domain) · `ai` (model access) · `audit` (governance) · `queue` (`procrastinate`-owned) | — |
**If a proposed name does not fit a row above, that is a signal the entity is wrong, not that the
convention is.** Bring it to Talha rather than adding a pattern.