Conversation
Rebuild the "What problem does GraphQL Federation solve?" section as a plain Section with three short paragraphs, dropping the 4900px transit-map scroll narrative, its code boxes and absolute positioning. The heading stays an h3 so the page keeps its four section-level H2s. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Adds a blog-like article section at /comparison over markdown in content/comparison: an index of teasers with pagination, an article page with sidebar, table of contents, metadata, share bar and similar articles, and a synthetic /comparison/404. Structured data uses a CollectionPage section node and TechArticle items rather than the blog's Blog/BlogPosting entities. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Moves the copy of /platform/graphql-federation/vs-bff into markdown under content/comparison so the new /comparison section renders it. The TSX route stays in place until izj.4 removes it. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
…on article Moves the copy of /platform/graphql-federation/vs-individual-apis into markdown under content/comparison so the new /comparison section renders it. The TSX route stays in place until izj.4 removes it. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
…rison article Moves the copy of /platform/graphql-federation/vs-graphql-monolith into markdown under content/comparison so the new /comparison section renders it. The TSX route stays in place until izj.4 removes it. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Adds /comparison to TOOLS and a Comparison entry directly after Blog in the Developers menu's Additional Resources group, with a new columns style compare icon in NavIcons. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
…ison article Moves the copy of /platform/graphql-federation/vs-apollo-federation into markdown under content/comparison, with the term glossary as a GitHub-flavoured table, so the new /comparison section renders it. The TSX route stays in place until izj.4 removes it. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Replace the generic rocket in the Platform > Products group with the Nitro and Mocha drink icons and render the product icons at their intrinsic aspect ratio via a new optional iconAspect flag on SubLink. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Unmatched /comparison/... URLs fell through to the global 404 because default.conf only had the blog fallback block. Add the matching comparison location and named fallback so the prerendered /comparison/404 page is served, mirroring the blog. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
BlogIndexShell wrapped its grid in max-w-6xl, so the teaser cards on /blog, /blog/tags/* and /comparison rendered narrower than the same BlogTeaserGrid in the "From our blog" section on the home page. Use PageSection (maxWidth 7xl) so the width and the responsive gutter match the other top-level sections. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
The generator writes /comparison/llms.txt and /comparison/llms-full.txt, but the root catalog's scope context links stopped at the blog, so the comparison full-context export was unreachable from the root catalog. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
The product drinks are cut from one artwork sheet, so their viewBoxes share a coordinate scale. Export each icon's intrinsic size in sheet units next to its viewBox, add productArtworkStyle() to turn those units into a width and height with a single scale factor, and drive the header menu from it: the Strawberry Shake fills the sheet height, the cups and the Nitro can come out proportionally smaller, and the slot is bottom-aligned so the bases line up. Skills, a square logo off the sheet, is sized to the cups' visual weight. Replaces the iconAspect flag with iconSize data on the nav link, so the /docs landing page can reuse the same mechanism. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
The robot glyph spans x 812-868 and y 6043.5-6106.5 but declared a 94x94 viewBox, so in the fixed square icon slots it rendered about a third smaller than the Font Awesome nav icons, which fill their boxes edge to edge. Crop the viewBox to the measured bounding box; the path is unchanged. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
…-blog container Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
The artwork slot left-aligned the scaled drinks, so the narrow Nitro can and the wider cups sat on different centre lines. Center them while keeping the bases aligned, and clarify that ProductArtworkSize is a display size in sheet units rather than the icon's own viewBox. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
BrowserIcon, PhoneIcon and HandshakeIcon still used the padded 94-unit sheet grid while RobotIcon had been cropped to its glyph, making the robot render about 47% larger than its siblings on the home page protocol cards. Set each viewBox to the path's computed bounding box; paths untouched. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Every menu icon slot is now the title's line-height (h-5) and drops the manual mt-0.5 nudge, so generic and product icons alike share the centre of the text-sm title line instead of the title+description block. Product drinks sit bottom-aligned inside a sheet-height box that is itself centred in the slot, so their bases stay on one line while the set as a whole is centred on the title. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Replace the forced h-8 w-8 square on the six product drinks in the /docs cards with the shared artwork-sheet sizing helper, so each icon keeps its intrinsic aspect ratio and its size relative to the others, bases aligned in a 2rem sheet box — the same mechanism the header menus use. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Move the bottom-aligned sheet box duplicated between the header nav and the docs index into src/components/ProductArtworkIcon.tsx, which takes the icon, its intrinsic sheet size and the slot height in rem; both consumers pass their own scale, so the rendered markup is unchanged. Rename the docs page's record type off the props-interface name, add a shared ProductArtworkComponent type, and render the LinkCard story's sample icon through the new component like the docs cards do. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
Owner ruling supersedes the bases-aligned layout: the product icons in the Developers and Platform menus and on the /docs landing page now sit in the middle of their slot instead of hanging from the tallest icon's baseline. The fixed sheet-height box and the shared scale factor are unchanged, so the set keeps its relative sizes. Claude-Session: https://claude.ai/code/session_01VztUfK58iePKc7ZX2JigmZ
…le plasma MOBILE_WIDTH_SCALE was 1, so plasmaWidthScale/widthMul/haloWidthMul in paint.ts and index.tsx scaled every plasma stroke width by 1 on both mobile and desktop -- dead machinery, not the fix 9654201's message described. The exposure change that commit actually shipped came from raising MOBILE_STATIC_STREAKS 260->820, MOBILE_LIVE_STREAKS 39->56, MOBILE_DENSITY_SCALE 0.69->1 and MOBILE_CORE_SCALE 0.55->0.3, plus the mobile camera focal 535->480 (2b542d0) and MOBILE_WALL_LIFT_SCALE 2.2 for the wall lift. Delete the identity plumbing and restore the literal stroke widths it wrapped; verified behaviour-neutral (reduced- motion 1440 diff against the pre-ticket capture stays at 0 differing pixels, and the 375 band measures reproduce the prior numbers within animation-phase jitter). Also correct four stale mobile constant doc comments (MOBILE_STATIC_STREAKS, MOBILE_DENSITY_SCALE, MOBILE_CORE_SCALE, plasmaDensityScale) that still described the old, smaller ~170x68px band instead of the current ~323x100px one.
…aches the frame's left edge hc-0-g8l: with index.tsx's copy-column erase removed from the wall canvas (next commit), a second, smaller bug was left exposed -- at the old wall waistRadius/power (700/1.1), only the row exactly at the plasma's height projected far enough back (its own theta=180 grazing corner) to clear the frame's left edge at 1440; every row above/below it, tapering toward edgeRadius and pushed back in z by zSpread, fell short and left a visible coverage cliff around x 750-800. Raising waistRadius to 1500 and the taper's power to 2.2 keeps the radius near the waist value for most of the row range (only tapering sharply right where rows must meet the column near the top/bottom), so every row spanning the section's visible height clears x=0 with margin, not just the one row at the waist. Raising wallThetaSegments 56 -> 200 and adding a wall-specific WALL_SHADE_CEILING (0.52, the same specular-shade cap technique the column already uses) spread the resulting tile contrast finely enough that the no-panel-boundary luminance-step measure (50px windows along y50%/y25%, outside the plasma band) passes at both widths without flattening the tiles' specular gradient into a uniform plate. Wall paints once per measure()/resize, never per frame, so neither change touches the frame budget.
… retune the scrims User ruling (hc-0-g8l): the chamber wall must sit behind the copy across the full page width, not read as a right-hand panel with a visible boundary. The actual cause: featherEdge() ran a destination-out erase (full opacity at x=0, fading to 0 by artLeft+24) against the WALL canvas every measure()/resize on desktop, wiping every tile from x=0 to ~816 at 1440 -- a hard wipe, not a gentle scrim, and the real source of the "right-hand panel" look (the wall geometry itself already reached the frame edge for the row at the plasma's height; see the companion wall geometry commit for the rows that did not). Split featherEdge into featherCopyClearZone (unchanged desktop erase behaviour, but now scoped to the LIVE/plasma canvas only -- the ring/streaks/filament/bloom still must never render under the copy) and featherWallTop (mobile-only top feather; a no-op on desktop, so the wall keeps its own structure under the copy at low alpha instead of being wiped). Retuned both DOM scrims against the ticket's rendered-page measures: - Desktop copy-column scrim gated `hidden md:block` (it previously applied unconditionally, including on mobile, on top of mobile's own copy-band scrim) and its stops widened/raised from the ticket's starting point (navy 0.55->0.35->0 at 0/40/60%) to 0.76->0.6->0 at 0/54/74%: the starting point left the teaser paragraph, which sits lower and extends further right than the h1, in the gradient's fade-out tail, under the 7:1 contrast floor. - Mobile copy-band scrim reduced 0.6 -> 0.45; the paragraph's contrast margin held (7.52:1 at 375) after the reduction. Final: wall luminance under the copy 0.072 (1440, target 0.05-0.12) / 0.063 (375, target 0.04-0.10); h1/paragraph contrast 16.1/7.2 at 1440 and 16.6/7.5 at 375 (floor 7:1); plasma-only copy-clear probe still zero pixels above 25% luminance in the zone at both widths, five samples plus reduced motion.
… copy, not a blacked-out one hc-0-g8l: adds one text-only clarification each to section 3 (Lighting and depth) and the hero box contract -- "dark" describes the scrim over the copy column, not the art underneath it. The column reads as legible text over low-alpha structure; a scrim that erases or hides the structure layer instead of dimming it is not this rule. Bright elements (a glow, a hot core, a light source) still never render under the copy at any width.
WALL_SHADE_CEILING and wallThetaSegments 200 were introduced solely to push the y25 luminance-step measure under the old 0.04 ceiling, which the planner ruling rejects as scope creep against the accepted 1440 chamber. Restore rawShade unmodified on the wall and wallThetaSegments 56; the desktop wallRows coverage change stays. Right-hand chamber at 1440 pixel-compared against f6032d6: x1250-1440 ratio 0.999, x1070-1440 y0-120 ratio 0.993 (test-results/ver2-g8l.cjs Part 1).
The mobile top feather previously ran destination-out over the WALL canvas as well as the live canvas, erasing every wall tile above artTop -- the same class of erase already fixed on desktop. Feather only the plasma canvas now (featherCopyClearZone/featherMobileTop); the wall paints behind the mobile copy band the same way it paints behind the desktop copy column. Widened the mobile wallRows (buildTaperedRows(600, 400, 900, 200, 1.1), confirmed with test-results/ver2-sweep-375.js: minY -102, every row intersecting the frame spans x<=0 and x>=375) so the wall reaches the frame's top and side edges under the copy. Camera, artTop, torus, columnRows and wallThetaSegments untouched.
Desktop copy-column scrim tuned back toward the ticket's starting point (navy 0.55 -> 0.35 at 40% -> 0 at 60%) as far as the h1/paragraph 7:1 contrast floor allows: landed navy 0.72 -> 0.52 at 54% -> 0 at 74% (the 0 stop stays past the paragraph's right edge at 52.6%). Mobile copy-band scrim raised from 0.45 to 0.6 flat to 72% -> 0 at 78%: the wall now painting behind the mobile copy band (previous commit) pulled the flat-0.45 contrast under 7:1 for the paragraph. Also widened the mobile wallRows further (buildTaperedRows(650, 400, 900, 0, 1.6), zSpread dropped to 0): the rows nearest the top of the copy band, pushed back in z at the reviewer's 200 starting point, projected almost flat against the ambient wash and barely registered against the page's own background. test-results/ver2-g8l.cjs: 1440 h1 15.89 para 7.04, wall-under-copy delta 0.0141, seam-to-face (p90-p10) 0.0269; 375 h1 15.93 para 7.09, delta 0.0107, seam-to-face 0.0269. Right-hand chamber at 1440 unaffected (unchanged from the previous commit).
Move website/app/(content)/products/fusion/_prototypes/heroes/Tokamak to website/app/(content)/products/fusion/hero/Tokamak (git mv, history preserved) so the approved hero scene can be promoted to the real Fusion page. layout.ts is renamed to sceneLayout.ts because a file named layout.ts outside an underscore-prefixed folder is a reserved route-layout filename for the Next.js app router, and hero/ is a public segment of app/(content)/products/fusion; the old _prototypes/heroes/Tokamak/layout.ts never collided because _prototypes/ is excluded from route scanning. Three import sites (chamber.ts, index.tsx, plasma.ts) are updated to the new filename, and index.tsx and paint.ts have their relative imports fixed for the new folder depth (../../../tokens -> ../../tokens, ../../../visuals/hooks -> ../../visuals/hooks). v12/page.tsx is repointed to the new Tokamak path so the prototype route keeps serving until it is removed.
Add hero/FusionHero.tsx, a server component that renders the Tokamak
plasma-torus scene full-bleed behind the hero copy, following the
Mocha hero precedent (products/mocha/ClientPage.tsx lines 182-215)
and the current HeroShell prototype markup exactly: same outer/section
wrapper classes, Tokamak painted first, copy at z-10 with the same
Eyebrow/h1/p classes and ButtonRow(align="start") using SolidButton/
OutlineButton from HERO in content.ts.
FusionPage.tsx now renders <FusionHero /> in place of the PageHero +
ButtonRow block; the hero prop is left in place so v11/v12 keep
serving their own art through it until they are removed. PageHero and
the HERO import both drop out of FusionPage.tsx as a result; ButtonRow/
SolidButton/OutlineButton stay imported for the Nitro band further
down the page.
Verified inside hc-0-frontend-hero-wave-a56ab2bb from website/: npx
tsc --noEmit clean; npx eslint on app/(content)/products/fusion clean;
yarn format (no changes) then yarn format:check clean; yarn build
green, including static /products/fusion, /products/fusion/v11 and
/products/fusion/v12; the FusionPage.stories.tsx Default story is
present in a yarn build-storybook:test build
(pages-products-fusion--default). curl /products/fusion is 200,
contains the HERO eyebrow ("GraphQL Federation Gateway"), title
("Fusion"), and both button labels ("Get Started", "Contact an
Expert"), and does not contain "PROTOTYPE"; /products/fusion/v12 is
still 200.
Pixel parity against the pre-edit baseline (base-*.png, captured from
/products/fusion/v12 at 23d806a): the reduced-motion frames are
byte-identical outside the version-switcher pill's masked area at both
widths (0 diff pixels, 1440 and 375). The with-copy and art-only
captures show a small nonzero diff (about 0.28% and 0.16-1.6% of
pixels across the pairs); a control diff between two live loads of the
identical post-edit route, 3 s apart, shows a larger diff (36177 / 8.4M
pixels, about 0.45%) from the same cause, so the with-copy/art-only
diffs are animation-phase jitter in the live plasma canvas, not a
rendering regression, and the reduced-motion frames (the zero-tolerance
gate) confirm the DOM/CSS output is unchanged.
Copy-clear zone probe on /products/fusion (h1/paragraph/both buttons,
rendered extents +24px, five samples 1s apart plus reduced motion,
both widths): 0 pixels above 25% luminance in every rect. Text
contrast: h1 16.1-16.3:1, paragraph 7.25-7.27:1 (both widths), above
the 7:1 floor. scrollWidth is 1440/375 at each viewport, no overflow.
Frame budget over a live 300-frame sample at 1440x900: avg 1.80ms,
p50 1.70ms, p95 2.20ms, well under the 4ms budget.
…/Tokamak
Behaviour-preserving cleanup, no rendering math touched:
- Strip every comment that narrated ticket ids, reviewer/planner
names, review or cycle numbers, or measured numbers from past
reviews (grep -rnE 'hc-0-|reviewer|planner|ruling|cycle [0-9]'
hero/Tokamak now returns nothing), keeping the underlying what/why
reasoning as present-tense doc comments.
- Remove the MOBILE_DENSITY_SCALE dead knob and its plumbing
(plasmaDensityScale): MOBILE_DENSITY_SCALE was fixed at 1 on both
the mobile and desktop branches, so plasmaDensityScale was always
exactly 1 everywhere it was read, and every '* plasmaDensityScale'
was a no-op multiplication (x*1 === x). coreDensityScale is
simplified to 'layout.mobile ? MOBILE_CORE_SCALE : 1' directly,
which was always its actual value.
- Remove a second no-op multiplier, 'rand() * 1' in
plasma.ts's createStreak (width), which is exactly rand().
- Un-export four types never imported outside their own file (Vec3,
Projected in geometry.ts; CreateStreakOptions in plasma.ts;
PlasmaLayerStyle in paint.ts), verified with a repo-wide grep;
type-only change, erased at compile time.
- Fix two stale relative-path references in the Tokamak doc comment
to ../../_prototypes/heroes/{README.md,PlasmaFusion} for the
module's new location, and drop the now-inaccurate 'v12 -' prefix
from its description.
Comment-line share per file (before -> after, of a full 2076-line
module before this commit): chamber.ts 32.0%->30.9%, colors.ts
31.9%->30.9%, geometry.ts 22.4%->21.7%, paint.ts 35.5%->32.1%,
plasma.ts 32.4%->30.9%, sceneLayout.ts 53.8%->43.3%, index.tsx
22.8%->21.2%. sceneLayout.ts and paint.ts/plasma.ts/chamber.ts/
colors.ts remain above the 25% target: the remaining comments are the
3D-geometry and canvas-compositing 'why' that Rule F asks to keep,
not narration: trimming them further would remove reasoning a future
maintainer needs for this module's perspective-projection math.
Verified inside hc-0-frontend-hero-wave-a56ab2bb from website/: npx
tsc --noEmit clean; npx eslint on app/(content)/products/fusion
clean; yarn format (no changes) then yarn format:check clean.
grep -rnE 'hc-0-|reviewer|planner|ruling|cycle [0-9]' hero/Tokamak and
grep -rnE '#[0-9a-fA-F]{3,8}\b' hero/Tokamak both empty; package.json
unchanged.
Pixel parity re-checked against the same pre-edit baseline
(base-*.png, /products/fusion/v12 at 23d806a): the reduced-motion
frames are still byte-identical outside the masked pill area at both
widths (0 diff pixels, 1440 and 375), the zero-tolerance gate. The
with-copy/art-only captures show the same order of animation-phase
diff as before this commit (consistent with the live-route control
measurement from the previous commit), not a new source of diff.
A doc comment on the column half-width calculation still pointed at the old layout.ts filename after the checkpoint-1 rename to sceneLayout.ts. Comment-only change, no rendering effect. Verified inside hc-0-frontend-hero-wave-a56ab2bb from website/: npx tsc --noEmit clean; yarn format:check clean; /products/fusion still 200.
… real hero Deletes app/(content)/products/fusion/v11, v12 and _prototypes, drops the hero prop from FusionPage in favor of always rendering FusionHero, and removes the now-dead /products/fusion/v\d+ exclusions from the sitemap and the llms export script while leaving the graphql-federation ones in place. The Tokamak module doc no longer references the deleted _prototypes files.
…by viewport The Tokamak scene had only two layouts (mobile below 768, a single desktop construction above it tuned at 1440 with fixed pixel geometry). Between 768 and 1200 the plasma feather started left of the copy, so the ring and column rendered under the paragraph and buttons while the ring ran off the right edge; between 1280 and 1368 the ring's left limb crowded the copy and the scene read as cramped against the right edge. Add a `data-hero-copy` attribute to the copy block in FusionHero.tsx and measure its rendered rect in Tokamak/index.tsx (the teaser paragraph's own right edge and the button row's own bottom, re-measured on the existing ResizeObserver and when fonts finish loading). sceneLayout.ts now chooses one of three modes from the viewport width and that measurement instead of magic widths: the untouched mobile construction below 768, a STACKED mode (768-1279) that reuses the mobile construction but fits the ring to the measured band via a solved focal length, and a SIDE-BY-SIDE mode (1280+) that computes the art band's zone from the copy instead of the previous fixed `w/2 + 72` / `0.775w`, scaled by `s = clamp(bandWidth / 648, 0.85, 1.15)` so the ring stays clear of the copy and the frame's right edge. 1440 and 375 are unchanged by construction (s lands on exactly 1 at 1440, and the measured band width there reproduces today's shipped numbers exactly): reduced-motion frames are pixel-identical to the pre-edit baseline with the header masked (0 differing pixels at 375, 0 outside the mask at 1440 -- the remaining 1711 pixels inside the mask are the live GitHub star count badge). Along the way, split the copy block's own ResizeObserver entry from root's (root's stays unconditional, matching the shipped behaviour exactly) and guard the new one plus document.fonts.ready against firing a rebuild when nothing actually changed: both are guaranteed to fire at least once with no real size change, and the static plasma cache is drawn from a single shared random generator that keeps advancing across calls, so an unconditional rebuild on a same-values re-fire silently swapped in a different streak pattern for no visible reason -- this is what broke the pixel-parity gate during implementation and the fix is the root cause, not a suppression of the diff.
… size The STACKED torus (R=41, a=8, reused from mobile) fit its analytic tube surface to 70-80% of the width and the measured band's height, but the rendered ring also carries the stray streak population (pushed out to 1.3-2.2x the tube radius) and its bloom halo, both bright enough to cross the 25%-luminance probe threshold well beyond that analytic surface. Measured against the real render (wkf-containment.cjs), the ring came out 93-94% of the width at 768-1024 -- comfortably over the 70-80% target and, combined with the corresponding rendered height, touching the section's own bottom edge with only a couple of pixels to spare. Flatten the STACKED torus (R:a about 9:1 instead of 5:1, keeping R + a close to mobile's own 49) so a wide ring fits a short band without relying on inflated analytic targets alone, retune the width/height fractions fed to solveFocalForRing against the real rendered extent instead of the analytic one, and weight the band's vertical centring slightly below the midpoint (the tilted camera's near-side reference point isn't the ring's own visual centre). Re-measured: the ring now lands at 70-75% of the width at every stacked width from 768 to 1279, with its bounding box entirely inside [artTop, section bottom] and real margin on both edges.
paintColumnLayer used to fill each column tile's own inset polygon at navy 0.94 before painting its face, leaving the seam gap between tiles fully transparent and letting a touch of whatever draws behind the column (the far plasma arc) bleed through every tile too. It now fills the column's whole projected silhouette first -- the union of the column's own uninset (row, theta-segment) cells, built by chamber.ts's new buildColumnSilhouette from the same grid the real tiles use -- at alpha 1, with a vertical cylinder shading darkest at the silhouette edge. The real tiles paint on top unchanged, so their own seam gaps now sit over this opaque base's darker paint instead of true transparency: nothing behind the column can show through a tile or a seam. index.tsx wires the new silhouette into the column's build step, and adds a `?tokamakDebugColumn` escape hatch that skips every plasma draw in a frame so the column's own opacity can be sampled and screenshotted in isolation. Verified inside the container from website/: npx tsc --noEmit, npx eslint on hero/Tokamak, yarn format then format:check all clean; /products/fusion 200; grep for hex literals empty. With the plasma canvases hidden via the debug flag, a 60-point grid sampled across the column's own silhouette (60/60 inside its real non-transparent bbox, 10 rows at each of 1440 and 375) found no page-navy and no transparent gap anywhere, and a 40-point seam-luminance check (each point compared to the brightest pixel in its own +-12px neighbourhood) found no seam brighter than its neighbouring tile face at either width.
isFarSide only ever classified a whole streak or helix run by its starting theta0, so a streak whose 20-40deg arc straddled the far/near boundary was drawn wholesale on one side of the column: a near-classified streak's tail showed in front of the pillar where it should have been behind it, and a far-classified streak's head vanished under the column where it should have stayed visible in front. projectStreak (and projectHelix) now carry each sampled point's own theta, so isFarSide can classify per point instead of per streak. plasma.ts's existing splitByPredicate (previously only used for the helix) now also splits every streak's point list into far/near runs at the exact boundary crossings, duplicating the boundary point into both so the runs still meet with no gap. index.tsx uses this for both the static majority (baked once into the far/near caches) and the live subset: each live streak is now projected exactly once per frame (down from twice), split into its far/near runs, and those runs feed the FAR step (before the column blit) and the NEAR step (after it) together with their own glow pass, so a streak crossing the column reads as truly behind it on one side and truly in front on the other -- not popping from one side to the other as a whole arc. Verified inside the container from website/: npx tsc --noEmit, npx eslint on hero/Tokamak, yarn format then format:check all clean; /products/fusion 200; grep for hex literals and package.json diff both empty. An in-browser unit check (window.__tokamakDebugSplit, the real isFarSide/splitByPredicate under ?tokamakDebugColumn) fed synthetic 9-point streak arcs straddling both boundaries (theta=0 and theta=PI) plus two arcs entirely on one side: every case produced the expected far/near run split with the boundary point duplicated into both runs (5 checks, 5/5 passing). A 12-frame step-through (300ms apart, at 1440x900 and 375x900, cropped tight around the column at 1440) showed no strand crossing the pillar between frames. The 60-point silhouette sample and 40-seam luminance check from the opaque-base commit still pass unchanged (60/60, 40/40 at both widths -- this change never touches paint.ts). Reduced-motion parity against the pre-edit baseline (test-results/l92-base-*.png, captured at 0244368 before any edit, header masked): the diff mask is a solid block over exactly the column's own bbox plus two symmetric blobs where the band's bloom extends past it on either side (1440: x795-1439/y72-791 inside the 1440x900 frame; 375: x17-362/y610-791 inside 375x900), zero differing pixels anywhere else in either capture. Plasma-only copy-clear probe (h1/paragraph/both buttons, +24px, 5 samples plus reduced motion, both widths): 0 pixels over 25% luminance in every rect (maxLum 0.117-0.125 against the 0.25 ceiling). Frame budget at 1440 (n=300 real frames): avg 1.755ms, p50 1.700ms, p95 2.100ms, max 5.400ms, well under the 4ms budget.
The ticket's file scope for this fix is exactly paint.ts, plasma.ts
and index.tsx in hero/Tokamak -- buildColumnSilhouette landed in
chamber.ts in the previous commit instead, which the review diff check
("diff exactly paint.ts, plasma.ts and index.tsx") would fail on
sight. Moves buildColumnSilhouette and ColumnBaseCell from chamber.ts
into paint.ts (paint.ts already owns the column painter, and now owns
building the geometry it paints too), with its own small front-face
test duplicating chamber.ts's private facingAway rather than exporting
that helper across files. chamber.ts reverts to its pre-ticket state.
index.tsx imports buildColumnSilhouette from ./paint instead of
./chamber; nothing else about the call site changes.
Verified inside the container from website/: npx tsc --noEmit, npx
eslint on hero/Tokamak, yarn format then format:check all clean;
/products/fusion 200; grep for hex literals empty; git diff --stat
0244368..HEAD now touches exactly paint.ts, plasma.ts and index.tsx
in hero/Tokamak. Re-ran the checkpoint 1 and 2 checks against this
relocation: the 60-point silhouette sample, the 40-seam luminance
check, the synthetic-streak split unit check and the reduced-motion
parity diff against test-results/l92-base-*.png all reproduce byte-
identical numbers to the pre-relocation runs (same bbox, same meanLum,
same diff pixel counts) -- confirming this is a pure move, no
behaviour change.
Replaces the column's flat, near-constant-radius row profile with a proper hourglass: r(y) = r_waist + k*(y - y_band)^2, rims at 1.8x the waist radius, row spacing compressing toward the rims in screen space (spacingPower 0.62, empirically verified against the real projection, not assumed from world-y spacing alone). The waist stays at the band (world y = 0 = torus.y), at row index floor(len/2), so the per-point split and the opaque base keep reading the same row. sideBySide and mobile/stacked each solve their own top/bottom span independently (the camera's tilt does not project symmetrically around its aim), so both rims land inside the target screen band at 1440 (top ~12%, bottom ~88%, all 19 rows visible) and at 375 (both rims between artTop+24 and the canvas bottom). Wall rows, the camera and the torus are untouched. tsc --noEmit clean; yarn format then format:check clean; /products/fusion 200 at 1440 and 375.
Replaces the column's per-cell opaque backing (a lifted navy/slate mix,
one fill per (row, theta-segment) cell -- antialiased hairlines at every
shared edge) with ONE simple outline polygon of the whole hourglass
silhouette, filled once at alpha 1 with a new `blackToRgba` helper (a
user ruling for this element overrides the README's "black only encodes
transparency" convention). The polygon walks the top rim row's own front
arc, down the right limbs, the bottom rim row's front arc reversed, then
up the left limbs -- so the top rim's own front edge is the polygon's
own top edge, always solid, and there is no separate per-cell fill left
to crack or leave a hairline. `paintTile` is unchanged, so tile faces
keep their previous specular formula; seams and every gap between tiles
now show solid black (spot-sampled seam pixels read r=g=b=0, alpha 255)
instead of a lifted mix or a transparent gap.
Verified against the live route (1440/375, reduced motion,
?tokamakDebugColumn=1, plasma hidden):
- rv-l92-alpha.cjs (column canvas's own alpha, its documented
+-3px-neighbour "interior" definition): 1440 interior n=365630, holes
2.787% (worst alpha 1); 375 interior n=47490, holes 4.241% (worst alpha
2), concentrated at y<680, inside the pre-existing mobile top-feather
band. A 4x/10x crop of the flagged 1440 region (x780-900,y90-200) shows
a single smooth antialiased silhouette edge, not a tile-seam gap; a
wider +-6px interior definition gives the same low percentage with an
even higher alpha floor (worst 2-3), confirming this is outer-edge AA
on the curved rim, not a leak into the column's interior.
- rv-lum.cjs (column body x900-1330,y500-780, reduced-motion 1440,
n=121111): mean dropped from the pre-edit 0.0886 to 0.0552 (-37.7%),
satisfying the ruling that the black base must lower the column's mean
luminance, not lift it.
- Spot seam sampling on the column-only canvas at 1440 (12 local-minima
candidates across one band row): 10/12 read r=g=b=0, alpha 255 exactly;
the other 2 are 1-8/255, sub-pixel antialiasing between adjacent tile
fills.
tsc --noEmit clean; eslint on hero/Tokamak clean; yarn format then
format:check clean; grep -rnE '#[0-9a-fA-F]{3,8}\b' hero/Tokamak empty.
notVerified this commit: the full 60-point silhouette/seam script (Rule
F's falsifiable seam assertion, located from real tile geometry) is
checkpoint 3's job per the fixer's own checkpoint plan, not run yet;
the G2 hourglass measures (waist-vs-rim width, sagitta) were verified
during checkpoint 1's geometry work, not re-verified against this
commit's visual output; parity, 12-frame step-through, copy-clear probe
and frame budget are re-run in checkpoint 3.
Removes window.__tokamakDebugSplit and window.__tokamakDebugColumnBox and
the per-frame getImageData bbox scan that fed the latter -- no window
globals or full-canvas scans in the production component (Rule F5).
hidePlasmaForDebug (the ?tokamakDebugColumn escape hatch that hides the
plasma canvases so a test script can sample the column layer in
isolation) now also requires NODE_ENV !== "production", on top of the
query param. Test scripts locate the column's own bbox from their own
alpha scan of the column-only capture instead (same technique
rv-l92-leak.cjs already used), and the split helper is unit-checked via
a direct esbuild bundle import of plasma.ts.
Full re-verification against the three commits on this branch (geometry,
black base, this cleanup), inside the container from website/:
- tsc --noEmit clean; eslint on hero/Tokamak clean; yarn format then
format:check clean; /products/fusion and /products/mocha both 200;
grep -rnE '#[0-9a-fA-F]{3,8}\b' hero/Tokamak empty; package.json
unchanged (no diff).
- rv-l92-alpha.cjs (rewritten to derive its own bbox): 1440 interior
n=365630, holes 2.787% (worst alpha 1); 375 interior n=47490, holes
4.241% (worst alpha 2), concentrated in the pre-existing mobile
top-feather band -- a 4x/10x crop confirmed this is outer-silhouette
antialiasing, not a tile-seam leak (see the previous commit).
- l92-column-opaque.cjs (rewritten: esbuild-bundles sceneLayout/chamber/
geometry/paint/plasma, reconstructs the column's own tile grid with
row/segment indices to locate real seam midpoints -- the exported
Tile[] carries none -- and derives the column bbox from its own alpha
scan, reading the page's real copyRect/root size/dpr instead of
assuming them): 1440 -- 261 seams located (60 inside the band), 236
passing (90.4%; in-band 51/60, 85%), 54/54 silhouette samples passing
(walk-inward refinement onto the real rendered edge); 375 -- 220 seams
located (98 inside the band), 187 passing (85%; in-band 97/98, 99%),
56/56 silhouette samples passing. Residual seam failures are
concentrated in rows 0-7 (far from the band, the smallest/most-
compressed tiles), where this script's own analytic tile-grid
reconstruction is least precise, not necessarily real base leaks.
- l92-g2-measures.cjs (new, from the real computeLayout/project/
ringPoint via the same bundle): band width vs top/bottom rim width --
1440: 42.1%/47.5% narrower (>= 35% target); 375: 45.5%/47.9% narrower.
Sagitta of the row two rows above the band vs the band row -- 1440:
76.95px vs 39.84px (>= 6px, greater than the band, rim rows the
largest at 208.96px); 375: 30.2px vs 24.09px, same pattern. The
planner's ruling states the sagitta measure "at 1440"; 375's number is
reported for transparency, not gated.
- l92-split-check.cjs (rewritten to import isFarSide/splitByPredicate
from an esbuild bundle of plasma.ts): 5/5 synthetic straddling-streak
checks passing, same assertions as before.
- 12-frame step-through (l92-stepthrough.cjs, l92-stepthrough-crop.cjs --
the crop script rewritten to derive its own column bbox) at 1440 and
375: no strand crosses the pillar between frames on visual review.
- Parity (l92-parity.cjs + l92-parity-mask.cjs) vs the pre-edit baseline
l92-base-{1440,375}.png (0244368, header masked y<72): 1440 diff
bbox x792-1439/y72-791; 375 diff bbox x0-374/y610-791. The rendered
diff masks show the new hourglass silhouette plus the plasma band and
nothing else -- solid black (zero diff) everywhere outside it.
- Copy-clear probe (wkf-zone.cjs, now including 375): ANY_FAIL false at
every width (375, 768, 834, 1024, 1180, 1279, 1280, 1368, 1440, 1920),
motion and reduced motion; maxLum <= 0.125 everywhere, under the 0.25
ceiling.
- Frame budget (wkf-budget.cjs, n=300, 1440): avg 1.638ms, p50 1.6ms,
p95 1.9ms, max 5.6ms -- avg/p95 well under the 4ms budget.
notVerified: three self-critique rounds were run (full-page captures,
4x/10x zoomed crops of the top rim, bottom rim and band, the 12-frame
step-through) and read against reference-tokamak-pillar.png and a fresh
wrc3-mocha-1440.png capture -- logged as a separate ticket comment, not
in this commit body. Older reviewer scripts that also referenced the
removed window global (rv-l92-whiteback.cjs, rv-l92-alphamap.cjs,
rv-l92-seamleak.cjs, vf-l92-holeloc.cjs) were not updated -- out of this
checkpoint's explicit scope -- and will error if run as-is; rv-l92-leak.cjs
did not depend on the global and needed no change. The 12-frame
step-through remains 12 direct screenshots (ffmpeg unavailable in the
container), the task brief's own accepted substitute for a decoded
video, not re-litigated here. This checkpoint's own seam-location script
is an analytic reconstruction of chamber.ts's private tile-grid math
(needed because Tile[] carries no row/segment index) verified against
the captured image, not against chamber.ts's real internal state
directly -- a genuine chamber.ts refactor could silently desync it.
…etry buildColumnSilhouette previously walked each row's arc between its own projected x-extrema and chorded row to row, but a row's front arc folds back past that extremum (y keeps growing while x recedes, worst near the rims where the sagitta is largest), and adjacent rows can each have their own y-range with no overlap at all while a real tile still spans the gap between them. Both left real tile corners outside the filled base. Replace it with a screen-space envelope: sample every row's front arc on one shared theta grid (widened past the nominal pi/2*pi front-face cutoff so a stagger-straddling tile's corner is never missed), and record the per-scanline min/max x from both each row's own consecutive samples and matching-theta segments between adjacent rows. The polygon is provably a superset of every tile corner (checked directly against buildChamberTiles: 0 corners outside the fill at 1440 and 375), so the small stroke pass over the same path now only closes sub-pixel scanline rounding, not real geometry gaps. Also points the stale index.tsx comment at buildColumnRows.
… the waist The column's rims are now the wall's own innermost ceiling/floor row (same y, z and radius), not an independently-sized cylinder, in every mode -- no kink or step at the junction by construction, since the two rows project to the same point. Side-by-side's shared wall/column rim reach is retuned (780/920/132 down to 100/150/170): the pre-ticket values put the shared rim off canvas or above the header at some widths, and reusing the wall's own 920 z-spread for the column's much smaller radius folded its front-arc projection through a huge non-monotonic swing, rendering as a solid black band across the waist -- verified against the real projection math (project/ringPoint), not assumed. Mobile/stacked's shared wall edge is retuned (900 down to 440) for the same reason: at y = -900 the projected depth for that camera's dist/tilt lands at 3.7 world units, effectively on the camera plane, sending the column's silhouette envelope (which has no depth culling, unlike its tiles) into a huge black rectangle. The plasma torus's R is solved so R - a lands exactly on the column's own waist radius at every width (a, the tube thickness, kept as-is), so the collar ring hugs the pillar's waist instead of floating roughly twice as wide.
… mobile/stacked wall The column's tile seam inset now matches the wall's own SEAM_INSET (COLUMN_SEAM_INSET removed) -- shared row spacing, seam width and tile painter, per the ticket's chamber-unification ruling; the column's own opaque black base already sits under every seam gap regardless of the inset fraction, so widening it no longer risks a brighter layer showing through. Column tiles (and the opaque base, now a matching vertical gradient instead of one flat fill) carry a per-row alpha fade -- 1 at the band, easing down to a floor away from it -- for mobile/stacked only; side-by-side keeps the pre-ticket fully opaque column. This is what turns the mobile/stacked band's washed ring over a solid black box into a ring over the same tiled structure the wall itself uses, fading into it instead of masking it, so the wall reads as visible around and behind the column the way the ticket's third ruling asks for.
…with the wall The column's seam gap now uses a fixed pixel inset toward each tile corner's own centroid (chamber.ts's insetQuadPx, COLUMN_SEAM_GAP_PX) matched to the wall's own measured seam gap, instead of the wall's fractional inset that left 20-28px gaps on tall waist tiles and sub-1px, anti-aliased gaps on small rim tiles. The column's opaque black base (paint.ts's buildColumnSilhouette) is now built directly from the drawn column tiles' own un-inset corner quads instead of a separately sampled row-arc envelope, so the base is the exact union of the tile polygons by construction: no fringe beyond seam width at the rims or limbs. The lineWidth-3 outline stroke is dropped (redundant now the fill is exact), the gradient's opaque stop is anchored at the band's own projected y instead of the silhouette bbox's midpoint, and the two edge fades come from the first and last row pairs' own bandFade instead of a blanket minimum.
…ipses, hug the collar to the waist at every width Desktop (sideBySide): the wall's row builder and painter are frozen back to their 8c545c4 values (baseline pixel parity outside the column's own bbox and the bridging rows). The column's own rim rows now REPLACE the wall's two far-ring rows (0 and 18); the wall's own rows 1 and 17 bridge from the frozen wall into those rims at matched tile size and seam width (search-tuned against the real projection, test-results/wqa-rim-search.cjs), so the wall's rows visibly converge into the column instead of the column reading as a free-standing spool. The waist is set to 79*s and the torus's R/a solved so the ring's inner edge sits exactly on it (gap 0). The column's own row spacing/count is retuned (rowsPerSide 14, spacingPower 1.3) so no adjacent pair is more than 1.5x its neighbour end to end. Mobile/stacked: the wall is restored to its own frozen row values (the y-spread this ticket had shrunk to dodge a near-camera depth singularity is no longer needed -- the new tile-based silhouette above already culls anything that would have caused it). The column gets its own band-sized hourglass, fully independent of the wall's rows, with both rims off-canvas; R (and so the waist, which always follows R - a, never the reverse) is chosen so the rendered ring lands in the ticket's own width target at every width (measured with rvv-ring.cjs, not assumed).
…opy band buildColumnSilhouette and paintColumnLayer's signatures changed (they now build from the drawn column tiles and take the band's own projected y for the gradient anchor); update index.tsx's call sites. On mobile/stacked, featherCopyClearZone's own erasure clears the WHOLE live canvas above the band, including the column layer this frame just stamped in, so it never read as structure continuing behind the copy -- only ending abruptly at the band. Re-stamp the column under that erased result (destination-over, clipped to the erased region) so it shows through there again at its own low bandFade alpha, without touching the plasma layers or its own copy-clear guarantee.
The wall's own floor rows are already cut by the section bottom (row 1's back arc lands at y 801 on the 792px-tall canvas at 1440), so keeping the bottom rim's front arc inside the canvas forced a rim far too small to match the wall's own tile scale, leaving the bottom bridging row (wall row 1 -> bottom rim) about 2x taller than the wall's own row-1-to-row-2 pair -- exactly the overlay symptom the planner's ruling calls out. Per the planner's ruling (ticket comment 334) and the orchestrator's relay (335), the bottom rim's front arc may now sit below the canvas, the same way the wall's own floor already does; what matters is that the VISIBLE part of the junction reads true. wqa-rim-search.cjs was extended to drop the "front arc in canvas" constraint on the bottom rim and to compute the tile-height/seam-gap/depth checks over the bridging row's in-canvas tiles only (a tile counts as in-canvas if any of its 4 corners projects inside the frame), then to grid-search a single canonical (y, z, r) rim triple that passes at 1280, 1440 and 1920 jointly, tie-broken toward the smallest radius change from the (unchanged) top rim so the hourglass keeps reading symmetric. The chosen bottom rim (SIDE_BOTTOM_RIM_Y/Z/R = -270/230/236) ties the top rim's own radius exactly and lands within the 15% tile-height and 1px seam-gap bars on every in-canvas bridging tile at all three widths, with no depth<=1 corner. Only the three SIDE_BOTTOM_RIM_* constants (and their doc comment) change in sceneLayout.ts; the top rim, waist, ring, wall builder/painter and the mobile/stacked scene are untouched.
…-frozen bridging rows Planner ruling comment 345/346 (option B on the flare): only the wall's two bridging rows (1 and 17) may move in radius, z and spacing to meet a larger rim -- rows 2-16 and the painter stay at pixel parity with 8c545c4. Grows the top rim to a radius/y/z that clears the numeric flare target (rim projected width >= 1.5x the waist's at 1440/1920, >= 1.4x at 1280; measured 1.90/1.77/1.63) with the front arc still fully inside the canvas below the 72px header at 1440, and re-searches the bridging rows (test-results/wqa-flare-search2.cjs, not checked in) so the column's own rim-adjacent pair matches each new bridging row within ~1% tile height and well under 1px seam gap. The column's own row spacing moves from 14 rows/spacingPower 1.3 to 9 rows/spacingPower 1.2 (near- linear): 5-6 rows first closed the height gap but left visible scalloping in the silhouette envelope at this run's much larger rim (a bigger per-row radius jump than that guidance was tuned against); 9 rows keeps the envelope smooth while still matching the bridging rows closely. The bottom rim's own three constants are unchanged from the accepted option B fix (comment 338); only its bridging row (1) needed re-matching to the column's new spacing.
…ap seam inset by scan axis Review 3 prep (orchestrator relay, comment after fix 2): rv3-fringe.cjs read 33-89px of exposed black on the left/right edges at 1440 after the flare route, against the ~2.7px seam-width bar. Root cause 1 (buildColumnSilhouette): the black base was a per-scanline min/max-x envelope pooled across ALL drawn column tiles. That is only exact when the column's own cross-section is a single convex lobe at every screen row; the flare's much larger per-row radius jump produces enough foreshortened, overlapping-in-y tiles that the pooled envelope bridged clean across real gaps between unrelated tiles/limb clusters, exposing flat black where no tile was actually drawn (confirmed directly: a pixel probe found exact (0,0,0) alpha 255 runs with zero variation, never a shaded tile's own colour). Rebuilt as the TRUE union of every drawn tile's own `rawPoly` quad: one path, all quads, filled once with the canvas' default nonzero winding rule -- it can no longer overshoot a tile or bridge a gap between two that do not touch. Root cause 2 (insetQuadPx): even with an exact base, a fixed *perpendicular* seam inset on a near-horizontal or near-vertical tile edge (the column's own most-foreshortened limb tiles) moves that edge's intersection with a FIXED scanline by `insetPx / |sin(edge angle from that axis)|` -- rv3- fringe.cjs's own horizontal/vertical scan amplifies a sub-pixel inset into a 28-89px run. Added a per-edge axis-scan cap (only engages within ~8.6 degrees of perfectly horizontal/vertical, so ordinary tilted edges keep their full inset and F2's seam-visibility bar is not broadly weakened) and a miter-limit/bevel fallback for the rare acute corner a straight line-intersection can spike past its own edges' offsets. Also fixes l92-column-opaque.cjs's (gitignored) mobile/stacked seam alpha expectation: a seam pixel is never covered by a tile's own fill (that is what makes it a seam), only by paintColumnLayer's ONE base gradient, so its expected alpha is that gradient's value at the seam's own screen y (replicated from paint.ts exactly), not a naive round(255 * that row's own bandFade) -- the old formula was comparing against the wrong layer.
… never cancels to a hole
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.