Evalium Skills & Competency Engine (Backend Design + BFF API Ideas)
1) Purpose and scope
Build a flexible, audit-proof Skills/Competency Engine that can:
- infer “competence” from immutable submission snapshots (Knowledge now; Observation/Evidence later),
- support retroactive mapping + recalculation when SMBs start tagging late,
- provide UI-ready read models so the frontend doesn’t do heavy lifting.
This stays aligned with the KOE concept: competence is a view layer over Evaluations + Programmes, not a separate HR module.
2) Architectural constraints we should lean on (already in Evalium)
Security & scoping
- PostgreSQL RLS is the security boundary, set via TxManager
SET LOCALGUCs (tenant_id,org_unit_id,org_scope_all). - All DB access must go through TxManager; no pool-level queries.
Audit-proof measurement
- Everything versioned and immutable, and
version_snapshotis canonical for historic reporting: never re-join live authoring tables when interpreting past attempts. - Submissions freeze tags, scoring settings, navigation, evaluation metadata, org scope, etc.
Known gap we can “pull forward”
- Aggregated reporting is not implemented yet and is planned via projection/materialized view layer. The Skills engine can be the first “real” projection consumer.
3) Core model (hybrid + retroactive mapping)
Key objects
-
Skill A global capability you track (“Breathing”, “Safe Lifting”, “Medication Handling”).
-
Competency-trusted tagging (governed) Separate from free author tags. Your matrix already notes tag conventions exist but validation is light—this is where you add guardrails.
-
Mapping Set (versioned) A governed “interpretation lens” that defines how Skills are inferred from evidence. Crucially: this is how you support “we forgot to tag; recalc old data” without mutating immutable versions/snapshots.
-
Evidence Facts (projection) A flattened, query-friendly table generated from submission snapshots + mapping sets (and later observation/evidence states).
4) Data model proposal (backend-focused)
This is intentionally aligned with Evalium’s versioning philosophy.
A) Skills
-
skills(identity) -
skill_versions(immutable definition)- name, description, optional level scheme, status (draft/published)
- (optional) “framework affinity” metadata if you want domain grouping
B) Competency tag dictionary (trusted taxonomy)
competency_tag_keys(e.g.,skill,level,framework)competency_tag_values(e.g.,breathing,safe_lifting,level:3,framework:healthcare_2005)- This enables permission-gated creation/editing of trusted values while still allowing free tags on questions.
C) Mapping sets (the hybrid selector engine)
-
skill_mapping_sets(identity) -
skill_mapping_set_versions(immutable ruleset) -
skill_mapping_rules(rows under a version)-
rule_typeenum:tag_predicate(matches frozen structured tags inversion_snapshot)evaluation/evaluation_version(strictness knob)question_version(precision backfill knob)framework(stable governance knob; recommended)
-
selector_json(predicate payload; keep it explicit and parseable) -
target_skill_id, contribution strategy (“score threshold”, “average”, “best-of N”, “pass/fail”, etc.) -
precedence: specific beats general (question_version > evaluation_version > evaluation/framework > tag_predicate)
-
D) Projection tables (read models)
You’ll likely want two layers:
- Skill Evidence Facts (atomic, auditable)
- keyed by: tenant/org/user/submission + skill + “evidence unit” (item/tag aggregation)
- fields like: contribution value, pass/fail, weight, evidence source (K/O/E), mapping_set_version_id, computed_at
- Skill Rollups (UI fast path)
-
per user per skill:
- status (not started / in progress / achieved)
- last updated time
- confidence/coverage
- last evidence summary
-
per cohort/team:
- distribution counts and gap lists
These sit naturally under the “reporting projection layer” you already planned.
E) Jobs (recalc/backfill)
A small job table similar in spirit to your existing worker patterns (reaper/privacy/remediation).
skill_projection_jobs: typerecalc, date range, mapping_set_version_id, org scope, status/progress- store “result summary” for UX (rows affected, coverage delta, etc.)
5) Projection & consistency rules
When to (re)compute
- On submission finalisation (new evidence arrives)
- parse
version_snapshot(canonical) and generate facts for active mapping set versions.
- On remediation apply/revert
- remediation can change scores/outcomes; derived skill facts must be updated accordingly. Your remediation is idempotent and score history is append-only—good foundation for deterministic reproject.
- On mapping set publish
-
triggers a targeted backfill job:
- “recalc last 12 months”
- “recalc for evaluation X”
- “recalc for org unit Y”
-
this is the main SMB “we started late” unlock.
Audit posture
Every derived record should reference:
submission_id(and optionallysubmission_score_version_id)mapping_set_version_id- “evidence basis” (which selector/rule matched)
That preserves defensibility without rewriting immutable snapshots.
6) Backend-for-Frontend API ideas (so UI does minimal work)
Your frontend shouldn’t be joining tables, computing rollups, or implementing rule logic. The backend should ship view-model endpoints.
Below are BFF-style endpoints (names are illustrative) with the shape the UI needs.
A) Skills Library (admin view)
GET /api/v1/skills
Returns a UI-ready list:
- skill name, status, last computed
- evidence coverage (% of submissions/items mapped)
- “needs recalculation” flag (mapping published since last compute)
- counts: users covered, users achieved, users missing evidence
Why: this becomes the Skills landing page without extra queries.
B) Skill detail (admin view)
GET /api/v1/skills/{skillId}
- skill definition (current version)
- active mapping set version summary
- coverage breakdown by evidence type (K/O/E) (future-proof to KOE)
- top cohorts/org units impacted
GET /api/v1/skills/{skillId}/evidence-sources
- resolved mapping rules in priority order
- “effective selector” preview (human readable + machine form)
C) Mapping set editor (governed workflow)
POST /api/v1/skill-mapping-sets/\{id\}/versions (create draft version)
POST /api/v1/skill-mapping-sets/\{id\}/versions/\{versionId\}/validate
Return:
- rule validation errors (unknown competency tags, invalid selectors, conflicting rules)
- warnings (broad tag predicate may overcount; missing scope in regulated mode)
This aligns with your existing pre-publish validation pattern on evaluations.
POST /api/v1/skill-mapping-sets/\{id\}/versions/\{versionId\}/impact-preview
Return a “dry run” summary so the UI can show:
- submissions affected
- users newly “achieved”
- users who lose evidence (if you ever allow downgrades)
- coverage delta
(Your feature matrix already calls out “impact preview” as a deferred-but-known concept in other workflows; this is a similar UX value.)
POST /api/v1/skill-mapping-sets/\{id\}/versions/\{versionId\}/publish
- publishes the mapping version and triggers recalc job options.
D) Recalculation jobs (progress UX)
POST /api/v1/skills/recalculate
Body includes:
- scope: org unit(s), evaluation/evaluation_version, date range, mapping_set_version Returns:
- job id + estimated affected partitions (no heavy client polling logic)
GET /api/v1/skills/recalculate/jobs/{jobId}
- status, progress, summary metrics
E) Person competence profile (consumption view)
GET /api/v1/users/\{userId\}/skills-profile
Return a complete profile card model:
- skill list (status, last evidence date, score/confidence/coverage)
- “missing evidence” reasons (e.g., “no mapped items in last 12 months”)
- quick drill links (submissions that contributed)
This matches the KOE doc’s “competence profile view” intent.
GET /api/v1/users/\{userId\}/skills-profile/{skillId}/evidence
- timeline of evidence facts with friendly labels (evaluation title, run_label, etc.)
Assignments already carry
run_labelto support cohort reporting semantics.
F) Cohort/team skill overview (later, but design now)
GET /api/v1/skills/overview with filters:
- org unit, group/cohort, date window, job role, etc. Returns:
- heatmap-ready matrix (skill × group) with counts + percentages
- “top gaps” list
Your reporting matrix explicitly ties cohort/group views to flattened reporting tables—this endpoint is the BFF face of that.
G) authoring helpers (so authors see “competency coverage” instantly)
GET /api/v1/evaluations/\{id\}/versions/\{versionId\}/competency-coverage
Return:
- % items with competency-trusted tags
- which skills appear (from tags)
- warnings: missing competency tags for items that otherwise have free-form tags
This keeps authoring UX “smart” without pushing logic into the client (and matches your “workflow intelligence in authoring” goal).
7) Permissions & enforcement
Introduce capabilities consistent with your existing model (atomic caps + Require(...) per route).
Suggested caps:
skills.read(view profiles/rollups)skills.manage(create/edit skill definitions)skills.mapping.manage(create/publish mapping sets)skills.taxonomy.manage(manage competency tag dictionary)
And keep org scoping consistent with the platform’s silo rules.
8) Compliance & retention (don’t accidentally break your GDPR posture)
You already have strong compliance workflows (forget/hybrid scrub, DSAR export, retention incidents). Derived skill facts are derived personal data, so:
- forget/retention jobs must also remove/anonymise rows in
skill_evidence_factsand rollups, - DSAR export should include competence profile (or explicitly omit it by policy).
This is important because your compliance subsystem is already “real” (jobs + ledger + worker + receipts).
9) Testing strategy (backend confidence without UI)
- Invariant tests: computing skills from
version_snapshotdoes not query live authoring tables. - RLS tests: skill projections respect tenant/org scope the same way submissions/reporting do.
- Remediation propagation tests: apply/revert updates skill facts deterministically (idempotency-friendly).
- Job idempotency tests: repeated recalc with same parameters results in stable outputs.