The complete HR Training backend

FeatureDocsService
Shipped
August 11, 2026 at 5:10 PM UTC
Author
Kamo
Commit
82ab45a

Course authoring, publishing, assignment, the member player, server-side grading, WORM evidence and the reminder sweep — the whole module behind /api/docs/training. AUTHORING. Courses carry an ordered list of items where a TEST is a PEER of a UNIT, never a child: that one decision lets a single model express both "a test at the end of each training show" and "a final exam" without a second structure. Content saves are replace-set because the editor owns the whole ordered list; a per-row patch API needs a delete verb plus a reorder, which is three round trips and three chances for the outline the author sees to diverge from the one stored. PUBLISHING freezes an immutable version that assignments pin. Every rule a member is held to is copied onto it IN THE SAME STATEMENT that writes the row — the legal module declared those columns and left them to a later caller that never came, which is why every legal version in production carries a null attestation. TRFP1 is length-prefixed rather than delimiter-joined so a crafted value cannot imitate a field boundary, and isCorrect is INSIDE the hash: the legal module shipped LPFP1 without the equivalent field, publish answered "nothing has changed", and a corrected layout could never be frozen. Publish refuses with a 409 naming the offending item — including a media slide with no alt text, because the authoring tool is the only enforcement point that actually holds. GRADING IS SERVER-SIDE, and this is a security property rather than a feature. The player is served **************** which are different classes from the author's DTOs and simply have no correctness field to populate — a shared class with a nulled field comes back the first time somebody reuses a mapper, and the leak is silent while the compliance record keeps looking valid. Shuffle and the question draw are computed here and recorded on the attempt, so a resumed session sees the same questions; a client-side shuffle over a client-held answer key is theatre. Correct answers come back only when show-answers permits it for that attempt, so a member cannot fail deliberately to harvest the key. Partial credit is floored at zero (without it, selecting everything scores negative and drags down the rest of the test). Percent is rounded half-up exactly once so the results screen, the email, the certificate and the CSV cannot disagree about 79.5% against an 80% bar. A course with no tests passes on completion — possible == 0 must never divide and must never mean "failed". EVIDENCE. TRMF1 (what they saw) and TRQZ1 (what they answered) are stored as digests AND verbatim canonical strings, because a digest nobody can recompute from stored evidence is not evidence. The attestation is written for every terminal outcome, pass or fail: "they took it and did not pass" is exactly as much a compliance fact as passing. REMINDERS sweep ORG-MAJOR and fire each org at its OWN local hour. Organization.timezone spans 21 hours, so the legal sweep's ZoneId.systemDefault() tells a Sydney tenant "due today" on the wrong day. The bucket stays a pure function of (dueDate, today), which is what makes Extend Due Date free and the sweep replayable; catch-up sends only the most recent unsent rung after an outage and records the older ones as superseded. Idempotency is a real table with a unique claim index rather than a Redis SET NX, so "did we tell Ada on 4 March" survives a FLUSHDB and is queryable — the table legal's own javadoc says is right and deferred. A dry run claims NOTHING; claiming would burn every bucket it inspected and suppress the real run behind it. DocsInternalAuthFilter gains the training internal path. Without it the cross-org sweep is world-reachable, because ResourceServerConfig is anyRequest().permitAll() and OTKPreAuthFilter continues the chain when no OTK is present. 455 tests pass.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing