Re-baselined 2026-09-05 against the code as built. The November 2025 version of this document recorded design intent; 14 of its 20 checkable claims no longer matched
src/(seedocs/PROJECT_STATE.md§4). Section numbering is unchanged so existing references still resolve. Where something was designed but not built, this document says so.
- Defines how the interactive quiz and analysis features are built, packaged, and integrated into DITA‑published HTML.
- Targets offline,
file://deployments on constrained, air‑gapped machines. - Out of scope: procurement, authoring workflows, detailed UX copy, server integration.
- Runtime: Single drop‑in JavaScript bundle (
sonar-quiz.iife.js) that progressively enhances DITA HTML pages onDOMContentLoaded. - Pattern: In‑place DOM upgrade of
table.qd-quizandtable.qd-analysis; Lit 3 custom elements for every piece of UI chrome (login, status, instructor tools, dialogs). - Layers (
src/):init/(bootstrap, component injection, event and session coordination) →enhancers/(table upgrades, overlays, badges) →services/(parsing, state, auth, storage) →services/storage/(IndexedDB adapter, codec, migrations).components/sit alongside and talk to services directly. - Data: IndexedDB for persistence;
sessionStoragefor the active session, instructor flag and R/A/G cache. - Isolation: Shadow DOM for components; one injected global stylesheet (
#qd-global-styles) for upgraded tables and the.qd-hiddenutility. - No network: No telemetry, no remote config, no CDNs, no dynamic imports.
- Framework: Lit 3 custom elements, all prefixed
qd-. 22 elements are registered (src/components/):qd-login,qd-status,qd-instructorand its four sub‑panels (-unlock,-scores,-export,-manage),qd-instructor-login,qd-modal,qd-password-modal,qd-confirm-dialog,qd-scores-modal,qd-student-table,qd-student-answers,qd-student-entries,qd-pin-reset-dialog,qd-lockout-banner,qd-migration-dialog,qd-help-trigger,qd-help-popup,qd-build-info,qd-spinner. - Why Lit: Small runtime, clean reactivity, strong encapsulation.
- Injection:
qd-login,qd-statusandqd-instructorare appended as children of the container matched by the#qd-status-containerselector (default.wh_top_menu_and_indexterms_link). There is no#qd-statusplaceholder; if the selector matches nothing, nothing is injected and a warning is logged. - Styling: Component styles live in Shadow DOM; a shared
sharedStylesmodule covers the instructor sub‑panels. CSS variables are not used for theming. - Author‑visible contracts: At most one
table.qd-quizand onetable.qd-analysisper page. All matching tables are enhanced; the "one per page" rule is a documented authoring constraint, not enforced at runtime.
- Bundles: IIFE
dist/sonar-quiz.iife.js(globalwindow.SonarQuiz, auto‑init) and ESMdist/sonar-quiz.esm.jswithindex.d.ts. - Byte budget: IIFE ≤ 40 KB min+gzip, enforced by
npm run size-checkin CI. Measured 37.6 KB at re‑baseline, so about 2.4 KB of headroom. The ESM build (~53 KB gzip) is not budgeted; it is for integrators who tree‑shake. - Assets: No external CSS or fonts.
- Release artifacts:
release.ymlruns on av*tag and attaches the built bundle to a GitHub Release (seedocs/RELEASE.md). There is no CHANGELOG file; release notes are generated from commits. - Version stamp:
VERSIONis injected frompackage.jsonat build time (__APP_VERSION__), alongsideBUILD_DATE, and shown by<qd-build-info>.
- Tooling: Vite (library mode) + Rollup; TypeScript 5;
vite-plugin-dtsfor typings. - Build‑time defines:
__BUILD_DATE__,__APP_VERSION__,__ENCRYPT_STORAGE__(from theENCRYPT_STORAGEenv var). - CI (
.github/workflows/ci.yml): lint + format; unit and integration tests with coverage thresholds (a ratchet, raised as tests are added); test‑gap ratchet (scripts/check-test-gaps.js --max-gaps N); build + bundle size; Playwright E2E in default and encrypted modes; Storybook build.
- Include: Single
<script defer src="sonar-quiz.iife.js">in the Oxygen template footer. - Configuration: Hidden
<span>elements injected bydita/template/xslt/inc/customHeader.xsl, read bysrc/config/dom-config-reader.ts:#qd-db-name— IndexedDB name. Required; init aborts with a logged FATAL if missing.#qd-status-container— selector for component injection (default above).#qd-title-selector— selector for the publication title used as the Release ID (default.wh_publication_title .title).#qd-instructor-hash— SHA‑256 of the instructor password truncated to 12 hex chars (optional; without it there is no instructor access).
- No
data-qd-*attributes are read. Runtime configuration by attribute was designed but never implemented. - MCQ authoring: Options are an
<ol>in the Detail column, 1‑indexed. Numeric questions put the tolerance in the Detail column. - Runtime validation: Authoring errors are reported to the console by the parsers (
quiz-parser.ts,analysis-parser.ts). The on‑page validation banner from the original design was never built; the standalonevalidation.tsmodule that was meant to feed it was removed as dead code in September 2026.
- Record key:
qd/{release}/u{serviceId}(src/services/storage/adapter-utils.ts). - Object stores:
students,backups,auditLog;DB_VERSION = 3(src/services/storage/idb-connection.ts). Missing stores trigger a delete‑and‑recreate recovery.backupsis currently never written (StorageService.backup()has no callers). - Schema:
SCHEMA_VERSION = 2; v1 records are migrated lazily on read (migration.ts). - Page object:
answers[],firstAttempted,lastAttempted,state, optionalanalysis{tableId, cells, firstEdited, lastEdited}(src/types/contracts.ts). - Persistence: Atomic read‑modify‑write per save through
StorageService→IndexedDBStorageAdapter. - Session (
sessionStorage):qd/session(active user),qd/state(R/A/G cache),qd/instructor(unlock flag),qd:pin-attempts:*(PIN rate limiting). - Cache policy: Rebuilt from IndexedDB on login (
qd:cache-rebuild), updated after each save, cleared on logout, expiry and erase.
See docs/SECURITY.md for the full description and known limitations. In brief:
- Instructor unlock: Password hashed client‑side (SHA‑256, truncated to 12 hex chars) and compared with
#qd-instructor-hash. Two UI paths (the login‑modal and the toolbar button) sharesrc/services/auth/instructor-auth.ts. - Student PIN: 4‑digit PIN hashed with Web Crypto; 3 failed attempts → 30 s lockout.
- Answer hiding: On load the Answer column text is deleted from the DOM and the Detail column (options, tolerances) is stripped and held in memory; both are restored only on the instructor reveal path.
- Storage obfuscation: Optional, build‑time (
ENCRYPT_STORAGE=true). XOR + base64 keyed by the release string only. This is obfuscation, not encryption; it deters casual DevTools inspection and nothing more. - Data residency: All data stays in the browser profile.
- PII: Name and service ID only.
- Parse once: Each table is parsed on first enhancement and its metadata cached in a
WeakMap; later upgrades (e.g. after login) reuse the parsed result. - Lazy overlays: Per‑student views are computed only when the instructor toggles them.
- Bundle: Kept within budget; Lit is the only runtime dependency.
- Keyboard: Inputs and buttons are native elements inside Shadow DOM and tab‑navigable.
- Announcements: Status panel uses
role="status"/aria-livewhere implemented inqd-status. - Colour + text: R/A/G badges carry text labels.
- Formal WCAG 2.1 AA verification has not been performed; see
docs/PROJECT_STATE.md.
- Strings: Inline in Lit templates. The "internal string dictionary" from the original design was not implemented; only help‑popup text is centralised (
src/config/help-content.ts). - Dates: Formatted with the
en-USlocale (src/utils/date-helpers.ts), noten-GBas originally specified. - Numbers: Dot decimal for numeric answers and tolerances.
- Author errors: Logged to the console with the offending row; enhancement continues where possible. No on‑page banner.
- Student errors: Inline validation on inputs.
- Storage faults: Logged; the failing operation is skipped and reading content is unaffected. There is no retry prompt and the
qd:storage-errorevent declared incontracts.tsis never dispatched. - Session expiry: Silent — the session is cleared and the login form reappears; no
qd:session-expiredevent is emitted.
- Unit (Vitest, jsdom, fake‑indexeddb): parsers, state calculation, services, components, init layer.
tests/unit/. - Integration (Vitest, separate config): DOM upgrade flows, storage round‑trips, instructor reveal.
tests/integration/. - E2E (Playwright, Chromium): Drives the committed DITA output under
dita-demo/overfile://; Storybook is started as the Playwright web server. Runs in default andENCRYPT_STORAGE=truemodes.tests/e2e/. - Visual regression: none. Chromatic was removed in September 2026 when the account lapsed; CI proves every story still compiles (
npm run build-storybook) but nothing compares rendered output. Playwright screenshot assertions are the obvious replacement if visual coverage is wanted again. - Coverage: Thresholds in the two Vitest configs are enforced in CI and raised as coverage improves.
- Minimum: Chromium ≥ 96, Edge ≥ 96, Firefox ≥ 102.
- File protocol: No dynamic imports in the IIFE; all assets relative and self‑contained.
- CSP: No
eval; works with default Oxygen output.
- Distribution: GitHub Release created by
release.ymlon avMAJOR.MINOR.PATCHtag (docs/RELEASE.md). - Install: Copy
sonar-quiz.iife.jsinto the Oxygen template resources (npm run copy-to-ditadoes this for the checked‑in template and demo). - Roll‑back: Keep the previous bundle alongside the new one; switch the template reference.
- Debug logging: Off by default. Enable at runtime with
window.SonarQuiz.setDebugMode(true)(src/utils/logger.ts). Nodata-qd-debugattribute is read. - Build info:
<qd-build-info>showsVERSIONandBUILD_DATE.
- Build‑time only:
ENCRYPT_STORAGE(src/config/feature-flags.ts). There are no runtime flags; thedata-qd-*mechanism in the original design was not implemented.
- Unlock: Via the "Instructor" button in
qd-login(password modal) or theqd-instructortoolbar; both verify against#qd-instructor-hash. Unlock is session‑scoped (qd/instructor). - Reveal answers:
src/enhancers/instructor-answer-reveal.tsrestores the Answer and Detail columns from in‑memory data. This is the only place correct answers are re‑injected into the DOM. - Per‑student views: Toggle overlays on the current page for quiz answers and analysis cell entries (
quiz-instructor-overlay.ts,analysis-instructor-overlay.ts). - Scores / export:
qd-scores-modaland CSV export (qd-instructor-export) read decoded records viagetStudentsByRelease(), so they work with obfuscation on. - Erase all data: Requires typing
DELETE ALL DATA; clears the three IndexedDB stores and allqd/*andqd:*sessionStoragekeys, then emitsqd:data-cleared. There is no automatic CSV‑export prompt before erasing; export is a separate button.
- Events: All bus events are dispatched on
document(notwindow), namespaceqd::- Session:
qd:login,qd:logout,qd:instructor-unlock,qd:instructor-lock - Quiz/analysis activity:
qd:answer-saved,qd:state-changed,qd:analysis-saved - Cache/UI coordination (emitted by
EventCoordinator):qd:cache-rebuild,qd:cache-update,qd:cache-clear,qd:badge-update - Instructor:
qd:instructor-show-answers,qd:instructor-hide-answers,qd:data-cleared - Component‑local (bubble from the element):
qd:pin-created,qd:pin-verified,qd:pin-reset,qd:lockout-expired,qd:help-open,qd:modal-close,qd:confirm,qd:cancel,qd:password-submit,qd:migration-complete,qd:migration-cancel - Declared but never dispatched:
qd:session-expired,qd:storage-error.
- Session:
- Public API (
window.SonarQuiz, the IIFE namespace ofsrc/index.ts):bootstrap(config),cleanup(),isInitialized(),injectComponents(), the enhancer functions, parsers,migrateObfuscation(),setDebugMode(),VERSION,BUILD_DATE, and the contract types. There is noinit();bootstrap()is guarded and is a no‑op if already initialised. - Add‑ons: New UI islands can be added as Lit elements without touching the upgrade logic.
- Legacy variance (Chromium 96): ES2020 target; no syntax newer than the floor without transpilation.
- Authoring drift: Parser errors are logged, not surfaced on the page; authors need the console or the demo pages to notice.
- Quota issues: Rare offline; erase‑all and CSV export exist but are independent actions.
- Multi‑tab conflicts: No cross‑tab broadcast; the session cache is per tab and IndexedDB is last‑write‑wins.
- Bundle headroom: ~2.4 KB under the 40 KB ceiling; new features will need to trim before they add.
- Bundle meets the byte budget and loads via
file://. - Enhancements are idempotent on re‑init (
bootstrap()guard; per‑tableWeakMapmetadata). - IndexedDB writes are atomic; the session cache reflects saved state.
- Instructor unlock, reveal and erase‑all work without leaving stale UI or stale data.
- Unit, integration and both E2E modes pass in CI; Storybook builds.