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 in03-database-design.md§32.1, where an API or security author reading05or06would 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.