Examulator — Changelog
Consolidated release history for the Examulator solution (both product generations, all supporting projects). This file supersedes the previous per-generation Examulator/CHANGELOG.md and Examulator3/CHANGELOG.md files (which were maintained as byte-identical mirrors and are removed as of 26231.01-closed).
Entries are ordered newest-first. Each release heading carries a scope line naming the components touched by that release; individual subsections may narrow that scope further via a project-list tag under the subsection heading.
Product components at a glance
Gen2 (com.certificationtoolanddie.examulator) — Examulator (PC/web) · ExamulatorCore (shared logic) · ExamulatorMaui (Android) · ExamulatorApi (Contribute Results endpoint) · ExamulatorDiag · ExamulatorTrace · ExamulatorUpdater · ExamulatorDiagnostics · ExamulatorTests · ExamulatorIrt (shared with Gen3)
Gen3 (com.certificationtoolanddie.examulator3) — Examulator3 · Examulator3Core · Examulator3Maui · Examulator3Api · Examulator3Diag · Examulator3Trace · Examulator3Diagnostics · Examulator3Tests
The four surfaces referenced by many entries are: PC Gen2 (Examulator), PC Gen3 (Examulator3), MAUI Gen2 (ExamulatorMaui), MAUI Gen3 (Examulator3Maui).
[26232.01-rc1] — 2026-08-20 (current)
Scope: Both generations, all four surfaces.
Adds a new Raw Question Review report (Tier 4 — Study Tools) and clears the compile-time breakage that surfaced after building 26228.01 against a fresh checkout. All changes mirrored across both product generations and all four surfaces (PC Gen2, PC Gen3, MAUI Gen2, MAUI Gen3).
Feature — Raw Question Review (Tier 4 — Study Tools)
ExamulatorCore · Examulator3Core · Examulator · Examulator3 · ExamulatorMaui · Examulator3Maui
A study-focused report — the first entry in a new Tier 4 "Study Tools" catalog band — that walks the user through questions across multiple sessions in the same detailed layout as the exam-result question review. Not an analysis: no charts, no scores, no aggregates. Purpose is re-drilling weak spots after several practice sessions have accumulated.
Configure screen additions. After picking an exam and sessions, an Include Questions section with four checkboxes: All questions (a check-all shortcut), Answered correctly, Answered incorrectly, Flagged for review. Filter semantics are a union — a question is included if it matches any selected category, so Answered correctly + Flagged for review surfaces every correct question plus every marked-for-review question that was answered incorrectly. Generate is gated on both ≥1 session and ≥1 filter category. The All checkbox and its three sub-boxes stay bidirectionally in sync (checking all three flips All on; toggling All flips the three).
Output screen. Sessions in chronological order, each with a heading (session index, date, mode, score/pass, matched-question count) followed by the matched questions in the order the user encountered them. Each question card mirrors the HistoricalResults render: status badge (correct/incorrect/unscored), review flag, domain, question text, answers with ✓/✗ markers and correct/wrong highlighting, and — on Gen3 — per-answer explanations, correct-answer explanation, and Further reading key reference.
Data path. New ResultsRepository.GetDetailsAsync(sessionGuids, username) batch loader loops the existing GetDetailAsync per session and returns the fully-enriched HistoricalSession list (question text, answers, explanations, marked-for-review), which ReportBuilder.BuildRawQuestionReview filters via a new RawQuestionFilter [Flags] enum (None / Correct / Incorrect / MarkedForReview / All).
PDF export. Uses the standard ReportPdfExportService path; renders each question in a two-column table (colored status stripe + content cell) so the block stays visually grouped across page breaks. Long reports (hundreds of questions) page naturally under MigraDoc. The BuildAsync signature gained an optional RawQuestionFilter rawFilter = RawQuestionFilter.All parameter — appended after Gen3's existing agency argument so no existing call site breaks.
Gen2 ↔ Gen3 renderer. Rather than fork the render code per generation, the Output pages (PC + MAUI) and the PDF service use small reflection helpers to resolve HistoricalQuestion.KeyReference, .CorrectExplanation, and HistoricalAnswerChoice.Explanation — properties present on Gen3, absent on Gen2. Absent properties resolve to empty strings, so the same markup renders on both.
Answer-required invariant. The exam UI (both PC and MAUI, both Linear/CAT and Quiz) does not allow advancing without a selected answer, and neither timer expiry (ExamSessionService.ApplyRootRule — RunOutOfTime) nor abort force-save the pending question, so IsAnswered is invariant on persisted rows. Marked-for-review therefore always implies answered. The filter still guards IsAnswered defensively.
DI. ResultsRepository was already registered as Scoped in every composition root (Examulator/Program.cs, Examulator3/Program.cs, both MauiProgram.cs), so the new ReportPdfExportService constructor dependency and the PC Reports/OutputModel dependency wire up automatically with no Program.cs changes.
Persistence between Configure and Output. PC threads rawFilter through TempData["ReportRawFilter"] alongside ReportKind / ReportExamCode / ReportSessions / ReportAgency; MAUI threads it through the URL query as &rawFilter=N. Both surfaces refresh cleanly.
Styling. Four new classes (.raw-filter-group, .raw-filter-item, .raw-session-heading, .raw-session-meta, .raw-empty-note, .raw-filter-summary) added byte-identically to site.css (both PC gens) and app.css (both MAUI gens), following the existing gold-accent visual language.
Catalog. Tier switches in Catalog.cshtml / Catalog.razor now map 4 => "Study Tools".
Fix — Razor tag-helper attribute conflict on <body> (_Layout.cshtml)
Examulator · Examulator3
.NET 10 treats <body> as a tag-helper element and rejects inline @(...) expressions in its attribute declaration area (RZ1031). The accessibility layer was emitting seven conditional data-a11y-* attributes with per-attribute expressions of the form @(!string.IsNullOrEmpty(x) ? $"data-a11y-x=\"{x}\"" : ""), which failed to compile.
Initial workaround concatenated the attributes in the top-level @{ } block and emitted them via <body@Html.Raw(bodyAttrs)>, which cleared RZ1031 but left the tag-helper parser unable to recognize the <body> start tag, so the matching </body> at file end triggered RZ1034 ("malformed body tag helper").
Correct fix: idiomatic ASP.NET tag-helper null-attribute pattern — give each attribute a proper name="@(cond ? value : null)" form. When the expression evaluates to null, the tag helper omits the attribute entirely (no empty data-a11y-theme="" rendered), no C# sits in the attribute-declaration area, and the tag remains well-formed. Also silences the four spurious CS0219 "assigned but never used" warnings on the a11yComfort / Motion / Focus / Timer locals — they were used, but the failed body-tag parse had hidden that from the analyzer.
Documentation — @ReadMe.md fully rewritten
@ReadMe.md
The distribution README had drifted badly since v26195.02-alpha (May 2026) — most of the document described a solution structure and roadmap that had been superseded by four to six months of shipped work. Rewritten from scratch to reflect the current product.
User-facing sections rewritten: - Supported Certifications — all nine ISC² certifications marked as Available (previously CC / CCSP / CGRC / CISSP / CSSLP available with SSCP / ISSAP / ISSEP / ISSMP as coming soon). Added a Native delivery mode column (CAT vs Linear per Pearson VUE) alongside Examulator's supported modes for each certification. New ISACA (via Examulator3 — in development) subsection. - Multi-platform framing — front matter now leads with "desktop/web AND Android"; previously implied desktop-only. Android installation subsection added with both Play Store package IDs (com.certificationtoolanddie.examulator for Gen2, com.certificationtoolanddie.examulator3 for Gen3). - Features — Reports subsystem added with all four tiers listed (Core / Advanced / Deep Dive / Study Tools including Raw Question Review); Accessibility program summarized (WCAG 2.1 AA target, ten settings, screen-reader chart tables, extended time, flag notes); Contribute Results named correctly (previously "Anonymous data sharing") and cross-referenced to Privacy-Policy.md §5.3; Diagnostic export mentioned. New Gen3 additions subsection covers per-answer explanations, key references, and the multi-agency schema. - Configuration table — verified against current appsettings.json; kept.
Developer-facing sections rewritten: - Solution Structure — was 6 projects (Examulator / ExamulatorCore / ExamulatorIrt / ExamulatorDiag / ExamulatorTrace / ExamulatorApi / ExamulatorTests), now correctly shows the current 18 projects grouped by Shared / Gen2 / Gen3. Root-namespace convention explained. - Tech Stack — added .NET MAUI Blazor Hybrid, MigraDoc/PdfSharp with the bundled Roboto font, Chart.js (both reportCharts.js and reportChartsMaui.js), and updated framework version. - Build Requirements — added MAUI workloads (dotnet workload install maui-android) and the AndroidTargetSdkVersion 36 Play Store deadline (Aug 31 2026). - Local Development — new note that Directory.Packages.props is the source of truth for NuGet versions and that per-project PackageReference versions must not be edited. - Data Model — new short section describing the exam DB vs Results.db split and the per-generation Results.db isolation. - Release Process — new short section explaining the YYDDD.VV-channel version format.
Sections removed: - Phase 2 Migration Notes (for developers merging v26195.02-alpha) — historical migration guidance for a release cycle four+ months in the past; no longer relevant. - MAUI Android Port (in progress) — MAUI ships as both Gen2 and Gen3 apps; the "in progress" framing was long stale. - Contact section expanded from a single mike@ address to all five published addresses (feedback@, contribute@, accessibility@, privacy@, mike@).
Infrastructure — Version bumped to 26232.01-rc1 (12 sites)
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui · ExamulatorDiag · Examulator3Diag · ExamulatorTrace · Examulator3Trace
Suffix changed from -closed to -rc1 for Google Play Early Access (open testing) submission. Standard 12-site bump. MAUI <ApplicationVersion> = 2623201 (26232 × 100 + 1). No display-format changes.
Infrastructure — Version bumped to 26231.01-closed (12 sites)
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui · ExamulatorDiag · Examulator3Diag · ExamulatorTrace · Examulator3Trace
Standard 12-site bump. MAUI <ApplicationVersion> = 2623101 (26231 × 100 + 1). No display-format changes.
[26228.01-closed] — 2026-08-16
Scope: Both generations, all four surfaces.
Major milestone release, versioned for Play Store closed-beta submission. Headline addition is the opt-in Contribute Results feature that anonymously transmits IRT calibration data to help refine question item parameters. Also lands a comprehensive accessibility program targeting WCAG 2.1 AA (settings, screen-reader chart tables, extended-time accommodation, flag notes — all four surfaces), the Exam Reports subsystem (three tiers of multi-session analysis), per-answer explanations and key references on Gen3, per-session kebab actions on the Single Exam Report configure screen (all four surfaces), MAUI action-sheet kebab pattern, collapsible Help sections on the web, two-column splash layout, build-timestamp via AssemblyMetadataAttribute reflection, returnUrl propagation for back-navigation, CSS contrast improvements across the web app, and a substantial bug-fix pass.
Feature — Contribute Results (opt-in IRT calibration data)
Examulator · ExamulatorCore · ExamulatorMaui · Examulator3 · Examulator3Core · Examulator3Maui · ExamulatorApi
Optional, opt-in feature that lets users transmit an individual exam result to a central calibration API to help refine question item parameters (discrimination, difficulty, guessing). Disabled by default; requires the user to request an API URL from contribute@certificationtoolanddie.com and paste it into a Setup screen.
Two-page flow. /Upload/Setup accepts the pasted URL (which contains the user's UUID API key as a query parameter) and validates its shape. /Upload/Confirm shows exactly which fields will be transmitted, offers a "Contribute" button and a Return button, and includes an Automatically contribute all future results checkbox the user can toggle any time.
Anonymous payload. Transmits only IRT parameters, per-question response outcomes, ability estimate (theta), timing, and session-level score/pass/termination. Explicitly excludes username, question text, answer text, explanations, key references, and any data from other sessions. Uses the existing ExamulatorApi POST /exam-results contract with X-Api-Key header (the key is stripped from the URL and travels as a header so it stays out of web-server logs).
Duplicate protection. The client stores an uploadedAt timestamp per session in Results.db (new column added via safe idempotent ALTER TABLE), and the server's existing IngestionService uses INSERT OR IGNORE on sessionGuid for idempotence. The confirmation screen shows "✓ Previously contributed on {date}" when applicable.
Settings storage. New SettingsRepository persists the API URL and auto-upload flag in a Settings table in Results.db, auto-created on first access.
Entry point. A "📤 Contribute Results" button on the bottom action bar of every HistoricalResults page (both PC and MAUI, both gens) opens the Confirm page for the currently-viewed session.
Privacy policy updated to add Section 5.3 documenting the feature in full, and Section 10 explaining the API-key-only deletion path.
Feature — Accessibility program (WCAG 2.1 AA target)
Examulator · ExamulatorCore · ExamulatorMaui · Examulator3 · Examulator3Core · Examulator3Maui
Comprehensive accessibility push spanning screen-reader support (Phase 1), low-vision and motion accommodations (Phase 2), cognitive aids (Phase 3), and motor accommodations (Phase 4). All four surfaces. Companion public statement in Accessibility-Statement.md.
Settings & persistence. SettingsRepository extended with ten a11y.* keys (theme, fontSize, readingFont, readingComfort, reducedMotion, enhancedFocus, hideTimer, extendedTime, flagNotes, largeTapTargets), persisted in the Settings table of Results.db.
Accessibility Settings page — all four surfaces. PC /Settings/Accessibility (both gens) with Visual / Reading / Timing / Review Aids / Motor sections, plus a new MAUI Blazor equivalent at the same route (both gens) using native label-wrapped checkboxes for full keyboard and TalkBack support. ♿ icon in the top nav on all four surfaces. A lightweight /Accessibility public statement page ships on PC.
Attribute plumbing. PC _Layout.cshtml reads settings and emits data-a11y-* attributes on <body> plus a skip-to-content link. MAUI adds a window.examA11y JS bridge in index.html and a new A11yInterop service; MainLayout applies settings on first render and the settings page re-applies them instantly on save — no restart needed.
CSS foundation (all 4 stylesheets). .sr-only and .skip-link utilities, light and high-contrast themes, large/x-large font scaling, Arial/Verdana/Tahoma reading fonts, reading-comfort typography, reduced motion (opt-in plus prefers-reduced-motion respect), enhanced :focus-visible rings, timer hiding, and — MAUI only — data-a11y-tap="large" rules enforcing 44 px minimum touch targets (WCAG 2.5.5 AAA) on buttons, answer rows, chips, selects, and nav links.
Chart table fallbacks (Reports). Every Reports chart was previously opaque to screen readers. All 8 <canvas> charts on the Reports Output page (both PC gens, both MAUI gens) now carry role="img" + descriptive aria-label, and 7 of them get a sibling sr-only data table rendered from the same view-model data that feeds Chart.js (score progression, domain trends, radar, difficulty mix, difficulty trends, pacing trend, pacing scatter summary; the weak-domains chart is covered by its adjacent visible ranked list). The Domain × Difficulty heat map gained a caption, proper scope headers, row headers, and per-cell sr-only "X of Y correct" text.
Extended-time accommodation (score-invalidating, clearly labeled). a11y.extendedTime (1.5× / 2×) is now consumed at session creation for Linear and CAT modes on all four surfaces via ExamSessionService.ApplyExtendedTime, multiplying both per-question and whole-exam limits so pacing scales too. The factor persists to Results.db (Results.extendedTimeFactor, schema v5 on Gen2 / v6 on Gen3) and both Results and Historical Results display a prominent "Extended time (N×) — score not comparable" banner for affected sessions. Quiz mode is untimed by default and unaffected.
Flag notes (cognitive aid). With a11y.flagNotes enabled, every flagged and answered question on Results and Historical Results (all four surfaces) offers a 500-character note field ("Why did you flag this question?"). Notes persist to a new ResultQuestions.reviewNote column via SetReviewNoteAsync / GetReviewNotesAsync; PC saves through a SaveNote AJAX handler with antiforgery protection, MAUI writes through the repository directly. Saved notes remain visible read-only if the setting is later switched off.
Form label fix (MAUI Upload/Setup). The API URL input on Upload/Setup.razor (both MAUI gens) now uses a real <label for="apiUrlInput"> instead of a plain <div>, matching the PC surface.
Non-native clickable fixes. MAUI QuizConfig check-rows (Timing / Difficulty / Domain / Pool — 7 rows) converted from <div @onclick> to the <label> + sr-only native <input type="checkbox"> pattern, giving full keyboard and TalkBack semantics. MAUI SelectMode mode cards (Quiz / Linear / CAT) converted from <div @onclick> to <button type="button"> with a UA-reset CSS block. PC SelectExam install cards and PC Reports/Configure session rows gained role="button" + tabindex="0" + Enter/Space onkeydown handlers; install cards also got a descriptive aria-label.
Emoji aria-hidden wrapping. Leading decorative emoji on buttons and labels across Results, Historical Results, and Question pages wrapped in <span aria-hidden="true"> so screen readers read the button text only.
Screen-reader fixes. Removed aria-label="Option A" overrides that hid answer text from AT; labeled the last unlabeled kebab menus and the Exam History search input; converted the question-count slider title to a real <label>; timer milestone announcements (10 m / 5 m / 1 m / 30 s / expiry) via role="status" live region without second-by-second chatter; answer-reveal announcements ("Correct. The answer is option A."); <PageTitle> added to 14 MAUI pages × 2 gens so TalkBack announces navigation.
Feature — Per-answer explanations & key references (Gen3-only)
Examulator3 · Examulator3Maui · Examulator3Core
The Gen3 exam schema carries a rationale on every answer choice — a full explanation for the correct answer and a distractor rationale for each wrong one — plus a per-question key reference (a further-reading citation). This release completes the UI rendering:
Results and Historical Results (PC + MAUI): each choice shows its own explanation inline. A "Further reading" key-reference line follows. When a question only carries the legacy single explanation, the previous correct-answer block is shown instead.
Live question view — Quiz mode (PC + MAUI): on "View Answer", if the user selected a wrong answer the card shows both the correct-answer explanation and a red-bordered distractor rationale explaining why their specific choice was wrong. Back-navigated reviewed questions show the same pair. Other distractors are not shown live — only the one the user actually chose. All four rationales remain available in post-exam Results review.
Key reference appears on the explanation card on reveal/review.
Report/results PDF export already emitted these fields; on-screen and exported content now match.
Gen2 is unchanged — it uses a single question-level explanation model.
Feature — Diagnostic export schema v3 (Gen3-only)
Examulator3Diagnostics · Examulator3Trace
DiagDoc.KnownSchemaVersion bumped "2" → "3". New fields on QuestionDetail:
KeyReference — further-reading citation from Questions.keyReference.
AnswerExplanations — per-answer pedagogical text fetched from the Answers table, keyed by answer GUID. Null when the exam DB was unavailable at export time.
DiagDocBuilder, SessionSnapshot, and DiagnosticExportService updated to populate and map the new fields. Gen2 stays at schema "2".
Hardening — Diagnostic export schema guard
ExamulatorTrace · Examulator3Trace
Both Trace tools now check a loaded diagnostic file's schemaVersion before rendering it, refusing (with a clear message) any document produced by a newer schema. A blank/absent version is treated as legacy v1 and still accepted. KnownSchemaVersion must be bumped in lockstep between the Diagnostics library and the Trace reader whenever the DiagDoc shape gains fields.
Feature — Exam Reports (three tiers of multi-session analysis)
Examulator · ExamulatorCore · ExamulatorMaui · Examulator3 · Examulator3Core · Examulator3Maui
A new Reports entry (📊, replacing the old Results nav link on MAUI) opens a report catalog; each report leads to a configuration screen where the user picks an exam and a set of sessions, then to a generated report with interactive charts.
Tier 1 — Single Exam Report: the classic single-session detail view, reached through the Reports catalog with the same interactions as Exam History.
Tier 2 — Domain Trends: per-domain scaled score over the selected sessions, with proficiency threshold overlay and colour-coded pass/fail bands.
Tier 3 — Comprehensive Analysis: aggregate summary combining domain trends, difficulty distribution, and time-per-question analysis across the selected sessions.
The ReportPdfExportService (both Core projects) produces a printable PDF matching the on-screen report. DomainTrendsViewModel.Series (was .Domains) fix applied when the per-domain aggregator was refactored.
Feature — Multi-session PDF export
ExamulatorCore · Examulator3Core
ReportPdfExportService renders reports covering many sessions to a single PDF using empira PdfSharp/MigraDoc. Roboto font is bundled so output is consistent across host platforms. Called from the Reports Output page on both PC and MAUI.
Feature — Single Exam Report row kebab (all four surfaces)
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
On the Single Exam Report configure screen the multi-select checkbox list is replaced with clickable rows in the history-item visual style used by Exam History. Tapping a row navigates directly to that session's Historical Results view — no separate "Generate Report" step is needed for a single-selection report. A per-row ⋮ kebab exposes the same actions available from the Exam History kebab: View, Export PDF, Delete (with confirmation), and Export Diagnostic. Multi-session reports retain the checkbox + Generate Report pattern unchanged.
PC uses a shared floating context-menu positioned relative to the clicked kebab button, populated by a small vanilla-JS handler.
MAUI uses a bottom-sheet action panel rendered at the page root level with a semi-transparent backdrop. This escapes the per-row stacking context that would otherwise cause the menu to be clipped beneath subsequent rows.
The Last 5 / Last 10 / All quick-pick buttons and the "N selected" counter are hidden on the Single Exam Report screen since the report by definition requires exactly one session.
Feature — Return URL for HistoricalResults back button
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
Both PC page models gained a [FromQuery] ReturnUrl parameter (default /ExamHistory) and both MAUI HistoricalResults gained [SupplyParameterFromQuery] ReturnUrl. All back buttons and post-delete navigation now respect the ReturnUrl query parameter. Callers from the Single Exam Report configure screen pass returnUrl=/Reports/Configure/0, so Back returns to the report selection screen rather than defaulting to Exam History. Callers from Exam History omit the parameter and get the previous behaviour.
Feature — Build timestamp via reflection
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
Splash screens now show "Built {timestamp} UTC" beneath the version stamp. The timestamp is embedded in each entry-point assembly at compile time via an MSBuild AssemblyMetadata item, then read back at runtime via Assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false). Works on Android where Assembly.Location is empty because the attribute is embedded in the binary metadata, not resolved from disk. No appsettings.json maintenance required.
Feature — Collapsible Help sections (web)
Examulator · Examulator3
All sections on the Help page now render collapsed by default with only the numbered title row visible. Clicking any header expands or collapses that section. A ▼ chevron rotates 90° when collapsed. Implementation is CSS-only display toggling driven by a three-line querySelectorAll listener — no framework overhead.
Feature — Web splash — 2-column layout
Examulator · Examulator3
The splash screen now uses a two-column layout at viewports ≥820px: logo on the left, Important Notice card + footer stacked vertically on the right, vertically centred against the logo's height and horizontally centred within the right column. Mobile viewports fall back to the original single-column stack.
Feature — Reports catalog entry for Exam History
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
"View Exam History" now renders as a full-width report-card entry in the Reports catalog, matching the width and visual style of the tier cards above it. On MAUI the 📋 Exam History icon has been removed from the splash screen and the top navigation bar — all history access flows through the Reports catalog.
Enhancement — MAUI Historical Results kebab
Examulator3Maui · ExamulatorMaui
HistoricalResults (the detail view for one session) gained its own ⋮ kebab in the header with Export PDF, Delete (with red confirmation bar), and Export diagnostic actions. Injections and delete-navigation identical to the Exam History kebab pattern.
Enhancement — MAUI Historical Results — difficulty by level collapsible
Examulator3Maui · ExamulatorMaui
The "By Difficulty Level — all domains" section moved from above the per-domain breakdown to below it, immediately before the Question Review divider. It is now collapsed by default with a ▼/▲ toggle in the same visual style as the Question Review toggle. Bottom bar "← Back to History" and "← History" labels simplified to "← Back".
Enhancement — Web quiz options readability & contrast
Examulator · Examulator3
Two CSS-variable-level contrast fixes:
--text-dim boosted from #3f5570 (~2.4:1 on card background) to #6080a2 (~4.6:1 — passes WCAG AA for small text).
--text-muted boosted from #7a95b0 to #8ab0cc (~6.5:1) for stronger small-label contrast.
Downstream effects (no markup changes needed): the "All" chip when no filter is applied, Time Remaining / Question # labels in the exam header, the "How this score was computed" title on results, metadata lines throughout the app, and the review objective strip all pick up the improved contrast automatically. .fc-label in Quiz Config (Difficulty, Domains, Question Pool labels) upgraded from --text-dim to --text-muted. The "10 (minimum)" and "N (full bank)" hint spans under the Question Count slider changed from text-dim to text-muted with the font size bumped from .78rem to .8rem.
Enhancement — Question triband icons removed (web)
Examulator · Examulator3
The clock icon next to "Time Remaining" and the question-mark icon next to "Question #" in the exam header removed; each stat block now goes directly label → value. The label row wrappers also removed since they only existed to hold the icons.
Fix — QuizConfig agency route regression (Gen3)
Examulator3 · Examulator3Maui
Gen3 QuizConfig was calling GetExamByCodeAsync(examCode) and GetDomainsAsync(examCode) with a single argument, but the Gen3 repository signatures require (agency, examCode). PC route changed from {code} to {agency}/{code}; MAUI route changed from /QuizConfig/{Code} to /QuizConfig/{Agency}/{Code}; both page models gained an Agency parameter and all repository calls updated. Missing using for ExamSession namespace added to both QuizConfig.cshtml.cs files.
Fix — MAUI Gen2 Configure agency drift (regression)
ExamulatorMaui
The Gen2 MAUI Configure page was accidentally carrying Gen3-specific AgencyCode references (exam.AgencyCode, dropdown value {agency}|{examCode}, filter comparing both fields). Reverted to the Gen2 signatures: dropdown value is exam.ExamCode alone, filter matches on ExamCode only, GetSessionsAsync called with the 3-arg Gen2 signature.
Fix — Razor @{ } regression in .NET 10
Examulator · Examulator3
The .NET 10 Razor compiler rejects @{ var x = ...; } code blocks placed between HTML elements inside an @if { } else { } body (RZ1010). Fixed by moving affected variable declarations (notes in Reports/Output.cshtml, reviewWrongExpl in Exam/Question.cshtml) to the top-level @{ } block at the head of each page.
Fix — Razor tag-helper attribute conflict on <body> (_Layout.cshtml)
Examulator · Examulator3
.NET 10 Razor treats <body> as a tag-helper element and rejects inline @(...) expressions in its attribute declaration area (RZ1031). The accessibility layer was emitting seven conditional data-a11y-* attributes with per-attribute @(!string.IsNullOrEmpty(x) ? $"data-a11y-x=\"{x}\"" : "") expressions, which failed to compile. Fixed by building the attribute string in the top-level @{ } block via StringBuilder and emitting it with @Html.Raw(bodyAttrs) on the <body> tag. This also silences the CS0219 "assigned but never used" warnings for the seven a11y* locals (they were used, but the failed body-attr parse hid that from the compiler).
Fix — DiagErrorBoundary ChildContent naming conflict
Examulator3Maui · ExamulatorMaui
In .NET 10 Blazor, using <ChildContent>@ChildContent</ChildContent> as an explicit slot inside <ErrorBoundary> when the outer component also declares [Parameter] public RenderFragment? ChildContent causes the source generator to emit a conflicting local variable (CS0841 / CS7014 cascade). Fixed with a private accessor _body => ChildContent referenced in the template. Also fixed literal newlines inside '\n' and "\n" char/string literals in the TrimStack method (CS1010 / RZ1000 / RZ1027 errors) with proper \n escape sequences.
Fix — ReportPdfExportService DomainTrends series name
ExamulatorCore · Examulator3Core
RenderDomainTrends iterated vm.Domains but DomainTrendsViewModel exposes per-domain data as Series (List<DomainTrendSeries>). Fixed in both Core projects.
Fix — SessionQuestion.Explanation → CorrectExplanation in Results review
Examulator3
Results.cshtml was referencing the non-existent q.Explanation in the legacy fallback block. Changed to q.CorrectExplanation (the computed property that returns the correct answer's explanation from the answers list).
Fix — File(inline: true) overload does not exist
Examulator3
PageModel.File() has no inline named parameter. Dropped to the two-argument overload File(pdf, "application/pdf") in HistoricalResults.OnPostExportPdfAsync — the browser displays inline by default when no Content-Disposition: attachment header is present.
Fix — Nullable analyser q.Answers in MAUI Question review
Examulator3Maui
Added a (q.Answers ?? []) fallback in the displayWrongExpl computation to satisfy the nullable analyser (CS8604), even though HistoricalQuestion.Answers is initialised to new().
Infrastructure — Version bumped to 26228.01-closed (12 sites)
Examulator · ExamulatorMaui · ExamulatorDiag · ExamulatorTrace · Examulator3 · Examulator3Maui · Examulator3Diag · Examulator3Trace
12 sites updated in lockstep: 4 appsettings.json (Version + Channel), 2 MauiProgram.cs fallback line pairs, 2 Diag/Program.cs (Version + VersionNumber const strings), 2 Trace/TraceInfo.cs (Version const), 2 MAUI .csproj (ApplicationVersion code = 2622801, ApplicationDisplayVersion = 26228.01-closed). Gen3 metadata (appsettings + csproj) advanced from placeholder 0.0.1-dev to full alignment with Gen2's release number for the first time.
Documentation — Privacy Policy expanded for Contribute Results
Section 5 gained a comprehensive new subsection describing the Contribute Results feature in full — how it is enabled, the exact fields transmitted, what is explicitly excluded, how the data is used, and how to disable or delete contributed data. Section 10 gained API-key-based deletion instructions. Sections 8, 9, 11, and 13 updated to reference the feature where relevant. Privacy-Policy.html now included at the root of the source zip alongside @ReadMe.md and both CHANGELOG-*.html files.
[26226.01-closed] — 2026-08-14
Scope: Both generations, all four surfaces.
Feature-heavy release: diagnostic-export workflow with a new shared library, kebab menu on the Exam History screen, print-preview page, and a click-the-exam-name Abort Exam mechanism. Also includes browser back-button protection on PC and a fix for a beforeunload false-positive that only surfaced in timed-quiz mode.
Feature — Diagnostic export workflow
The standalone ExamulatorDiag tool's DiagDoc-generation logic is now extracted into a shared library that both the standalone tool and the running application consume. Users can export a diagnostic JSON directly from the Exam History screen without needing to run the desktop tool.
ExamulatorDiagnostics (new class library, both generations)
Two new class-library projects: ExamulatorDiagnostics (namespace Examulator.Diagnostics) and Examulator3Diagnostics (namespace Examulator3.Diagnostics). Each references only ExamulatorIrt.
Contains the DiagDoc schema previously duplicated inside the standalone Diag tools: SessionSummary, QuestionDetail, DomainSummary, DiagnosticSummary, IrtTraceStep, plus input types (SessionSnapshot, QuestionSnapshot) that any producer populates and hands to the shared factory.
DiagDocBuilder.Build(session, questions, trace, version, supportEmail) is the single entry point. The ExamulatorVersion field is now runtime-passed rather than a hardcoded const — standalone Diag passes its build-time constant, the app passes the currently running App:Version + App:Channel combination, so every export self-identifies with the exact producing build.
IrtTraceBuilder.Build is a straight port of Diag's IrtEngine.BuildTrace. Algorithm and math delegated to ExamulatorIrt unchanged, so live-engine and diagnostic-tool output remain bit-for-bit reproducible.
Added SchemaVersion field to DiagDoc for future evolution — consumers should reject documents whose SchemaVersion is greater than what they understand.
Both libraries nested under their respective solution folders (Generation 2 / Generation 3) in Examulator.sln.
ExamulatorDiag standalone tool refactored
ExamulatorDiag/Models.cs and ExamulatorDiag/IrtEngine.cs removed; their content now lives in the shared library.
Database.cs rewritten to return library snapshot types directly instead of Diag-private row types.
Program.cs rewritten to call DiagDocBuilder.Build(...) and IrtTraceBuilder.Build(...) from the shared library. The standalone tool is now a thin CLI wrapper — new diagnostic features should land in the library, not here.
Same refactor applied to Examulator3Diag.
DiagnosticExportService (new, in ExamulatorCore + Examulator3Core)
App-side entry point for the "Export diagnostic" kebab action. Loads session and question rows via targeted SQL (bypasses HistoricalQuestion, which doesn't carry the IRT-relevant fields), populates snapshots, invokes the shared library, and returns (byte[] json, string filename) ready for download (PC) or share (MAUI).
Filename convention matches the standalone Diag tool: {ExamCode}_{Mode}_{Date}_{PASS|FAIL|UNKNOWN}_DIAG.json.
Gen3 version injects ExamRepository to resolve the exam's agency (Gen3 CreateExamConnection requires the (agency, examCode) pair, and Results.db doesn't carry agency). Falls back to "ISC2" if the master catalog is unavailable.
Registered as Scoped in all four startup projects.
Feature — Exam History kebab menu
Each row on the Exam History screen gets a ⋮ button on the right, opening a four-item floating menu:
👁 View — same as tapping the row (opens Results detail).
🖨 Print — navigates to the new print-preview page.
🗑 Delete — invokes the existing confirmation flow.
📤 Export diagnostic — builds the DiagDoc JSON and delivers it: PC downloads via File(bytes, "application/json", filename); MAUI hands to the Android Share sheet via Share.Default.RequestAsync with a ShareFile attachment. Users can email, save to Files/Drive, or attach to a message from the share sheet.
Row-tap behavior unchanged — still opens Results detail directly. Consistent affordance across PC and MAUI on both generations.
Feature — Print preview page
New page at /Exam/PrintPreview?sessionGuid=... for all four surfaces:
Paper-optimised layout: Georgia serif, muted borders, no navigation chrome, page-break avoidance on questions and headings.
"Include question review" toggle above the printable content — user can uncheck to get just the score + domain breakdown, avoiding a 30-page printout when only the summary is wanted.
Auto-fires window.print() after a 400 ms layout stabilisation delay.
On Android, window.print() hands off to the system print framework, giving real Save-as-PDF, connected printers, and share targets without any platform-specific PrintManager code.
@media print rules hide the toolbar during actual printing.
PC uses Layout = null with a standalone HTML doctype so site chrome doesn't leak into the printed output.
MAUI uses [SupplyParameterFromQuery] for route-parameter binding — Blazor idiomatic and needs no Microsoft.AspNetCore.WebUtilities reference.
Feature — Abort Exam
Discoverable way to abandon an in-progress exam without saving results, on all four surfaces.
Clicking (PC) or tapping (Android) the exam name in the upper-left of the question screen opens the abort dialog. Subtle hover/press opacity and a focus-visible outline confirm interactivity — no visible chrome added at rest.
Timer pauses while the dialog is open. On Continue Exam, ExamSession.StartedAt shifts forward by the exact pause duration so Elapsed stays continuous — works whether the dialog is open for 5 seconds or 10 minutes.
On PC, the client posts the pause duration to a new OnPostResume handler that compensates the server-side session.
Confirming discards the in-memory session and clears HttpContext.Session (PC) or deletes the checkpoint file (MAUI). Nothing is written to Results.db — the session is never SaveAsync-ed, so no partial rows leak.
Dialog copy: "Abort Exam? The results will be discarded." with a muted subtitle noting the timer is paused. Focus lands on Continue Exam by default so Enter/Space triggers the safe action.
Question state (selected answer, review flags) preserved on cancel — the dialog is a UI overlay, not a page unmount.
Escape key dismisses the dialog on PC (same as Continue).
FAQ entry added on all four Help surfaces documenting the mechanism.
Feature — Diag --version prints numeric-only
The standalone ExamulatorDiag --version and Examulator3Diag --version flags now print just the numeric portion (e.g. 26226.01) without the channel suffix. Build scripts embedding the version in a ZIP filename get a clean value without needing to strip the suffix themselves. The full display version continues to be used in the banner and embedded in DiagDoc exports.
Fix — Browser back button / tab close on PC
Prior to this release, hitting the browser's Back button, closing the tab, or refreshing the page during an exam silently abandoned the session with no confirmation.
window.onbeforeunload on the Question page now triggers the browser's native Leave site? confirmation for browser back, tab close, or refresh.
Suppressed via an _allowUnload flag for intentional form posts (Next / Back / Reveal / Abort / Resume / TimeExpired) so normal answering doesn't prompt.
No data-integrity risk in either case — nothing is written to Results.db until session completion, so an abandoned session leaves no partial rows.
MAUI (Android) hardware back button was already handled correctly via the existing LocationChangingHandler.
Fix — beforeunload guard false-positive on Next button (PC, timed exams)
Reported by user: "User does quiz, unchecks the Untimed Quiz option. Gets to question 1, answers, clicks Next and gets a browser warning 'Leave site? Changes will not be saved'."
Root cause: HTMLFormElement.submit() called programmatically does NOT fire the form's submit event. Only user-initiated submissions (clicking a <button type="submit">, pressing Enter in a text input) fire that event. This is a well-documented HTML/DOM quirk.
The timed-exam Next button uses <button type="button" onclick="submitAnswer()"> because it needs JS validation before submitting. submitAnswer() calls document.getElementById('answerForm').submit() directly — bypassing the submit event entirely. The _allowUnload bypass flag was being set by a form.addEventListener('submit', ...) listener that never fired, so beforeunload correctly saw an "unarmed" navigation and prompted.
The untimed-non-quiz path used <button type="submit"> on the answer form which naturally fires the submit event, so that path worked fine — which is why the bug only surfaced when the Untimed Quiz option was disabled.
Fix:
submitAnswer() now calls armAllowedNavigation() explicitly before .submit().
Added a click-listener on every <button type="submit"> as belt-and-braces for the natural-submit case.
Kept the old form.addEventListener('submit', ...) listener as an additional safety net for Enter-key submits — costs nothing.
Infrastructure — Per-project publish scripts removed
Every code project had a publish.cmd / publish.sh pair used for early ad-hoc publish scripting. Superseded by the top-level publish-aab.ps1 workflow (for MAUI Play Store submissions) and standard dotnet publish invocations for the other projects, so the per-project scripts were adding clutter without adding value. All 16 have been deleted (8 Gen2 + 8 Gen3 projects).
Infrastructure — Gen3 project rename to Examulator3{Module} convention
All Gen3 projects moved from the legacy ExamulatorX3 suffix form to the consistent Examulator3X prefix form:
Old name New name ---------------— ------------------ ExamulatorCore3 Examulator3Core ExamulatorMaui3 Examulator3Maui ExamulatorDiag3 Examulator3Diag ExamulatorApi3 Examulator3Api ExamulatorTests3 Examulator3Tests ExamulatorTrace3 Examulator3Trace
Each rename touched:
Project directory + .csproj filename
AssemblyName in every csproj
RootNamespace (except Examulator3Core, which retains Examulator3 as its shared root — mirrors Gen2's convention where ExamulatorCore uses Examulator as its root)
Every namespace declaration and using directive in .cs and .razor files
XAML x:Class and clr-namespace attributes
Every <ProjectReference> path in every csproj
Examulator.sln project paths + solution-folder nesting under "Generation 3"
Comments and docstrings referencing the old names
Zero legacy references remaining anywhere in the Gen3 tree after the rename.
[26223.02-closed] — 2026-08-11
Scope: Both generations, all four surfaces.
Consolidation release. Beta-line bug fixes, closed-testing track preparation, and copy corrections.
Fix — Abandon Exam button unresponsive (Android)
ExamulatorMaui · Examulator3Maui
LocationChangingHandler was firing recursively on confirm-abort. Fixed by adding an _exitConfirmed flag that short-circuits the handler on user-initiated abort.
Feature — Splash-screen version stamp
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
PC and MAUI splash screens render Version {version} read at runtime from App:Version + App:Channel. Gracefully absent if keys are missing.
Fix — Data-sharing disclaimer copy
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
Help, About, and FAQ updated to lead with "does not share any information" and mark the future IRT-upload feature as "Under development — not currently active".
Fix — ISC² CAT overclaims removed
Examulator · ExamulatorMaui · Examulator3 · Examulator3Maui
Help copy now claims only the class of technique without asserting identity with ISC²'s operational system. Linear mode card no longer describes itself as "paper-based".
[26222.10-alpha] — 2026-08-10 (Gen3-only)
Scope: Examulator3 · Examulator3Maui · Examulator3Core (Gen3-only).
Examulator3 · Examulator3Core · Examulator3Maui
Foundational Gen3 agency schema, repository signature refactor, and download-hang fix ported from Gen2. New Agencies table; Exams.agencyGuid FK; AgencyRepository; agency-tab UI on SelectExam. File-naming conventions locked. Every per-exam method in Examulator3Core now takes (agency, examCode).
[26222.02-alpha] — 2026-08-10 (Gen3 fork established)
Scope: Gen3 fork established — Examulator3 · Examulator3Core · Examulator3Maui · Examulator3Diag · Examulator3Trace · Examulator3Api · Examulator3Tests · Examulator3Diagnostics.
Examulator3 · Examulator3Core · Examulator3Maui · Examulator3Diag · Examulator3Trace · Examulator3Api · Examulator3Tests
Gen3 forked from Gen2 checkpoint 26221.17-alpha. Namespaces renamed (Examulator.* → Examulator3.*); fresh ApplicationId and ApplicationTitle (Examulator3). Schema additions: Question.KeyReference, Answer.Explanation, AnswerChoice.Explanation, Exam.AgencyCode, Exam.AgencyGuid. Removed: Question.Explanation (replaced by per-answer explanations on the correct choice). DatabaseFactory takes (agency, examCode) on every per-exam method.
[26221.17-alpha] — 2026-08-09 (Gen2 checkpoint — immutable pre-Gen3 baseline)
Scope: Examulator · ExamulatorCore · ExamulatorMaui and supporting projects — immutable Gen2 checkpoint; Gen3 projects did not yet exist.
Examulator · ExamulatorCore · ExamulatorMaui
Snapshot taken immediately before the Gen3 fork. Preserved as the immutable reference baseline; always included in handoff zips. All entries below are Gen2 only.
[26220.06-alpha] — 2026-08-08
Scope: Examulator · ExamulatorCore · ExamulatorMaui (Gen2 only — pre-Gen3 fork).
Examulator · ExamulatorCore · ExamulatorMaui
Closed-testing push. Quiz History filter corrected for mixed Unanswered + Answered selections. Central Package Management adopted (Directory.Packages.props); CentralPackageTransitivePinningEnabled = false; CentralPackageFloatingVersionsEnabled = true.
[26216.02-alpha] — 2026-08-04
Scope: Examulator · ExamulatorCore · ExamulatorMaui (Gen2 only — pre-Gen3 fork).
Examulator · ExamulatorCore · ExamulatorMaui
Quiz Config difficulty filter chips — restrict the question pool to selected difficulty bands. Question history filters — Unanswered Only / Include Incorrect / Include Correct / Include Marked-for-Review; shared BuildHistoryFilterAsync service. MAUI question-count slider — replaces numeric input, steps by 5.
[26216.01-alpha] — 2026-08-04
Scope: Examulator · ExamulatorCore · ExamulatorMaui (Gen2 only — pre-Gen3 fork).
Examulator · ExamulatorCore · ExamulatorMaui
Exam History detail page — tapping a row opens paginated question-by-question review with answers and explanations. MAUI session-persistence checkpoint — written on every answer submit, restored on cold-start if the user was mid-exam.
[26182.01-alpha] — 2026-07-01
Scope: Examulator · ExamulatorCore · ExamulatorMaui (Gen2 only — pre-Gen3 fork).
Examulator · ExamulatorCore · ExamulatorMaui · ExamulatorIrt
IRT-powered CAT mode — full 3PL IRT: Newton–Raphson MLE theta, maximum-information item selection, three-rule termination (CI boundary, max questions, time), scaled score anchored at θ = 0 → 700. Linear exam mode — stratified random selection across domains weighted by published ISC² percentages, difficulty-balanced allocation per domain.
[26167.01-alpha] — 2026-06-16
Scope: Examulator · ExamulatorCore · ExamulatorMaui (Gen2 only — pre-Gen3 fork).
Examulator · ExamulatorMaui · ExamulatorCore
Quiz mode — untimed (or timed) with immediate answer reveal, Mark for Review, and Feedback flags. Back-navigation through answered questions. Database download and installation — Google Drive–hosted exam DB ZIP download, extraction, and placement on both PC and MAUI. Progress UI on both surfaces.
[26160.01-alpha] — 2026-06-09
Scope: Examulator · ExamulatorCore (Gen2 only — pre-Gen3 fork; MAUI port not yet in tree).
ExamulatorMaui
MAUI Blazor Hybrid app scaffolded — net10.0-android, shared Razor components, IDataPathProvider abstraction. autostart="false" on blazor.webview.js required to prevent Blazor starting before the MAUI native handler is ready.
[26155.01-alpha] — 2026-06-04 (Initial alpha release)
Scope: Examulator (Gen2 only — initial alpha; single-project solution).
Examulator · ExamulatorCore · ExamulatorIrt
PC web application — ASP.NET Core 10 Razor Pages. SelectExam, SelectMode, Question (CAT / Linear / Quiz), Results, Exam History. ExamulatorCore — SQLite via SourceGear.sqlite3, DatabaseFactory, repository layer. ExamulatorIrt — 3PL IRT math: raw-to-float conversion, P(θ), Newton–Raphson MLE, maximum-information selection. Shared by PC, MAUI, Diag, and unit tests.