Round 3 fixer report for the mind-standard group. 2026-10-13. Files changed: prototype/standard.html, prototype/mind.html, prototype/server.py. Not touched: prototype/data/ets.sample.json (nothing additive was needed), shared.css, index.html, prototype/img/, brief/, research/. Work list: verify-02-mind-standard.md (claim 13, claim 15, section 5) plus N5.
Method: headless Chrome for Testing (chromium-1234) through the Playwright library at 1440x900 (DPR 2) and 390x844 (DPR 3, isMobile, hasTouch, pointer: coarse matched). The demo and mocked pass ran against a plain python3 -I -m http.server 8791 --bind 127.0.0.1 with /api/health mocked 200 and a context-level route that aborted any unmocked /api/mind (leak counter 0 over 11 steps), so no model call was possible there. The live pass ran against uv run --script server.py --pid-file <scratch>/server.pid on 127.0.0.1:8777: 3 Claude exchanges of the 6 allowed (mind 1440, mind 390, standard dock 1440), all outcome=ok. --selftest is green (scanner fuzz 400/400, sanitizer, history echo, budget, history trust, the new public-hosts check, digest 38,488 chars). Console: zero console errors, zero page errors, zero failed requests and zero HTTP 4xx/5xx in every non-mocked context (the mocked contexts produce only the failures the mocks were built to cause). Every capture cited below was read as pixels before judging. Both servers were stopped by their exact PIDs (uv/python 46750; static 32497); lsof shows 8777 and 8791 free; the server removed its own pid file in finally. MODEL, EFFORT, MAX_TOKENS, LIMITS and the budget numbers were not touched.
| Before (verify 02, row 13) | After | |
|---|---|---|
| Provisional 3-speaker grid, final 1-speaker grid | Page kept 3 under // showing 3 speakers |
Merge-bar sequence observed by a MutationObserver: (none) → [ clear ]// showing 3 speakers · sample → [ clear ]// showing 1 speaker · sample. Final page: one visible card (spk-erin-autin), 76 hidden; the turn carries one note // filtered on the page · [ jump there ], one sheet component (choices, 2 chips), caption Just one, in the end.; status Just one, in the end. Showing 1 speaker, sample data, 2 follow-up suggestions. Capture r3-standard-mock-reconcile-1440x900 |
invalid error after a merge |
Lineup stayed filtered; the failed turn kept // filtered on the page · [ jump there ] (v2-standard-mock-error-after-merge-1440x900) |
Sequence (none) → showing 3 speakers → (none). Final page: 77 visible, 0 hidden, no merge bar; the failed turn has 0 notes and 0 components, caption Something came back garbled. Try again., retry chips ask that again, who's speaking, the agenda, where is it. Capture r3-standard-mock-error-after-merge-1440x900 |
Code (standard.html): each dock turn now records pageTrees (every page-backed tree handled while streaming, merged or materialized) and mergedTrees (the ones that actually merged; the note element carries data-tree). reconcile(ctx, ui) compares the final answer's page-backed trees with pageTrees; on any difference it calls clearMerges(false), drops the turn's notes and sheet components, and rebuilds the turn from the final ui through addDockComp(ctx, tree, true) (merge or materialize per component, still components without the entry animation). When they match, only the sheet components are diffed by position, excluding the trees that merged (this also fixes the old filter that dropped a page-backed tree that had materialized). showError calls clearMerges(false) when the turn merged and drops its notes for every failure code, so a failed turn leaves no mark on the page. The phone peek still keys off ctx.merged, which now reflects the final state.
| Problem | Before | After | Capture |
|---|---|---|---|
| Phone: head above the lockup | Head 57 to 259 px, copy from 361 px, gradient covering nothing (v2-standard-top-390x844) |
--head-w: min(62vw, 260px) is a page token; the hero's padding-top is calc(var(--head-w) * .3333) (40 % of the head's height, the slot SVG is 240x200) and the hero aligns to the top instead of the bottom. Measured at 390: head 57 to 259 (202 tall), copy block top 138 = 40 % of the head, eyebrow at 52 %, mark from 69 % to 94 %, theme line from 101 %: the lower 60 % of the cranium sits behind the eyebrow, the mark and the theme line, on the copy's white gradient (now 0 → .78 white at 24 % → white at 50 % of the copy block) with the slot's mask moved to 64 % so the hollows keep some presence under the veil |
r3-standard-top-390x844 |
| Desktop: facts hairline across the forehead | Rule at about 615 px crossing the head (v2-standard-top-1440x900) |
.facts{max-width:58%; gap: var(--sp-4) var(--sp-6)}: the rule and the four numerals end at x 828 in one row (all four at y 664); the head slot is now sized from the hero (width:min(35%,520px); right:5%), 505 px wide at 1440 from x 864 to 1368, top 447, cut plane on the hero's lower edge. Nothing crosses it |
r3-standard-top-1440x900 |
| Desktop, dock open: head under the description and the numerals | main padded by 440 px reflowed the hero over the head (v2-standard-dock-open-1440x900) |
body.dock-open .hero-head{opacity:0} with a 320 ms transition (the mind is in the dock, whose head carries the glyph) and body.dock-open .facts{max-width:none}; both reset at 820 px and below where the dock does not pad the page. Measured: head opacity 0, copy 48 to 572, facts 48 to 952 in one row, hero bottom 893 inside the 900 viewport, no overlap. A first attempt that only shrank the head to the padded box measured the head at y 725 to 1018 because the 524 px copy column wrapped the facts to two rows; that is why the fade was chosen |
r3-standard-dock-open-1440x900 |
| Nav Register under the dock | Register 1223 to 1312 with the dock from 1000 | body.dock-open .nav{padding-right:var(--dock-w)} (0 at 820 px and below): Register 863 to 952, dock at 1000, nav and main content aligned at x 48 |
r3-standard-dock-open-1440x900, r3-standard-live-merge-1440x900 |
Slot contract kept: .hero-head is still one wrapper whose inner child is the unchanged inline SVG; only the wrapper's position, size, mask, breathe, transition and dock-open rules changed. #head in mind.html was not touched. The 24 px gHead symbol is unchanged.
Before: // may 3–6, 2027 · austin, tx (lowercased in JS). After: <p class="when tag tag--caps">// <span id="heroWhen">May 3-6, 2027</span> // <span id="heroWhere">Austin, TX</span></p>, text // May 3-6, 2027 // Austin, TX in the DOM, rendered uppercase by .tag--caps (computed text-transform: uppercase, 12 px, 0.14 em tracking, muted-strong, the same class index.html uses for its lockup line). Both widths read // MAY 3-6, 2027 // AUSTIN, TX (r3-standard-top-1440x900, r3-standard-top-390x844). The hyphen is the decided string's, not an en dash.
// spacingBefore: //who's speaking, //mon 3 under pointer: coarse because display: inline-flex collapsed the trailing space of the ::before (v2-mind-empty-390x844, v2-standard-agenda-390x844). After: .chip::before and .daytab::before in both pages use content: "//" / "" with margin-right: 1ch (computed 7.2 px), which survives the flex formatting. At 390 with hasTouch: pointer: coarse matched, chips and day tabs display: flex, 44 px tall, reading // who's speaking and // mon 3 (r3-mind-empty-390x844, r3-standard-agenda-390x844). Desktop rendering is unchanged (r3-mind-empty-1440x900).
Before: section{scroll-margin-top:73px} against a 56 px phone nav left a 17 px band between two rules; the desktop deep-link landing drew rules at 72 and 74 (v2-standard-escape-landing-1440x900, v2-standard-agenda-390x844). After: section, .daypanel{scroll-margin-top: var(--nav-h)} (72 px, 56 px under 720 px) and applyHash scrolls explicitly to section.top + scrollY - navHeight() (instant on the initial load, smooth on hashchange, auto under reduced motion); mergeIntoPage's scrollIntoView honours the same margin. Measured: deep link #panel-d2 at 1440 lands with the agenda section top at 72 and the nav bottom at 73 (the section's top rule sits under the nav's own hairline), tue 4 selected, only panel-d2 visible; a hashchange to #panel-d3 lands at 72 with wed 5. Phone [ menu ] → Agenda: section top 56, nav bottom 57. After the phone merge the lineup lands at 56. Pixels: r3-standard-escape-landing-1440x900 and its crop r3-standard-escape-landing-1440x900-navcrop (rows 60 to 90 px, 4x), r3-standard-agenda-390x844 and r3-standard-agenda-390x844-navcrop (rows 50 to 77 px, 4x) each show a single rule.
| Item | After | Evidence |
|---|---|---|
| Status grammar | plural() in both summarize functions: speaker(s), session(s), follow-up suggestion(s) |
Mind mock: One person. Showing 1 speaker, sample data, 1 follow-up suggestion.; dock mock: … Showing 1 speaker, sample data, 2 follow-up suggestions.; live: Showing 12 speakers, sample data, 4 follow-up suggestions. |
source: in the DOM |
src() appends source: + text in both pages; .src::before is "// " / "" only |
DOM text source: lineup sample · ets26 stand-ins; ::before content "// " (r3-mind-demo-after1-1440x900, r3-mind-after1-1440x900) |
| Door explainer | Everything laid out: agenda, lineup, venue, tickets. (the fork's string) |
r3-mind-mock-unavailable-1440x900 (mocked unavailable error: quiet caption, door card, composer disabled with the quiet placeholder) |
| Level names | .sc{align-content:start} in both pages |
Row tops [438, 438, 438, 438, 438] live at 1440; [407, 407, 407, 407] demo; the phone peek shot shows Erin Autin and Carlos Ayala level |
server.py --public-host |
--public-host demo.example[:port],... adds Host values (public_hosts(), selftest-covered); on a remote bind (TOKEN set) _hosts() also accepts the address of the interface the connection arrived on (getsockname), for Host and for Origin. Loopback default and the --allow-remote + --token gate unchanged; --public-host without a value exits --public-host needs a value |
Section 4 |
composerFirst() runs synchronously at the top of the boot, after the submit handler is attached and before the fixture and health awaits: it reads and removes sessionStorage['ets27.draft'] (or a ?q=, stripped from the URL), fills #q, focuses it, puts the caret at the end, and buffers any printable key that arrives while #q is not focused (appended to the value, caret moved, focus re-attempted; Enter and modified keys are ignored, so nothing can send). The buffer removes itself on the first focus. The late input.focus() after the health probe is gone, so a visitor who moved focus during the load is not pulled back. Test (init script sets the draft to who, then mind.html#compose): measured before the fixture resolved {value: who, focused: q, caret: 3, draft: null}; after boot {value: who, caret 3 to 3, turns: 0, state: empty, draft: null}; typing x afterwards gives whox. Capture r3-mind-draft-1440x900.
| Check | Result |
|---|---|
| Overflow probe at 390 | scrollWidth 390 in the empty state, after the demo answer and after the live day-2 answer; no element outside the viewport apart from the realm's intentionally oversized fixed cloud layers and the chips inside their horizontal scroller |
| Status region | One role=status per page; texts above |
| Dock inert when closed | inert present, visibility: hidden |
| Peek bar after a phone merge | dock-peek set, #dockExpand focused, dock 57 px tall, 8 of 77 cards visible, lineup under the nav at 56 (r3-standard-dock-peek-390x844) |
| Demo merge and Escape at 1440 | [ clear ]// showing 8 speakers · sample, 8 visible; Escape clears (77 visible, no bar) and focuses the dock input (r3-standard-demo-merge-1440x900) |
| Head after an answer | #head opacity 0 at both widths; the glyph in the top bar |
| Guard matrix sample | Section 4 |
| Selftest | Green, exit 0 |
| Turn | Surface | Outcome | Cache read / write | Out | First ui | Total | Request id |
|---|---|---|---|---|---|---|---|
| 1 | mind 1440, "who's speaking" | ok, 12 speakers + 4 choices | 19,293 / 34 | 264 | 4.5 s | 4.9 s | req_011Cfz93AFwZASLgu9auJz8L |
| 2 | mind 390, "what's on day 2" | ok, 10 sessions (day 2) + 3 choices | 19,293 / 35 | 573 | 4.9 s | 5.1 s | req_011Cfz93kpL92yduRsjqtrs2 |
| 3 | standard 1440, "who's speaking on day 2" | ok, filtered 12 of 77, [ clear ]// showing 12 speakers · sample, one note, choices in the sheet |
19,293 / 65 | 247 | 3.2 s | 3.7 s | req_011Cfz94PkaYieFTgwTD13KS |
No refusals, fallbacks, resets, dropped ids or dropped components. Captures r3-mind-after1-1440x900, r3-mind-after1-390x844, r3-standard-live-merge-1440x900; the empties r3-mind-empty-1440x900 and r3-mind-empty-390x844 were taken live too (no demo tag).
Six-request guard sample over raw sockets against the live server: Host evil.example:8777 → 421 · foreign Origin → 403 · text/plain → 415 · chunked with no length → 411 · length 300000 → 413 · invalid JSON → 400.
Remote bind, a few seconds on --host 0.0.0.0 --allow-remote --token <random> --public-host demo.example: GET with Host demo.example:8777 → 200 · Host 127.0.0.1:8777 → 200 · Host 0.0.0.0:8777 (the bind literal) → 200 · Host evil.example:8777 → 421 · POST with Origin http://demo.example:8777 and no token → 401 · the same with the token and a bad JSON body → 400 (the Origin check passed, the body check stopped it before any model call) · Origin http://evil.example:8777 with the token → 403. Stopped by PID; its pid file was removed by the server. The interface-address path (getsockname) could not be exercised on this Mac: its en0 address 192.168.1.34 lies inside the tailnet-advertised 192.168.1.0/24 (the known home-LAN prefix collision), so a connection to that address never completed and the probe was stopped. That path is verified by code reading only.
prototype/shots/, prefixes r3-mind-, r3-standard-)Live: r3-mind-empty-1440x900, r3-mind-empty-390x844, r3-mind-after1-1440x900, r3-mind-after1-390x844, r3-standard-live-merge-1440x900.
Demo: r3-mind-demo-after1-1440x900, r3-mind-demo-after1-390x844, r3-standard-top-1440x900, r3-standard-top-390x844, r3-standard-agenda-390x844 (+ -navcrop), r3-standard-escape-landing-1440x900 (+ -navcrop), r3-standard-dock-open-1440x900, r3-standard-demo-merge-1440x900, r3-standard-dock-peek-390x844.
Mocked: r3-mind-draft-1440x900, r3-mind-mock-unavailable-1440x900, r3-standard-mock-reconcile-1440x900, r3-standard-mock-error-after-merge-1440x900.
Earlier rounds' shots were not touched. The r3-opening-* files belong to the opening fixer.
r3-standard-live-merge-1440x900 is the 2 s reveal hairline, captured inside its window.data/ets.sample.json needed no addition this round.scroll-margin-top: var(--nav-h) on sections and day panels); anyone scrolling a section programmatically should subtract --nav-h.--dock-w, like main.mind.html honours sessionStorage['ets27.draft'] and #compose as index.html writes them; the composer is focused before any await, nothing is auto-sent.server.py: --public-host H[,H2] for remote demos; the interface address a request arrives on is accepted automatically on a remote bind.