Skip to main content

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

IntegrationPatternFrom → ToStatus
GR-02 completion creation on passSame-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 voidSame-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 busEvent PublisherGR-18 publishes assessment_passed / assessment_failed on gr_events📝 Planned
GR-02-EN-02 Platform Integration LayerPIL exportsGR-18 → @/platform/training (useStartAssessment, useSubmitAssessment, useAssessmentAttempt, useCompetencyEvidence)📝 Planned
GR-02-EN-03 picklist value assessmentPicklist coordinationGR-18 depends on gr.training.verification_method enum value assessment (PF-15 picklist)📝 Planned
PF-30 permissionsPlatform LayerGR-18 reads gr.training.assessments.author / review / take📝 Planned
PF-08 formsPlatform LayerGR-18 author UI uses ConfigurableForm primitives📝 Planned
PF-11 templated PDFsPlatform LayerGR-18 surveyor evidence export via useGenerateTemplatedPdf (template competency_evidence)📝 Planned
GR-19 in-service matrix consumerEvent Consumer (downstream)GR-19 subscribes to assessment_passed to color compliance cells📝 Planned
HR-22 LMS overlay readerPIL read-onlyHR-22 reads attempt evidence via @/platform/training (per ADR-014)📝 Planned
PF-96 jurisdiction profilesFuture enhancementNot 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.
FunctionAuthPurpose
gr_start_assessment_attempt(_assessment_id UUID, _enrollment_id UUID) RETURNS UUIDCaller 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 JSONBCaller 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 BOOLEANInternal helper (called by gr_submit_assessment_attempt); never exposed to clients via RESTCompare 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 VOIDCaller 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 VOIDCaller must have gr.training.assessments.authorAllow employee to retry beyond max_attempts; write audit row in gr_assessment_reset_log
gr_revoke_training_completion(_attempt_id UUID) RETURNS VOIDInternal 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).

Platform Integration Layers Used

GR-18 exposes its own surface through @/platform/training so cross-core consumers (HR-22, GR-19) never import directly from @/cores/gr/....
PIL hookSourceConsumers
useStartAssessment(enrollmentId)@/platform/training (re-export from GR-18 hook)Employee runtime UI; HR-22 transcript “Take assessment” CTA
useSubmitAssessment(attemptId)@/platform/trainingEmployee runtime UI
useAssessmentAttempt(attemptId)@/platform/trainingEmployee runtime UI; admin attempt detail page
useCompetencyEvidence(employeeId, courseId)@/platform/trainingSurveyor evidence view; HR-22 transcript drawer; GR-19 in-service matrix tooltip
useEmployeeAssessmentAttempts(employeeId)@/platform/trainingHR-22 transcript; GR-19 monitor
Other PILUsage
@/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:
PermissionFormatPurpose
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.
PicklistValueOwnerCoordination
gr.training.verification_methodassessmentGR-02-EN-03Coordinated 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

FailureBehavior
Submit after expires_atRPC marks attempt status='expired'; no event published; UI shows “time elapsed — please contact your administrator”
random_count > active item count at startgr_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 passInspect 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 failsWrapped 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-02Phase 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:
  • Event payloads documented with TypeScript schema (above)
  • No PHI in event payloads (verified above)
  • All RPCs marked SECURITY DEFINER SET search_path = public
  • Tenant isolation pattern documented (above)
  • Permission keys registered in PF-30 seed migration AND src/platform/permissions/constants.ts
  • PIL hooks exported from @/platform/training (no direct cross-core imports)
  • Cross-core matrix entry added to CROSS_CORE_INTEGRATIONS.mdTODO
  • Event contracts registered in EVENT_CONTRACTS.mdTODO

References