This document answers the three feasibility questions raised in the project brief, with the concrete API/mechanism choices the scaffolding is built on.
Validated component by component:
| Component | Mechanism | Server needed? |
|---|---|---|
| Reading the puzzle (grid, clues, selection, solved state) | DOM of the already-loaded NYT page, via a content script | No. The user's browser already has the puzzle; we never call NYT APIs and inherit their login/subscription for free. |
| Understanding the puzzle (numbering, crossings, patterns) | Pure JS (puzzle-model/) |
No |
| Homophone matching | Bundled dictionary (~90 curated sets) + pure JS | No |
| Conversation policy | Pure JS state machine | No |
| Text-to-speech | chrome.tts / speechSynthesis — voices are local (OS/browser voices) |
No |
| Speech-to-text | Web Speech API | No server of ours — but see the asterisk below |
| Entering answers | Synthetic DOM events from the content script | No |
The one asterisk — speech recognition backend. Chrome's classic webkitSpeechRecognition
implementation streams audio to Google's speech service (not a server we run or pay for — it's
part of the browser, but audio does leave the machine). Recent Chrome (139+, Aug 2025) added
on-device recognition (SpeechRecognition.available() / processLocally: true) for en-US
on capable hardware. Our STT wrapper feature-detects this and can prefer local processing
(REQ-FUT-006). Either way: no first-party server, no API keys, no accounts, nothing to deploy.
This is enforced mechanically — extension-test/unit/arch.test.js fails if any fetch/XHR/WebSocket
appears in extension source (REQ-NFR-001), and MT-15 audits the network tab live.
Conclusion: the assumption in the brief is correct. The extension is a pure client.
chrome.tts— extension-only API ("tts"permission). Works from extension pages and the service worker, unaffected by web-page autoplay/user-gesture policies. Primary choice.speechSynthesis(Web Speech API) — available in any document context (content script, side panel). Subject to autoplay/user-activation rules on ordinary pages, which is why it's the fallback, not the primary.- Neither supports SSML in practice, so question intonation can't be forced; we keep the
trailing
?(voices often inflect naturally) and speak the words "question mark" (REQ-READ-004 / REQ-SPCH-006) — exactly the fallback behavior the brief asked for.
webkitSpeechRecognition(Web Speech API) is available in document contexts: content scripts, side panel, popup, offscreen documents. It is not available in the MV3 service worker — which is fine, because our conversation runs in the content script (the puzzle page is a document).- It returns an n-best list (
maxAlternatives), which we exploit for homophone handling (REQ-ANS-004) — a genuine bonus for this use case. - Caveats and how the design absorbs them:
- Mic permission context. Permission is granted per-origin. Recognition runs in the content
script, so the grant belongs to nytimes.com — the prompt reads as the site asking, and the
grant persists across NYT visits. Accepted deliberately (rev. 2): it buys a zero-UI product.
The STT port pre-flights
navigator.permissions.query({name:'microphone'})+ a one-timegetUserMediato surface the prompt cleanly (REQ-SPCH-003, MT-05). - No recognition in service workers. Solved by architecture: recognition lives in the page;
TTS lives in the service worker (
chrome.tts, relayed over the session port). - Cloud vs on-device — see §1's asterisk.
- Mic permission context. Permission is granted per-origin. Recognition runs in the content
script, so the grant belongs to nytimes.com — the prompt reads as the site asking, and the
grant persists across NYT visits. Accepted deliberately (rev. 2): it buys a zero-UI product.
The STT port pre-flights
- The product is speech-only by decision: no captions, no panel, no visual surface (diagnostics go to the page console, REQ-SPCH-007/008). That removes the side panel's main reason to exist.
- A content script is a document, so recognition works; the mic prompt coming from nytimes.com and the session dying on page reload/navigation are both accepted — the latter is even wanted (REQ-LIFE-008 for free).
- TTS is not hosted in the page:
speechSynthesisthere is subject to nytimes.com's autoplay/user-activation rules, so speaking is relayed to the service worker'schrome.tts(immune to page policies) over the session port. - A popup would close on any click into the page (killing the mic); the side panel worked but was a permanent visual appendage the product doesn't need.
- The extension icon still toggles the whole session (click = start, click again = stop), matching the brief's interaction model (REQ-LIFE-001/002).
What the page gives us:
- Reading state is straightforward: the grid is an SVG (
g.xwd__cellcells with letter/number text), the clue lists are<ol>s with rich-text<li>s, selection is expressed via CSS classes, and solving triggers a congratulations modal. All selectors are quarantined inpage-adapter/selectors.jswith an on-demand probe (REQ-PAGE-009, run per MT-01) because NYT can rename classes any day. - Writing (primary approach): do what the user's fingers do —
- click the target cell (selects it),
- dispatch a
keydownKeyboardEvent with the letter (NYT listens at document level), - repeat per letter; then re-read the cells to verify (REQ-PAGE-007) and report honestly
(REQ-ANS-013).
Per-cell addressing sidesteps NYT's cursor-skip settings.
Live-page hardening (post-MT-02): synthetic key events carry the legacy
keyCode/which/charCodefields (bare{key}events construct them as 0, which keyCode-reading handlers ignore), a short settle delay separates the selection click from the keystroke, and verification polls the grid for up to ~1.5 s instead of reading it synchronously — the live app is a React page and repaints after our dispatch returns.
- The risk: synthetic events from a content script have
isTrusted:false. Community solvers have driven the NYT grid with synthetic keyboard events successfully, but NYT could start filtering. This is why MT-02 (live injection spike) is ordered as the second manual test — validate before building further. - Fallbacks if NYT filters untrusted events (in order):
- Dispatch from the main world (
chrome.scripting.executeScript({world:'MAIN'})/ main-world content script) directly at the app's own listeners — beats naiveisTrusted-adjacent checks that inspect event provenance indirectly. - Drive NYT's own React handlers via the clue-list/keyboard UI (their virtual keyboard buttons on mobile layout are plain buttons — clickable).
chrome.debugger+Input.dispatchKeyEvent— produces fully trusted events. Guaranteed to work, at the cost of Chrome's "is being debugged" infobar. Acceptable last resort for a personal assistive tool.
- Dispatch from the main world (
- The write path is abstracted behind
pageAdapter.enterAnswer()so swapping strategies never touches conversation logic.
| Brief assumption | Verdict |
|---|---|
| "Can run fully on the client side" | Confirmed (one documented asterisk: Google-hosted recognition unless on-device is available) |
| "There are speech-to-text and text-to-speech APIs usable from an extension" | Confirmed (webkitSpeechRecognition + chrome.tts/speechSynthesis; service-worker limitation designed around) |
| "There'll be a way to input answers into the crossword" | Feasible, primary approach implemented; must be validated on the live page early (MT-02); three fallbacks documented |
Because §3 carries the only real unknown, the manual test plan front-loads it:
MT-01 (selector probe) and MT-02 (injection spike) are designed to be run on a live NYT
page in under five minutes, before investing in polish. Everything else in the system is testable
offline against the faithful fake page (extension-test/fixtures/fake-nyt/).