Spaces:
Running on Zero
Running on Zero
docs(02-08): complete the word-lookup popover plan
Browse files
.planning/ROADMAP.md
CHANGED
|
@@ -61,7 +61,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|
| 61 |
- [x] 02-05-PLAN.md β The canonical analyze(text) + the 28-sentence golden fixture set (SC-1) [wave 3]
|
| 62 |
- [x] 02-06-PLAN.md β tokens on the directive; analyze / translate / language_info server functions; warm-up on load; turn-loop + facade surface; parity [wave 4]
|
| 63 |
- [x] 02-07-PLAN.md β Furigana: transcript.js renderer, three-mode control + level picker, persistence, three-layer numbers (JPN-02) [wave 5]
|
| 64 |
-
- [
|
| 65 |
- [ ] 02-09-PLAN.md β Inline EN reveal with per-line cache, translation latency harness (JPN-04) [wave 7]
|
| 66 |
- [ ] 02-11-PLAN.md β LICENSES.md rows + on-page credits + README mirror + docs/LANGUAGE.md; credits test extension [wave 8]
|
| 67 |
- [ ] 02-10-PLAN.md β Owner pushes; deployed rows on the Space; translation latency + container record; phone rows; requirements marked (user-gated) [wave 9]
|
|
|
|
| 61 |
- [x] 02-05-PLAN.md β The canonical analyze(text) + the 28-sentence golden fixture set (SC-1) [wave 3]
|
| 62 |
- [x] 02-06-PLAN.md β tokens on the directive; analyze / translate / language_info server functions; warm-up on load; turn-loop + facade surface; parity [wave 4]
|
| 63 |
- [x] 02-07-PLAN.md β Furigana: transcript.js renderer, three-mode control + level picker, persistence, three-layer numbers (JPN-02) [wave 5]
|
| 64 |
+
- [x] 02-08-PLAN.md β Word-lookup popover with published numbers, desktop + Pixel 7 rows (JPN-03) [wave 6]
|
| 65 |
- [ ] 02-09-PLAN.md β Inline EN reveal with per-line cache, translation latency harness (JPN-04) [wave 7]
|
| 66 |
- [ ] 02-11-PLAN.md β LICENSES.md rows + on-page credits + README mirror + docs/LANGUAGE.md; credits test extension [wave 8]
|
| 67 |
- [ ] 02-10-PLAN.md β Owner pushes; deployed rows on the Space; translation latency + container record; phone rows; requirements marked (user-gated) [wave 9]
|
.planning/STATE.md
CHANGED
|
@@ -3,13 +3,13 @@ gsd_state_version: 1.0
|
|
| 3 |
milestone: v1.0
|
| 4 |
milestone_name: milestone
|
| 5 |
status: unknown
|
| 6 |
-
stopped_at: "Completed 02-
|
| 7 |
-
last_updated: "2026-09-
|
| 8 |
progress:
|
| 9 |
total_phases: 6
|
| 10 |
completed_phases: 1
|
| 11 |
total_plans: 22
|
| 12 |
-
completed_plans:
|
| 13 |
---
|
| 14 |
|
| 15 |
# Project State
|
|
@@ -24,7 +24,7 @@ See: .planning/PROJECT.md (updated 2026-08-08)
|
|
| 24 |
## Current Position
|
| 25 |
|
| 26 |
Phase: 02 (japanese-language-core) β EXECUTING
|
| 27 |
-
Plan:
|
| 28 |
|
| 29 |
## Performance Metrics
|
| 30 |
|
|
@@ -68,6 +68,7 @@ Plan: 8 of 11
|
|
| 68 |
| Phase 02 P05 | 12 | 2 tasks | 7 files |
|
| 69 |
| Phase 02 P06 | 38 | 3 tasks | 10 files |
|
| 70 |
| Phase 02 P07 | multi-session | 3 tasks | 13 files |
|
|
|
|
| 71 |
|
| 72 |
## Accumulated Context
|
| 73 |
|
|
@@ -156,6 +157,9 @@ Recent decisions affecting current work:
|
|
| 156 |
- [Phase 02]: getDebug().furigana does not exist at ready - host.js binds after boot from the shared template - so every furigana read waits for window.Avatar.__debug.furigana; reading the selects in that window made a passing D-14 reload look like a failure
|
| 157 |
- [Phase 02]: The learner line's analyze round trip measures 14.6-22.7 s in-browser against 0.02-0.19 ms of server work (Gradio serving it around a 12-19 s headless synthesis), so the plan's 5 s budget is recorded and printed, never asserted; the product consequence is logged to deferred-items with two levers
|
| 158 |
- [Phase 02]: Deployed rows are rehearsed through scripts/rehearse_deployed.py (free port, DISABLE_GPU=1, /config polled, app stopped, pytest's exit code) - the reusable local-deployed harness for the rest of Phase 2; 02-07's three furigana rows are verified locally and await the owner's push, and JPN-02 stays Pending for 02-10
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
### Pending Todos
|
| 161 |
|
|
|
|
| 3 |
milestone: v1.0
|
| 4 |
milestone_name: milestone
|
| 5 |
status: unknown
|
| 6 |
+
stopped_at: "Completed 02-08-PLAN.md (wave 6): the word-lookup popover (89a4bf6) and its three-layer proof incl. the Pixel 7 column (fd38c7c). Tap a word -> an anchored read-only card with reading, up to three JMdict senses, dictionary form and a level badge, built from the token the line already holds: request_counter records ZERO requests under both transports AND on the deployed page. Two fixes the phone row found: the card could be clamped ON TOP of its own anchor (the next tap was then swallowed silently), and #transcript-text had no edges at all - it is now a 40vh scrolling log, which also makes host.js's scrollTop = scrollHeight do something for the first time. Standalone 6 passed; both transports 2 passed with requests []; deployed rehearsal 2 passed (desktop senses ['study', 'diligence; working hard', 'experience; knowledge; lesson (for the future)'], Pixel 7 column 324x336 with insideTranscript true at the top and the bottom of the line) - verified locally, awaiting the owner's push. Quick loop 334 passed / 34.5 s; whole e2e 36 passed 23 skipped 0 failed. JPN-03 NOT marked. Next 02-09."
|
| 7 |
+
last_updated: "2026-09-07T05:10:00.000Z"
|
| 8 |
progress:
|
| 9 |
total_phases: 6
|
| 10 |
completed_phases: 1
|
| 11 |
total_plans: 22
|
| 12 |
+
completed_plans: 19
|
| 13 |
---
|
| 14 |
|
| 15 |
# Project State
|
|
|
|
| 24 |
## Current Position
|
| 25 |
|
| 26 |
Phase: 02 (japanese-language-core) β EXECUTING
|
| 27 |
+
Plan: 9 of 11
|
| 28 |
|
| 29 |
## Performance Metrics
|
| 30 |
|
|
|
|
| 68 |
| Phase 02 P05 | 12 | 2 tasks | 7 files |
|
| 69 |
| Phase 02 P06 | 38 | 3 tasks | 10 files |
|
| 70 |
| Phase 02 P07 | multi-session | 3 tasks | 13 files |
|
| 71 |
+
| Phase 02 P08 | 95 | 2 tasks | 8 files |
|
| 72 |
|
| 73 |
## Accumulated Context
|
| 74 |
|
|
|
|
| 157 |
- [Phase 02]: getDebug().furigana does not exist at ready - host.js binds after boot from the shared template - so every furigana read waits for window.Avatar.__debug.furigana; reading the selects in that window made a passing D-14 reload look like a failure
|
| 158 |
- [Phase 02]: The learner line's analyze round trip measures 14.6-22.7 s in-browser against 0.02-0.19 ms of server work (Gradio serving it around a 12-19 s headless synthesis), so the plan's 5 s budget is recorded and printed, never asserted; the product consequence is logged to deferred-items with two levers
|
| 159 |
- [Phase 02]: Deployed rows are rehearsed through scripts/rehearse_deployed.py (free port, DISABLE_GPU=1, /config polled, app stopped, pytest's exit code) - the reusable local-deployed harness for the rest of Phase 2; 02-07's three furigana rows are verified locally and await the owner's push, and JPN-02 stays Pending for 02-10
|
| 160 |
+
- [Phase 02]: The word lookup is a PURE CLIENT ACTION and it is asserted as one: a request_counter wraps the taps under both transports and on the deployed page and records [] every time. The glosses ride inside the token record the line already holds, which is why the iframe transport needed no new bridge call for JPN-03
|
| 161 |
+
- [Phase 02]: A popover clamped into the visible column can land ON TOP of the word it is anchored to, and the next tap is then swallowed in silence (the capture-phase dismiss returns early because the target is inside the card, and pointerup finds no .tok). Placement now picks the side of the word with more room and caps the card to that room, so it scrolls internally instead of growing over its anchor
|
| 162 |
+
- [Phase 02]: insideTranscript can only be a fact if the transcript has edges: #transcript-text was an auto-height block, so ANY card anchored to the last line hung below it and the plan's phone must-have was unreachable on the real page. It is now height: 40vh / min-height: 14em / overflow-y: auto (335 px on a Pixel 7), which also makes host.js's long-standing el.scrollTop = el.scrollHeight do something for the first time. A 196 px min-height was tried first and was still too short - the card covered the line above its anchor
|
| 163 |
|
| 164 |
### Pending Todos
|
| 165 |
|
.planning/phases/02-japanese-language-core/02-08-SUMMARY.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
phase: 02-japanese-language-core
|
| 3 |
+
plan: 08
|
| 4 |
+
subsystem: ui
|
| 5 |
+
tags: [popover, lookup, jmdict, glosses, jlpt, dom, transcript, pointer-events, touch, pixel-7, playwright, parity, deployed-rehearsal, jpn-03]
|
| 6 |
+
|
| 7 |
+
# Dependency graph
|
| 8 |
+
requires:
|
| 9 |
+
- phase: 02-japanese-language-core
|
| 10 |
+
plan: 07
|
| 11 |
+
provides: "avatar/transcript.js (createTranscript, the retained tokens Map, the bindPointer seam, debug.furigana), transcript.css, transcript-harness.html, host.js's bindHost(avatar, doc, transcriptModule), scripts/rehearse_deployed.py"
|
| 12 |
+
- phase: 02-japanese-language-core
|
| 13 |
+
plan: 05
|
| 14 |
+
provides: "the 14-key token record - surface, reading, lemma, jlpt ('N5'..'N1'|'N1+'|'name'), gloss (senses x glosses), tappable - and the 28-sentence golden fixture"
|
| 15 |
+
- phase: 02-japanese-language-core
|
| 16 |
+
plan: 06
|
| 17 |
+
provides: "AvatarDirective.tokens on every turn, so the glosses are already on the page when a word is tapped"
|
| 18 |
+
- phase: 01-voice-avatar-loop-skeleton
|
| 19 |
+
plan: 09
|
| 20 |
+
provides: "the deployed suite, --space-url, wait_for_avatar_ready, speech_events, request_counter"
|
| 21 |
+
provides:
|
| 22 |
+
- "avatar/transcript.js: openPopover(lineId, tokenIndex) / closePopover() / bindPointer(), the #lookup-popover card built once from the token's own record, and debug.popover {open, lineId, tokenIndex, surface, reading, level, glossCount, left, top, width, insideTranscript, openCount, closeCount}"
|
| 23 |
+
- "avatar/transcript.css: #lookup-popover and its .lk-* parts, five level-rank hues on the badge and a 1 px rank underline on tappable words; #transcript-text is now a 40vh scrolling column"
|
| 24 |
+
- "avatar/host.js: one transcript.bindPointer() at bind and transcript.debug.popover published on __debug (POPOVER_DEBUG_SEED for the module-less fallback)"
|
| 25 |
+
- "avatar/transcript-harness.html: bindPointer() called, window.__popoverDebug live, ?narrow=1 is a 300 px SCROLLING column"
|
| 26 |
+
- "tests/test_transport_seam.py: test_popover_is_dom_only_and_read_only, test_popover_publishes_its_position, test_host_binds_pointer_once"
|
| 27 |
+
- "tests/e2e/test_transcript_standalone.py: test_popover_anchors_and_dismisses, test_popover_stays_inside_narrow_column (real touchscreen taps)"
|
| 28 |
+
- "tests/e2e/test_facade_parity.py: test_lookup_popover_under_both_transports[inline|iframe] - zero requests under either"
|
| 29 |
+
- "tests/e2e/test_avatar_loop.py: test_lookup_popover (desktop) and test_lookup_popover_mobile (Pixel 7 descriptor), plus _tap_mouse / _tap_touch / _box helpers"
|
| 30 |
+
affects: [02-09, 02-10, 02-11, phase-3-tutor-turns]
|
| 31 |
+
|
| 32 |
+
# Tech tracking
|
| 33 |
+
tech-stack:
|
| 34 |
+
added: []
|
| 35 |
+
patterns:
|
| 36 |
+
- "The lookup is a PURE CLIENT ACTION and that is asserted, not asserted-about: a request_counter wraps the taps at both the parity and deployed layers and must record an empty list. It is the one number that proves the glosses really ride inside the token rather than being fetched on demand"
|
| 37 |
+
- "The card is placed on whichever side of the tapped word has more room and is capped to that room (it scrolls internally), so 'it stayed inside the column' and 'it did not cover the word' are true by construction rather than by luck about content length"
|
| 38 |
+
- "insideTranscript can only be a fact if the transcript has edges: #transcript-text became a real 40vh scrolling log, which also makes host.js's long-standing `el.scrollTop = el.scrollHeight` do something for the first time"
|
| 39 |
+
- "Phone rows use playwright.devices['Pixel 7'] with page.touchscreen.tap on a has_touch context, never page.click - research Pitfall 5. Elements are scroll_into_view_if_needed() first, because on a phone the transcript is below the fold and a tap at an off-viewport coordinate lands on nothing"
|
| 40 |
+
- "The badge is compared against the level in the page's OWN lastTokens record, never a literal in the test - the same rule the furigana oracle follows, so a wrong record and a wrong badge cannot agree"
|
| 41 |
+
|
| 42 |
+
key-files:
|
| 43 |
+
created: []
|
| 44 |
+
modified:
|
| 45 |
+
- avatar/transcript.js
|
| 46 |
+
- avatar/transcript.css
|
| 47 |
+
- avatar/transcript-harness.html
|
| 48 |
+
- avatar/host.js
|
| 49 |
+
- tests/test_transport_seam.py
|
| 50 |
+
- tests/e2e/test_transcript_standalone.py
|
| 51 |
+
- tests/e2e/test_facade_parity.py
|
| 52 |
+
- tests/e2e/test_avatar_loop.py
|
| 53 |
+
|
| 54 |
+
key-decisions:
|
| 55 |
+
- "A card clamped into the visible column can land ON TOP of the word it is anchored to, which silently makes 'tap the next word to re-anchor' impossible - the second tap hits the card, the capture-phase dismiss returns early because the target is inside #lookup-popover, and nothing happens at all. Placement now picks the side of the word with more room and caps the card to that room"
|
| 56 |
+
- "A 196 px column is not tall enough to hold a two-line conversation AND a 90 px card: the card covered the line above its anchor and made those words untappable. #transcript-text is a 40vh scrolling log (335 px on a Pixel 7, 288 px on a 720 px desktop) with a 14em floor, rather than an auto-height block with a max"
|
| 57 |
+
- "insideTranscript is measured against the container's own rect with a 0.5 px tolerance, and the offsets are computed in the container's PADDING-box coordinates (clientLeft / clientTop off the viewport rects, plus scrollTop) - the plan's formula used the border-box left, which is 9 px out in the harness"
|
| 58 |
+
- "The Popover API was not used (research Β§ Q6): iOS 16 Safari lacks it and its top-layer behaviour inside the Hub's cross-origin frame is unverified. The positioned div is ~60 lines and every one of its decisions is a published number"
|
| 59 |
+
- "The card is read-only (D-08) and the seam test enforces it as an absence: no fetch(, no XMLHttpRequest, no localStorage, no sessionStorage, no contextmenu and no long-press plumbing anywhere in transcript.js"
|
| 60 |
+
|
| 61 |
+
patterns-established:
|
| 62 |
+
- "Three-layer lookup proof: standalone harness (no server, both mouse and touch) -> both transports (two live Gradio apps, request counter) -> deployed rows rehearsed locally on a free port, one of them on a phone descriptor. A standalone pass with a deployed failure localises the fault to the host page by construction"
|
| 63 |
+
- "Every geometry claim publishes the rect it was derived from, so a failure prints the card rect AND the container rect rather than a bare boolean"
|
| 64 |
+
|
| 65 |
+
requirements-completed: [] # JPN-03 deliberately NOT marked - 02-VALIDATION binds it to 02-10's deployed rows against the pushed Space
|
| 66 |
+
|
| 67 |
+
# Metrics
|
| 68 |
+
duration: 95min
|
| 69 |
+
completed: 2026-09-07
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
# Phase 02 Plan 08: Word-Lookup Popover Summary
|
| 73 |
+
|
| 74 |
+
**Tapping a word opens an anchored, read-only card carrying its reading, its JMdict senses, its dictionary form and its JLPT badge, built entirely from the token record the line already holds - proven with a request counter that records ZERO requests under both transports and on the deployed page - and the two placement bugs that only a phone-width column could expose (a card clamped on top of its own anchor, and a column too short to hold the conversation and the card at once) were found by the Pixel 7 row and fixed.**
|
| 75 |
+
|
| 76 |
+
## Performance
|
| 77 |
+
|
| 78 |
+
- **Duration:** ~95 min, single session.
|
| 79 |
+
- **Completed:** 2026-09-07
|
| 80 |
+
- **Tasks:** 2 of 2
|
| 81 |
+
- **Files created:** 0. **Files modified:** 8.
|
| 82 |
+
|
| 83 |
+
## Task Commits
|
| 84 |
+
|
| 85 |
+
1. **Task 1: the popover in transcript.js + CSS, pointer wiring, harness tests, host hook** β `89a4bf6` (feat)
|
| 86 |
+
2. **Task 2: both-transports and deployed rows, desktop and Pixel 7, plus the two placement fixes they found** β `fd38c7c` (test)
|
| 87 |
+
|
| 88 |
+
## What shipped
|
| 89 |
+
|
| 90 |
+
`transcript.js` gained `openPopover(lineId, tokenIndex)`, `closePopover()` and a real `bindPointer()`:
|
| 91 |
+
|
| 92 |
+
- the `#lookup-popover` element is built once, appended to `#transcript-text`, `role="dialog" aria-label="Word lookup"`, hidden with the `hidden` attribute, every field written with `textContent`;
|
| 93 |
+
- the card carries `.lk-surface` / `.lk-reading` / `.lk-level` (with `data-level` for the five rank hues), `.lk-lemma` (`dictionary form: X`, only when the lemma differs from the surface), an `<ol class="lk-gloss">` of the first three senses (each sense's glosses joined with `; `), and `.lk-none` ("no dictionary entry") when the word has none;
|
| 94 |
+
- the badge reads `N5`..`N1` as itself, `N1+` as **`N1+ / beyond lists`** and `name` as `name` (D-10 / D-12) β the card never invents a level;
|
| 95 |
+
- `pointerup` on the column opens, a **capture-phase `pointerdown` on the document** dismisses unless the target is inside `#lookup-popover, .tok` (so a tap on another word re-anchors without passing through a closed state β `closeCount` stays 0 across a re-anchor, and that is asserted), `Escape` closes, `Enter`/`Space` open the focused word, and the column's `scroll` closes;
|
| 96 |
+
- `setMode` / `setLevel` / `setTokens` close the card first (Pitfall 10), enforced by a seam test that reads each function's body.
|
| 97 |
+
|
| 98 |
+
`host.js` calls the renderer's `bindPointer()` exactly once and publishes `transcript.debug.popover` on `__debug`; it owns none of the card's DOM.
|
| 99 |
+
|
| 100 |
+
## Measured numbers
|
| 101 |
+
|
| 102 |
+
### Standalone (`tests/e2e/test_transcript_standalone.py`, **6 passed**)
|
| 103 |
+
|
| 104 |
+
`η°δΈγγγ―ζ±δΊ¬γ«δ½γγ§γγΎγγ` as an avatar line, 1280x720:
|
| 105 |
+
|
| 106 |
+
| tap | published |
|
| 107 |
+
| --- | --- |
|
| 108 |
+
| `η°δΈγγ` (a name, no JMdict entry) | `level 'name'`, `glossCount 0`, `openCount 1`, `insideTranscript true`, card **218.25 x 85.13 px**; `.lk-level` reads `name`, `.lk-lemma` reads `dictionary form: η°δΈ`, `.lk-none` visible |
|
| 109 |
+
| empty column space | `open false`, `closeCount 1` |
|
| 110 |
+
| `δ½γγ§γγΎγ` | `tokenIndex 4`, `level 'N5'` (= the fixture record's), `glossCount 1`, `openCount 2`, `.lk-lemma` contains `δ½γ` |
|
| 111 |
+
| `Escape` | `open false`, `closeCount 2` |
|
| 112 |
+
| Tab from `#level-select` -> `.tok[data-token=0]`, Enter | `open true`, `openCount 3` |
|
| 113 |
+
|
| 114 |
+
`.tok` count 3 and `.plain` count 3 on that line: `γ―` / `γ«` / `γ` are not words (D-11).
|
| 115 |
+
|
| 116 |
+
**The 300 px phone column** (`?narrow=1`, 412x839, `has_touch`, six copies of the long fixture β column **318 x 353.6 px**, `clientWidth 316`, `clientHeight 352`, `scrollHeight 431`, so it really scrolls):
|
| 117 |
+
|
| 118 |
+
| tap (real `touchscreen.tap`) | published |
|
| 119 |
+
| --- | --- |
|
| 120 |
+
| the LAST word of the LAST line (`θ‘γγΎγγ`) | `open true`, **`insideTranscript true`**, `left 8`, `width 300`, `glossCount 3`; card rect `left 25 / top 157.59 / right 325 / bottom 355.52` (**300 x 197.92**) β flipped ABOVE the word |
|
| 121 |
+
| scroll the column | `open false`, `closeCount 1` |
|
| 122 |
+
| the FIRST word of the FIRST line (`δ»ζ₯`) | `open true`, `openCount 2`, **`insideTranscript true`**, card **243.63 x 89.95** β below the word |
|
| 123 |
+
| scroll again | `open false`, `closeCount 2` |
|
| 124 |
+
|
| 125 |
+
### Both transports (`test_lookup_popover_under_both_transports`, **2 passed**)
|
| 126 |
+
|
| 127 |
+
Typed turn `η°δΈγγγ―ζ±δΊ¬γ«δ½γγ§γγΎγγ`, then two taps inside a `request_counter`:
|
| 128 |
+
|
| 129 |
+
| number | inline | iframe |
|
| 130 |
+
| --- | --- | --- |
|
| 131 |
+
| `η°δΈγγ` | `level 'name'`, `glossCount 0`, `openCount 1` | identical |
|
| 132 |
+
| `δ½γγ§γγΎγ` | `tokenIndex 4`, `level 'N5'`, `glossCount 1`, `openCount 2`, `closeCount 0` | identical |
|
| 133 |
+
| card left/top/width | `161 / 66 / 252.86` | `161 / 66 / 252.86` |
|
| 134 |
+
| **requests during both taps** | **`[]`** | **`[]`** |
|
| 135 |
+
| tap `#intro` | `open false`, `closeCount 1` | `open false`, `closeCount 1` |
|
| 136 |
+
|
| 137 |
+
The iframe column is the one that mattered: a design that fetched glosses on tap would have needed a new bridge call there, and this row would have recorded it.
|
| 138 |
+
|
| 139 |
+
### Deployed rows, rehearsed locally (`scripts/rehearse_deployed.py`, **2 passed**)
|
| 140 |
+
|
| 141 |
+
App answered `/config` after **4.1 s**.
|
| 142 |
+
|
| 143 |
+
**`test_lookup_popover` (desktop, 1280x720).** `ζ₯ζ¬θͺγεεΌ·γγ¦γγΎγγ`, whole turn **24,000 ms**; tap `εεΌ·γγ¦γγΎγ` (token 2):
|
| 144 |
+
|
| 145 |
+
- `popover` = `{open: true, lineId: 'L2', tokenIndex: 2, surface: 'εεΌ·γγ¦γγΎγ', reading: 'γΉγγγγγγ¦γγΎγ', level: 'N5', glossCount: 3, left: 100, top: 66, width: 291.84, insideTranscript: true, openCount: 1, closeCount: 0}`
|
| 146 |
+
- card rect `left 880 / top 347.33 / right 1171.84 / bottom 477.91` = **291.84 x 130.58 px**
|
| 147 |
+
- senses shown: **`['study', 'diligence; working hard', 'experience; knowledge; lesson (for the future)']`**
|
| 148 |
+
- badge `N5` == the page's own `lastTokens[2].jlpt`; lemma line `dictionary form: εεΌ·γγ`; reading `γΉγγγγγγ¦γγΎγ`
|
| 149 |
+
- **requests during the tap: `[]`**
|
| 150 |
+
- the three `.plain` spans on that line carry no `role`
|
| 151 |
+
- tapping the learner's own `ζ₯ζ¬θͺ` re-anchors: `lineId 'L1'`, `level 'N1+'`, `openCount 2`, `closeCount 0` (D-03 / D-18 parity); `Escape` closes it
|
| 152 |
+
|
| 153 |
+
**`test_lookup_popover_mobile` (Pixel 7 descriptor, 412x839, real touch).** `δ»ζ₯γ―γγ倩ζ°γ§γγγγε
¬εγζ£ζ©γγ¦γγγθ²·γη©γ«θ‘γγΎγγγ`, the avatar's line **35,031 ms** after Enter:
|
| 154 |
+
|
| 155 |
+
- transcript column: `{left: 44, top: 332.52, width: 324, height: 335.59, clientWidth: 324, clientHeight: 336, scrollHeight: 336, scrollTop: 0}`
|
| 156 |
+
- tap the LAST word (`θ‘γγΎγγ`): `{open: true, tokenIndex: 14, level: 'N5', glossCount: 3, left: 8, top: 125, width: 308, insideTranscript: true, openCount: 1}`; card rect `left 52 / top 457.52 / right 360 / bottom 607.36` = **308 x 149.84 px** β inside a column that runs 332.52 -> 668.11
|
| 157 |
+
- tap the FIRST word (`δ»ζ₯`): `{open: true, tokenIndex: 0, level: 'N5', glossCount: 2, insideTranscript: true, openCount: 2}`; card **208.20 x 87.33** at `left 88 / top 428.52`
|
| 158 |
+
- tap the intro text: `open false`, `closeCount 1`
|
| 159 |
+
|
| 160 |
+
**These deployed rows are verified LOCALLY, against a `DISABLE_GPU=1 app.py` on loopback, and await the owner's push.** Nothing was pushed; the only remote is the owner's HF `space`.
|
| 161 |
+
|
| 162 |
+
### Suite totals
|
| 163 |
+
|
| 164 |
+
| suite | result |
|
| 165 |
+
| --- | --- |
|
| 166 |
+
| `tests/e2e/test_transcript_standalone.py` | **6 passed** |
|
| 167 |
+
| `tests/test_transport_seam.py` | **100 passed** |
|
| 168 |
+
| `test_lookup_popover_under_both_transports` | **2 passed** |
|
| 169 |
+
| `rehearse_deployed.py ... -k lookup_popover` | **2 passed** |
|
| 170 |
+
| whole `tests/e2e/` in ONE invocation | **36 passed, 23 skipped, 0 failed in 323 s** |
|
| 171 |
+
| quick loop (`tests --ignore=tests/e2e -m "not mt"`) | **334 passed / 17 deselected in 34.5 s** (budget 40 s) |
|
| 172 |
+
| `-m mt` | 17 passed |
|
| 173 |
+
| `git diff --stat -- avatar/avatar.js avatar/avatar-iframe.js avatar/stage.html avatar/turn-loop.js avatar/facade.js` | **0 lines** β no new surface needed |
|
| 174 |
+
| ruff check + format --check | clean (57 files) |
|
| 175 |
+
|
| 176 |
+
## Deviations from Plan
|
| 177 |
+
|
| 178 |
+
### Auto-fixed issues
|
| 179 |
+
|
| 180 |
+
**1. [Rule 1 - Bug] The card could be clamped on top of the word it was anchored to, which broke re-anchoring**
|
| 181 |
+
|
| 182 |
+
- **Found during:** Task 2, by `test_lookup_popover_under_both_transports` β the second tap read `tokenIndex 0` with `openCount 1`, i.e. it did nothing at all.
|
| 183 |
+
- **Issue:** the plan's placement rule was "below, flip above if it overflows, otherwise clamp into view". On a column shorter than word + card, neither side fits, so the clamp put the card *over* its own line. The second tap then landed on the card: the capture-phase dismiss returns early (the target is inside `#lookup-popover`) and `pointerup` finds no `.tok`, so the tap was swallowed silently β the worst shape of failure, because nothing errors and nothing moves.
|
| 184 |
+
- **Fix:** the card is placed on whichever side of the word has more room (`roomBelow >= roomAbove`) and its `max-height` is set to that room, so it scrolls internally rather than growing over the anchor. Offsets are computed in the container's padding-box coordinates (`clientLeft` / `clientTop` off the viewport rects) rather than the plan's border-box `left`, which was 9 px out in the harness.
|
| 185 |
+
- **Files modified:** `avatar/transcript.js`
|
| 186 |
+
- **Commit:** `fd38c7c`
|
| 187 |
+
|
| 188 |
+
**2. [Rule 2 - Missing critical functionality] `insideTranscript` was not a fact anywhere, because the transcript had no edges**
|
| 189 |
+
|
| 190 |
+
- **Found during:** Task 1's first standalone run: `insideTranscript` read `false` on a perfectly correct card, because `#transcript-text` was an auto-height block one line tall and *any* card anchored to the last line hangs below it. The plan's must-have ("on a 412 px phone column the popover stays inside the transcript") was unreachable on the real page, not just in the harness.
|
| 191 |
+
- **Fix:** `#transcript-text` is now a real scrolling log β `height: 40vh; min-height: 14em; overflow-y: auto` (335 px on a Pixel 7, 288 px on a 720 px desktop). This also makes `host.js`'s long-standing `el.scrollTop = el.scrollHeight` do something for the first time; on an auto-height element it had always been a no-op, so a long conversation grew the page instead of scrolling the transcript.
|
| 192 |
+
- **Second iteration:** a `min-height: 14em` (196 px) block was still too short β the Pixel 7 row then showed the card covering the line *above* its anchor, so the first word of the line was untappable. A viewport fraction, not a content-driven minimum, is what gives the card room beside the words.
|
| 193 |
+
- **Files modified:** `avatar/transcript.css`, `avatar/transcript-harness.html` (its own `min-height: 2em` was dropped so the harness measures the shape the app ships)
|
| 194 |
+
- **Commits:** `89a4bf6` (the column), `fd38c7c` (40vh)
|
| 195 |
+
|
| 196 |
+
### Knowing deviations from the plan's letter
|
| 197 |
+
|
| 198 |
+
- **The plan's standalone narrow row said "render fixture #25 three times so the container scrolls".** Three copies do not overflow a 300 px column; six do, and the row now *asserts* `scrollHeight > clientHeight` before relying on it, so the scroll-dismiss claim can never pass vacuously.
|
| 199 |
+
- **`test_popover_anchors_and_dismisses` asserts `insideTranscript` too**, which the plan only asked of the narrow row. It is free once the column has edges, and it is the assertion that caught deviation 2.
|
| 200 |
+
- **The seam test spells "no long-press" as `contextmenu` absent plus `longpress` absent** rather than the plan's bare `long`, which would match `belongs`, `along` and `longest`.
|
| 201 |
+
- **`transcript.css` uses double-quoted attribute selectors** (`[data-level="N5"]`) because the plan's acceptance grep is written with double quotes; the file has no other quoted selectors, so there is no inconsistency.
|
| 202 |
+
- **The mobile row waits for the avatar's line (its `.tok` spans), not for speech-end.** The line is rendered from the `turn` event, which arrives before playback; waiting for the audio would have added ~20 s to a row that says nothing about audio.
|
| 203 |
+
- **Task 2's commit is `test(...)` although it carries the two renderer fixes above.** The bulk of it is the two new test layers, and the fixes exist only because those layers found them; both are named in the commit body.
|
| 204 |
+
|
| 205 |
+
### Out of scope, logged not fixed
|
| 206 |
+
|
| 207 |
+
Nothing new. The two known-flaky items in `deferred-items.md` (Phase 1's `running_after_click_ms` ceiling and `test_asr_wasm_fallback` under whole-file load) did not fire in this plan's runs.
|
| 208 |
+
|
| 209 |
+
## Known Stubs
|
| 210 |
+
|
| 211 |
+
None. Every field on the card is filled from the token record; there is no placeholder text and no hardcoded gloss anywhere in `transcript.js`.
|
| 212 |
+
|
| 213 |
+
## Requirements
|
| 214 |
+
|
| 215 |
+
**JPN-03 deliberately NOT marked complete.** `02-VALIDATION.md` binds it to 02-10's deployed rows against the *pushed* Space; everything here is verified locally. Nothing was run through `requirements mark-complete`.
|
| 216 |
+
|
| 217 |
+
## For The Next Plan
|
| 218 |
+
|
| 219 |
+
- **02-09 (inline EN reveal):** the transcript is now a **scrolling** column, so anything that grows a line (a translation under it) must expect the container's `scroll` event β which closes the lookup card. If the reveal scrolls the line into view, do it deliberately and expect `popover.open` to go false.
|
| 220 |
+
- `transcript.getTokens(lineId)` and `openPopover(lineId, tokenIndex)` are exported; `debug.popover` is live on `getDebug()` under both transports. **Wait for `window.Avatar.__debug.popover` (`POPOVER_BOUND`), not `ready`** β same bind race as `furigana`.
|
| 221 |
+
- `_tap_mouse` / `_tap_touch` / `_box` in `tests/e2e/test_avatar_loop.py` are the shared raw-input helpers (they scroll the element into view first, which a phone viewport needs); `playwright.devices["Pixel 7"]` + `browser.new_context(**...)` is the phone-row recipe.
|
| 222 |
+
- **Any new absolutely-positioned UI inside `#transcript-text` must publish where it landed**, and be capped to the room beside its anchor rather than clamped over it β the failure mode is a silently swallowed tap, not an error.
|
| 223 |
+
|
| 224 |
+
## Self-Check: PASSED
|
| 225 |
+
|
| 226 |
+
All eight modified files exist on disk and both commits (`89a4bf6`, `fd38c7c`) are in `git log`.
|