Skip to content

feat(compliance-audit): apply wedge core spec via declarative schemas (closes #25) - #33

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feature/wedge/compliance-audit
May 11, 2026
Merged

feat(compliance-audit): apply wedge core spec via declarative schemas (closes #25)#33
rubenvdlinde merged 1 commit into
developmentfrom
feature/wedge/compliance-audit

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

  • Regulation schema (9 fields + lifecycle): draft → published → archived; 4 cross-schema aggregations (mandatoryEnrolledCount, mandatoryCompletedCount, attestationCount, validCredentialCount); 2 materialised calculations (coveragePercent, ragStatus with per-regulation RAG thresholds); 2 notifications (officerAlertOnCoverageDrop on calculatedChange: ragStatus → red, onPublished); 2 declarative widgets (coverageGrid, boardProof) consumed by the Compliance dashboard.
  • Attestation schema (12 fields + appendOnly: true): lifecycle drafted → signed → revoked; sign transition declares requires: OCA\Scholiq\Lifecycle\AttestationSigningGuard; 3 relations (learner/course/lesson). Immutability enforced by OR's appendOnly abstraction per ADR-022.
  • No ComplianceCampaign schema — per design §1.3, campaigns are sets of Enrolment objects sharing regulationSlug + bulkJobId; no separate schema needed at this stage.

PHP (ADR-031 legitimate exceptions only)

  • lib/Lifecycle/AttestationSigningGuard.php — single check() method: (a) queries OR for XapiStatement with verb.id in {completed, passed} for (learnerId, lessonId); if absent returns false (OR returns 422); (b) calls AuditTrailService::getCurrentTenantKey($tenantId) for the HMAC key; if unavailable fails hard per spec; (c) builds canonical JSON payload (sorted keys, signature/signingKeyId/lifecycle excluded), computes hash_hmac('sha256', ...), injects signature + signingKeyId into $transitionContext['payload'].
  • lib/Controller/AuditPackExportController.php — single export() method: receives {regulationSlug, dateFrom, dateTo}, calls AuditTrailService::query() (OR auto-emits compliance.audit_pack.exported), calls AuditTrailService::verifyChain(), builds 4-file ZIP (audit-trail.ndjson, audit-trail.csv, manifest.json per ADR-008 §6, signature-verification.txt), streams as application/zip.

Manifest entries (6 new pages)

  • Regulations — OR index of Regulation schema
  • RegulationDetail — OR detail with auditTrail tab (immutable evidence log surface = OR built-in)
  • Attestations — OR index, readOnly: true (appendOnly schema)
  • AttestationDetail — OR detail, readOnly: true, with auditTrail tab
  • AuditPackExport — custom page wired to AuditPackExportModal Vue component
  • Compliance dashboard — two widget-ref entries pointing at Regulation.x-openregister-widgets.coverageGrid and .boardProof

Audit-pack approach

Thin PHP controller (ADR-031 document-generation exception). All heavy lifting delegated to OR's AuditTrailService. AuditPackExportModal.vue POSTs to /api/compliance/audit/export and triggers a client-side Blob download.

Deviations from 100% declarative

Two files only, both per spec's explicit ADR-031 exceptions:

  1. AttestationSigningGuard — HMAC-SHA256 cryptographic operation + lifecycle-guard exception
  2. AuditPackExportController — ZIP/document generation exception; OR has no built-in multi-format ZIP export endpoint

Not shipped (per design §2)

AttestationService, CoverageComputationService, AuditPackExportService, HmacKeyService, ComplianceHmacRotationJob, EvidenceLogService, ComplianceController, AttestationController, scholiq-audit-event schema, AuditedController, ComplianceCampaignService — replaced by declarative schema extensions or consumed from OR.

Test plan

  • lib/Settings/scholiq_register.json validates against OR schema-extension contract
  • Unit: AttestationSigningGuard::check() — missing xAPI completion returns false; present completion returns true with non-empty signature in payload
  • Integration: AuditPackExportController::export() — seed 20 entries, call export, assert 4 files + manifest.event_count = 20
  • Playwright: AuditPackExportModal — select regulation + dates, click Export, assert download triggered
  • composer check:strict and npm run lint passing in CI

🤖 Generated with Claude Code

…closes #25)

- Add Regulation schema (9 fields, lifecycle draft→published→archived, 4 aggregations,
  2 calculations coveragePercent+ragStatus, officerAlert+onPublished notifications,
  coverageGrid+boardProof widgets) to scholiq_register.json
- Add Attestation schema (12 fields, appendOnly:true, lifecycle drafted→signed→revoked
  with AttestationSigningGuard on sign transition, 3 relations) to scholiq_register.json
- Create lib/Lifecycle/AttestationSigningGuard.php — single check() method: verifies
  cmi5.completed xAPI statement for (learnerId,lessonId) then computes HMAC-SHA256
  using OR tenant-key API; injects signature+signingKeyId into transition payload
- Create lib/Controller/AuditPackExportController.php — single export() method;
  queries OR audit-trail, builds 4-file ZIP (ndjson+csv+manifest+verification.txt),
  streams as Content-Disposition attachment; ADR-031 document-generation exception
- Extend src/manifest.json: Regulations index, RegulationDetail (with auditTrail tab),
  Attestations index (readOnly), AttestationDetail (readOnly), AuditPackExport custom
  page, Compliance dashboard with 2 widget-ref entries pointing at Regulation widgets
- Add src/views/AuditPackExportModal.vue: regulation dropdown + date pickers + POST
  to /api/compliance/audit/export → Blob download; registered via manifest customComponents
- No ComplianceCampaign schema (design §1.3: campaigns are Enrolment sets, not a schema)
- No CoverageComputationService, no AuditPackExportService, no RegulationController,
  no ComplianceCampaignService (all replaced by declarative schema extensions)
@rubenvdlinde
rubenvdlinde force-pushed the feature/wedge/compliance-audit branch from 4c47682 to 1063f3e Compare May 11, 2026 21:55
@rubenvdlinde
rubenvdlinde merged commit 4932b93 into development May 11, 2026
19 of 28 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/scholiq @ 650e7ae

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-05-11 21:57 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request May 12, 2026
…m manifest to nc-vue v1.4.0; drop dead scaffold files (#40)

## Root cause

All 5 Wave-2 worktrees were branched off the same pre-Wave-2 base, so the sub-agents
worked blind to each other. The dashboard agent wrote intentional STUB Enrolment +
Regulation schemas ("declare a stub if it doesn't exist yet"). When the dashboard PR
was cherry-picked LAST onto a development that already had #31's full Enrolment and
#33's full Regulation, git's line-based JSON merge kept the stub (the schemas were
declared at different file positions, so no textual conflict — git appended both and
`json.load` resolved the duplicate key to the stub). The merge-resolver script was
only re-run on manifest.json, not scholiq_register.json.

## Fixes

- **Restore full Enrolment schema** (10 props + x-openregister + lifecycle pending→active→completed|withdrawn|failed + relations learner/course + calculations isOverdue/daysRemaining/ragStatus + notifications welcomeOnActivate/completionOnComplete/reminderT30/T7/T1/managerAlertOnOverdue) and merge in the dashboard's myMandatoryTraining widget.
- **Restore full Regulation schema** (12 props + x-openregister + lifecycle draft→published→archived + aggregations mandatoryEnrolledCount/mandatoryCompletedCount/attestationCount/validCredentialCount + calculations coveragePercent/ragStatus + notifications officerAlertOnCoverageDrop/onPublished) and merge widgets coverageGrid+boardProof+attestationCount.
- **Move `appendOnly: true` to top level** on Credential, XapiStatement, Attestation — OR's Schema::hydrate() only reads top-level `appendOnly` (it calls setAppendOnly()); when nested inside `x-openregister` the flag was silently dropped, so these schemas were NOT actually append-only.
- **Conform src/manifest.json to nc-vue manifest schema v1.4.0**: removed `visibleIf` from pages, `public` from CredentialVerify, fixed dashboard widgets to {id,title,type:'custom'}, stripped non-schema keys (href, visibleIf) from the viewInMydash action. `tests/validate-manifest.js` now PASSes (was FAIL with 14 errors).
- **Delete dead scaffold files**: src/router/index.js, src/views/Dashboard.vue, src/navigation/MainMenu.vue (none imported anywhere), lib/Controller/DashboardController.php (no route).

Follow-ups: nc-vue needs `visibleIf` page predicates, `public` pages, and `widget-ref` page-content (umbrella #200) before the dashboard can be fully declarative; until then the dashboard pages are conformant-but-minimal.
@rubenvdlinde
rubenvdlinde deleted the feature/wedge/compliance-audit branch August 14, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant