Audit of the dormant BrowserTest ("Sonar Quiz System") codebase ahead of a formal restart.
Written for an incoming maintainer. Every number below comes from commands run on
2026-09-05 against main at 5383ca2 (last commit 2026-06-19), on Node 22.22 / npm 10.9.
Existing documents were audited, not rewritten.
Correction (2026-09-05). This report originally stated that the release workflow had never run and that no tag had ever been pushed. That is false. Tag
v0.2.0and a GitHub Release, withsonar-quiz.iife.jsand its source map attached, were published on 2025-11-29 by the workflow itself. The error came from checkinggit tagin a clone whose fetch refspec is+refs/heads/*:refs/remotes/origin/*— heads only, no tags — and concluding from an empty local list that no releases existed, without checking the remote. Any claim in a future audit about tags or releases should be verified withgit ls-remote --tags originor the Releases API, notgit tag. The affected rows below have been corrected in place.
- The code is healthy; the project's picture of itself is not. Type-check, lint, format, build, size budget, 831 unit tests, 84 integration tests and 105 Playwright E2E tests all pass. The documentation, CI signals and version stamps say otherwise or say nothing.
- Chromatic visual regression has been silently broken since the June 2026 refactor.
storybook buildfails on an orphan story (stories/components/qd-error-banner.stories.tsimports a component deleted in spec 012). CI shows green because the job hascontinue-on-error: true. PR-preview Storybook deploys skip for the same reason. - Two quality gates are decorative. The 80% coverage thresholds in
vitest.config.tsare never evaluated in CI (CI runstest:unit, nottest:coverage), and would fail if they were (62% unit lines, 40% integration lines). The E2E job inci.ymlis commented out. - "Erase All Data" does not erase IndexedDB. It clears sessionStorage only; the adapter's
clearAll()has no callers. The E2E test documents this in a comment rather than failing. This contradicts Constitution VI and the CLAUDE.md description of the button. Technical_Design.mddescribes a system that was never built as written. Of 20 checked claims, 6 match, 8 have drifted and 6 are not implemented (Section 4). Event names, the component model,data-qd-*flags, the publicinit()API and cross-tab sync are all wrong.- Two of the largest root docs are actively misleading (they cite files deleted in June),
five
SECURITY_*.mdfiles (125 KB) overlap heavily, and README/CLAUDE.md link to two documents that do not exist. There is no maintainer guide;CLAUDE.mdis the de facto one. - Loose ends are few and specific: spec 009 has three open tasks for an E2E file that was
never written; four modules (
validation.ts,csv-export.ts,runtime-config.ts,qd-pin-create.ts) are fully built but unwired; the toolbar unlock path reads a config span the DITA template never emits.
| Item | Value |
|---|---|
| Last commit | 5383ca2 2026-06-19, merge of PR #94 (spec 012 complete) |
| Working tree at audit start | clean; no WIP, no open PRs, no tags |
package.json version |
0.2.0 |
src/index.ts VERSION |
'0.1.0-phase3.1' (stale; surfaced in <qd-build-info>) |
| Source | 91 files, ~14,840 LOC under src/ |
| Tests | 52 unit, 12 integration, 16 E2E spec files (~20,400 LOC) |
TODO/FIXME in src/ |
0 |
CI on main |
green (lint, unit+integration, build+size); E2E disabled; Chromatic masked |
| Releases | release.yml triggers on v* tags. v0.2.0 was published 2025-11-29 with both bundle assets attached, so the automation is proven. Nothing has been released since. |
Activity came in two bursts: Nov 2025 (all design/security docs, phases 0–7) and June 2026
(specs 005–012, a large refactor). Only CLAUDE.md was updated during the second burst.
| Command | Result | Headline |
|---|---|---|
npm run typecheck |
pass | |
npm run lint |
pass | 0 errors |
npm run format:check |
pass | |
npm run test:coverage:unit |
tests pass, thresholds fail | 52 files / 831 tests; lines 62.4%, branches 86.8%, functions 71.6% vs 80% |
npm run test:coverage:integration |
tests pass, thresholds fail | 12 files / 84 tests; lines 39.6%, branches 71.0%, functions 43.5% |
npm run test:gaps |
ran | 39 of 86 source files reported without a test (see caveat below) |
npm run build |
pass | IIFE 148 KB raw, ESM 239 KB raw |
npm run size-check |
pass | IIFE 37.61 KB gzipped, limit 40 KB (2.39 KB headroom) |
npm run build-storybook |
fail | Could not resolve "../../src/components/qd-error-banner.js" |
npm run test:e2e |
pass | 105 passed in 1.6 min (Storybook dev server started manually, see note) |
npm run test:e2e:encrypted |
pass | 104 passed, 1 skipped (Storybook started with ENCRYPT_STORAGE=true) |
npm run analyze:e2e-gaps |
ran | regenerated docs/test-coverage-report.md (was dated 2025-11-26) |
| Chromatic | not runnable locally | needs CHROMATIC_PROJECT_TOKEN; CI log inspected instead |
Notes:
- Unit and integration coverage are separate runs with separate reports; there is no merged
figure. Per-directory unit line coverage:
services/auth98%,utils82%,components74%,config72%,services71%,enhancers17%,init0%,index.ts0%. Integration coversenhancers71% andinit60% butcomponents10%. Neither suite alone represents the code. test:gapsis structural and path-mirrored. 7 of its 39 "missing" files have a test at a flat path it does not look for (e.g.tests/unit/auth-service.test.ts). Real gaps remain insrc/init/(all four modules),src/services/storage/(codec, connection, repositories),src/enhancers/persistence modules andsrc/services/state-calculator.ts.analyze:e2e-gapsscanstests/e2e/workflows/only;tests/e2e/help-popups.spec.tsis invisible to it. Its feature inventory is hard-coded, so "11/11 covered" is a floor, not proof.- E2E: in this sandbox Playwright's
webServer(npm run storybook) failed to spawn a browser opener (spawn none ENOENT). Starting Storybook with--ci --no-openand letting Playwright reuse it worked. Storybook dev mode tolerates the orphan story; build mode does not.
The design doc (7 KB, last edited 2025-11-28) predates the PIN, help, migration, obfuscation, audit-log and build-info features and the June 2026 refactor. Checked claim by claim:
| § | Claim | Verdict | Evidence |
|---|---|---|---|
| 3 | Components are <qd-login> and <qd-status>; rest by DOM upgrade |
Drifted | 23 custom elements registered under src/components/ |
| 3 | #qd-status placeholder panel |
Not implemented | Components appended into the #qd-status-container selector by src/init/component-injector.ts; #qd-status never queried |
| 3 | Zero or one table.qd-quiz.qd-page per page |
Drifted | .qd-page appears nowhere; all table.qd-quiz matches are enhanced in a loop, no cardinality check (src/init/bootstrap.ts) |
| 7 | Page object fields; qd/state cache key; qd/{release}/u{serviceId} record key |
Matches | src/types/contracts.ts, src/services/storage/adapter-utils.ts |
| 8 | Obfuscation is "future", keyed by {release, serviceId} |
Drifted | Shipped behind build-time ENCRYPT_STORAGE; key derived from release only (src/services/storage/obfuscation.ts) |
| 11 | UI strings in an internal dictionary; en-GB dates |
Not implemented / Drifted | Strings inline in Lit templates; src/utils/date-helpers.ts uses en-US |
| 12 | Author-error banner; storage-fault retry prompt | Not implemented | src/services/validation.ts (364 lines) has no importer; qd:storage-error is declared but never dispatched |
| 16 | data-qd-debug attribute enables debug |
Not implemented | No data-qd-debug read; setDebugMode() in src/utils/logger.ts has no callers, so debug is permanently off. CLAUDE.md's "DEBUG_MODE constant in src/index.ts" does not exist either |
| 17 | Feature flags via data-qd-* attributes |
Not implemented | Only flag is build-time ENCRYPT_STORAGE; src/config/runtime-config.ts defines a full runtime config with zero importers |
| 18 | CSV export prompt before erase | Not implemented | qd-instructor-manage.ts goes from typed confirmation straight to clear |
| 18 | Erase clears IndexedDB + sessionStorage | Drifted (defect) | clearQuizData() in src/utils/storage-helpers.ts iterates sessionStorage only; StorageService.clearAll() has no callers |
| 19 | Events dispatched on window |
Drifted | All bus events dispatch on document (src/utils/event-helpers.ts) |
| 19 | Event names qd:status-changed, qd:unlock, qd:lock, qd:analysis-cell-change, qd:show-all-responses, qd:erase, qd:error |
Drifted | Actual: qd:state-changed, qd:instructor-unlock/-lock, qd:analysis-saved, qd:instructor-show-answers/-hide-answers, qd:data-cleared; no error event. ~14 further events undocumented; qd:analysis-saved, qd:cache-*, qd:badge-update, qd:pin-* have no listeners |
| 19 | window.SonarQuiz.init() re-init API |
Not implemented | Global is Rollup's IIFE namespace; export is bootstrap() (guarded, no re-init) plus cleanup() |
| 20 | Cross-tab broadcast | Not implemented | No BroadcastChannel or storage listener |
| 7 | 30-minute session timeout | Matches | src/init/session-coordinator.ts; expiry is silent (no event, no UI notice) |
| 5 | CI runs e2e and size check | Half true | size-check yes; E2E commented out |
| VIII | Answers removed from DOM on load | Matches, with a gap | hideAnswerColumn() blanks the answer cell; hideDetailColumn() only adds a CSS class, so the Detail column (MCQ <ol> options, numeric tolerances) stays in the DOM; the leak is the tolerance value, since options are shown to students anyway (src/enhancers/quiz-table-columns.ts) |
Undocumented in the design doc but present in code: PIN authentication (service, rate limiter, three components), help popups, obfuscation migration dialog, audit-log and backup object stores (DB version 3, three stores; CLAUDE.md says two), schema v1→v2 migration, scores modal, build-info stamp, CSS answer hiding.
Two further code-level findings a maintainer should know:
- Duplicate instructor-hash config.
src/services/auth/instructor-auth.tsreads#qd-instructor-hash(emitted bydita/template/xslt/inc/customHeader.xsl).src/config/instructor-password.tsreads#instructor.password.hash, which the template never emits; it is used only by the toolbar<qd-instructor-unlock>button, so that path always fails in DITA output. Stories and unit tests provide the second id, which is why nothing catches it. - Dead modules:
src/services/validation.ts,src/services/csv-export.ts,src/config/runtime-config.ts,src/components/qd-pin-create.tshave no importers. Their presence inflates the untested-file count and the reader's mental model.
ARCHITECTURE_FLOWS.md and CLAUDE.md are broadly consistent with the code at the level of
flows and file paths. CLAUDE.md is wrong on: DEBUG_MODE constant, object-store count, and
the erase-all description. It also says "Phase 1 Complete → Starting Phase 2", which is false.
Status key: Current = safe to trust; Historical = accurate for its time, clearly dated; Stale = will mislead; Generated = tool output. Disposition is a recommendation only.
| File | KB | Last commit | Status | Reason | Suggested disposition |
|---|---|---|---|---|---|
CLAUDE.md |
23 | 2026-06-17 | Current (minor errors) | Only doc updated in 2026; see §4 | Keep; fix the three errors; it is the maintainer guide until a real one exists |
README.md |
6 | 2025-11-29 | Stale | "Status: In active development", "Phase 0 ← current", links Delivery_Plan.md and Storybook_Plan.md which do not exist |
Rewrite the status/phase section; fix links |
Technical_Design.md |
7 | 2025-11-28 | Stale | See §4: 14 of 20 claims wrong | Either re-baseline against code or mark "original design intent, not as-built" |
ARCHITECTURE_FLOWS.md |
11 | 2025-11-18 | Current-ish | Flows still hold; event names partially outdated | Keep; update event names |
System_Requirements.md |
8 | 2025-11-18 | Historical | Requirements baseline; predates PIN/help/encryption features | Keep as baseline; note additions from specs 003–010 |
Contracts.md |
3 | 2025-11-18 | Current | Summary of src/types/contracts.ts |
Keep |
CODE_REVIEW_REFACTORING_HOTSPOTS.md |
22 | 2025-11-19 | Historical (banner added today) | Seeded spec 012; cites deleted files | Archive under docs/history/ |
POST_PHASE_7_REVIEW.md |
31 | 2025-11-18 | Stale | Cites src/utils/attributes.ts, src/utils/events.ts (gone); pre-refactor structure |
Archive |
SECURITY_BEST_PRACTICES.md |
54 | 2025-11-15 | Historical | Generic guidance from one commit; largest doc in repo | Merge the five SECURITY files into one project-specific page; archive the rest |
SECURITY_IMPLEMENTATION_GUIDE.md |
19 | 2025-11-15 | Historical | Remediation plan, largely executed by spec 001 | Merge/archive |
SECURITY_TEST_EXAMPLES.md |
30 | 2025-11-15 | Historical | Sample tests, some now real tests | Merge/archive |
SECURITY_README.md |
12 | 2025-11-15 | Historical | Index for the above | Merge/archive |
SECURITY_QUICK_REFERENCE.md |
10 | 2025-11-15 | Historical | Cheat-sheet | Merge/archive |
docs/COMPONENT_SPEC.md |
14 | 2025-11-18 | Stale | Component list predates the qd-instructor split and PIN/help components | Re-baseline or archive |
docs/INSTRUCTOR_PASSWORD_IMPLEMENTATION.md |
35 | 2025-11-18 | Historical | Design notes; does not mention the duplicate hash id | Archive |
docs/CODE_REDUCTION_PROPOSAL.md |
3 | 2025-11-24 | Historical | Executed as spec 005 | Archive |
docs/RELEASE.md |
3 | 2025-11-29 | Current | Matches the workflow; the v0.2.0 release confirms it works | Keep |
docs/CHROMATIC_SETUP.md |
2 | 2025-11-15 | Stale | Says "Chromatic is configured"; build is broken and token status unknown | Update after fix |
docs/test-coverage-report.md |
2 | regenerated today | Generated | See §3 caveat on scope | Keep regenerating; do not hand-edit |
Specs (specs/): 12 folders. 005–012 have spec/plan/tasks; all tasks closed except
009-encrypt-stored-data (T034, T035, T038 open: tests/e2e/encrypted-storage.spec.ts never
written). 000/001 predate the tasks convention. 003 and 004 have a spec and research but
no plan or tasks, yet their features (instructor improvements, PIN auth) are in the code, so
their implementation record is missing. Folder 002 does not exist.
Unit (Vitest, jsdom, fake-indexeddb): 831 tests, all green, ~20 s. Strong on
services/auth, utils, components; near-zero on init/ and enhancers/. Console is noisy
with intentional [ERROR] logs from negative-path tests, which will hide a real error.
Integration (Vitest, separate config): 84 tests, green. Covers enhancers and init
reasonably; almost nothing in components.
Coverage thresholds: 80% on all four metrics in vitest.config.ts. Not evaluated in CI
(ci.yml runs test:unit/test:integration, uploads coverage/lcov.info to Codecov with
if: always(); the upload step succeeded on the last run but a Codecov token is not visible in
the workflow, so whether reports land is unknown). Both suites fail the thresholds locally.
Effective coverage gate: none.
E2E (Playwright, Chromium, against Storybook stories on :6006): 105 tests, all green,
1.6 min. Per-test timeout is 2 s. Not run in CI ("temporarily disabled" comment in ci.yml,
job body still present). 4 tests in pin-authentication.spec.ts are test.skip unless
ENCRYPT_STORAGE=true. The encrypted variant requires Storybook to be started with the flag.
Run here with Storybook started under ENCRYPT_STORAGE=true: 104 passed, 1 skipped, 0 failed,
1.5 min. So both storage modes pass end-to-end today; neither is exercised in CI.
Visual regression (Chromatic): configured in ci.yml (push to main only,
continue-on-error: true, onlyChanged: true) and in .storybook/. On the last main run
(2026-06-19, run 27829306417) the step reported success after 8 s; its log shows the Storybook
build failing on the orphan qd-error-banner story. So: no visual baseline has been captured
since before the June refactor, and whether CHROMATIC_PROJECT_TOKEN is even set cannot be
determined from the repo. docs/CHROMATIC_SETUP.md claims it works.
PR previews: pr-preview.yml builds Storybook with a warn-and-skip on failure, so the
Storybook half of each PR preview has been silently absent since June.
Bundle: 37.61 KB gzipped IIFE against a 40 KB budget, enforced in CI. 2.4 KB of headroom is thin for a restart that adds features; the ESM build is 52.9 KB gzipped and unbudgeted.
What an incoming maintainer needs, and where it is today:
| Need | State | Where |
|---|---|---|
| Purpose, constraints, constitution | Exists | CLAUDE.md, System_Requirements.md |
| Repo map (what lives where, what is dead) | Partial | CLAUDE.md layers section; dead modules and duplicate config not mentioned anywhere |
| Setup, build, test, lint commands | Exists | CLAUDE.md Development Commands; Definition of Done |
| How to run E2E locally, incl. Storybook quirks | Partial | CLAUDE.md says auto-managed; sandbox needed --ci --no-open; encrypted variant undocumented |
| Config contract (hidden spans, DITA XSL) | Exists, with a hole | CLAUDE.md Constitution VII; second hash id undocumented |
| Data model and storage layout | Partial | Contracts.md, CLAUDE.md; object-store count wrong; obfuscation key derivation only in code |
| Event catalogue | Wrong | Technical_Design.md §19 and ARCHITECTURE_FLOWS.md disagree with code; no single list |
| CI, deploy, PR preview, release | Exists | CLAUDE.md GitHub Pages section; docs/RELEASE.md |
| Chromatic / visual testing | Wrong | docs/CHROMATIC_SETUP.md |
| Known issues / deferred work | Missing | No list anywhere; this report is the first |
| Which documents to trust | Missing | 13 root docs, no index; this report's §5 is the first classification |
| Decision log (why Lit, why obfuscation not encryption, why PIN) | Partial | Scattered across specs/*/research.md and plan.md; not linked from anywhere |
| Authoring guide for DITA content authors | Partial | CLAUDE.md Author Constraints; the runtime validator that would enforce them is unwired |
Verdict: the raw material for a maintainer guide exists, mostly in CLAUDE.md and specs/,
but it is written for an AI agent, has factual errors, and there is no known-issues list or
document index. A restart should produce a short docs/MAINTAINERS.md from this table rather
than another design document.
Ranked by risk. Effort: S (< half a day), M (1–2 days), L (more). Status column added 2026-09-05 after the owner asked for every item to be applied; see §9.
| # | Item | Why it matters | Risk | Effort | Status |
|---|---|---|---|---|---|
| 1 | Delete or rewrite stories/components/qd-error-banner.stories.ts; remove continue-on-error from the Chromatic job |
Restores Storybook build, PR-preview Storybook and visual regression; ends a false-green signal | H | S | Done: story deleted; Storybook build is a hard CI step; Chromatic runs only with a token |
| 2 | Decide what "Erase All Data" must do, then wire StorageService.clearAll() (or document the current behaviour and rename the button) |
Constitution VI promises complete erasure; instructors will believe cohort data is gone when it is not | H | S–M | Done: erase-all clears the three IndexedDB stores and all qd/*/qd:* keys; E2E asserts it |
| 3 | Re-enable the E2E job in ci.yml (body is already written; add --ci to the Storybook command) |
105 passing tests currently guard nothing on PRs | H | S | Done: E2E job restored as a default/encrypted matrix |
| 4 | Make coverage a real gate or delete the thresholds: run test:coverage:* in CI with thresholds set to today's numbers, ratchet up |
Thresholds that never run and would fail are worse than none | M | S | Done: CI runs the coverage configs; thresholds set to measured levels as a ratchet |
| 5 | Resolve the duplicate instructor-hash id (#instructor.password.hash vs #qd-instructor-hash) to one reader |
Toolbar unlock cannot work in DITA output; two code paths for one secret | M | S | Done: both paths use instructor-auth.ts and #qd-instructor-hash; instructor-password.ts removed |
| 6 | Confirm CHROMATIC_PROJECT_TOKEN exists; capture a fresh baseline after #1 |
Visual regression has had no baseline since before the refactor | M | S | Will not do: Chromatic dropped (account lapsed). Storybook build kept as a CI gate; no visual regression remains — see §9 |
| 7 | Close spec 009: write tests/e2e/encrypted-storage.spec.ts or formally drop T034/T035/T038 |
Only open tasks in the repo; encrypted mode is otherwise untested end-to-end in CI | M | M | Done: tests/e2e/encrypted-storage.spec.ts (3 tests); spec 009 tasks closed |
| 8 | Re-baseline Technical_Design.md against §4, or retitle it "original design intent" and point readers to an as-built doc |
14 of 20 claims wrong; a formal review will read it as truth | M | M | Done: re-baselined in place, numbering kept |
| 9 | Remove or wire the dead modules (validation.ts, csv-export.ts, runtime-config.ts, qd-pin-create.ts) |
900+ lines that mislead readers and skew coverage; validation.ts is the only path to author-error banners |
M | S–M | Done: four modules and their tests removed |
| 10 | Fix CLAUDE.md errors (phase status, DEBUG_MODE, store count, erase-all) and add a Known Issues section |
It is the document agents and people actually read | M | S | Done: phase status, debug mode, store count, erase-all corrected; doc index links added |
| 11 | Fix README status/phase text and the two dead links; align VERSION in src/index.ts with package.json |
First thing a reviewer sees | M | S | Done: status/phase text and links fixed; VERSION now injected from package.json |
| 12 | Archive POST_PHASE_7_REVIEW.md, CODE_REVIEW_REFACTORING_HOTSPOTS.md, docs/CODE_REDUCTION_PROPOSAL.md, docs/INSTRUCTOR_PASSWORD_IMPLEMENTATION.md under docs/history/; merge the five SECURITY_*.md into one |
Halves the root doc set; removes files that cite deleted code | L | S | Done: four docs archived to docs/history/; five SECURITY files merged into docs/SECURITY.md and archived |
| 13 | Strip the numeric tolerance from the Detail column in quiz-table-columns.ts as the answer column is, or accept that it is visible in view-source |
Constitution VIII is only partly honoured; low impact (tolerance, not the answer) | L | S | Done: Detail column content stripped to memory on load, restored on instructor reveal |
| 14 | Add unit tests for src/init/* and src/services/state-calculator.ts; make test:gaps path-aware or move the 7 flat tests, then run it --strict in CI |
Bootstrap and state calculation are the two least-covered critical paths | M | M | Done: 124 unit tests for src/init/* and state-calculator.ts (init 0% → 99%); gap script accepts flat paths; --max-gaps ratchet in CI |
| 15 | Write docs/MAINTAINERS.md from §7 and a document index from §5 |
No maintainer-facing entry point exists | M | S | Done: docs/MAINTAINERS.md and docs/README.md index |
| 16 | Backfill plan/tasks for specs 003 and 004, or mark them "implemented without plan" | Two shipped features have no implementation record | L | S | Done: STATUS.md in specs 003 and 004 |
| 17 | Exercise the release workflow once with a v0.2.0 tag |
Release automation has never run | L | S | Withdrawn — the premise was wrong. See the correction below: v0.2.0 shipped in November 2025. What remains is a version bump for the unreleased work, which is the owner's call. |
| 18 | Budget the ESM bundle or state it is unbudgeted; note IIFE headroom is 2.4 KB | Any new feature will hit the limit | L | S | Done: ESM stated as unbudgeted in Technical_Design.md §4; headroom noted |
Trivial fixes applied in the first consolidation PR (#95): regenerated
docs/test-coverage-report.md, deleted the stray temp-e2e/ folder, added a "superseded"
banner to the hotspots review (now under docs/history/), and linked this report from CLAUDE.md.
The second consolidation PR applied every item above except the two that need the owner (#6 token check, #17 release tag after merge). Additional defects found and fixed while doing so, none of which were in the original backlog:
- Stored XSS into the instructor's browser.
qd-pin-reset-dialogbuilt a confirmation message from the unescaped student name andqd-confirm-dialogrendered it withunsafeHTML. Fixed with a newescapeHtml()insrc/utils/dom-helpers.ts(unit-tested). - Instructor password compare was not constant-time on the login-modal path
(
verifyInstructorPasswordused===). Now usesconstantTimeCompare. - No rate limiting on the login-modal instructor path. CLAUDE.md claimed it; only the
unreachable toolbar path had it. The modal now uses the same
RateLimiter. - Erase-all left
qd:pin-attempts:*keys behind.clearQuizData()now stripsqd:as well asqd/keys. - CSV export had no formula-injection guard. A student name or free-text answer starting
with
=,+,-or@became a live formula when the instructor opened the export in Excel or Sheets.escapeCSVFieldnow prefixes a single quote (unit tested). - The toolbar unlock never recorded failed attempts, so its rate limiter never engaged.
Both instructor paths now share one policy: two wrong passwords are free, the third starts the
existing 2/4/8/16/30 s backoff. This matches the student PIN policy already in
contracts.ts(PIN_CONSTANTS.MAX_ATTEMPTS) and is covered by new unit tests. - Playwright's
reporter: 'html'never exits locally, because it serves the HTML report at the end of every run. Any scripted invocation hung. Now[['list'], ['html', {open: 'never'}]].
Chromatic was then dropped entirely (owner decision, September 2026: the account lapsed). The
service, its two dev dependencies, chromatic.config.json, the npm script, the Storybook addon
and docs/CHROMATIC_SETUP.md are removed. What it was actually catching in practice is kept and
strengthened: CI builds Storybook as a hard gate, now on pull requests as well as main, so a
story importing a deleted module fails before merge instead of silently breaking PR previews.
Backlog item 6 is therefore closed as "will not do", and the project now has no visual
regression testing at all — nothing compares rendered output between commits. Playwright
screenshot assertions (expect(page).toHaveScreenshot()) are the natural replacement, using the
E2E suite that already exists, if that coverage is wanted again.
setDebugMode()is now exported onwindow.SonarQuizso debug logging can actually be enabled.
Measured after this pass: unit 55 files / 876 tests, lines 69.4% (was 62.4%); integration 84 tests,
lines 42.6% (was 39.6%); 25 source files without a test (was 39, of which 7 were false positives).
E2E: 105 passed / 6 skipped in default mode, 110 passed / 1 skipped with ENCRYPT_STORAGE=true,
no failures in either. Storybook builds. Coverage thresholds and the --max-gaps value are set to
these levels and enforced in CI.
Bundle headroom is now the tightest constraint. The IIFE is 39.10 KB gzipped against the
40 KB ceiling, leaving 0.90 KB. The security fixes above account for most of the growth (it was
37.61 KB before this pass). The next feature will need to trim before it can add; removing the
unsafeHTML dependency from qd-confirm-dialog is the obvious first candidate.
Still open after this pass (recorded in docs/MAINTAINERS.md §6): no authoring-error banner,
no qd:storage-error / qd:session-expired events, no cross-tab sync, backups store never
written, injectComponents() and SessionCoordinator.cleanup() are not idempotent, no formal
WCAG audit.