Open work
Everything this repository has started and not finished, in one place. It was gathered on 2026-08-31 from the "still open", "not built" and "deferred" sections that were scattered across a dozen separate plans, and each item names the file and the code it was read out of so it can be checked rather than believed.
Two neighbours to this file:
- [
FUTURE-DIRECTIONS.md](FUTURE-DIRECTIONS.md) — ideas recorded but not started. Nothing there is owed. - [
CROSS-REPO-HANDOFFS.md](CROSS-REPO-HANDOFFS.md) — work that is finished here and waiting on the API, the database, or R2 write credentials. Nothing there can be closed from this repository.
An item leaves this file when it ships, and its plan — if it had one — moves to [archive/](archive/README.md).
0. Kerr black hole optics — the museum, the frame budget, and the art
[KERR-BLACK-HOLE-OPTICS.md](KERR-BLACK-HOLE-OPTICS.md) records what is built and every number it was measured at. Four things are open, and the first is by far the largest.
The museum (the plan's M5) — in, 2026-09-07, with the caveats the plan predicted
The Kerr black hole now stands at the end of the Golden Funnel, at the formula's origin five metres under the floor, in the owner's numbers: a* = 0.382 turning clockwise seen from above, a disk out to 60 r_g, H/r = 0.060, an optical depth of 0.35, everything else the study page's default. One and a half metres to the gravitational radius (2026-09-08: three times the diameter it opened at), so the disk is 180 m across in a room 240 m wide, the shadow 15 and the horizon nearly 6.
At that size the shadow is wider than the 5 m the exhibit hangs below the floor, so the dark region rises above the floor plane and a visitor standing beside it sees the black disc cut into the room rather than lie under it. Nothing solid moved: the horizon is 2.9 m and still well under. A shadow is the set of directions whose light was captured, not a surface, and those directions reach above the plane at every scale — it only shows as a rise once the shadow is bigger than the drop. The orb's vote now dresses the same hole in one of the tracer's three skins: the two words already on the wire (white, warm) name the ice and NASA skins and physical is new, so a server still on the two-word list drops a physical vote and the orb's own 2.5 s fallback wears it locally until the server's copy of presenceProtocol.js gains the word.
How it sits in the room (render/kerrExhibit.ts, KERR_MUSEUM_MAIN_GLSL, model/manifoldKerr.js), because each of these is a choice:
- On a sphere, not a quad. Every fragment of a 100 r_g sphere aims a ray from the visitor's eye through itself; the eye becomes the tracer's ZAMO camera through the same
cameraFramethe page uses, in Kerr coordinates one and a half metres to the unit, room y-up mapped to Kerr z-up right-handedly. - The sky is the sky, bent. Escaping rays sample a cube map rendered from the hole's own centre, and what ramps between the two blend angles is THE DEFLECTION, not the opacity of a second picture. It began as a crossfade and a crossfade shows both: the same nebula twice, a few degrees apart, half strength each — what the owner saw and circled. A ramped deflection has one image everywhere. It is zero at the sphere's rim, where the sample is then the sky in the direction the ray was already going, so the rim is seamless rather than a step; it is the true deflection by the radius the integrator takes over, so the traced core is exact. In between the sky is under-bent by up to a couple of degrees, which is the price. Baked at entry, again at +6 s and +20 s while the room is still streaming, and whenever a ride or the paint is rebuilt.
- Depth is written where the light STOPPED, per fragment: the gas that absorbed it, or the hole for a ray the horizon swallowed. A ray that escaped did not stop — its light came from the room's far field through the cube map — so it is a backdrop at the far plane and every real thing in the room is in front of it. So a rail in front of the shadow shows, one behind it is hidden, and the depth buffer never has to know a curved path.
- The far field is closed form. Outside 46 r_g a ray that would not meet the disk takes the weak-field bend (4M/b)(1 + 15πM/16b), scaled by how much of it is still ahead of the eye, so standing near the hole and looking away still sees the room bent, continuously with the traced core, for a few multiplications. The gas sample length grows with the eye's distance (0.5 r_g at 26, 4 at 200) and the step budget is 700 at 1e-4.
- The disk turns at the study page's rate, 40 t_g per real second, which is a display convention: at this scale a real t_g is five nanoseconds. The museum ran at one t_g per second at first — the same disk, forty times too slow, one turn at the inner edge every 66 seconds against the page's 1.7.
- One ray per CSS pixel. The sphere is drawn by its own pass into a target the drawing buffer's size over the pixel ratio (never below half), and a full-screen quad puts it back, writing the depth the pass recorded. Measured on the owner's machine at 2560 × 1440 (ratio 2): 3.7 ms for the room without the exhibit, 10.4 ms with it from the floor forty metres away, 16.6 ms twelve metres away with the disk filling the view; before the pass those were 25 and 51 ms, and the gas sample length moved them by a tenth, because the cost is the ray count. The study page's Full tier is one ray per CSS pixel too.
- A backdrop is baked as pure direction. The museum re-centres the sky shell — and the Live Painting marks that are its children — on the eye every frame, which is what makes it a backdrop rather than a place. Baked from the hole while still centred on the eye, its stars landed at angles wrong by the whole eye-to-hole offset (six degrees at 26 m on a 240 m shell) while everything at a real place in the room was baked at its true angle, so the two slid against each other as the visitor walked: the owner's report of the background and its particles moving apart. The bake now re-centres it on the cube camera. Measured: baking with the visitor 85 m from where they were changed 161 501 pixels of the frame before, and changes none of 3 686 400 after. A test holds the list handed to the bake equal to the list
setCameramoves, so a third camera-follower cannot be forgotten. - Nothing of the room is in the cube map. A map rendered from the hole's centre is exact for a backdrop — a thing with a direction and no position — and wrong for anything at a real place a few metres away, because the visitor is not standing at the hole. The rides were in it for a while, and near the shadow their wrapped arcs were the best thing in the frame; but once the map has to be sampled at the ray's true direction everywhere for the sky's sake, there is no weight left to fade a misplaced ride out with, and at the rim it would sit wherever it happens to lie from the hole. One or the other, and the sky is the one that fills the frame. The visitors are out too, or they would hang around the shadow as ghosts. The room composes with the picture in the frame instead: the composite is drawn first among the transparent things, and the room's glass, rails, grid and sparkles — none of which write depth — test against the depth it wrote, so a rail in front of the hole stays in front and one behind the shadow is hidden.
Still open, and named here rather than smoothed over:
- The composite sits at -4, between the sky's own Live Painting marks (-5) and the corridor's glass (-3/-2). A mark on the sky writes no depth and the composite writes the far plane for an escaping ray, so a mark drawn after it painted itself over the black hole — a scatter of black specks across the disk, which is what the owner found. Measured: with the composite at -10 the mark changed 16 135 pixels of the frame; at -4 it changes none.
- Ten metres down, not five. The shadow's radius is 7.7 m at this scale, so at the old drop the dark region rose through the floor. What has to clear the floor is the critical impact parameter — a shadow is a set of captured directions, not a surface — and the drop is now larger than it with 2.3 m to spare.
tests/the-manifold-room.test.jsholds that, and is the test that fails if the scale is raised again without the exhibit going down with it. The funnel went down with it, so its slice through the floor plane is wider (1.83 m of radius against 0.83) and the launch orb moved from 4 m to 5 to stay beside the point rather than against it. - No pad ring over the exhibit. The room draws a gold ellipse on the floor at each of its four reserved attraction areas; the Golden Funnel's lay directly across the middle of the disk. It is skipped by position, so moving the ride moves the exception with it.
- The ride's view follows the hole, between a floor of 15 degrees below level and a cap of 50. A fixed tilt lost it — the centre runs from 30 to 80 degrees below level across the descent — and following the point with nothing to stop it is the older failure this ride already had, where 80 degrees is a rider looking at their own feet. Clamped, the shadow is never more than 30 degrees off the aim against a 65-degree frame. The horizontal aim moved from halfway to the axis at the same time.
- The tour follows by construction, and is now held to it: its establishing shots are framed from each ride's own sampled frames, so the funnel's moved down with the funnel, and a test checks that the hole is 6.9 degrees off that shot's aim and subtends 5.7 rather than trusting the construction.
- The third skin cannot win a vote until the server has the word. The orb asks the room, and
net/presenceProtocol.jsis shared VERBATIM with the server inlucas-academy-inception-space: this repository's copy now listsphysicalbesidewhiteandwarm, and a server still on the two-word list drops aphysicalvote unread. What the chooser sees is the orb's own fallback — after 2.5 s with no tally it wears the answer locally — so the skin works for them and the room never settles on it for everyone. The fix is one word in the API repository's copy of the same file, which this repository may not make. - A local
npm run devopens no socket at all, by design (net/presenceClient.ts): a standalone museum votes alone, which is why the orb reads "Exploring solo, so your choice is the room's". To exercise the real vote, run the API repository'snpm run apiand open the museum with?api=dev, or build withVITE_MUSEUM_APIset. The window is 7 seconds (FLOOR_VOTE_SECONDS, cut from 27 and before that 60). - The third-person boom is back on the aim. It was moved onto the track on 2026-08-24, when the owner sat against one side of their own ribbon; the aim was a shallow 15 degrees then, so the boom was mostly sideways and the eye sat 0.04 m above the ribbon — level with it, looking across. The aim now points at the hole and is pitched 30 to 50 degrees down, so the same offset is mostly upward: at least 0.94 m above the ribbon and 28 degrees of elevation at its shallowest. On the track the hole was simply not in frame, the funnel being a spiral whose direction of travel is 72 to 85 degrees away from it. The elevation is the test's guard against a shallower aim bringing the old complaint back.
- The colour tables are float where a device can filter float. They were RGBA32F with linear filtering, which WebGL2 does not guarantee: in ES 3.0 the 32-bit float formats are renderable but not filterable, and filtering one needs
OES_texture_float_linear. Desktop exposes it; iOS Safari commonly does not, and a texture asked to filter a format it cannot is incomplete — every sample black. The gas's colour is one such sample, so on an iPad the shadow, the lensing and the room drew and the accretion disk did not. RGBA16F is filterable by core everywhere WebGL2 runs and is the fallback; a device with the extension keeps the table it had, bit for bit. Measured on the owner's desktop, forcing the fallback on the same frame: of 3 686 400 pixels (2 275 706 lit) the mean difference is 0.015 of an 8-bit level, 11 pixels differ by more than one, and the worst is two. - The sphere's pass adapts, and cannot cost a healthy machine anything. It steps down only after a sustained frame interval past 26 ms (38 fps) and back up below 19; a 60 Hz display reports 16.7 and a 120 Hz one 8.3, so neither reaches the trigger, and the owner's desktop holds one ray per CSS pixel — verified live at
x1.00, 922 k rays. Nothing is sniffed: the input is the interval between the exhibit's own frames.?debug&hud=1prints the line that says which float, how many rays, which step and the frame time, so the next device report is answerable rather than a guess. - Measured on two machines, and a phone is still not one of them. On the owner's desktop at three times the size the exhibit costs 11.9 ms from 95 m, 16.2 from 36 and 11.0 from straight above, against 3.3 for the room alone — unchanged by the trebling, because the sphere's extra area takes the cheap analytic path. On the owner's iPad over the LAN (2026-09-08) the room runs at 30–40 fps with a p95 of 47 ms and reads as comfortable. That is above the 26 ms step-down, so the pass is adapting there rather than sitting at full, which is the arrangement working rather than a number to fix; if it is ever wanted smoother, the levers are
PASS_STEPS(add a lower one), the trigger itself, andquality/proxyRadiusin the room spec. A phone is still unmeasured, and so is anything with a slower GPU than an iPad's. - The coaster is not bent around the hole. It is drawn as itself and correctly hidden where the shadow or the gas is in front of it, but it has no lensed image. Bringing that back means a SECOND cube map carrying the near geometry alone, sampled with its own weight that fades to nothing at the rim while the sky's stays at one — about fifty lines and six more megabytes. Worth doing if the wrapped arcs are wanted; they are nearly invisible with the gas on, which is why they were not worth an artefact.
- A ride behind the hole but outside the shadow is not hidden. An escaping ray writes the far plane, so nothing occludes it there. The alternative — writing the closest approach — is what put a black disc across the coaster in the first place, since the funnel's turns above the hole sit at exactly that distance.
- The blend annulus. Between 2.3° and 5° of bend (23–50 m from the axis in projection) the room as drawn and its lensed image both show, misregistered by up to 5° for far walls; a soft ghost rather than a step, and the two angles are the knobs.
- Not seen on a phone, and the ride's last metres through the ergosphere have been reasoned about (the horizon flag paints black) rather than watched.
The ride falls — a capture orbit, through the horizon, 2026-09-09
The Golden Funnel ends in a near-critical capture orbit watched from inside it (KERR-BLACK-HOLE-OPTICS.md §9b). The whole coaster stands 25 m above the hole, the ride runs to the spiral's point (0.6 m from the axis, 21 r_g from the hole), the camera is pushed in to the eye a turn and a half before the end and rides the last turns from it, and is thrown ALONG THE TRACK at 0.3221 c — the last speed this side of the separatrix, E = 1.0049, nearly polar — whirls about two turns between 5 and 4 r_g, falls, crosses the horizon and stops at 0.6 r₊, over 45 s of playback on a four-stretch schedule that shows the far part quickest and the crossing slowest, with the gas on the camera's clock and the aim riding the shadow's rim once the shadow is too big to look at; then three seconds of black, and the room. (A glowing line along the curve was tried and removed at the owner's word.) Recorded uncut from the museum's own canvas: assets/kerr-plunge/museum-capture.mp4.
Open:
- Nothing has been measured playing the 45 s interactively. The status line during the stepped recording read 12–22 ms through the fall and 27 ms at the horizon (the pass adapted to 0.75 of its rays) against 12–14 ms on the track, on the owner's desktop through a hidden pane; not an interactive frame time. The plan's targets (desktop p95 ≤ 33.3 ms, mobile ≤ 50, three consecutive plays) are unmeasured on every device.
- Inside the horizon the dark half of the sky is dark by budget: the rays that never cross r₊ spend their steps approaching it from inside. Right for a hole that formed from a collapse, and a numerical outcome rather than a modelled one.
- The disk is one colour. With the physical (Planck) skin the gas near its peak is white-hot and the display transform desaturates it; the Doppler asymmetry is there as brightness, not as colour. Not touched, by instruction.
- The event boundaries were checked at 60 and 30 Hz, not at 15 or 120, and not with a frame delta that crosses both the hand-over and the release at once — the code handles that in order and the pure-function tests hold the timing, but it has not been watched.
1. Discovery — the three requirements that are still unmet
[DISCOVERY-SEO-GEO.md](DISCOVERY-SEO-GEO.md) lists these under "Still open". All three were re-checked against the build on 2026-08-31 and all three are still true.
R4 — the preview image is shared, not per room
Half done, 2026-08-31. The gap was total: no og:image existed anywhere, so no page had a link-preview thumbnail — and the landing page was worse than bare, because twitter:card = summary_large_image promises a big-picture card and there was no picture, so it rendered as a text box.
There is now one card for the whole site: public/og-cover-v1.jpg, 1200 x 630, a crop of the museum's own constellation portal. src/inception/discovery/socialCard.js owns it, both generated templates call it, index.html repeats it under test, and the build refuses if the file is missing from dist/.
What is still owed is what R4 actually asks for: a curated screenshot per room, rendered headlessly at build time — "a room, not a logo", because the card beside an answer about Van Gogh House should show Van Gogh House. The seam is ready for it: give socialCard.js a per-page lookup and let the shared card stay the fallback for pages that are not a room.
Two things to know before building it. Headless capture of this museum means booting WebGL, waiting for a room's textures to upload (see [PERFORMANCE.md](PERFORMANCE.md) — arrival is not instant) and reading the canvas back, which needs preserveDrawingBuffer or a capture inside the render loop. And a scraper caches a card by URL, so a new or changed image ships under a new -vN name rather than replacing bytes at the old one.
R5 — no hreflang pair
There is no hreflang in src/, scripts/ or index.html. The policy is EN + ZH, both indexable, paired. The blocker named in the requirement still stands: the room documents carry no translated title, so there is nothing to build a ZH twin out of yet. That is the first piece of work, and it is a content change to content/inception/rooms/*.room.json, not a renderer change.
R6 — noindex has been removed, and the rest of R6 has not been re-checked
The requirement says index.html carries <meta name="robots" content="noindex"> and keeps it until the public surface exists. That meta tag is no longer in index.html. Whether the public surface now genuinely satisfies R6 — public-snapshot layer, no real names, no class membership, no draft history — is a decision that should be recorded rather than inferred from a deleted line. The two automated guards (no owner email, no password, and a room in the sitemap only if its document says visibility: public) still run and still pass; they are narrower than R6.
Per-artwork pages
Rooms have pages; individual paintings do not, because the public-snapshot layer that decides what an artwork may say (execution plan §2.3) is not built.
2. Standalone mode — the missing half of import/export
From [UI-REPOSITORY-SCOPE.md](UI-REPOSITORY-SCOPE.md), which calls standalone mode the primary acceptance target.
There is exactly one file input in the whole client and it belongs to the publish flow. Standalone therefore has export and file-keeping only. Missing:
- local
.ispackimport (the inbound importer was retired upstream); - local GLB import for display;
- single-object GLB export.
The document is explicit that building local import is the next piece of work and not a capability it should claim. Note the ordering rule in [CROSS-REPO-HANDOFFS.md](CROSS-REPO-HANDOFFS.md) Part 2: the .ispack contract belongs to Lucas Visual Art Lab and must not be re-derived here.
Sealed-collision coverage does not travel
The solidCollisionBox invariant — a sealed interior must cover a measured object's real geometry, not its centred footprint — is asserted by tests/inception-solid-interior.test.js in the source repository, because that test reads server/rooms.js. This repository has the collider fix and library-glb-collider.test.js, but no coverage of sealed collision. Splitting the server-free half of that test out is separate, unclaimed work.
3. Narration and subtitles
The Manifold rides have no recordings
[MANIFOLD-TOUR-NARRATION.en.md](MANIFOLD-TOUR-NARRATION.en.md) is confirmed by the owner and shipping as subtitles — src/inception/tour/manifoldNarration.js is the runtime source of truth — but no voice exists. The subtitles stand on their own until one does. English only, deliberately: the field for a Chinese caption track is left out rather than left empty.
The Van Gogh opening revision is still a proposal
[VAN-GOGH-DEMO-NARRATION.en.md](VAN-GOGH-DEMO-NARRATION.en.md) carries one proposed Phase 2 opening for the first painting that is editorial until the owner finalizes it; recorded cue 01-lines-and-people is unchanged and stays that way until then. tests/demo-narration-docs.test.js pins this: it asserts the document still says "This is a script proposal, not a new recording", so finalizing the copy means changing that test in the same commit.
The Chinese track is a review document, not a voice
[VAN-GOGH-DEMO-NARRATION.zh.md](VAN-GOGH-DEMO-NARRATION.zh.md) mirrors the English cue ids for review only. The demo plays the English recordings; no Chinese audio has been generated, and nothing authorizes it into the runtime.
4. The standalone demo runtime is scheduled for retirement, and still here
Issue #12 made the guided demo an ordinary museum visit driven by ?room=<id>&tour=<id>; the result is recorded in [archive/ROOM-LAUNCH-AND-TOURS.md](archive/ROOM-LAUNCH-AND-TOURS.md). The owner cancelled the earlier plan to relocate the standalone demo to lucasgame-academy (engine mismatch — that site has its own lesson-player), and accepted that the standalone runtime is retired rather than moved.
It has not been retired yet. Still present and still built:
demo/src/main.js
demo/src/shell.js
demo/src/world/{demoWorld,scene,input,readers}.js
Two pieces remain, and both are gated on a human watching the in-museum tour at real frame rates first — nobody has:
- reduce
/demoto a thin shell that forwards into the museum entry; - delete the old runtime modules above.
Do not schedule this from a browser-pane capture. The pane throttles requestAnimationFrame when hidden, so tour pacing measured there is not what a visitor sees.
5. The object library needs content, not code
[OBJECT-LIBRARY.md](OBJECT-LIBRARY.md) is complete end to end — ownership, R2 serving and the GLB prop renderer are all built and the museum draws from a bucket that holds reviewed objects. What it says remains is content:
- more objects;
- the editor control for placing them per room.
The browsing half of that shipped as the object storage building and its item bar ([archive/OBJECT-STORAGE-BUILDING.md](archive/OBJECT-STORAGE-BUILDING.md)); the per-room placement control did not.
A room's door has no art and no editor control
[archive/ROOM-DOOR-AND-TRANSIT.md](archive/ROOM-DOOR-AND-TRANSIT.md) records the ownership rule as accepted by the owner, and the door's own art and its editor control as not built. A centre-out dissolve was tried and rejected the same day; the door stays the plain sheet of light the document describes. Anyone reopening this should read that rejection first.
6. .lpr — one guard gap, and a stale status line
[LPR-FORMAT.md](LPR-FORMAT.md) is the format's own contract and stays a living document. Two things in it are owed.
The recompile tripwire cannot see shader edits
npm run check recompiles every .lpp and compares to the committed .lpr.json byte for byte, so a compiler or registry change that would alter an existing painting fails before it can be committed. That guard is blind to GLSL: editing the shader for photo-nebula-breath@2 leaves every package byte-identical, check stays green, and every painting using that behavior quietly looks different. Today only the two-implementation cross-check tests stand in the way. The document names the fix if the vocabulary grows — a reference-render snapshot per behavior, hashing a small offscreen render — and it has not been built.
The status line is out of date
The document's own header still says "Nothing here is wired into the museum runtime — the inherited renderer still draws every room." That is no longer true: src/inception/artwork/lprSurface.ts, src/inception/artwork/lprMarksSurface.ts, src/inception/lpr/markShader.js and src/inception/world/render/paintScheduler.js are all in the runtime, and the GPU marks path has its own section in [PERFORMANCE.md](PERFORMANCE.md) dated 2026-08-17. The header needs rewriting by someone who can state what v2 still owes.
The corpus it points at, content/live-projects/, does not exist in this repository — the .lpp authoring masters stayed on the other side of the split, and only the compiled content/lpr/*.lpr.json outputs are here. So node scripts/lpp-report.mjs cannot be run from this checkout either.
7. Stale claims worth correcting when someone next edits these files
Small, and none of them affect the running museum — but each one will mislead the next reader.
| Where | What it says | What is true on 2026-08-31 |
|---|---|---|
[UI-REPOSITORY-SCOPE.md](UI-REPOSITORY-SCOPE.md) | Matthew's L-City floor is uploadState: pending, its R2 URL 404s, and it costs 451,541 bytes from the static host per cold visit | The manifest has no pending rows at all; matthew-l-city/floor-v1.jpg carries a real publicUrl, hash and distFile. The bill it describes has been paid |
[DISCOVERY-SEO-GEO.md](DISCOVERY-SEO-GEO.md) | index.html carries noindex and keeps it | The tag is gone — see §1 above |
[LPR-FORMAT.md](LPR-FORMAT.md) | nothing is wired into the museum runtime | It is — see §6 above |
[archive/R2-STATIC-ASSET-MIGRATION-EXECUTION-PLAN.en.md](archive/R2-STATIC-ASSET-MIGRATION-EXECUTION-PLAN.en.md) | Status: ready for implementation | Phases A–F are done; this is why it is in the archive. Phase G belongs to the other repository |
8. A prerequisite, for whenever a preview origin is wanted
Not work, but the thing that is always discovered too late. From [UI-REPOSITORY-SCOPE.md](UI-REPOSITORY-SCOPE.md): a preview origin needs two allowlist additions, not one.
- the API's
ALLOWED_ORIGINS, so connected-mode QA can reach it; - the R2 bucket's CORS allowlist, which today permits
https://is.lucasacademy.org,https://inception-space.onrender.comandhttp://localhost:5173only.
Verified locally: a production build served from an unlisted origin has every R2 asset blocked by CORS, and the transit chamber then fails entry rather than downgrading silently. Both allowlists live in the other repository.