Technical Design Document: Skills & Competency Engine
1. Introduction & Goals
1.1. Objective
This document outlines the technical design for the Evalium Skills & Competency Engine. The goal is to build a flexible, audit-proof engine that can infer "competence" from immutable submission snapshots, support retroactive mapping and recalculation, and provide UI-ready read models. This feature will be implemented as a view layer over the existing Evaluations and Programmes features, not as a separate HR module.
1.2. Architectural Alignment
The implementation will strictly adhere to the following architectural principles as defined in the project's Architecture Decision Records (ADRs):
- ADR-0008: Modular, Event-Driven Service Architecture: The engine will be implemented as a new, single-responsibility service that communicates with other services via a durable, event-driven pattern.
- ADR-0009: SMB-First Unified UX & Safe Immutability: The engine will support safe, backend-driven impact checks and abstract versioning complexity from the user.
- ADR-0010: The Ledger Pattern for Mutable State: The engine will use the ledger pattern for all high-stakes state changes, such as publishing new mapping set versions and running backfills.
- ADR-0011: Compliance Centre Hybrid Scrub + Compliance Ledger: The engine will integrate with the existing compliance workflows to ensure that skill and competency data is handled correctly during "Forget User" and DSAR export operations.
2. High-Level Architecture
2.1. Service Diagram
[ To be added: A component diagram showing the new SkillsService, its database tables (including `skill_work_queue`), and its event-driven interactions with ResultsService, RemediationService, and PrivacyService. ]
2.2. Event Flow (Durable Outbox / Work Queue)
The Skills engine will rely on a durable, outbox-based eventing pattern to ensure no events are missed and to decouple processing from request threads, aligning with NFRs.
- Trigger: An event occurs in a core service (e.g.,
SubmissionFinalized,RemediationApplied,UserForgotten,MappingSetPublished). - Outbox Write: Within the same transaction, the core service writes a work item to a dedicated
skill_work_queuetable. This guarantees that the work item is created if and only if the original transaction commits. - Worker Poll: A background worker (
internal/workers/skills_projection_worker.go) polls theskill_work_queuetable for pending jobs (recommended:FOR UPDATE SKIP LOCKED). - Process & Project: The worker processes the job, computing evidence facts and rollups. The work item’s idempotency key ensures that re-processing the same event (e.g., due to a worker restart) is safe and does not create duplicate data.
- Update Status: The worker marks the job as completed or failed (with retry/backoff policy).
This pattern ensures high reliability and respects the "no long-running task blocks request threads" NFR.
2.3. Effective Mapping Resolution (Priority / Cascading Overrides)
The engine supports a cascading override model so different scopes can define “what counts” without breaking tenant-wide consistency.
Scopes (most specific wins):
evaluation_versionprogramme(applies only in programme context; see §6.2)org_unit(nearest ancestor binding wins; sub-org overrides org)tenant(default)
Within the same scope:
- highest
prioritywins, - tie-breaker: deterministic rule (e.g., newest published mapping set version, then newest binding row).
Every computed fact records which binding/scope was applied for audit and explainability.
3. Detailed Data Model
3.1. SQL Schema
-- Skills (Tenant-scoped taxonomy)
CREATE TABLE skills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
name text NOT NULL,
description text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skills_tenant_name_unique UNIQUE (tenant_id, name)
);
CREATE TABLE skill_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
skill_id uuid NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
version integer NOT NULL,
name text NOT NULL,
description text,
level_scheme jsonb,
status text NOT NULL DEFAULT 'draft', -- draft | published | archived
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skill_versions_skill_version_unique UNIQUE (skill_id, version)
);
-- Competency Tags (Tenant-scoped taxonomy)
CREATE TABLE competency_tag_keys (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT competency_tag_keys_tenant_name_unique UNIQUE (tenant_id, name)
);
CREATE TABLE competency_tag_values (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
key_id uuid NOT NULL REFERENCES competency_tag_keys(id) ON DELETE CASCADE,
value text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT competency_tag_values_key_value_unique UNIQUE (key_id, value)
);
-- Mapping Sets (Org-scoped definitions for linking tenant skills to org evidence rules)
CREATE TABLE skill_mapping_sets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
org_unit_id uuid NOT NULL,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skill_mapping_sets_tenant_org_name_unique UNIQUE (tenant_id, org_unit_id, name)
);
CREATE TABLE skill_mapping_set_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
mapping_set_id uuid NOT NULL REFERENCES skill_mapping_sets(id) ON DELETE CASCADE,
version integer NOT NULL,
status text NOT NULL DEFAULT 'draft', -- draft | published | archived
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skill_mapping_set_versions_mapping_set_version_unique UNIQUE (mapping_set_id, version)
);
CREATE TABLE skill_mapping_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
mapping_set_version_id uuid NOT NULL REFERENCES skill_mapping_set_versions(id) ON DELETE CASCADE,
rule_type text NOT NULL, -- tag_predicate | evaluation | evaluation_version | question_version | framework
selector_json jsonb NOT NULL,
target_skill_id uuid NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
contribution_strategy text NOT NULL, -- score_threshold | average | best_of_n | pass_fail
precedence integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Mapping Bindings (Priority / cascading override selection)
CREATE TABLE skill_mapping_bindings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
-- scope_type determines override precedence (more specific wins)
scope_type text NOT NULL, -- tenant | org_unit | programme | evaluation | evaluation_version
scope_id uuid, -- NULL for tenant default
-- Optional org boundary for bindings that should only apply within a specific org tree.
-- For scope_type='org_unit', org_unit_id == scope_id.
org_unit_id uuid,
mapping_set_id uuid NOT NULL REFERENCES skill_mapping_sets(id) ON DELETE RESTRICT,
priority integer NOT NULL DEFAULT 0,
is_enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Work Queue (durable outbox)
CREATE TABLE skill_work_queue (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
org_unit_id uuid, -- nullable for tenant-scoped jobs
work_type text NOT NULL, -- submission_finalized | remediation_applied | remediation_reverted | mapping_published | user_forgotten | recalc_requested
idempotency_key text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending', -- pending | in_progress | completed | failed
attempts integer NOT NULL DEFAULT 0,
available_at timestamptz NOT NULL DEFAULT now(),
last_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skill_work_queue_idempotency_unique UNIQUE (tenant_id, idempotency_key)
);
-- Projections
CREATE TABLE skill_evidence_facts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
org_unit_id uuid NOT NULL,
user_id uuid NOT NULL,
submission_id uuid NOT NULL,
skill_id uuid NOT NULL,
evidence_unit_id text NOT NULL, -- e.g., question_version_id or evaluation_version_id
contribution_value numeric,
pass_fail boolean,
weight numeric,
evidence_source text NOT NULL, -- K | O | E
-- Provenance for audit and explainability
mapping_set_version_id uuid NOT NULL REFERENCES skill_mapping_set_versions(id) ON DELETE RESTRICT,
skill_version_id uuid NOT NULL REFERENCES skill_versions(id) ON DELETE RESTRICT,
submission_score_version_id uuid NOT NULL, -- FK to submission score version table when available
mapping_rule_id uuid NOT NULL REFERENCES skill_mapping_rules(id) ON DELETE RESTRICT,
-- Binding provenance (why this mapping set applied)
mapping_binding_id uuid NOT NULL REFERENCES skill_mapping_bindings(id) ON DELETE RESTRICT,
applied_scope_type text NOT NULL,
applied_scope_id uuid,
evidence_ref jsonb, -- Details of what matched (e.g., item_id, tag_key/value)
computed_at timestamptz NOT NULL DEFAULT now(),
-- Uniqueness constraint for idempotency (safe reprocessing)
CONSTRAINT skill_evidence_facts_unique_for_idempotency
UNIQUE (tenant_id, org_unit_id, submission_id, skill_id, evidence_unit_id, mapping_set_version_id, submission_score_version_id)
);
CREATE TABLE skill_rollups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
org_unit_id uuid NOT NULL,
user_id uuid NOT NULL,
skill_id uuid NOT NULL,
status text NOT NULL, -- not_started | in_progress | achieved
last_updated_at timestamptz NOT NULL,
confidence numeric,
coverage numeric,
last_evidence_summary jsonb,
CONSTRAINT skill_rollups_tenant_org_user_skill_unique UNIQUE (tenant_id, org_unit_id, user_id, skill_id)
);
-- Jobs (admin-triggered, long-running recalcs/backfills; may enqueue work queue items)
CREATE TABLE skill_projection_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
org_unit_id uuid NOT NULL,
job_type text NOT NULL, -- recalc | backfill
idempotency_key text NOT NULL,
date_range_start timestamptz,
date_range_end timestamptz,
mapping_set_version_id uuid,
status text NOT NULL DEFAULT 'pending', -- pending | in_progress | completed | failed
progress jsonb,
result_summary jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT skill_projection_jobs_idempotency_key_unique UNIQUE (tenant_id, org_unit_id, idempotency_key)
);
-- Partial unique indexes (Postgres-correct) for "one published version"
CREATE UNIQUE INDEX skill_versions_one_published_version
ON skill_versions (skill_id)
WHERE status = 'published';
CREATE UNIQUE INDEX skill_mapping_set_versions_one_published_version
ON skill_mapping_set_versions (mapping_set_id)
WHERE status = 'published';
3.2. Indexing strategy
-
skill_evidence_facts:(tenant_id, org_unit_id, user_id, skill_id)(submission_id)(mapping_set_version_id)(mapping_binding_id)- Unique constraint provides an additional index for idempotency.
-
skill_rollups:(tenant_id, org_unit_id, user_id)(tenant_id, org_unit_id, skill_id)
-
skill_work_queue:(tenant_id, status, available_at)(tenant_id, org_unit_id, status, available_at)(optional if you shard polling by org)- Unique
(tenant_id, idempotency_key)
-
skill_projection_jobs:(tenant_id, org_unit_id, status)- Unique
(tenant_id, org_unit_id, idempotency_key)
3.3. Tenancy & Scoping Model
The Skills engine adopts a hybrid model:
- Tenant-scoped taxonomy:
skills,skill_versions,competency_tag_*are tenant-level resources (noorg_unit_id). This enables a unified skills framework across a tenant. - Org-scoped execution and evidence:
skill_mapping_sets, projections, jobs and the work queue are org-scoped (or org-aware) to preserve strict silo behaviour. - Cascading override bindings:
skill_mapping_bindingsprovides tenant defaults plus scoped overrides (org/sub-org/programme/evaluation_version). The worker resolves the effective binding for each submission context and records the applied scope in evidence facts.
4. Service Implementation (internal/services/skills)
[ To be further detailed. Will include: mapping resolution, rule evaluation, fact/rollup upserts, and “explain” assembly. ]
5. Worker & Job Implementation (internal/workers)
[ To be further detailed. Will include: skills_projection_worker consuming skill_work_queue, retry/backoff, and admin jobs enqueueing work items. ]
6. API Specification (BFF)
The API provides UI-ready view models to minimize frontend logic, leveraging provenance stored in projection tables and binding resolution.
6.1. Admin Endpoints
GET /skills(Skills Library): Lists all tenant skills + published version; includes org-scoped coverage/confidence for the caller’s current org.GET /skills/{skillId}(Skill Detail): Returns skill definition + published mapping context (effective binding) for current org; includes draft mapping sets and optional impact preview references.GET /skills/mapping/effective(Effective Mapping Resolver): Returns the effective mapping binding and a resolution trace for a given context (org unit + optional programme + optional evaluation version).POST /skill-projection-jobs(Run Recalculation): Creates a job to backfill/recalculate for a scope and mapping set version (may enqueue work items).
6.2. User-Facing Endpoints (Overall vs Programme Context)
Programme overrides apply only when the view is requested in programme context (to avoid confusing “overall skill” changes).
-
GET /me/skills?view=overall(User Skills Profile – Overall): Uses tenant/org/sub-org defaults and evaluation/version overrides. -
GET /me/skills?view=programme&programmeId=...(User Skills Profile – Programme): Uses programme binding overrides (and any evaluation/version overrides) only within that programme view. -
GET /users/\{userId\}/skills/{skillId}/explain(Explain Skill Result): Returns:mapping_set_version_id,skill_version_idmapping_binding_id,applied_scope_type,applied_scope_id- matched
mapping_rule_ids - evidence submissions +
submission_score_version_idfor each
7. Compliance & Data Lifecycle
The Skills engine must integrate with Evalium’s compliance flows (Forget/Anonymize/DSAR). Forget/anonymize must delete or anonymize corresponding rows in:
skill_evidence_factsskill_rollupsskill_projection_jobs(or scrubuser_idfields)skill_work_queue(scrub payload user references where applicable)
8. Testing strategy
[ To be further detailed. Explicitly cover: mapping resolution precedence, idempotency, worker retry semantics, remediation propagation, and explainability provenance. ]
9. Open Questions & Risks
- Performance of projection worker on large datasets needs benchmarking.
skill_mapping_rules.selector_jsoncomplexity may require strict validation + tests.- Impact preview for draft mapping sets may require asynchronous calculation.
- Define definitive
submission_score_version_idFK target and remediation semantics for re-projection.