diff --git a/DESIGN.md b/DESIGN.md index e020dd9..58c54de 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -90,3 +90,9 @@ The local Docs tab row contains Docs, Tokenomics and BASK on one line down to 32 Deposit, wider than 960px: the panel's body under the "Make a deposit" heading is a two-column grid (equal columns, 40px gap, items aligned to the top). The left column holds the form as before (rows, search, total, Preview deposit), the preview box and its buttons, the status lines, and then, under a thin line, the "Before you deposit" notes with their heading; the right column holds the stock list, always open: the heading line "All stocks you can deposit (N)" as plain bold text, the sentence "Press a stock to add it to your deposit." and the stock buttons in an auto-fill grid of at least 84px columns. At 960px and below the markup and look are v13's: one column, the list inside the form (open while the form is empty, a closed fold when the wallet holds stock), the notes as a separate panel below. The sentence about grey stocks is gone on every width; the warning note reads "Issuer restrictions and pool checks can temporarily halt deposits." Docs, default tab: five more panels in the same grid between "Risks" and "Find Basket" (ITEM; Earn ITEM by providing liquidity, with a gauge table in the module's table style; Earn BASK by staking ITEM; Rewards FAQ with its questions as h3 like the FAQ panel; ITEM risks). BASK tab: two more sections and a fifth worked example in the module's own blocks (paragraphs, bold formula lines, a two-column table). + +## v15 presentation details + +Vault title, 660px and below: the picture's left edge is `max(0, min((panel - picture) / 2, panel + 14px - 4px - picture x 0.635 - 138px))`: the middle of the panel when her bubble fits (375px and wider), otherwise as far right as leaves the bubble 4px inside the yellow frame. The bubble is 138px wide (three lines at 16px; it was 170px, and 146px under 360px) and keeps its offset from the picture, so its tail still points at her mouth. Her shelf line (the 3px yellow line under her feet) runs from the left frame to the right frame. The heading is centred. The panel's height, the picture's height and every vertical distance are v14's. Above 660px nothing changes. + +Deposit and Redeem titles, 660px and below, while the heading shows the focus marker (`:focus-visible`: a 3px ink outline inside a 6px yellow band; iPhone browsers show it after a tab is tapped): her shelf line sits 6px under her feet instead of 3px and ends 6px before the heading's left edge, which is the marker's outer edge, so the line and the marker's bottom band are one 3px yellow line into the corner. The rule uses `:has()` and applies only when forced colours are off; without the marker the line is v14's. diff --git a/README.md b/README.md index 6819ea0..cdf5fe1 100644 --- a/README.md +++ b/README.md @@ -223,3 +223,18 @@ What the checks allow in this update: - check-calldata (the v13 gate with the module's pins): edited files are Docs.tsx and Flows.tsx, restored to v13 through documented pieces; wallet.tsx is frozen whole; the four module files are pinned by digest; the reviewed source digests, the ordered added-call pins (Docs: the tab state, its keyboard handler and the tab buttons' map; Flows: the desktop state and its matchMedia listener, and the stock list's and notes' calls moved into JSX constants), the four moved display calls, the one changed declaration (listOpen also open on desktop), the byte-identical send(), connect/disconnect texts, counter uses, hook return, raw protected blocks and the differential encoding (496 identical, 1,773 refused) all stay. The approved module CSS is an exact prefix of styles.css; the v14 Deposit rules follow it. - check-bundle: source scope Docs, Flows, Tokenomics, tokenomicsData, BaskGuide, baskGuideData and styles; exactly the four module files are new; shell, share JPEG copies, public files, configuration, packages, lockfile, pinned source and contracts protected; the limit stays 8,388,608 bytes. - check-visibility compares v13 at 375×812 (phones unchanged): the Docs panels are matched by title, the five ITEM panels sit between Risks and Find Basket, every retained panel keeps its size and is displaced only by the tab row, the copy change and the inserted panels. check-art keeps its pinned words. The fork keeps all 86 v13 check positions and the same 70 page sends, replaces the words of three checks for the owner's desktop decisions (the list open in the right-hand column, the notes under the form, the new note) and adds the module's Tokenomics and BASK checks and one for the five ITEM panels; its 24 screenshots keep their names and order at JPEG quality 18 (26 before), because the Docs page is five panels and a tab row longer and the delivery must stay under 8,388,608 bytes. check-tokenomics, check-tokenomics-live and check-bask-guide (five examples, five tables, ten formula lines) are part of the suite. + +## Site update v15 + +The current patch is applied to v14, commit c05c174f639d352fb6184636bcc6e083031daa65. The owner's two phone decisions of 2026-10-13, both made with style rules only (added at the end of styles.css); desktop is unchanged: +- Vault, phones (660px and below): the title picture stands in the middle of the panel. Before, the picture and its bubble were centred together as one group, which left her in the left half with the lower right of the panel empty. Her bubble keeps its three lines and its place beside her head (it is 138px wide now); from 375px up she stands exactly in the middle, and on narrower phones only as far left of it as the bubble needs to stay 4px inside the frame. Her shelf line runs across the whole panel and "ALWAYS IN STOCK" is centred under her. No height and no vertical place changes. +- Deposit and Redeem, phones: iPhone browsers draw the keyboard focus marker (a 3px ink outline inside a 6px yellow band) around the heading after a tab is tapped, because the page moves the focus to the heading on a change of page. Her shelf line used to run into that marker's lower left corner 3px above the band. While the marker is drawn, the line is now 3px lower and ends at the marker's outer edge, so it meets the corner as one line. Without the marker (Android, a page opened directly, after the next tap) the line is where it was. The marker itself, the focus move and every other focus style are unchanged. +- No script changes at all: every web/src file except styles.css is byte-identical to v14, wallet.tsx, Flows.tsx, Docs.tsx, main.tsx and the four Docs module files included. No transaction input, send preparation, chain read, wallet request, storage key or application network call changes. No word and no picture changes. + +What the checks allow in this update: + +- check-calldata (the v14 gate moved to HEAD = v14): no edited script file, so the lists of documented pieces, attribute changes, call changes and added names are empty and every source is frozen whole against v14; the four module files keep their reviewed digests and sink scans, Docs.tsx and Flows.tsx their reviewed source digests; styles.css must start with v14's whole text, and the rules after it are pinned by one reviewed digest and scanned (two phone media queries, six properties, nothing that loads a file). The byte-identical send(), connect/disconnect texts, counter uses, hook return, raw protected blocks and the differential encoding (496 identical, 1,773 refused) all stay. +- check-bundle: source scope styles.css only; no new file; shell, share JPEG copies, public files, configuration, packages, lockfile, pinned source and contracts protected; the limit stays 8,388,608 bytes. +- check-visibility compares v14 at 375×812 with no exception left: every first-screen element keeps its words and its vertical place on all six pages; the Docs panels, the open Deposit stock list and the status notices equal v14; the Deposit and Redeem titles equal v14 across the panel; the Vault title is asserted across the panel (picture in the middle, bubble beside her head inside the frame, shelf line across the panel, heading centred) with v14 proving the look it replaces. +- check-art keeps every earlier assertion (whole bubble words, the bubble off her face, hands and the crate and off every opaque picture pixel, the strip clear of the text, no taller Vault title) at every width from 320 to 2560px in Chromium and WebKit, and adds: the Vault phone layout at every phone width; the live v14 title beside it (nothing in a title moves above 660px; on phones the Deposit and Redeem titles equal v14 and the Vault title differs only sideways as decided), with the v15 check rejecting the v14 look; and the focus marker at 660, 430, 393, 375, 360, 359 and 320px (the line on the marker's bottom band up to its outer edge, the v14 line put back is rejected, the line returns after blur, no shelf line at 661px). +- The fork walk is unchanged in what it checks: all 91 v14 checks under the same names in the same order, none added, the same 70 page sends, the same 24 screenshot names at JPEG quality 18. check-motion, check-fixes, inspect-export, check-guards, check-preservation, check-interface, check-tokenomics, check-tokenomics-live and check-bask-guide are unchanged. diff --git a/web/scripts/check-art.mjs b/web/scripts/check-art.mjs index 596057d..5fba25a 100644 --- a/web/scripts/check-art.mjs +++ b/web/scripts/check-art.mjs @@ -272,8 +272,56 @@ function titleGeometry({R, name, pixels}) { out.tailCovers = scan({left: Math.min(tip.x, ox), top: Math.min(tip.y, b1.y), right: Math.max(tip.x, ox), bottom: b2.y}, inTail); out.textCovers = textRects.reduce((n, x) => n + Object.values(scan(x)).reduce((s, v) => s + v, 0), 0); } + // v15: where the picture, the bubble, the heading and her shelf line (the holder's ::after, phones only) sit across + // the panel, and whether the heading shows the focus marker. + const h1 = title.querySelector(":scope > h1"), hr = r(holder), H = r(h1), hcs = getComputedStyle(h1), line = getComputedStyle(holder, "::after"); + out.width = innerWidth; + out.titleTop = T.top; + out.frame = {left: T.left + bw, right: T.right - bw}; + out.pictureOff = (T.left + T.right) / 2 - (D.x + D.w / 2); + out.bubbleGap = T.right - bw - br.right; + out.bubbleFromPicture = br.left - D.x; + out.headingAlign = hcs.textAlign; + out.headingOff = (T.left + T.right) / 2 - (Math.min(...headingRects.map(x => x.left)) + Math.max(...headingRects.map(x => x.right))) / 2; + out.h1 = {left: H.left, right: H.right, top: H.top, bottom: H.bottom}; + out.ring = {shown: h1.matches(":focus-visible"), outline: `${hcs.outlineStyle} ${hcs.outlineWidth} ${hcs.outlineColor}`, shadow: hcs.boxShadow, ink: getComputedStyle(bubble).borderLeftColor}; + const drawn = line.content !== "none" && line.display !== "none"; + out.shelf = !drawn ? null : {left: hr.left + parseFloat(line.left), right: hr.left + parseFloat(line.left) + parseFloat(line.width), top: hr.bottom - parseFloat(line.bottom) - parseFloat(line.height), bottom: hr.bottom - parseFloat(line.bottom), colour: line.backgroundColor}; return out; } +// v15 (owner, 2026-10-13: "the graphic on mobile is off to the left and doesnt look good"; he chose the picture +// centred with the heading centred). On phones the Vault picture stands in the middle of the panel, at 375 px and +// wider exactly; narrower, only as far left of the middle as her three-line bubble needs to stay 4 px inside the +// frame. Her shelf line runs across the panel inside the frame and the heading is centred. +function assertVaultPhone(g, label) { + assert.equal(g.bubbleLines, 3, `${label}: v15 Vault bubble keeps three lines`); + assert.ok(g.bubbleGap >= 3.5, `${label}: v15 bubble closer than 4 px to the frame (${g.bubbleGap})`); + assert.ok(g.pictureOff > -0.5 && (Math.abs(g.pictureOff) < 0.5 || Math.abs(g.bubbleGap - 4) < 0.5), `${label}: v15 picture neither in the middle nor as near it as her bubble allows (off ${g.pictureOff}, bubble gap ${g.bubbleGap})`); + if (g.width >= 375) assert.ok(Math.abs(g.pictureOff) < 0.5, `${label}: v15 picture not in the middle (${g.pictureOff})`); + assert.ok(g.shelf && Math.abs(g.shelf.left - g.frame.left) < 0.5 && Math.abs(g.shelf.right - g.frame.right) < 0.5, `${label}: v15 shelf line not across the panel ${JSON.stringify([g.shelf, g.frame])}`); + assert.equal(g.headingAlign, "center", `${label}: v15 heading not centred`); + assert.ok(Math.abs(g.headingOff) < 0.75, `${label}: v15 heading off the middle (${g.headingOff})`); +} +// v15 (owner, 2026-10-13: "the corner of this graphic is slightly off with the yellow line"; he chose the box stays +// and the line meets its corner). While the Deposit or Redeem heading shows the focus marker on a phone (a 3 px ink +// outline inside a 6 px yellow band), her shelf line is that band's own height and colour, runs from the frame to the +// marker's outer left edge, and lies exactly on the band's bottom strip: one line into the corner. +function assertShelfMeetsMarker(g, label) { + assert.equal(g.ring.shown, true, `${label}: the heading does not show the focus marker`); + assert.equal(g.ring.outline, `solid 3px ${g.ring.ink}`, `${label}: marker outline`); + assert.equal(g.ring.shadow, `${g.shelf.colour} 0px 0px 0px 6px`, `${label}: marker band is not 6 px of the shelf line's yellow`); + assert.ok(Math.abs(g.shelf.left - g.frame.left) < 0.5, `${label}: shelf line does not start at the frame (${g.shelf.left})`); + assert.ok(Math.abs(g.shelf.right - (g.h1.left - 6)) < 0.5, `${label}: shelf line does not end at the marker's outer edge (${g.shelf.right} against ${g.h1.left - 6})`); + assert.ok(Math.abs(g.shelf.top - (g.h1.bottom + 3)) < 0.5 && Math.abs(g.shelf.bottom - (g.h1.bottom + 6)) < 0.5, `${label}: shelf line is not on the marker's bottom band (${g.shelf.top} to ${g.shelf.bottom}, heading bottom ${g.h1.bottom})`); +} +// What v15 may not move: the placing of everything in a title panel, to a hundredth of a pixel. Across the page as +// measured; down the page from the panel's own top edge (what stands above the panel depends on the chain reads, +// which the reference page does not make). +const placed = g => { + const y = v => v - g.titleTop; + return JSON.parse(JSON.stringify({picture: {...g.picture, y: y(g.picture.y)}, bubble: {...g.bubble, y: y(g.bubble.y)}, bubbleLines: g.bubbleLines, h1: {...g.h1, top: y(g.h1.top), bottom: y(g.h1.bottom)}, + shelf: g.shelf && {...g.shelf, top: y(g.shelf.top), bottom: y(g.shelf.bottom)}, headingAlign: g.headingAlign, titleHeight: g.titleHeight}, (k, v) => typeof v === "number" ? Math.round(v * 100) / 100 : v)); +}; function assertGeometry(g, label, pixels) { assert.equal(g.overflow, false, `${label} overflow`); assert.deepEqual(g.split, [], `${label}: a bubble word is split`); @@ -294,6 +342,8 @@ function assertGeometry(g, label, pixels) { assert.deepEqual(g.clipped, [], `${label}: picture clipped`); assert.ok(g.walletGap >= 8, `${label}: wallet hint gap ${g.walletGap}`); assert.equal(g.walletInside, true, `${label}: wallet hint outside its box`); + if (g.phone && g.name === "vault") assertVaultPhone(g, label); + if (!g.phone) assert.equal(g.shelf, null, `${label}: a shelf line above phone widths`); if (pixels) { assert.deepEqual(g.bubbleCovers, {}, `${label}: bubble covers the picture`); assert.deepEqual(Object.keys(g.tailCovers).filter(k => k !== "other"), [], `${label}: tail covers face, hands or held item`); @@ -796,9 +846,10 @@ try { await page.evaluate(()=>document.getElementById('wide-label-test').remove()); checks.push(label+': one-line width at least 240 px; wraps at word boundaries inside pill and panel, clear of character/bubble; identical check rejects v11 nowrap'); } - // v12 SPEC 1: compare the Vault height at every width with the live v11 + // v12 SPEC 1: compare the Vault height at every width with the live // title. Its public data does not affect title sizing, so this second page - // loads only local files and never adds a public RPC read. + // loads only local files and never adds a public RPC read. v15: the same + // page (the live v14 site) is the reference for the placing of every title. baselinePage=await browser.newPage({viewport:{width:1440,height:1000},reducedMotion:'reduce'}); await baselinePage.route('**/*',route=>route.request().url().startsWith(base+'/')?route.continue():route.abort()); await baselinePage.goto(base+'/baseline/#vault'); @@ -813,20 +864,96 @@ try { await page.waitForFunction(routeShown,name); await page.evaluate(()=>document.fonts.ready); await page.waitForFunction(()=>[...document.images].every(i=>i.complete&&i.naturalWidth>0)); - const lines={}; + await baselinePage.setViewportSize({width:1440,height:1000}); + await baselinePage.goto(base+'/baseline/#'+name); + await baselinePage.waitForFunction(routeShown,name); + await baselinePage.evaluate(()=>document.fonts.ready); + await baselinePage.waitForFunction(()=>[...document.images].every(i=>i.complete&&i.naturalWidth>0)); + // No focus marker in this sweep (the page moves the focus to the heading on a change of page, and WebKit + // draws the marker for that focus): it has its own check below. + for(const p of [page,baselinePage]) await p.evaluate(()=>document.activeElement?.blur?.()); + const lines={};let compared=0,centred=0,nearest=0; for(let width=320;width<=2560;width++) { await page.setViewportSize({width,height:1000}); const g=await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:width%40===0}); assertGeometry(g,`${engine} ${name} ${width}`,width%40===0); - if(name==='vault') { + assert.equal(g.ring.shown,false,`${engine} ${name} ${width}: a focus marker in the sweep`); + // v15: the live v14 title at the same width (Vault: every width; Deposit and Redeem: every phone width and + // every 40 px above). Above phones nothing moved. On phones the Deposit and Redeem titles are where they + // were while the heading has no focus marker; the Vault title moved only as decided: the picture and its + // bubble sideways together towards the middle, the bubble narrower, the shelf line longer, the heading + // centred; no height or vertical place changed. + if(name==='vault'||width<=660||width%40===0) { await baselinePage.setViewportSize({width,height:1000}); - const previous=await baselinePage.locator('.page-title').evaluate(e=>e.getBoundingClientRect().height); - assert.ok(g.titleHeight<=previous+0.5,engine+' vault '+width+': title grew from '+previous+' to '+g.titleHeight); + const was=await baselinePage.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}),label=`${engine} ${name} ${width}`; + if(name==='vault') assert.ok(g.titleHeight<=was.titleHeight+0.5,engine+' vault '+width+': title grew from '+was.titleHeight+' to '+g.titleHeight); + if(name!=='vault'||width>660) assert.deepEqual(placed(g),placed(was),label+': v15 moved something in the title'); + else { + const a=placed(g),b=placed(was); + assert.deepEqual([a.picture.y,a.picture.w,a.picture.h,a.bubble.y,a.bubble.h,a.bubbleLines,a.h1,a.titleHeight,a.shelf.top,a.shelf.bottom,a.shelf.left,a.shelf.colour],[b.picture.y,b.picture.w,b.picture.h,b.bubble.y,b.bubble.h,b.bubbleLines,b.h1,b.titleHeight,b.shelf.top,b.shelf.bottom,b.shelf.left,b.shelf.colour],label+': v15 changed a height or a vertical place in the Vault title'); + assert.ok(Math.abs(g.bubbleFromPicture-was.bubbleFromPicture)<0.05,label+': the bubble left its place beside her head (layout places snap to 1/64 px) '+(g.bubbleFromPicture-was.bubbleFromPicture)); + assert.ok(was.pictureOff>g.pictureOff+10&&was.shelf.rightassertVaultPhone(was,label+' live v14'),{name:'AssertionError'},label+': the v15 check accepts the live v14 look'); + Math.abs(g.pictureOff)<0.5?centred++:nearest++; + } + compared++; } lines[g.bubbleLines]=(lines[g.bubbleLines]||0)+1; } + checks.push(name==='vault' + ?`${engine} vault v15: on phones (320-660 px) the picture stands in the middle of the panel at ${centred} widths and, on the ${nearest} narrowest, as near it as her three-line bubble allows 4 px inside the frame; shelf line across the panel, heading centred; the bubble keeps its place beside her head; no height or vertical place differs from the live v14 title, which the same check rejects; above 660 px the title equals the live v14 title at every width` + :`${engine} ${name} v15: the title equals the live v14 title (picture, bubble, heading, shelf line) at every phone width and every 40 px above (${compared} widths) while the heading has no focus marker`); checks.push(`${engine} ${name}: every width 320-2560 px keeps whole bubble words (bubble lines ${JSON.stringify(lines)}), the strip clear of the title text, the title text centred beside her (Vault heading 17 px under her on phones), the bubble in its frame and the wallet hint gap; picture pixels checked every 40 px`); } + // v15: the heading's focus marker. A key press first, so that the focus the page itself moves to the heading + // on a change of page counts as keyboard focus in every engine (iPhone browsers draw the marker after a tap too). + for(const name of ['deposit','redeem','vault']) for(const width of [661,660,430,393,375,360,359,320]) { + const label=`${engine} ${name} ${width} focus marker`; + await page.setViewportSize({width,height:1000}); + await page.goto(base+'/preview/#'+name); + await page.waitForFunction(routeShown,name); + await page.evaluate(()=>document.fonts.ready); + await page.waitForFunction(()=>[...document.images].every(i=>i.complete&&i.naturalWidth>0)); + // Opening the page from another page already moved the focus to the heading: take it away for the plain state. + await page.evaluate(()=>document.activeElement?.blur?.()); + const plain=await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}); + assert.equal(plain.ring.shown,false,label+': marker shown without focus'); + await page.goto(base+'/preview/#docs'); + await page.waitForFunction(routeShown,'docs'); + await page.keyboard.press('Tab'); + await page.evaluate(n=>{location.hash='#'+n;},name); + await page.waitForFunction(routeShown,name); + await page.waitForFunction(()=>document.activeElement?.tagName==='H1'); + await page.waitForFunction(()=>[...document.images].every(i=>i.complete&&i.naturalWidth>0)); + const marked=await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}); + assert.equal(marked.ring.shown,true,label+': the heading does not show the marker after keyboard focus'); + if(width>660) assert.equal(marked.shelf,null,label+': a shelf line above phone widths'); + else if(name==='vault') { + // Vault: the line is 17 px above the heading; the marker's band stays clear of it and nothing moves. + assert.deepEqual(placed(marked),placed(plain),label+': the Vault title moved with the marker'); + assert.ok(marked.h1.top-6-marked.shelf.bottom>=7.5,label+': the marker touches the shelf line'); + } else { + assertShelfMeetsMarker(marked,label); + assert.deepEqual({...placed(marked),shelf:null},{...placed(plain),shelf:null},label+': something besides the shelf line moved with the marker'); + // The live v14 line (ending at the heading's own edge, 3 px above the band) put back: the same check rejects it. + const legacy=await page.addStyleTag({content:'.page-title .title-character:after{bottom:-3px!important;width:calc(var(--pad) + var(--gx) + var(--w) + 12px)!important}'}); + const old=await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}); + assert.throws(()=>assertShelfMeetsMarker(old,label+' with the v14 line'),{name:'AssertionError'},label+': the check accepts the v14 corner'); + assert.deepEqual(old.shelf,plain.shelf,label+': the put-back line is not the line without a marker'); + await legacy.evaluate(e=>e.remove()); + assertShelfMeetsMarker(await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}),label+' restored'); + if([430,375,320].includes(width)) { + const clip=await page.evaluate(()=>{const t=document.querySelector('.page-title').getBoundingClientRect(),i=document.querySelector('.character-picture').getBoundingClientRect(),top=Math.max(0,Math.min(t.top,i.top)+scrollY-12);return {x:0,y:top,width:innerWidth,height:t.bottom+scrollY+12-top};}); + await page.screenshot({path:path.join(captures,`${engine}-${name}-${width}-focus-marker.png`),clip,fullPage:true}); + } + } + // Focus gone: the line is back where it is without a marker. + await page.evaluate(()=>document.activeElement.blur()); + const after=await page.evaluate(titleGeometry,{R:REGIONS,name,pixels:false}); + assert.equal(after.ring.shown,false,label+': marker still shown after blur'); + assert.deepEqual(placed(after),placed(plain),label+': the title is not back in place after blur'); + } + checks.push(`${engine} v15 focus marker (keyboard focus moved to the heading by a change of page) at 660, 430, 393, 375, 360, 359 and 320 px: on Deposit and Redeem her shelf line runs from the frame to the marker's outer left edge on the marker's bottom band, same height and colour, and nothing else moves; the same check rejects the live v14 line put back; without the marker and after blur the line is the live v14 line; on Vault the marker stays clear of the line; at 661 px there is no shelf line`); await baselinePage.close();baselinePage=undefined; await browser.close();browser=undefined; } diff --git a/web/scripts/check-bundle.mjs b/web/scripts/check-bundle.mjs index d973bda..0a38415 100644 --- a/web/scripts/check-bundle.mjs +++ b/web/scripts/check-bundle.mjs @@ -6,9 +6,9 @@ import {createHash} from 'node:crypto'; const root=process.env.BASKET_SOURCE_ROOT||path.resolve('..'); const git=args=>execFileSync('git',args,{cwd:root,encoding:'utf8'}); const changed=git(['diff','--name-only','HEAD']).trim().split('\n').filter(Boolean); -// v14 (owner decisions of 2026-10-13: the Docs module and ITEM panels, the Deposit desktop columns, two word changes): only these sources may change. -// check-calldata independently pins each reviewed source and every protected transaction block. -const v14Source=['Docs.tsx','Flows.tsx','Tokenomics.tsx','tokenomicsData.ts','BaskGuide.tsx','baskGuideData.ts','styles.css'].map(f=>'web/src/'+f); +// v15 (owner decisions of 2026-10-13: the Vault title picture centred on phones; the shelf line meeting the heading's focus marker on Deposit and Redeem): only the style sheet may change. +// check-calldata independently freezes every script source and pins the added style rules. +const v15Source=['styles.css'].map(f=>'web/src/'+f); const SHELL_SHA256='82f66899a0c19f61299254c0005ee5117af0a912b97e8e57d249288e9dbc962d'; const SHARE_SHA256='dc14dd6f4068ec9325426583104b1c6ae8748fbb17d8326863c9612280eacd85'; // The new favicon: red panel, yellow frame, the header's basket in yellow. @@ -23,12 +23,12 @@ for(const p of changed){ // v12 section 5: the exact page shell is pinned below; build configuration and validation fixtures stay unchanged. assert.ok(!['web/vite.config.ts','web/tsconfig.json'].includes(p)&&(!p.startsWith('web/validation/')||p==='web/validation/README.md'),'Page shell, build configuration or validation fixture changed: '+p); assert.ok(!/(^|\/)(lib|node_modules|\.github|\.git)(\/|$)|(^|\/)\.env(?:\.|$)|(^|\/)(foundry\.toml|foundry\.lock|remappings\.txt|\.gitmodules|package\.json|[^/]*lock[^/]*)$/.test(p),'Protected path changed: '+p); - if(p.startsWith('web/src/'))assert.ok(v14Source.includes(p),'Source outside the v14 scope changed: '+p); + if(p.startsWith('web/src/'))assert.ok(v15Source.includes(p),'Source outside the v15 scope changed: '+p); assert.ok(!p.startsWith('src/'),'Root contract source changed'); } -// Tokenomics: exactly four new sources; no new public asset. +// v15: no new source and no new public asset. const untracked=git(['ls-files','--others','--exclude-standard','--','web/src','web/public']).trim().split('\n').filter(Boolean); -assert.deepEqual(untracked,['web/src/BaskGuide.tsx','web/src/Tokenomics.tsx','web/src/baskGuideData.ts','web/src/tokenomicsData.ts'],'New files in web/src or web/public'); +assert.deepEqual(untracked,[],'New files in web/src or web/public'); assert.equal(sha256('web/index.html'),SHELL_SHA256,'exact v12 source shell'); for(const p of ['web/public/share-card.jpg','dist/share-card.jpg']) { assert.equal(sha256(p),SHARE_SHA256,'exact v12 link picture: '+p); diff --git a/web/scripts/check-calldata.mjs b/web/scripts/check-calldata.mjs index ac1f3aa..9ba9b13 100644 --- a/web/scripts/check-calldata.mjs +++ b/web/scripts/check-calldata.mjs @@ -11,35 +11,41 @@ const git=args=>execFileSync('git',args,{cwd:root,encoding:'utf8'}); const baseline=p=>git(['show','HEAD:'+p]); const read=p=>fs.readFileSync(path.join(root,p),'utf8'); const sha256=s=>createHash('sha256').update(s).digest('hex'); -// v14 on HEAD=v13: the owner's decisions of 2026-10-13. Flows.tsx (DepositPage only): on desktop the stock list is -// always open in a right-hand column and the "Before you deposit" notes sit under the form; one sentence removed and -// the warning note re-worded. Docs.tsx: the Docs-local Tokenomics and BASK tabs (four new display-only sources, -// pinned by reviewed digests below), the live fee sentence, and five ITEM panels in the default tab. Transactions -// and all chain reads remain HEAD's; wallet.tsx and every other web/src file are byte-identical to HEAD. +// v15 on HEAD=v14: the owner's two phone decisions of 2026-10-13, both style rules only. styles.css gains rules after +// HEAD's text (the Vault title picture centred on phones; on Deposit and Redeem the shelf line meets the heading's +// focus marker). No script changes: every web/src file except styles.css is byte-identical to HEAD, the two v14 files +// (Docs.tsx, Flows.tsx) and the four Docs module files included. Transactions and all chain reads remain HEAD's. const protectedFiles=[ + "BaskGuide.tsx", + "Docs.tsx", + "Flows.tsx", "Losses.tsx", "Owner.tsx", "Scenery.tsx", + "Tokenomics.tsx", "Vault.tsx", "components.tsx", "main.tsx", "wallet.tsx", + "baskGuideData.ts", "chain.ts", "deployment.ts", "governance.ts", "model.ts", "newYork.ts", "poolMath.ts", + "tokenomicsData.ts", "vault.abi.json" ]; for(const f of protectedFiles) assert.equal(read('web/src/'+f),baseline('web/src/'+f),f); -// The Docs module (owner approved A and Docs-local tabs): exactly four new display-only sources, each pinned by an -// independently reviewed digest and scanned for wallet, storage, network and send sinks; the ITEM statistics read is -// behind the null-address guard. styles.css starts with HEAD's text followed by the exact approved module CSS; v14's -// Deposit desktop rules are appended after that prefix. -const NEW_SOURCES=['Tokenomics.tsx','tokenomicsData.ts','BaskGuide.tsx','baskGuideData.ts']; +// v15 adds no source file. The Docs module of v14 (four display-only sources) is part of HEAD now: frozen above, and +// still pinned by its independently reviewed digests and scanned for wallet, storage, network and send sinks; the +// ITEM statistics read stays behind the null-address guard. styles.css starts with HEAD's whole text (its first 63880 +// bytes are still the exact approved module CSS); the v15 rules are appended after it and pinned below. +const NEW_SOURCES=[]; +const MODULE_SOURCES=['Tokenomics.tsx','tokenomicsData.ts','BaskGuide.tsx','baskGuideData.ts']; const allSources=fs.readdirSync(path.join(root,'web/src'),{recursive:true}).filter(p=>fs.statSync(path.join(root,'web/src',p)).isFile()).map(p=>p.replaceAll('\\','/')).sort(); -assert.deepEqual(allSources,[...git(['ls-tree','-r','--name-only','HEAD','web/src']).trim().split('\n').map(p=>p.slice('web/src/'.length)),...NEW_SOURCES].sort(),'exactly four new sources'); +assert.deepEqual(allSources,[...git(['ls-tree','-r','--name-only','HEAD','web/src']).trim().split('\n').map(p=>p.slice('web/src/'.length)),...NEW_SOURCES].sort(),'no new source'); const modulePins={ "Tokenomics.tsx": "9090a62ee2c36373beb7712fea6489ba6aa5f4852ba110be71beb1c11c27f1f6", "tokenomicsData.ts": "18c92542419fcde454a8640705ebe2b1579d3e01eaa88cbf5edfb1e7053eccfe", @@ -47,7 +53,7 @@ const modulePins={ "baskGuideData.ts": "009ed45d5af35bbb34bc62c89bcc90b46253894d2aca60f793ec3e4ac465b169", }; for(const [f,digest] of Object.entries(modulePins))assert.equal(sha256(read('web/src/'+f)),digest,'independent reviewed module pin: '+f); -for(const f of NEW_SOURCES){ +for(const f of MODULE_SOURCES){ const s=read('web/src/'+f); assert.ok(!/ethereum|\b(?:fetch|localStorage|sessionStorage|indexedDB|sendTransaction|writeContract|sendRawTransaction|eth_call)\b|\.request\s*\(/.test(s),f+': no wallet, storage, raw network or send sink'); assert.deepEqual([...s.matchAll(/https?:[^\s\x60"']+/g)].map(m=>m[0]),f==='Tokenomics.tsx'?['https://robin.etherscan.io/address/${address}']:[],'only explorer link pattern'); @@ -55,46 +61,24 @@ for(const f of NEW_SOURCES){ assert.match(read('web/src/Tokenomics.tsx'),/if \(!hasAddresses\(addresses\)\) return null;[\s\S]*const results = await reader/,'null guard before the sole optional existing many reader'); assert.ok(read('web/src/styles.css').startsWith(baseline('web/src/styles.css')),'CSS additions only'); assert.equal(sha256(fs.readFileSync(path.join(root,'web/src/styles.css')).subarray(0,63880)),'556616a829d32cd47a07e81280a030b9adcd49ffa1a665776ca894c63e593ad8','entire approved module CSS is an exact prefix; v14 Deposit rules follow it'); -// The v14 files: byte-identical to HEAD once their documented pieces [v14, HEAD] are put back (each exactly once), so +// v15: the only source difference is the text after HEAD's styles.css. It is pinned whole by a reviewed digest, and +// it can load nothing, name no file and run nothing: no url(), @import, @font-face, image-set(), src, expression or +// content, no backslash escape (which could spell one of those), and only the two phone media queries. +const cssTail=fs.readFileSync(path.join(root,'web/src/styles.css')).subarray(Buffer.byteLength(baseline('web/src/styles.css'))).toString('utf8'),cssRules=cssTail.replace(/\/\*[\s\S]*?\*\//g,''); +assert.equal(sha256(cssTail),'75e11c77094ccfe8889f6a224e973cf9091a289e99b30cf34e81ecc7d9cadd1a','v15: the reviewed style rules after the text of HEAD'); +assert.ok(!/url\s*\(|@import|@font-face|@namespace|image-set|\bsrc\b|expression|javascript:|behavior|content\s*:|\\|<|http/i.test(cssRules),'v15 style rules: nothing that loads, names a file or runs'); +assert.deepEqual([...cssRules.matchAll(/@[a-z-]+[^{]*/g)].map(m=>m[0].trim()),['@media (max-width: 660px)','@media (max-width: 660px) and (forced-colors: none)'],'v15 style rules: phones only'); +assert.deepEqual([...new Set([...cssRules.matchAll(/^\s*(--[a-z-]+|[a-z-]+)\s*:/gm)].map(m=>m[1]))].sort(),['--bw','--gx','bottom','right','text-align','width'],'v15 style rules: only these properties'); +// v15 edits no script file, so this list is empty and every source is frozen above. (For an edited file it holds the +// documented pieces [now, HEAD]: byte-identical to HEAD once they are put back, each exactly once, so // every word, call and attribute in them is pinned. The list between the two marker lines was generated from the // difference to HEAD; the pins further down do not depend on it: they compare with HEAD itself or with texts written -// out in this file, so a piece cannot carry a change to a transaction, a wallet request or a chain read past them. -// >>> v14 pieces (generated from the difference to HEAD, not edited by hand) +// out in this file, so a piece cannot carry a change to a transaction, a wallet request or a chain read past them.) +// >>> v15 pieces (generated from the difference to HEAD, not edited by hand) const editedFiles={ - "Docs.tsx":[ - ["import { useRef, useState, type KeyboardEvent } from \"react\";\nimport { Tokenomics } from \"./Tokenomics\";\nimport { BaskGuide } from \"./BaskGuide\";\nimport { PageTitle, AddressLink } from \"./components\";\n", - "import { PageTitle, AddressLink } from \"./components\";\n"], - ["export function DocsPage() {\n const [tab, setTab] = useState(0);\n const buttons = useRef<(HTMLButtonElement | null)[]>([]);\n function move(event: KeyboardEvent) {\n const next = event.key === \"Home\" ? 0 : event.key === \"End\" ? 2 : event.key === \"ArrowRight\" ? (tab + 1) % 3 : event.key === \"ArrowLeft\" ? (tab + 2) % 3 : undefined;\n if (next === undefined) return;\n event.preventDefault();\n setTab(next);\n buttons.current[next]?.focus();\n }\n return (\n", - "export function DocsPage() {\n return (\n"], - [" \n
\n {[\"Docs\", \"Tokenomics\", \"BASK\"].map((label, index) => (\n \n ))}\n
\n \n \n"] - ], - "Flows.tsx":[ - [" const [listOpenFor, setListOpenFor] = useState();\n // v14: wider than 960px the panel has two columns (the form left, the stock\n // list always open on the right); phones and tablets keep the one column.\n const [desktop, setDesktop] = useState(\n () => window.matchMedia(\"(min-width: 961px)\").matches,\n );\n useEffect(() => {\n const query = window.matchMedia(\"(min-width: 961px)\");\n const update = () => setDesktop(query.matches);\n query.addEventListener(\"change\", update);\n return () => query.removeEventListener(\"change\", update);\n }, []);\n // The stock an Add button just added, until its row is on screen.\n", - " const [listOpenFor, setListOpenFor] = useState();\n // The stock an Add button just added, until its row is on screen.\n"], - [" }, [emptyForm, accountKey]);\n const listOpen = desktop || emptyForm || listOpenFor === accountKey;\n const StockList = listOpen ? \"div\" : \"details\";\n", - " }, [emptyForm, accountKey]);\n const listOpen = emptyForm || listOpenFor === accountKey;\n const StockList = listOpen ? \"div\" : \"details\";\n"], - [" }\n // Every stock the vault takes now. On phones: inside the form, closed until\n // pressed while the form has rows, open across the panel while the form is\n // empty (no wallet, or a wallet without a listed stock) and then for the\n // rest of the visit for that account (v13). On desktop: always open, in the\n // right-hand column (v14). A press is the same Add as in the search; a stock\n // that has a row stays in the list, switched off.\n const stockList = (\n \n {listOpen ? (\n

All stocks you can deposit ({open.length})

\n ) : (\n All stocks you can deposit ({open.length})\n )}\n

Press a stock to add it to your deposit.

\n
    \n {listed.map((a) => (\n
  • 5 ? \"wide\" : undefined}>\n x.a.token === a.token)\n }\n onClick={() => add(a.token)}\n >\n {a.symbol}\n \n
  • \n ))}\n
\n
\n );\n // \"Before you deposit\": its own panel under the form on phones, under the\n // form inside the panel on desktop (v14).\n const notes = (\n <>\n

Before you deposit

\n

Size limit: {usd(s.globals.NAV_CAP)}.

\n

\n First deposit: 0.001 BASK is locked forever. The preview already\n subtracts this lock and any fee.\n

\n

\n You approve each stock for exactly the amount you deposit. Each approval is its own wallet prompt; the deposit prompt comes last.\n

\n

\n A prepared deposit must be sent within ten minutes. The BASK goes to your connected wallet.\n

\n \n Issuer restrictions and pool checks can temporarily halt deposits.\n \n \n );\n return (\n", - " }\n return (\n"], - ["

Make a deposit

\n {/* v14: two columns on desktop (the form, its preview and buttons and\n the notes left; the stock list right), one column otherwise. */}\n
\n
\n {!open.length ? (\n", - "

Make a deposit

\n {!open.length ? (\n"], - ["
\n {/* The stock list (above): in the form on phones, in the\n right-hand column on desktop. */}\n {!desktop && stockList}\n {counted + badAmount.length + noPrice.length > 0 && (\n", - "
\n {/* Every stock the vault takes now. Closed until pressed while\n the form has rows; shown open across the panel while the\n form is empty (no wallet, or a wallet without a listed\n stock), and then for the rest of the visit for that account\n (v13). A press is the same Add as in the search; a stock\n that has a row stays in the list, switched off. */}\n \n {listOpen ? (\n

\n All stocks you can deposit ({open.length})\n

\n ) : (\n \n All stocks you can deposit ({open.length})\n \n )}\n

Press a stock to add it to your deposit.

\n

Grey stocks are already in the form above.

\n
    \n {listed.map((a) => (\n 5 ? \"wide\" : undefined}\n >\n x.a.token === a.token)\n }\n onClick={() => add(a.token)}\n >\n {a.symbol}\n \n \n ))}\n
\n \n {counted + badAmount.length + noPrice.length > 0 && (\n"], - ["

\n {desktop &&
{notes}
}\n \n {desktop && open.length > 0 && (\n
{stockList}
\n )}\n \n \n {!desktop && }\n \n", - "

\n \n \n \n"] - ] }; -// <<< v14 pieces -assert.deepEqual(Object.keys(editedFiles),[ - "Docs.tsx", - "Flows.tsx" -],'the v14 files'); +// <<< v15 pieces +assert.deepEqual(Object.keys(editedFiles),[],'v15 edits no script file'); const putBack=(text,pieces,what)=>{for(const [n,o] of pieces){assert.equal(text.split(n).length,2,what+': expected once: '+n);text=text.split(n).join(o);}return text;}; for(const [f,edits] of Object.entries(editedFiles)) assert.equal(putBack(read('web/src/'+f),edits,f),baseline('web/src/'+f),f+' changed beyond the documented pieces'); // Calls that build, encode, simulate or send a transaction (or parse its arguments) or read the chain. @@ -148,53 +132,16 @@ function difference(a,b){ } // Guarded calls: the exact, documented differences from HEAD, each added call with the place it must sit in // (everything equals HEAD, in order and place). The existing receipt read and display-only amount parser are -// already part of HEAD. v14 adds no guarded call, wallet request or chain read. -const expected={ - "Docs.tsx": { - "removed": [], - "added": [] - }, - "Flows.tsx": { - "removed": [], - "added": [] - } -}; +// already part of HEAD. v15 adds no call at all: no script file changes. +const expected={}; // Action attributes: the exact differences from HEAD, in page order, each [element, attribute, why]. Everything else // equals HEAD in order, text for text. None of them builds or starts a transaction (no onSubmit, getSpec, build or // claim) and none contains a guarded call (both asserted below). The list between the marker lines was generated // from the difference to HEAD, as the pieces were; each why was written by hand, and an entry without one fails. -// >>> v14 attributes (generated from the difference to HEAD; each why written by hand) +// >>> v15 attributes (generated from the difference to HEAD; each why written by hand) const attributeChanges={ - "Docs.tsx":{ - removed:[], - added:[ - ["div","role=\"tablist\"", - "Docs module (owner approved Docs-local tabs): the tab row is marked as a tablist for assistive technology. Presentation only; no action, read or request."], - ["button","role=\"tab\"", - "Docs module: each of the three local tab buttons (Docs, Tokenomics, BASK) is marked as a tab for assistive technology. Presentation only."], - ["button","onClick={() => setTab(index)}", - "Docs module: pressing a tab button sets the local tab state (which of the three Docs panels is shown). Display state only; no read, send, storage or network use."], - ["button","onKeyDown={move}", - "Docs module: arrow, Home and End keys move between the three tab buttons (focus and the local tab state). Display only; no read, send, storage or network use."], - ["div","role=\"tabpanel\"", - "Docs module: each of the three panels (the default Docs grid, Tokenomics, BASK) is marked as a tabpanel linked to its tab. Presentation only."], - ["div","role=\"tabpanel\"", - "Docs module: each of the three panels (the default Docs grid, Tokenomics, BASK) is marked as a tabpanel linked to its tab. Presentation only."], - ["div","role=\"tabpanel\"", - "Docs module: each of the three panels (the default Docs grid, Tokenomics, BASK) is marked as a tabpanel linked to its tab. Presentation only."]]}, - "Flows.tsx":{ - removed:[ - ["button","disabled={working || w.busy || rows.some((x) => x.a.token === a.token)}", - "v14: the stock list's buttons moved unchanged into a JSX constant rendered either inside the form (phones) or in the right-hand column (desktop); the disabled rule is HEAD's text, character for character."], - ["button","onClick={() => add(a.token)}", - "v14: the stock list's buttons moved unchanged into a JSX constant rendered either inside the form (phones) or in the right-hand column (desktop); the press is HEAD's add(), character for character, which only adds a row to the form."]], - added:[ - ["button","disabled={working || w.busy || rows.some((x) => x.a.token === a.token)}", - "v14: the stock list's buttons moved unchanged into a JSX constant rendered either inside the form (phones) or in the right-hand column (desktop); the disabled rule is HEAD's text, character for character."], - ["button","onClick={() => add(a.token)}", - "v14: the stock list's buttons moved unchanged into a JSX constant rendered either inside the form (phones) or in the right-hand column (desktop); the press is HEAD's add(), character for character, which only adds a row to the form."]]} }; -// <<< v14 attributes +// <<< v15 attributes // Every call expression: HEAD's calls, in order and place, are all still there, except the removed ones listed here; // a call that only gained the documented piece inside it (put back before comparing) counts as kept. Every added // call, including display helpers and effects, is independently pinned below, not only the guarded calls. @@ -206,26 +153,8 @@ const attributeChanges={ // read is added; the existing receipt read and answer handling are independently byte-pinned. // The guarded startup eth_accounts answer is part of HEAD and must remain exact. const restoreAccountReply=(x)=>x.text; -// Reviewed v14 differences in calls and watched-name uses. Independent of the generated pieces. -const callChanges={ - "Docs.tsx": { - "why": "Docs module (owner approved A and Docs-local tabs) and the five ITEM panels: a local tab state and its keyboard handler, the two display-only tab components, the live fee sentence and the removed not-live sentence, five new section panels of text. No read, send, storage or network use is added; the module files are pinned by reviewed digests above.", - "pieces": [], - "removed": [], - "uses": [] - }, - "Flows.tsx": { - "why": "v14 (owner decision 2026-10-13): on desktop (wider than 960px, read once through matchMedia and kept in one display-only state) the Deposit panel is two columns: the form, its preview and transaction buttons and the Before-you-deposit notes on the left, the stock list always open on the right; on phones the v13 layout and behaviour are unchanged. The sentence about grey stocks is removed and the warning note re-worded. No call of HEAD is removed beyond the documented moves; no read, send, storage or network use is added.", - "pieces": [], - "removed": [ - "listed.map((a) => (
  • 5 ? \"wide\" : undefined}>
  • ))", - "rows.some((x) => x.a.token === a.token)", - "add(a.token)", - "usd(s.globals.NAV_CAP)" - ], - "uses": [] - } -}; +// Reviewed differences in calls and watched-name uses: none in v15. Independent of the generated pieces. +const callChanges={}; const WATCHED=[...names].filter(x=>!x.includes('.')).concat('client'); // The state setters and refs a file declares: const [x, setX] = useState(...) gives setX, const r = useRef(...) gives r. function stateNames(text,file){ @@ -274,9 +203,9 @@ function localDeclarations(file,text,fn,list){ return out; } const sources=git(['ls-tree','-r','--name-only','HEAD','web/src']).trim().split('\n').filter(p=>/\.tsx?$/.test(p)).map(p=>p.slice(8)); -assert.deepEqual(git(['ls-files','--cached','--others','--exclude-standard','web/src']).trim().split('\n').filter(p=>/\.tsx?$/.test(p)).map(p=>p.slice(8)).sort(),[...sources,...NEW_SOURCES].sort(),'No source file added or removed beyond the four module files'); -// Every source is either frozen (byte-identical to HEAD) or a v14 file with its documented pieces (above). -assert.deepEqual([...sources].sort(),[...protectedFiles,...Object.keys(editedFiles)].filter(f=>/\.tsx?$/.test(f)).sort(),'every web/src source is frozen or a documented v14 file'); +assert.deepEqual(git(['ls-files','--cached','--others','--exclude-standard','web/src']).trim().split('\n').filter(p=>/\.tsx?$/.test(p)).map(p=>p.slice(8)).sort(),[...sources,...NEW_SOURCES].sort(),'No source file added or removed'); +// Every source is frozen (byte-identical to HEAD): v15 has no edited script file. +assert.deepEqual([...sources].sort(),[...protectedFiles,...Object.keys(editedFiles)].filter(f=>/\.tsx?$/.test(f)).sort(),'every web/src source is frozen'); const scans={},facts={attributes:{},guarded:{},calls:{}}; for(const f of sources){ const old=calls(baseline('web/src/'+f),f),now=calls(read('web/src/'+f),f),c=callChanges[f]??{pieces:[],removed:[],uses:[]},used=c.pieces.map(()=>0); @@ -299,18 +228,10 @@ if(process.env.BASKET_GATE_FACTS)fs.writeFileSync(process.env.BASKET_GATE_FACTS, // ordered callee names make the list readable without repeating large nested JSX bodies. No regeneration block // includes these pins. Changing a callback, receiver, argument, location, order or count changes the digest. // >>> reviewed calls (manual; never refreshed by put-pieces) -const addedCallPins={ - "Docs.tsx": { - "sha256": "07495e37aa44c937913a52d41cb024b6f39ca041e9e3889f9bf99fb1bec5f184", - "callees": "useState useRef preventDefault setTab focus map setTab" - }, - "Flows.tsx": { - "sha256": "9420ba314dc7417826b92076310403045ce72c76fd871e45203c121a6da4ff78", - "callees": "useState matchMedia useEffect matchMedia setDesktop addEventListener removeEventListener map some add usd" - } -}; +const addedCallPins={}; // <<< reviewed calls -// v14: exact reviewed source of the two edited files, beyond either generated block (the four module files are pinned above). +// The exact reviewed source of the two v14 files, kept as a second pin beside the frozen-file comparison with HEAD +// (the four module files are pinned above). const reviewedSourcePins={ "Docs.tsx": "6c9cc7e308109e50b7b9a6e5c861fc4d9feec927e7d568f4c238e9a807930e77", "Flows.tsx": "b9452b067fabd93c4ce9718d19e4c70906263068bcb81589b2e151beb411c309" @@ -484,7 +405,7 @@ function loadsOf(text,file){ })(tree); return out; } -// v14 adds nothing a page loads or opens in the HEAD sources: every file equals HEAD here (the module files are scanned above). +// v15 adds nothing a page loads or opens: every file equals HEAD here (the module files are scanned above too). const loadDisplayAdded={}; const loadUses={}; for(const f of sources){ @@ -554,7 +475,7 @@ function sendParts(text){ } const headSend=sendParts(baseline('web/src/wallet.tsx')),nowSend=sendParts(read('web/src/wallet.tsx')); assert.equal(nowSend.head,headSend.head,'wallet.tsx send(): byte-identical head through receipt wait'); -// v14 does not touch wallet.tsx: send() is byte-identical to HEAD, head and tail (no piece); the mined-hash +// v15 does not touch wallet.tsx: send() is byte-identical to HEAD, head and tail (no piece); the mined-hash // statements are pinned by sendTailWrites below as well. const SEND_TAIL_PIECES=[ ]; @@ -653,7 +574,7 @@ for(const name of ['short','approvals'])rawProtected('Deposit '+name,'Flows.tsx' // component or hook (parameters included) is still declared there once, by the same text: the same import, the same // useState or useRef, the same expression. So `vault`, `token`, `amount`, `depositArgs`, `VAULT`, `quote`, `inputs`, // `open`, `approvals`, `account` and every other name the pinned transaction code uses still mean what they meant at -// HEAD. The names v14 adds at the top of a file are listed here (the Docs module imports); a name v14 adds at the top of a component or hook +// HEAD. The names an update adds at the top of a file are listed here (none in v15); a name it adds at the top of a component or hook // must be new there and must not hide a name declared at the top of the file. function scopes(text,file){ const tree=parse(text,file),print=printerOf(tree),module=Object.create(null),functions=Object.create(null); @@ -714,37 +635,11 @@ for(const f of Object.keys(editedFiles)){ assert.deepEqual(n,h,f+': a name used by the transaction code is declared around it differently from HEAD'); transactionCode[f]={pieces:n.count,namesDeclaredAroundThem:n.names}; } -const namesAdded={ - "Docs.tsx": { - "BaskGuide": "import BaskGuide from \"./BaskGuide\"", - "KeyboardEvent": "import type KeyboardEvent from \"react\"", - "Tokenomics": "import Tokenomics from \"./Tokenomics\"", - "useRef": "import useRef from \"react\"", - "useState": "import useState from \"react\"" - }, - "Flows.tsx": {} -}; -// v14 changes no declaration at the top of either file. -const moduleChanges={ - "Docs.tsx": [], - "Flows.tsx": [] -}; -// v14 changes one declaration at the top of DepositPage: the list is also open on desktop; never generated by put-pieces. -const scopeChanges={ - "Docs.tsx": [], - "Flows.tsx": [ - { - "function": "DepositPage", - "name": "listOpen", - "head": [ - "listOpen = emptyForm || listOpenFor === accountKey" - ], - "now": [ - "listOpen = desktop || emptyForm || listOpenFor === accountKey" - ] - } - ] -}; +const namesAdded={}; +// v15 changes no declaration at the top of a file. +const moduleChanges={}; +// v15 changes no declaration at the top of a component or hook; never generated by put-pieces. +const scopeChanges={}; const newNames={}; for(const f of Object.keys(editedFiles)){ const h=scopes(baseline('web/src/'+f),f),n=scopes(read('web/src/'+f),f); @@ -768,7 +663,7 @@ for(const f of Object.keys(editedFiles)){ } } // The whole-source digest is additional to the specific structural assertions above. -for(const [f,digest] of Object.entries(reviewedSourcePins))assert.equal(sha256(read('web/src/'+f)),digest,f+': reviewed v14 source differs; regeneration does not authorize source edits'); +for(const [f,digest] of Object.entries(reviewedSourcePins))assert.equal(sha256(read('web/src/'+f)),digest,f+': reviewed source differs; regeneration does not authorize source edits'); // Differential encoding: HEAD and current governance/model/chain bundled side by side. For the same inputs the // calldata must be byte-identical whenever the current page accepts; v12 adds no refusal, never accepts what HEAD // refused, and refuses with HEAD's words (model.ts, chain.ts and governance.ts are byte-identical to HEAD in v12). diff --git a/web/scripts/check-visibility.mjs b/web/scripts/check-visibility.mjs index 1af8e8e..c87413c 100644 --- a/web/scripts/check-visibility.mjs +++ b/web/scripts/check-visibility.mjs @@ -46,25 +46,27 @@ const base=`http://127.0.0.1:${server.address().port}`; const b=await chromium.launch(),page=await b.newPage({viewport:{width:375,height:812},reducedMotion:'reduce'}); const results=[]; -// v13 on the v12 baseline: the only permitted first-screen change is on the -// Deposit page, where the stock list under the search is shown open across the -// panel while the form is empty (both variants here: no wallet, and the -// synthetic account that holds nothing): the fold line becomes a plain heading -// line with the same words in the same place, and what follows moves down by -// exactly the open list's height. Every v12 state (Vault heading, Docs -// paragraphs, Redeem layout, status notices) must equal the baseline. All prior -// first-screen elements otherwise retain their words and remain on screen. -// The header, slogan strip, and start of each title character stay pinned. +// v15 on the v14 baseline (the live site): no first-screen element changes its +// words or its vertical place on any page; nothing is excepted. The one decided +// difference is across the Vault title panel on phones: the picture stands in +// the middle (it stood left of it), her bubble keeps its place beside her head, +// her shelf line runs across the panel and the heading is centred. The Deposit +// and Redeem titles equal the baseline (no heading focus marker is shown here: +// the pages are opened directly; check-art covers the marker). Every earlier +// state (Vault heading, Docs paragraphs and panels, the open Deposit stock list, +// Redeem layout, status notices) must equal the baseline. The header, slogan +// strip, and start of each title character stay pinned. const characterPages=['vault','deposit','redeem']; const docsAdded=[ 'The basket has no set weights. Any listed stock can be deposited, in any amount, as long as the vault’s checks pass and the vault stays under its size limit. So the mix is whatever holders have put in, and it changes with every deposit and redemption.', 'BASK’s backing moves with every stock in the vault, so its market price can drift from the value behind it. Traders close that gap: they deposit stocks and sell BASK when it trades high, or buy BASK and redeem when it trades low. That keeps BASK near its backing.', - 'The 0.5% fee on each deposit and redemption is set to go to ITEM staking once ITEM is live, so more trading through the vault means more fees for ITEM.', - 'ITEM is not live yet. Its address will be listed here when it is.', + 'The 0.5% fee on each deposit and redemption goes to ITEM staking, so more trading through the vault means more fees for ITEM.', ]; +const ITEM_PANELS=['ITEM','Earn ITEM by providing liquidity','Earn BASK by staking ITEM','Rewards FAQ','ITEM risks']; const words=t=>String(t).replace(/\s+/g,''); const normalize=t=>String(t).replace(/\s+/g,' ').trim(); const near=(a,b)=>Math.abs(a-b)<=0.5; +const rounded=x=>JSON.parse(JSON.stringify(x,(k,v)=>typeof v==='number'?Math.round(v*100)/100:v)); await page.addInitScript(({account})=>{ if(location.search.includes('connected')) window.ethereum={ request:async({method})=>{ @@ -200,6 +202,15 @@ function layoutSnapshot() { sentences:[...document.querySelectorAll('.all-stocks p')].map(e=>e.textContent.replace(/\s+/g,' ').trim()), grey:(()=>{const p=[...document.querySelectorAll('.all-stocks p')].find(e=>e.textContent.trim()==='Grey stocks are already in the form above.');if(!p)return null;const cs=getComputedStyle(p);return {height:p.getBoundingClientRect().height,marginTop:parseFloat(cs.marginTop),marginBottom:parseFloat(cs.marginBottom)};})()}, depositSummary:box('.deposit-summary'), + // v15: where the title picture, her bubble and the heading sit across the panel, and her shelf line (the holder's ::after). + across:(()=>{ + const t=document.querySelector('main .page-title'),h=t?.querySelector(':scope > .title-character');if(!h)return null; + const R=e=>{const r=e.getBoundingClientRect();return {left:r.left,right:r.right};},line=getComputedStyle(h,'::after'),hr=h.getBoundingClientRect(),bw=parseFloat(getComputedStyle(t).borderLeftWidth),tr=t.getBoundingClientRect(),h1=t.querySelector(':scope > h1'); + const rg=document.createRange();rg.selectNodeContents(h1);const hs=[...rg.getClientRects()].filter(x=>x.width>0); + return {frame:{left:tr.left+bw,right:tr.right-bw},picture:R(h.querySelector('img.character-picture')),bubble:R(h.querySelector('p.speech-bubble')),heading:R(h1),headingText:{left:Math.min(...hs.map(x=>x.left)),right:Math.max(...hs.map(x=>x.right))},headingAlign:getComputedStyle(h1).textAlign, + marker:h1.matches(':focus-visible'), + shelf:line.content==='none'?null:{left:hr.left+parseFloat(line.left),right:hr.left+parseFloat(line.left)+parseFloat(line.width),bottom:parseFloat(line.bottom),height:parseFloat(line.height)}}; + })(), led:ledGeometry(), sideways:document.documentElement.scrollWidth>innerWidth, }; @@ -258,16 +269,38 @@ try { const docsGrowth=newLayout.docsAddedHeight; if(name==='vault') { assert.equal(newLayout.heading,'Always in stock'); - assert.equal(oldLayout.heading,newLayout.heading,'Vault heading equals the v12 baseline'); + assert.equal(oldLayout.heading,newLayout.heading,'Vault heading equals the baseline'); assert.deepEqual(oldLayout.titleParagraphs,[]); assert.deepEqual(newLayout.titleParagraphs,[]); assert.ok(newLayout.title.height<=oldLayout.title.height+0.5,'Vault panel must not get taller'); assert.equal(oldLayout.capacity.label,'Maximum Basket Capacity','Baseline carries the v12 capacity label'); assert.deepEqual(newLayout.capacity,oldLayout.capacity,'Capacity label and figure value remain unchanged'); - checks.push('v13 Vault: unchanged; heading, empty title paragraphs, panel height, capacity label and figure value equal the v12 baseline'); + checks.push('Vault: heading, empty title paragraphs, panel height, capacity label and figure value equal the baseline'); + // v15 (owner, 2026-10-13: "the graphic on mobile is off to the left and doesnt look good"; he chose the picture + // centred with the heading centred): across the panel only. The baseline proves the look it replaces. + const mid=a=>(a.frame.left+a.frame.right)/2,centre=r=>(r.left+r.right)/2,o=oldLayout.across,n=newLayout.across; + assert.ok(near(centre(n.picture),mid(n)),'v15: the Vault picture stands in the middle of the panel: '+(mid(n)-centre(n.picture))); + assert.ok(mid(o)-centre(o.picture)>10,'Baseline carries the picture left of the middle'); + assert.ok(near(n.bubble.left-n.picture.left,o.bubble.left-o.picture.left),'v15: the bubble keeps its place beside her head'); + assert.ok(n.bubble.right<=n.frame.right-3.5&&n.bubble.left>=n.frame.left,'v15: the bubble stays 4 px inside the frame'); + assert.ok(near(n.shelf.left,n.frame.left)&&near(n.shelf.right,n.frame.right),'v15: her shelf line runs across the panel'); + assert.ok(near(o.shelf.left,o.frame.left)&&o.shelf.right10,'Baseline carries the heading at the left'); + assert.deepEqual(rounded([n.frame,n.heading]),rounded([o.frame,o.heading]),'v15: the panel and the heading box are where they were'); + assert.deepEqual(rounded([newLayout.title,newLayout.picture]),rounded([oldLayout.title,oldLayout.picture]),'v15: the Vault title panel and picture keep their vertical place and height'); + checks.push('v15 Vault: the picture in the middle of the panel (left of it at the baseline), the bubble in its place beside her head and inside the frame, the shelf line across the panel, the heading centred; panel, heading box, picture height and every vertical place equal the baseline'); + } + if(name==='deposit'||name==='redeem') { + assert.equal(newLayout.across.marker,false,'No heading focus marker when the page is opened directly'); + assert.deepEqual(rounded(newLayout.across),rounded(oldLayout.across),'v15: the '+name+' title equals the baseline across the panel'); + assert.deepEqual(rounded([newLayout.title,newLayout.picture]),rounded([oldLayout.title,oldLayout.picture]),'v15: the '+name+' title panel and picture equal the baseline'); + checks.push('v15 '+name+': the title (picture, bubble, heading, shelf line) equals the baseline while the heading has no focus marker'); } assert.ok(statusFixtureReads>beforeFixtureReads,'State comparison consumes the existing depositStatus fixture'); - assert.equal(oldLayout.led.signs.length,newLayout.led.signs.length,'Baseline and v13 carry the same sign count'); + assert.equal(oldLayout.led.signs.length,newLayout.led.signs.length,'Baseline and v15 carry the same sign count'); if(statusCase==='normal') { assert.equal(newLayout.depositStatus.length,0,'Normal operation has no extra status area'); assert.equal(newLayout.led.signs.length,0,'Normal operation has no sign'); @@ -286,8 +319,8 @@ try { assert.equal(newLayout.depositOpen,null,'Deposit no longer uses the open-only line'); if(statusCase==='closed')assert.equal(oldLayout.depositStatus[0].paragraphs[0].text,name==='vault'?'Closed':ISSUE_NOTICE,'Baseline proves the same reason fixture'); else assert.ok(weekendBlockReads>beforeBlocks,'Weekend comparison uses the public-block timestamp fixture'); - assert.deepEqual(newLayout.depositStatus,oldLayout.depositStatus,'v13: the status area equals the v12 baseline'); - assert.deepEqual(newLayout.led,oldLayout.led,'v13: the sign geometry equals the v12 baseline'); + assert.deepEqual(newLayout.depositStatus,oldLayout.depositStatus,'The status area equals the baseline'); + assert.deepEqual(newLayout.led,oldLayout.led,'The sign geometry equals the baseline'); assert.equal(newLayout.led.signs.length,name==='deposit'?1:0,'Only Deposit has the known-closed sign'); if(name==='deposit') { const sign=newLayout.led.signs[0]; @@ -301,37 +334,30 @@ try { assert.equal(newLayout.walletLine,null,'Owner removed the wallet-switch instruction from Deposit'); assert.equal(newLayout.flowSteps.text,oldLayout.flowSteps.text,'Task instructions keep their words'); if(oldLayout.flowSteps.bottom<=812)assert.ok(newLayout.flowSteps.bottom<=812,'Previously visible Deposit task instructions remain on the first screen'); - // v13: both variants are empty forms (no wallet; the synthetic account holds nothing), so the list is open - // across the panel with the fold's words as a plain heading line in the fold line's place; the baseline shows the closed fold. - assert.deepEqual([oldLayout.stockList.open,oldLayout.stockList.summary,newLayout.stockList.open,newLayout.stockList.summary],[true,false,true,false],'v13: the open list replaces the closed fold in the empty states'); - assert.equal(newLayout.stockList.title,oldLayout.stockList.title,'The list heading keeps the fold line\'s words'); + // Both variants are empty forms (no wallet; the synthetic account holds nothing), so the list is open across + // the panel with its plain heading line (v13); the baseline shows the same list. + assert.deepEqual([oldLayout.stockList.open,oldLayout.stockList.summary,newLayout.stockList.open,newLayout.stockList.summary],[true,false,true,false],'The open list, not the closed fold, in the empty states'); + assert.equal(newLayout.stockList.title,oldLayout.stockList.title,'The list heading keeps its words'); assert.match(newLayout.stockList.title,/^All stocks you can deposit \(\d+\)$/); assert.ok(near(newLayout.stockList.top,oldLayout.stockList.top),'The list starts where the fold line started'); assert.ok(newLayout.stockList.width>0.98,'The open list spans the panel: '+newLayout.stockList.width); assert.ok(newLayout.stockList.buttons>0&&newLayout.stockList.title.endsWith('('+newLayout.stockList.buttons+')'),'Every open stock has its button'); - checks.push('v13 Deposit: open stock list across the panel in both empty states, heading words and start unchanged, no fold, one button per open stock'); + assert.deepEqual(rounded(newLayout.stockList),rounded(oldLayout.stockList),'The whole stock list equals the baseline'); + checks.push('Deposit: open stock list across the panel in both empty states, no fold, one button per open stock; the whole list equals the baseline'); } checks.push(statusCase==='normal'?'Owner follow-up: no normal status area/sign':'Owner follow-up: exact single '+statusCase+' sentence, compact Vault Closed notice, Deposit-only CLOSED sign'); if(name==='docs') { - assert.equal(oldLayout.docsParagraphs.length,5); + // The v14 Docs tab equals the baseline: the four paragraphs of "What Basket is" (the live fee sentence last, no + // not-live sentence), the five ITEM panels between Risks and Find Basket, every panel in its place with its size. + assert.equal(oldLayout.docsParagraphs.length,4); assert.equal(newLayout.docsParagraphs.length,4); - assert.deepEqual(oldLayout.docsParagraphs.slice(1).map(p=>p.text),docsAdded,'Baseline carries the four v12 paragraphs'); - assert.deepEqual(newLayout.docsParagraphs.map(p=>p.text),oldLayout.docsParagraphs.slice(0,4).map(p=>p.text.replace('is set to go to ITEM staking once ITEM is live','goes to ITEM staking')),'Only approved live-oriented Docs edits'); - // v14: five ITEM panels are inserted after "Risks" and before "Find Basket"; every retained panel is matched by its - // title, keeps its height, and moves down only by the tab row, the copy change and (Find Basket) the inserted panels. - const ITEM_PANELS=['ITEM','Earn ITEM by providing liquidity','Earn BASK by staking ITEM','Rewards FAQ','ITEM risks']; - assert.deepEqual(oldLayout.docsTitles.slice(-2),['Risks','Find Basket'],'Baseline ends with Risks and Find Basket'); - assert.deepEqual(newLayout.docsTitles,[...oldLayout.docsTitles.slice(0,-1),...ITEM_PANELS,'Find Basket'],'v14: the five ITEM panels sit between Risks and Find Basket'); - const tabShift=newLayout.docs[0].top-oldLayout.docs[0].top; - assert.ok(tabShift>0&&tabShift<100,'Only a small local tab row displaces Docs'); - const copyDelta=newLayout.docs[0].height-oldLayout.docs[0].height; - const at=t=>newLayout.docs[newLayout.docsTitles.indexOf(t)]; - for(let i=1;i0,'The five ITEM panels take room between Risks and Find Basket'); - assert.ok(near(at('Find Basket').top-oldLayout.docs.at(-1).top,tabShift+copyDelta+inserted)&&near(at('Find Basket').height,oldLayout.docs.at(-1).height),'Find Basket keeps its size and moves down by exactly the tab row, the copy change and the inserted panels'); - for(const t of ITEM_PANELS)assert.ok(at(t).height>0,'ITEM panel rendered: '+t); - checks.push('v14 Docs: the four v12 paragraphs with the live fee sentence; five ITEM panels between Risks and Find Basket; every retained panel keeps its size, displaced only by the tab row, the copy change and the inserted panels'); + assert.deepEqual(oldLayout.docsParagraphs.slice(1).map(p=>p.text),docsAdded,'Baseline carries the two v12 paragraphs and the live fee sentence'); + assert.deepEqual(rounded(newLayout.docsParagraphs),rounded(oldLayout.docsParagraphs),'The Docs paragraphs equal the baseline'); + assert.deepEqual(newLayout.docsTitles.slice(-7),['Risks',...ITEM_PANELS,'Find Basket'],'The five ITEM panels sit between Risks and Find Basket'); + assert.deepEqual(newLayout.docsTitles,oldLayout.docsTitles,'The Docs panels equal the baseline, in order'); + assert.deepEqual(rounded(newLayout.docs),rounded(oldLayout.docs),'Every Docs panel keeps its place and size'); + for(const d of newLayout.docs)assert.ok(d.height>0,'Docs panel rendered'); + checks.push('Docs: the four paragraphs with the live fee sentence, the five ITEM panels between Risks and Find Basket; every panel equals the baseline in place and size'); } if(name==='redeem') { assert.equal(oldLayout.redeemAside,null); @@ -342,43 +368,26 @@ try { assert.equal(newLayout.quiet,variant==='connected'?'Nothing is owed to this wallet.':'Connect the receiver wallet to read and claim its owed stocks.'); assert.equal(newLayout.quiet,oldLayout.quiet,'Quiet claims line equals the baseline'); assert.equal(newLayout.walletLine,null,'No wallet-switch instruction'); - checks.push('v13 Redeem: unchanged; one column, no aside, exact quiet claims line, no wallet-switch line'); + checks.push('Redeem: unchanged; one column, no aside, exact quiet claims line, no wallet-switch line'); } - // v13: on Deposit everything after the fold line moves down by exactly the open list's added height. + // The stock list has the baseline's height and words; the sentence v14 removed is in neither. const listGrowth=name==='deposit'?newLayout.stockList.height-oldLayout.stockList.height:0; if(name==='deposit'){ - // v14: the sentence about grey stocks is removed from the list; the list is shorter by exactly that line (its box plus the bottom margin that no longer separates it from the buttons). - assert.ok(oldLayout.stockList.grey&&!newLayout.stockList.grey,'v14: the grey-stocks sentence is gone from the list'); - assert.deepEqual(newLayout.stockList.sentences,oldLayout.stockList.sentences.filter(t=>t!=='Grey stocks are already in the form above.'),'v14: the list keeps its other words'); - assert.ok(near(listGrowth,-(oldLayout.stockList.grey.height+oldLayout.stockList.grey.marginBottom)),'v14: the list is shorter by exactly the removed sentence: '+listGrowth); + assert.ok(!oldLayout.stockList.grey&&!newLayout.stockList.grey,'The grey-stocks sentence (removed in v14) is in neither list'); + assert.deepEqual(newLayout.stockList.sentences,oldLayout.stockList.sentences,'The list keeps its words'); + assert.ok(near(listGrowth,0),'The list keeps its height: '+listGrowth); } for(const x of prior) { const y=counterpart(x); - // Owner selected Docs-local tabs and the two exact copy edits. Name only these consequences. - if(name==='docs'&&x.docsIndex>=0){ - if(words(x.text)===words(docsAdded[3])){assert(!now.some(z=>words(z.text)===words(x.text)));exceptions.push({reason:'Approved not-live sentence removal',element:x});continue;} - const target=y??now.find(z=>words(z.text)===words(x.text.replace('is set to go to ITEM staking once ITEM is live','goes to ITEM staking'))); - assert(target,'Every retained Docs first-screen word remains'); - const tabShift=newLayout.docs[0].top-oldLayout.docs[0].top; - assert.ok(near(target.y-x.y,tabShift+(x.docsIndex>0?newLayout.docs[0].height-oldLayout.docs[0].height:0)),'Exact local tab/copy displacement'); - exceptions.push({reason:'Approved Docs local tab row and exact copy layout',element:x,now:target});continue; - } - if(name==='deposit'&&x.tag==='SUMMARY'&&/^All stocks you can deposit \(\d+\)$/.test(normalize(x.text))) { - const line=now.find(z=>z.tag==='P'&&words(z.text)===words(x.text)); - assert.ok(line&&near(line.y,x.y),'v13: the fold line is a plain heading line with the same words in the same place'); - exceptions.push({reason:'v13 open stock list: heading line in the place of the fold line',element:x,now:line});continue; - } - if(name==='deposit'&&x.y>=oldLayout.stockList.top-0.5&&y&&near(y.y-x.y,listGrowth)&&near(y.bottom-x.bottom,listGrowth)) { - exceptions.push({reason:'v13 open stock list: exact displacement by the open list height',element:x,now:y});continue; - } + // v15 changes no word and no vertical place: every prior first-screen element is there, where it was. No exception. if(y&&y.bottom<=812) { - if(name==='vault'&&(y.y>x.y+0.5||y.bottom>x.bottom+0.5))regressions.push({...x,now:y,reason:'Vault content moved downward'}); + if(!near(y.y,x.y)||!near(y.bottom,x.bottom))regressions.push({...x,now:y,reason:'First-screen content moved vertically'}); continue; } regressions.push({...x,now:y}); } // Original slogan-strip and title-character assertions, with explicit header - // height and overflow pins added. No v13 exception applies to these parts. + // height and overflow pins added. No exception applies to these parts. checks.push('header retains its exact position and height'); if(!near(newLayout.header.top,oldLayout.header.top)||!near(newLayout.header.height,oldLayout.header.height))regressions.push({tag:'HEADER',text:'header',old:oldLayout.header,now:newLayout.header}); checks.push('slogan strip in the same place and size, still and full under reduced motion'); @@ -397,6 +406,6 @@ try { } assert.deepEqual(routingErrors,[],'Only local files and allowed public RPC reads; fixture account reads stay synthetic'); for(const r of results)if(r.regressions.length)throw Error(JSON.stringify({variant:r.variant,name:r.name,regressions:r.regressions})); - console.log('PASS 375x812 baseline visibility: six pages, no-wallet and connected;',results.reduce((n,r)=>n+r.prior.length,0),'previous first-screen elements preserved except the exact approved Docs tab row and copy edits; header height, slogan strip and character assertions retained'); + console.log('PASS 375x812 baseline visibility: six pages, no-wallet and connected;',results.reduce((n,r)=>n+r.prior.length,0),'previous first-screen elements with their words in the same vertical place, no exception; the Vault title across the panel as decided in v15, the Deposit and Redeem titles equal to the baseline; header height, slogan strip and character assertions retained'); fs.writeFileSync('../artifacts/phone-visibility.json',JSON.stringify(results,null,2)+'\n'); }finally{await b.close();await new Promise(r=>server.close(r));} diff --git a/web/scripts/run-browser.mjs b/web/scripts/run-browser.mjs index 939caf1..6811ada 100644 --- a/web/scripts/run-browser.mjs +++ b/web/scripts/run-browser.mjs @@ -2211,17 +2211,10 @@ try { // The existing fork clock advances with real elapsed time. Only its derived deadlines vary by run. // The gate separately compares exact calldata/deadlines at six fixed clocks; send capture checks live clock above. const normalized=records=>records.map(record=>['deposit','redeem'].includes(record.functionName)?{...record,args:[...record.args.slice(0,-1),'browser-clock-plus-600']}:record); - assert.deepEqual(normalized(serialized),normalized(previous.sends),'All 70 sends equal v13 apart from wall-clock-derived deadlines'); - const currentChecks=checks.filter(c=>!c.startsWith('Tokenomics:')&&!c.startsWith('BASK:')&&!v14Checks.includes(c)); - // v14's authorized label replacements (owner decisions: desktop columns, the notes under the form, the new note). - const renamed={ - 'Docs: original paragraph and four approved paragraphs, word for word':'Docs: original paragraph and approved live-oriented copy, word for word', - "All stocks you can deposit (25): closed until pressed, the tickers of all 25 open stocks, only the stocks without a row can be pressed, and a press adds that stock's empty row with the focus in its amount box":"All stocks you can deposit (25): open in the right-hand column on desktop with the notes under the form, the tickers of all 25 open stocks, only the stocks without a row can be pressed, and a press adds that stock's empty row with the focus in its amount box", - 'Deposit: Before you deposit shows both approved paragraphs without a fold; full deposit status follows the 24/7 pill':'Deposit: Before you deposit under the form on desktop shows both approved paragraphs and the approved note without a fold; full deposit status follows the 24/7 pill', - "Deposit stock list: open across the panel with a plain heading and no fold while the form is empty (no wallet; a wallet without a listed stock), a press adds the row and the list stays open for that account, the owner's wallet keeps the closed fold; no wallet request or send":"Deposit stock list on desktop: always open in the right-hand column with a plain heading and no fold (no wallet; a wallet without a listed stock; the owner's wallet), a press adds the row and the list stays open; no wallet request or send", - }; - const previousChecks=previous.checks.map(c=>renamed[c]??c); - assert.deepEqual(currentChecks,previousChecks,'Every prior page check retained in order'); + assert.deepEqual(normalized(serialized),normalized(previous.sends),'All 70 sends equal v14 apart from wall-clock-derived deadlines'); + // v15 changes style rules for phones only: the previous record is the v14 walk, and every one of its checks is + // here under the same name in the same order, with none added, renamed or left out. + assert.deepEqual(checks,previous.checks,'Every v14 page check retained under its name, in order, and none added'); assert.deepEqual(shots,previous.shots,'Every prior screenshot retained in order'); } assert.equal(revoked.length,0); diff --git a/web/src/styles.css b/web/src/styles.css index 10eaf34..d4ac5f1 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -2982,3 +2982,42 @@ button.quiet.small-toggle { font-weight: 700; color: var(--royal); } +/* v15: on phones the Vault title picture stands in the middle of the panel + (before, she and her bubble were centred as one group, which left her in + the left half). Her bubble keeps its three lines beside her head; on the + narrowest phones she stands only as far left of the middle as the bubble + needs to stay 4px inside the frame. Her shelf line runs across the whole + panel and the heading is centred under her. Deposit and Redeem keep their + side-by-side layout. */ +@media (max-width: 660px) { + .page-title.has-character.character-vault { + --bw: 138px; + } + .page-title.character-vault .title-character { + --gx: max( + 0px, + min( + calc((100% - var(--w)) / 2), + calc(100% + var(--pad) - 4px - var(--w) * var(--bx) - var(--bw)) + ) + ); + } + .page-title.character-vault .title-character:after { + right: calc(-1 * var(--pad)); + width: auto; + } + .page-title.character-vault > h1 { + text-align: center; + } +} +/* v15: Deposit and Redeem on phones. iPhone browsers draw the focus marker + around the heading after a tab is tapped. While it is drawn, her shelf line + sits 3px lower and ends at the marker's outer edge, so the line runs into + the marker's lower left corner as one line. */ +@media (max-width: 660px) and (forced-colors: none) { + .page-title.character-deposit:has(> h1:focus-visible) .title-character:after, + .page-title.character-redeem:has(> h1:focus-visible) .title-character:after { + bottom: -6px; + width: calc(var(--pad) + var(--gx) + var(--w) + var(--col-gap) - 6px); + } +} diff --git a/web/validation/README.md b/web/validation/README.md index 1c08a26..c43c346 100644 --- a/web/validation/README.md +++ b/web/validation/README.md @@ -50,3 +50,5 @@ BASK adds `check-bask-guide.tsx` for independently audited arithmetic expectatio The expected values follow `web/pinned/src/BaskVault.sol`: `_value`, `_depositShares`, `_fee`, `redeem`, `_available` and `previewRedeem`. Fee ceil, mint floor, first lock, reserved debt, balance deficit and unreadable fallback cases are independently tested. Static example helpers are not a full contract preview emulator, including the zero-supply/zero-redemption boundary. No real wallet or ITEM chain read is required. The v14 baseline is the v13 commit 2b7d55bc32d09610fed936d963d9a8c492f4eecf. check-calldata permits Docs.tsx and Flows.tsx through documented v14 pieces and the four module sources through reviewed digests; wallet.tsx and every other source equal HEAD. check-bundle's source scope adds Flows.tsx to the module's. check-visibility compares the v13 six-route phone view at 375×812, matching Docs panels by title around the five inserted ITEM panels; the Deposit phone layout is unchanged. The v14 fork keeps all 86 v13 check positions and 70 page sends, replaces the words of three Deposit checks for the desktop two-column layout, adds the Tokenomics and BASK checks and one Docs check for the five ITEM panels, and takes the same 24 screenshots at JPEG quality 18 (26 before) so that the longer Docs page keeps the delivery under 8,388,608 bytes. check-bask-guide proves the arbitrage flywheel example through the audited helpers and counts five examples, five tables and ten formula lines; check-tokenomics and check-tokenomics-live are unchanged. + +The v15 baseline is the v14 commit c05c174f639d352fb6184636bcc6e083031daa65. v15 changes style rules only (the end of styles.css): check-calldata has no edited script file, freezes every source whole against v14, keeps the module and source digests and pins the added style rules by one reviewed digest with a scan; check-bundle's source scope is styles.css alone with no new file. check-visibility compares the v14 six-route phone view at 375×812 with no exception: every first-screen element keeps its words and vertical place, the Deposit and Redeem titles equal v14 across the panel, and the Vault title is asserted across the panel (picture in the middle, shelf line across the panel, heading centred) against the v14 look. check-art keeps its assertions at every width from 320 to 2560px in both engines and adds the Vault phone layout, the comparison of every title with the live v14 title, and the heading's focus marker meeting the shelf line on Deposit and Redeem at seven phone widths, each with the v14 look rejected by the same check. The v15 fork keeps all 91 v14 checks under their names in order with none added, the same 70 page sends and the same 24 screenshots at JPEG quality 18.