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

12 KiB

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_typehuman/system/ai actor_kinduser/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.