Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Feature ID: GR-18
Spec: GR-18 Competency Validation & Assessment Engine
Plan: GR-18 Implementation Plan
Version: 1.0
Status: 📝 Planned
Last Updated: 2026-04-24
Owner: GR (Governance & Compliance)
Constitution Reference: §1 Architecture — no direct core-to-core imports; cross-core via Platform Integration Layer and events.
Auto-created by validate-spec --auto-fix (2026-04-24) to satisfy the Integration Doc Existence requirement (constitution §2.1). Pre-filled from the Integration Points section of the spec; sections marked TODO require author review before Phase A migration ships.
Overview
GR-18 ships the competency assessment engine on top of GR-02 (Training & CEU Tracking). It owns assessments, attempts, responses, and the answer-key isolation pattern. It does not own courses, enrollments, or completions — those remain GR-02. Cross-core consumers (HR-22 transcripts, GR-19 in-service matrix) read assessment evidence exclusively through @/platform/training (PIL) and the assessment_passed / assessment_failed events.
Architectural pattern: Same-core writer (within GR) + PF-bus event publisher + PF Platform Integration Layer consumer. No direct core-to-core imports.
Related documents:
Integration Summary
| Integration | Pattern | From → To | Status |
|---|
| GR-02 completion creation on pass | Same-core RPC (intra-GR) | GR-18 → gr_complete_training (GR-02) — production signature (_enrollment_id UUID, _score INTEGER, _verification_method TEXT, _verifier_id UUID, _time_spent_minutes INTEGER) RETURNS JSONB per migration 20260211182655 | 📝 Planned |
| GR-02 completion revocation on void | Same-core RPC (intra-GR) | GR-18 → gr_revoke_training_completion (GR-02; stub added by GR-18 Phase A — function does NOT exist in current production schema) | 📝 Planned |
| GR-02-EN-01 event bus | Event Publisher | GR-18 publishes assessment_passed / assessment_failed on gr_events | 📝 Planned |
| GR-02-EN-02 Platform Integration Layer | PIL exports | GR-18 → @/platform/training (useStartAssessment, useSubmitAssessment, useAssessmentAttempt, useCompetencyEvidence) | 📝 Planned |
GR-02-EN-03 picklist value assessment | Picklist coordination | GR-18 depends on gr.training.verification_method enum value assessment (PF-15 picklist) | 📝 Planned |
| PF-30 permissions | Platform Layer | GR-18 reads gr.training.assessments.author / review / take | 📝 Planned |
| PF-08 forms | Platform Layer | GR-18 author UI uses ConfigurableForm primitives | 📝 Planned |
| PF-11 templated PDFs | Platform Layer | GR-18 surveyor evidence export via useGenerateTemplatedPdf (template competency_evidence) | 📝 Planned |
| GR-19 in-service matrix consumer | Event Consumer (downstream) | GR-19 subscribes to assessment_passed to color compliance cells | 📝 Planned |
| HR-22 LMS overlay reader | PIL read-only | HR-22 reads attempt evidence via @/platform/training (per ADR-014) | 📝 Planned |
| PF-96 jurisdiction profiles | Future enhancement | Not used in v1 (jurisdiction-neutral schema); future EN may resolve passing-score / max-attempt overrides | 📝 Deferred |
Events Published by GR-18
assessment_passed
- Channel:
gr_events (PF event bus; same pattern as GR-02-EN-01.training_completed)
- Publisher: GR-18 —
gr_submit_assessment_attempt SECURITY DEFINER RPC, on the same DB transaction as the grade write (at-least-once delivery; consumers MUST dedupe on attempt_id).
- Subscribers:
- GR-02 completion-gating logic (intra-RPC; creates
gr_training_completions row + CEU credit when linked course verification_method = 'assessment')
- GR-19 In-Service Compliance Monitor (optional; flips cell to “compliant” when course is assessment-gated)
- HR-22 LMS transcript surface (optional; refreshes employee transcript view)
- Purpose: Notify downstream consumers that an employee passed an assessment and (if applicable) a GR-02 completion has been created.
- Status: 📝 Planned
Payload Schema:
{
organization_id: string; // UUID — required for tenant routing
attempt_id: string; // UUID — gr_assessment_attempts.id (idempotency key)
assessment_id: string; // UUID — gr_assessments.id
course_id: string; // UUID — gr_training_courses.id (linked course)
enrollment_id: string; // UUID — gr_training_enrollments.id
employee_id: string; // UUID — hr_employees.id at attempt start
attempt_number: number; // 1-based; excludes voided priors
score_pct: number; // 0.00–100.00
passed: true; // always true for this event
submitted_at: string; // ISO 8601 timestamp
}
Security note: No PHI in payload. Question stems, response text, and answer keys are NEVER included. Payload is metadata only.
assessment_failed
- Channel:
gr_events
- Publisher: GR-18 —
gr_submit_assessment_attempt, same transaction as the grade write.
- Subscribers:
- GR-02 enrollment status updater (sets
status = 'failed' when attempt_number = max_attempts and last attempt failed)
- PF-10 notifications (TODO: register notification key for “retry available” / “no attempts remaining” messages)
- GR-19 (optional; surfaces failure in compliance monitor)
- Purpose: Notify downstream consumers that an employee failed an attempt; on final failure, the enrollment is marked
failed and an admin reset is required.
- Status: 📝 Planned
Payload Schema:
{
organization_id: string;
attempt_id: string;
assessment_id: string;
course_id: string;
enrollment_id: string;
employee_id: string;
attempt_number: number;
score_pct: number;
passed: false; // always false for this event
submitted_at: string;
attempts_remaining: number; // 0 when this was the final allowed attempt
cooldown_until: string | null; // ISO 8601 — when the next attempt becomes available
}
Security note: Same as assessment_passed. No PHI; no question content.
Events Consumed by GR-18
None. GR-18 publishes events; it does not consume events from other cores. (Future EN: may consume enrollment_overdue from GR-02-EN-01 to surface “due soon” CTAs in the assessment runtime.)
API Contracts
SECURITY DEFINER RPCs (Internal)
All GR-18 mutations go through SECURITY DEFINER functions; no direct table writes from clients.
| Function | Auth | Purpose |
|---|
gr_start_assessment_attempt(_assessment_id UUID, _enrollment_id UUID) RETURNS UUID | Caller must have gr.training.assessments.take AND own the enrollment (RLS-validated) | Create a new attempt; validate attempt_number < max_attempts against non-voided priors and cooldown elapsed against most recent non-voided submitted attempt; raise if random_count > active item count |
gr_submit_assessment_attempt(_attempt_id UUID, _responses JSONB) RETURNS JSONB | Caller owns the attempt (RLS) | Server-side grade via gr_grade_response; reject if now() > expires_at (sets status='expired'); publish assessment_passed / assessment_failed in same transaction; on pass + verification_method='assessment', call gr_complete_training |
gr_grade_response(_question_id UUID, _response JSONB) RETURNS BOOLEAN | Internal helper (called by gr_submit_assessment_attempt); never exposed to clients via REST | Compare response to correct_answer per item type without returning the answer to caller |
gr_void_assessment_attempt(_attempt_id UUID, _reason TEXT, _revoke_completion BOOLEAN DEFAULT true) RETURNS VOID | Caller must have gr.training.assessments.author (admin-only) | Set status='voided'; void does NOT count toward attempt_number or cooldown; if _revoke_completion = true and the voided attempt produced a completion, call gr_revoke_training_completion; write gr_assessment_reset_log row |
gr_reset_assessment_attempts(_enrollment_id UUID, _reason TEXT) RETURNS VOID | Caller must have gr.training.assessments.author | Allow employee to retry beyond max_attempts; write audit row in gr_assessment_reset_log |
gr_revoke_training_completion(_attempt_id UUID) RETURNS VOID | Internal helper called by gr_void_assessment_attempt (stub if not yet present in GR-02) | Reverse the GR-02 completion + CEU credit row created by the now-voided attempt |
Pattern: All RPCs use SECURITY DEFINER SET search_path = public. Tenant isolation enforced inside each function via pf_current_organization_id() check; never trust client-supplied organization_id.
REST/HTTP APIs
None. GR-18 has no Edge Functions or external HTTP endpoints in v1. All client interaction is via Supabase rpc() calls to the SECURITY DEFINER functions above, plus Supabase select() against the gr_assessment_questions_safe view (employee role) or full gr_assessment_* tables (author / review roles, RLS-gated).
GR-18 exposes its own surface through @/platform/training so cross-core consumers (HR-22, GR-19) never import directly from @/cores/gr/....
| PIL hook | Source | Consumers |
|---|
useStartAssessment(enrollmentId) | @/platform/training (re-export from GR-18 hook) | Employee runtime UI; HR-22 transcript “Take assessment” CTA |
useSubmitAssessment(attemptId) | @/platform/training | Employee runtime UI |
useAssessmentAttempt(attemptId) | @/platform/training | Employee runtime UI; admin attempt detail page |
useCompetencyEvidence(employeeId, courseId) | @/platform/training | Surveyor evidence view; HR-22 transcript drawer; GR-19 in-service matrix tooltip |
useEmployeeAssessmentAttempts(employeeId) | @/platform/training | HR-22 transcript; GR-19 monitor |
| Other PIL | Usage |
|---|
@/platform/forms (PF-08) | ConfigurableForm primitives for question authoring UI |
@/platform/documents (PF-11) | useGenerateTemplatedPdf({ template: 'competency_evidence', data }) for surveyor PDF export |
@/platform/permissions (PF-30) | useHasPermission('gr.training.assessments.author' | 'review' | 'take') and <RequirePermission> route guards |
@/platform/notifications (PF-10) | (TODO: confirm whether assessment retry-available notifications are in v1 scope) |
@/platform/wizards (PF-41) | WizardShell for the 4-step “Create assessment” flow (steps: details, bank link, items + scoring, course link) |
Permissions
Three new permission keys, registered in PF-30 via the Phase A seed migration AND added to src/platform/permissions/constants.ts:
| Permission | Format | Purpose |
|---|
gr.training.assessments.author | {module}.{entity}.{action} ✅ | Create / edit / retire question banks, items, assessments; admin reset/void; full-row SELECT on gr_assessment_questions (incl. correct_answer) |
gr.training.assessments.review | {module}.{entity}.{action} ✅ | Read attempt evidence; export surveyor PDF; full-row SELECT on gr_assessment_questions (incl. correct_answer) for preview only |
gr.training.assessments.take | {module}.{entity}.{action} ✅ | Start/submit attempts; SELECT only on gr_assessment_questions_safe view (no correct_answer column exposed) |
Audit: npm run audit:permissions MUST pass (typed entries match seed migration).
Picklists
Depends on GR-02-EN-03 which adds the value assessment to the gr.training.verification_method picklist.
| Picklist | Value | Owner | Coordination |
|---|
gr.training.verification_method | assessment | GR-02-EN-03 | Coordinated landing — if EN-03 slips, GR-18 Phase A migration adds the value inline via ALTER TYPE … ADD VALUE IF NOT EXISTS 'assessment' |
Cross-Core Reads (via PIL only — no direct imports)
GR-18 reads no other core’s tables directly. Cross-core touch points:
- HR (
hr_employees) — referenced by gr_assessment_attempts.employee_id FK. Read via existing GR-02 patterns (already established as same-core dependency for GR core’s training tables). No new HR PIL hook required.
- GR-02 tables (
gr_training_courses, gr_training_enrollments) — same core; FK references. Not cross-core.
Tenant Isolation & Security
- Every GR-18 table includes
organization_id (defense-in-depth).
- RLS enabled on all 7 tables (6 entity + 1 audit log) with SECURITY DEFINER helpers
pf_current_organization_id(), pf_user_has_permission(), pf_current_employee_id() (no recursive RLS — constitution §5.7).
- Answer-key isolation pattern: employee role has NO direct SELECT policy on
gr_assessment_questions; reads go through gr_assessment_questions_safe view (omits correct_answer + rationale).
- Server-side time enforcement on submit; client timer is advisory only.
- Attempts + responses are immutable after submission (no UPDATE policy); admin void via SECURITY DEFINER + audit-log row.
- Event payloads contain UUIDs and metadata only — no question stems, no response text, no PHI.
Failure Modes
| Failure | Behavior |
|---|
Submit after expires_at | RPC marks attempt status='expired'; no event published; UI shows “time elapsed — please contact your administrator” |
random_count > active item count at start | gr_start_assessment_attempt raises with deterministic message: “Assessment requires N items but only M are active in the linked bank.” |
GR-02 gr_complete_training fails on pass | Inspect the JSONB return: if success = false AND error = 'Training already completed', treat as non-error retry (do not re-publish assessment_passed). For any other failure, the whole gr_submit_assessment_attempt transaction rolls back via RAISE EXCEPTION; attempt remains in_progress; client receives error; retry idempotent on attempt_id (protected by SELECT ... FOR UPDATE row lock on the attempt) |
| Event publish fails | Wrapped in same DB transaction as the grade write — if publish fails, grade write rolls back; at-least-once delivery means consumers MUST dedupe on attempt_id |
gr_revoke_training_completion not yet shipped in GR-02 | Phase A includes a stub with RAISE NOTICE 'GR-02 revoke RPC not present; completion will need manual revoke' and writes the gr_assessment_reset_log row regardless |
Validation Checklist
Per docs/architecture/integrations/CONTRACT_VALIDATION_CHECKLIST.md:
References