From 8047dbb40a66c3622b406ca8941febf892768349 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 13:49:53 +0000 Subject: [PATCH 1/2] fix: state the 4-digit PIN length in login guidance, correct release claims Closes #91: the login help popup said "Provide a new PIN" without giving the length, while the input's aria-label and the validation error both say "4-digit". Only the guidance was inconsistent. Also corrects a factual error in docs/PROJECT_STATE.md. The report claimed the release workflow had never run and no tag had ever been pushed. Tag v0.2.0 and a GitHub Release with both bundle assets were published by that workflow on 2025-11-29. The error came from reading `git tag` in a clone whose fetch refspec covers refs/heads only, so no tags were present locally, and concluding from that empty list that no releases existed. A correction notice at the top of the report records the mistake and how to check properly (git ls-remote --tags, or the Releases API). Backlog item 17 is withdrawn rather than marked done, since its premise was wrong; what remains is a version bump for the unreleased work, which is the owner's decision. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015u1mfVebscuZA3YDc6hc87 --- docs/MAINTAINERS.md | 2 +- docs/PROJECT_STATE.md | 17 +++++++++++++---- src/config/help-content.ts | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md index c65d4c9..bde7c4d 100644 --- a/docs/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -57,7 +57,7 @@ The demo pages load `dist/sonar-quiz.iife.js` and provide the hidden config span | Bundle size | `npm run build && npm run size-check` | CI, 40 KB gzip on the IIFE | | E2E | `npm run test:e2e` and `npm run test:e2e:encrypted` | CI matrix | | Storybook build | `npm run build-storybook` | CI, on every push and PR | -| Release | Tag `vX.Y.Z` on `main` | `release.yml` builds and publishes; see `docs/RELEASE.md` | +| Release | Tag `vX.Y.Z` on `main` | `release.yml` builds and publishes; see `docs/RELEASE.md`. Last release: `v0.2.0`, 2025-11-29. Bump `package.json` first — `VERSION` in the bundle is injected from it. | **Raising the ratchets.** The coverage thresholds and `--max-gaps` value are set to the measured level. When you add tests, raise them in the same PR so the improvement is locked in. diff --git a/docs/PROJECT_STATE.md b/docs/PROJECT_STATE.md index 1e4427f..ccba16a 100644 --- a/docs/PROJECT_STATE.md +++ b/docs/PROJECT_STATE.md @@ -5,6 +5,15 @@ Written for an incoming maintainer. Every number below comes from commands run o 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.0` and a GitHub Release, +> with `sonar-quiz.iife.js` and its source map attached, were published on 2025-11-29 by the +> workflow itself. The error came from checking `git tag` in 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 with `git ls-remote --tags origin` or the Releases +> API, not `git tag`. The affected rows below have been corrected in place. + ## 1. Summary - **The code is healthy; the project's picture of itself is not.** Type-check, lint, format, @@ -43,7 +52,7 @@ Existing documents were audited, not rewritten. | 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; no tag has ever been pushed, so it is unexercised | +| 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. @@ -149,7 +158,7 @@ Status key: **Current** = safe to trust; **Historical** = accurate for its time, | `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 (untested) | Process never exercised; no tags exist | Keep; exercise once | +| `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 | @@ -207,7 +216,7 @@ What an incoming maintainer needs, and where it is today: | 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` (never exercised) | +| 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 | @@ -242,7 +251,7 @@ Ranked by risk. Effort: S (< half a day), M (1–2 days), L (more). | 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 | **After merge**: tag `v0.2.0` on `main` (owner approved) | +| 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 diff --git a/src/config/help-content.ts b/src/config/help-content.ts index 4f69d43..172b471 100644 --- a/src/config/help-content.ts +++ b/src/config/help-content.ts @@ -18,7 +18,7 @@ export interface HelpContent { export const HELP_CONTENT: Record = { login: { title: 'Login Help', - body: '

Enter Name and Service ID to log in. Provide a new PIN if this is your first visit to this release of this document, otherwise use the PIN you previously created. Your instructor is able to reset PINs. See the Feedback page for more support.

Instructors: click "Instructor" for instructor login page (password accompanies distribution).

', + body: '

Enter Name and Service ID to log in. Provide a new 4-digit PIN if this is your first visit to this release of this document, otherwise use the PIN you previously created. Your instructor is able to reset PINs. See the Feedback page for more support.

Instructors: click "Instructor" for instructor login page (password accompanies distribution).

', }, status: { From adbcb4de1b1be8f7c038f888d1276ce491361265 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 15:13:50 +0000 Subject: [PATCH 2/2] fix: explain why the Login button is disabled Reported from the gh-pages demo: the login controls were visible but the Login button could not be enabled. The form was not broken. The button is disabled until name, service ID and a 4-digit PIN are all valid, and the PIN is easy to miss: it is a narrow unlabelled box whose placeholder said only "PIN", while the other two fields carry worked examples. Worse, an invalid form gave no feedback at all. The validation messages exist but only run on submit, and submit is unreachable precisely because the button is disabled; pressing Enter does not help either, since a disabled submit button blocks implicit submission. Verified in a browser: with no PIN, a 3-digit PIN, a service ID containing a space, or a service ID over ten characters, the button stayed disabled and nothing was displayed. The form now shows the first outstanding validation message live, so "PIN required" and "PIN must be exactly 4 digits" appear as the user types. A pristine form stays quiet rather than scolding someone before they have typed anything. The PIN field also gains a title tooltip to go with its existing aria-label. Not done, deliberately: widening the PIN field to fit a "PIN (4 digits)" placeholder. Measured in a browser, the header bar has about 4px of slack at 1280px, so 45px of field width fits on one row and 50px pushes the Instructor button onto a second row. The hint carries the rule instead, and the header layout is byte-for-byte the height it was. Bundle 39.36 KB gzip against the 40 KB limit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015u1mfVebscuZA3YDc6hc87 --- src/components/qd-login.styles.ts | 15 ++++++++ src/components/qd-login.ts | 26 +++++++++++++- tests/unit/qd-login.test.ts | 60 +++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/components/qd-login.styles.ts b/src/components/qd-login.styles.ts index 543d451..f9d09b7 100644 --- a/src/components/qd-login.styles.ts +++ b/src/components/qd-login.styles.ts @@ -52,6 +52,10 @@ export const loginStyles = css` } input.pin-input { + /* Kept narrow deliberately: the header bar has only a few pixels of + slack at 1280px, and a wider field wraps the Instructor button onto a + second row. The "4 digits" rule is carried by the hint message and the + field's title/aria-label instead. */ width: 45px; min-width: 45px; max-width: 45px; @@ -104,6 +108,17 @@ export const loginStyles = css` background: #5a6268; } + .hint-message { + width: 100%; + color: #555; + font-size: 11px; + margin-top: 3px; + padding: 4px 8px; + background: #f1f3f4; + border-radius: 3px; + border-left: 3px solid #9aa0a6; + } + .error-message { width: 100%; color: #d32f2f; diff --git a/src/components/qd-login.ts b/src/components/qd-login.ts index 43f8e14..19f5e31 100644 --- a/src/components/qd-login.ts +++ b/src/components/qd-login.ts @@ -165,6 +165,7 @@ export class QdLogin extends LitElement { name="pin" class="pin-input" placeholder="PIN" + title="4-digit PIN" inputmode="numeric" pattern="[0-9]*" maxlength="4" @@ -183,7 +184,11 @@ export class QdLogin extends LitElement { Login - ${this.errorMessage ? html`
${this.errorMessage}
` : ''} + ${this.errorMessage + ? html`
${this.errorMessage}
` + : this.validationHint + ? html`
${this.validationHint}
` + : ''} { // Note: Instructor Modal tests moved to tests/unit/components/qd-password-modal.test.ts // The old imperative modal was replaced with component + describe('Validation hint', () => { + const hintText = () => + element.shadowRoot?.querySelector('.hint-message')?.textContent?.trim() ?? null; + + it('should stay quiet on a pristine form', () => { + // Nothing typed yet: do not scold the user before they start + expect(hintText()).toBeNull(); + }); + + it('should say what is missing once the user starts typing', async () => { + const nameInput = element.shadowRoot?.querySelector('input[name="name"]') as HTMLInputElement; + nameInput.value = 'J Smith'; + nameInput.dispatchEvent(new Event('input')); + await element.updateComplete; + + expect(hintText()).toBe('Service ID required'); + }); + + it('should name the PIN length rule when the PIN is too short', async () => { + const set = (name: string, value: string) => { + const input = element.shadowRoot?.querySelector( + `input[name="${name}"]`, + ) as HTMLInputElement; + input.value = value; + input.dispatchEvent(new Event('input')); + }; + set('name', 'J Smith'); + set('serviceId', '30012345'); + set('pin', '123'); + await element.updateComplete; + + expect(hintText()).toBe('PIN must be exactly 4 digits'); + }); + + it('should clear the hint once the form is valid', async () => { + const set = (name: string, value: string) => { + const input = element.shadowRoot?.querySelector( + `input[name="${name}"]`, + ) as HTMLInputElement; + input.value = value; + input.dispatchEvent(new Event('input')); + }; + set('name', 'J Smith'); + set('serviceId', '30012345'); + set('pin', '1234'); + await element.updateComplete; + + expect(hintText()).toBeNull(); + }); + + it('should state the 4-digit requirement on the PIN field', () => { + // The field stays narrow to keep the header on one row, so the rule is + // carried by the tooltip and aria-label rather than the placeholder. + const pinInput = element.shadowRoot?.querySelector('input[name="pin"]') as HTMLInputElement; + + expect(pinInput.title).toBe('4-digit PIN'); + expect(pinInput.getAttribute('aria-label')).toContain('4-digit PIN'); + }); + }); + describe('UI Behavior', () => { it('should disable Login button when form is invalid', () => { const buttons = element.shadowRoot?.querySelectorAll('button');