Commit Graph
196 Commits
Author SHA1 Message Date
Senrokai cea39c7186 Merge pull request #30 from avalon-vanguard/fix/route-search-budget
Tell a search that gave up from a route that is not there
2026-09-18 19:00:03 +02:00
Senrokai 03b3d3b2d6 Merge pull request #29 from avalon-vanguard/fix/etl-gaia-floor
Refuse a Gaia answer that came back short, and read the body inside the retry
2026-09-18 18:59:28 +02:00
SenrokaiandClaude Opus 5 0c5efec505 Measure the ring span along the plane the rings lie in
The span went to `distanceRings` as the target's straight-line distance from the
Sun, but a ring of radius r passes within |r - p| of the view's centre, where p
is how far out that centre is *along* the galactic plane. For a target above the
plane the two differ by its height, so the band was centred on a radius no ring
has — and `ringLabels` picks its bearing by comparing its own in-plane distance
against the innermost ring, a comparison the new first ring quietly broke.

Two comments and a constant, from the same review. A frame short of the survey
edge gets its callout only when its last ring overshoots it: 245 pc does, 235 pc
does not, which is now a test rather than a sentence. The ring count can reach
16, not 14, now that the span need not start at the Sun. And a ring label was
measured as 135 px of star name when "50 pc" is a third of that, which rejected
rungs a hand's breadth clear of the name: RING_LABEL_REACH_NDC, 0.23, is the
widest of them — "1.5 kpc" with "Survey edge" under it.

Three mutants, three caught. Measured again in the app: unchanged for a star in
the plane (4 labels at 20 pc above it, 7 at 2 pc), and the rings now follow the
plane for one 195 pc above it rather than ringing a place the grid does not
reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 15:57:44 +02:00
SenrokaiandClaude Opus 5 337602f606 Make the budget test spend the budget, and bound the probes that earn nothing
The fixture built for "exactly MAX_VISITED stars reachable" was 338 short: its
random cloud leaves clumps the departure never reaches (the LCG gives 12 212
distinct positions for 39 999 stars), so the search settled 39 662 and the
pre-fix code answered `gaveUp: false` too. The test could not fail on the code
it was written to pin — and the mutant that seemed to prove otherwise was
failing to compile, not failing the test. It is now a line of 40 000 a parsec
apart with the island off the line: settled 40 000 exactly, 115 ms, and the
pre-fix code does report a give-up. Both mutants now compile and are caught.

The give-up cap also has to hold while the bisection has earned nothing: the
exception added for that case had no bound at all, so a search could spend the
resolution's own eight full-budget probes — about 17 s of "Plotting…" — where
two used to cost 4 s. Bounded at five. On the repo's crowded-knot fixture:
1.9 s for the unearned ceiling figure with the old cap, 7.2 s for a range the
bisection earned, and five probes is where that lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 15:51:38 +02:00
SenrokaiandClaude Opus 5 1a5785fb63 Keep the row cap live for the queries that can reach it
Gating it on `jobsQuery` switched it off for every override that *widens* the
query — which is the only way to fill `select top N` at all. `ETL_GAIA_MAGNITUDE_LIMIT=14`
asks for 500 000 rows, the sky holds more, and the answer is the limit rather
than the filters: exactly what the tripwire is for, and it no longer fired. It
now reads the row limit itself, so only a deliberately smaller slice is silent.

Measured with a synthetic answer of exactly 500 000 rows in the cache, under the
key the widened query hashes to: refused. With the `jobsQuery` gate back, the
same run keeps 500 000 Gaia stars and goes on to publish them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 15:46:42 +02:00
SenrokaiandClaude Opus 5 dd56eafba4 Answer the review: hold the offer to the same test as the button
`canPlot()` guarded the Plot button and not `raiseTo`, which is the other way
into `plot()`. So with a departure typed but never chosen, clicking "1.8 pc
would reach." moved the range control and plotted nothing: the panel then read
"No route at this range. 1.8 pc would reach." beside a control already set to
1.8. The offer carries the same `disabled` as the button, since it is the same
request by another route.

And the departure guard is trimmed, as the scene trims the same text before
offering matches for it: one space in the field left it looking empty, with no
suggestions to pick from, and Plot dead for no reason on screen.

Measured in the app, from inside Barnard's Star with Sirius as the destination:
offer enabled with the field empty, disabled once "Sol" is typed and never
chosen — a forced click then moves nothing — and enabled again when the field is
cleared, where it raises the range to 2.40 pc and plots 7 jumps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 15:05:42 +02:00
SenrokaiandClaude Opus 5 7fba48c808 Answer the review: size the rings to the band the frame covers, and clear the text
The rings are centred on the Sun and the frame need not be. Sizing their step
from how far the frame reaches — 210 pc for a star at 190 with the camera 20 pc
back — gives 20 pc rings at 180 and 200, both outside a frame 19 pc deep, so a
view away from the Sun still had no ring on it and no ladder of labels either.
`distanceRings` now takes the span the frame covers rather than its far edge,
and the step is a fifth of that: 5 pc rings from 165 to 210 for the same view.

Measured in the app, centred on a star 187 pc out in the galactic plane: 4 ring
labels drawn 20 pc above the plane and 7 from 2 pc, against 1 and none before.

The clearance was a radius around the anchor, and a label is a line of text
hanging 135 px to one side of its anchor: at 0.065 NDC apart, past the radius,
"50 pc" printed inside "Alpha Centauri". It is now tested against the span the
name occupies, on the side it hangs, with the radius kept for the pair whose
text runs the other way.

Also from the review: the ladder in the clearance test was built at exactly the
constant it tests, so 1057 of 2000 camera poses would have decided it by float
round-trip error — the rungs now sit 0.02 either side of the rule. And two
comments that were wrong: a frame one step short of the survey edge does get its
callout, and CSS2DRenderer hides a label behind the camera rather than drawing
it at the page edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 15:01:05 +02:00
SenrokaiandClaude Opus 5 1a53f26474 Answer the review: say which search gave up, and let the bisection earn its offer
The panel printed "No route at this range." beside the range it was offering —
which is the sentence this branch exists to stop it printing. It was gated on
there being no offer, and a search that gives up usually has one: Sol to
HD 120147 at 4.5 pc spends the budget, offers 4.83 pc, and says there is no
route where a 71-jump route exists. The wording now follows the search at the
range that was asked for, and nothing else.

That needs the two give-ups kept apart, so `least` travels beside `gaveUp` to
the panel: one says the asked range was not searched out, the other that the
search for a range that would work was. HIP 69445 at 3 pc — asked-range search
exhaustive in 44 ms, ceiling probe out of budget — used to read "Too many stars
to search at this range." and now reads "No route at this range.", with nothing
claimed after it.

Two more from the same review. The budget flag was read off the settled count,
so a search that proved a dead end with the last star it was allowed reported a
give-up; it now records why the loop stopped. And the bisection's cap could fire
before a single probe had narrowed anything, leaving the ceiling route's own
longest hop as the answer: star 1000115173 at 3 pc was told to go to 8.00 pc,
the control's maximum, for a crossing that works at 6. It now offers 6.93.

Measured in the app, all three: "Too many stars to search at this range.
4.90 pc would reach.", "No route at this range." alone, and 7.00 pc in place of
8.00. The duplicated dead-end test now asks the question it was named for —
exactly the budget's worth of stars reaching each other and none of them the
destination — and each fix kills its own mutant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 14:46:47 +02:00
SenrokaiandClaude Opus 5 b7f277ea04 Answer the review: a short answer makes more survivors, and must not be skipped
Three things this got wrong. The direction: truncating Gaia leaves the HYG rows
whose counterpart it dropped without one, so survivors rise — 10 886 today,
12 711 at half the rows, 16 258 at a third — which the comment claimed was the
other way, and which decides whether the 15 000 ceiling can be leaned on at all
(it catches a truncation past about two thirds, and nothing shallower).

The throw: `fetchStars` catches everything a source throws and skips it, so a
truncated CSV was reported as "the archive was unreachable" one step after
`writeStarAssets` had already overwritten the published catalogue. Marked with
`GaiaAnswerError` and rethrown there, so an answer that cannot be worked with
fails the run where it happened. Measured end to end in a throwaway working
directory, 300 000 rows in the cache: fails, names the cache file to delete,
assets untouched. With the rethrow taken back out again: assets written, then
"the archive was unreachable".

The row limit: `rows.length >= ROW_LIMIT` is true for every reduced
ETL_GAIA_ROW_LIMIT, so the tripwire fired on exactly the deliberate slice the
override exists for — and told the operator to raise it. Gated on the same flag
as its neighbour. `ETL_GAIA_ROW_LIMIT=20000` now runs through; without the gate
it dies on the limit it was given.

Also: the row floor names the one cache file it is about rather than a glob that
takes the Hipparcos cross-match with it, and says an edited query is a third
reason it can fire — DEFAULT_QUERY_ROWS now sits under the query it counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 14:35:17 +02:00
SenrokaiandClaude Opus 5 7ab92e61a1 Give the budget tests room and cells to run in
Both make a search spend its whole 40 000-star budget, twice over in the bisection, and the
CI runner timed out at the default five seconds. The crowds are now indexed in cells sized for the
ranges asked of them, as the real catalogue is, and the two tests carry their own 30 s timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 13:50:47 +02:00
SenrokaiandClaude Opus 5 365252f534 Let the Routes panel be clicked as soon as it is back, and stop it departing from elsewhere
From the review of #24. Two defects, both in a real browser.

The panel keeps its entries across a trip to another tab, but still replayed the acquire wipe on
the way back, and for the 380 ms that runs, its clip path swallows clicks: type "Siri", leave for
Readout, come back and click the Sirius suggestion, and the click lands on the star field behind
it — measured, the element under the pointer is the canvas, and the field stays "Siri". The wipe is
gone from this one panel: it is not acquiring anything it did not already have.

The departure field fell back to the star the view is in whenever nothing had been chosen, text in
the field or not. So a field reading "Sol" that was never resolved plotted from Barnard's Star:
"1 Barnard's Star, 2 Sol, 3 Sirius", the panel naming one departure and the route leaving from
another. Text nobody chose is no longer a departure, and the button waits until it is one or the
field is empty again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 13:42:11 +02:00
SenrokaiandClaude Opus 5 5dec528cee Size the distance rings by what the frame reaches, and keep their labels off the star names
From the review of #19. The rings are distances from the Sun, but their step was taken from
`effectiveDistance`, which under the plan view means the extent of the frame rather than how far
the camera is from the Sun. Centred on a star 200 pc out and flipped to 2D, the grid became rings
of 2 to 20 pc: not one of them on screen. The step now comes from where the view is centred plus
how far the camera is orbiting it, which is the same distance under either projection.

The set was also rebuilt while the grid was hidden, and every rebuild disposes the rings and
builds every vertex again; it now happens only while the grid is drawn.

The ring labels went straight to the overlay: never culled to the frame, and free to land on a
star's name. They now have to be on screen and clear of the names already placed, by half the
separation two names keep — they are a ladder up one ray a twentieth of the screen apart, and
holding them apart from each other would take "Survey edge" off the map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 13:24:16 +02:00
SenrokaiandClaude Opus 5 7971ec4007 Simplify: without a give-up the bisection can only have closed on the resolution
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 12:56:51 +02:00
SenrokaiandClaude Opus 5 862fb65ea4 Tell a search that gave up from a route that is not there
The route search stops after MAX_VISITED stars and returned null, which everything downstream read
as "the catalogue holds no chain". On the real catalogue that was wrong for real questions: Sol to
HD 120147 (136 pc) at 5 pc is 50 jumps, and the panel said there was no route. The budget also sat
under what the shipped catalogue needs, so it is now 40 000 rather than 20 000: both that route and
a star at 170 pc are found, and Sol to HD 2626 at 6 pc, which used to be refused after 4.7 s, plots
56 jumps in about 2 s.

A search now reports whether it gave up. The range search no longer counts a give-up as proof that
nothing routes below it — that is what reported ranges up to 29% too wide — and it stops after two
of them, since those are the probes that cost the most and settle the least: for HD 2626 at 3 pc it
offers 5.92 pc in about 4 s, against 6.13 pc in 4.7 s. At the panel's widest range the refused route
and the range search are the same question, so it is asked once. Where nothing can be said, the
panel says "Too many stars to search at this range." rather than claiming there is no route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 12:49:18 +02:00
SenrokaiandClaude Opus 5 44f6a8d086 Refuse a Gaia answer that came back short, and read the body inside the retry
The merge gate asks whether Gaia contributed any stars, never how many. The TAP service truncates
on its own timeout and still serves a well-formed CSV with a 200, ordered by magnitude — so a half
answer is the bright half, which is the half HYG overlaps. Every gate passes: Gaia stars are
present, HYG survivors go down rather than up, unmerged twins can only fall. The weekly job would
publish a catalogue missing two hundred thousand stars and the runner would cache it for the weeks
after. `fetchGaiaStars` now refuses fewer than 95% of the 412 765 rows its query holds, as its
sibling query already did, and refuses an answer that fills the row limit.

`fetchText` retried the request but not the body: a connection reset part-way through the 57 MB
CSV rejected out of the loop, with no wait and no second attempt. The read now happens inside it.

Also corrected: the merge gate's account of the HYG survivors (two thirds of them are stars Gaia
measures but the main query never downloads, since Gaia puts them past the 250 pc cutoff), and the
refresh workflow's comment on what happens when the archive is unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 12:12:49 +02:00
SenrokaiandClaude Opus 5 b4017fcafc Merge pull request #28 from avalon-vanguard/perf/link-budget
Budget the jump-link layer in pixels of line, nearest the view's centre first

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-18 12:07:20 +02:00
SenrokaiandClaude Opus 5 539a0f0a3e Answer the review: budget in drawn pixels, re-ask when the budget moves, sort only the band it runs out in
- The budget counted CSS pixels; lines are drawn in device pixels, so a screen scaled to 150% or
  200% drew 1.5-2x the calibrated line. It now counts the canvas's drawn pixels.
- A graph was re-asked only when the drawn stars changed, so with a star budget covering the whole
  catalogue, or a resize, its budget and centre stayed wherever the layer was turned on. A view that
  chose its stars again now asks, and a graph is rebuilt when the stars, the range or the budget
  changed (the budget by more than half the margin, or its centre by more than 5 pc).
- From inside a system the budget was worked out in astronomical units about the system's origin.
  Graphs are now asked for in parsec space only; the flight back out asks.
- Comparing budgets let a request re-asked with a slightly different one supersede its twin, and the
  twin's rejection cleared the state of the request that replaced it. A rejection now clears it only
  for the latest request.
- The worker sorted every link to keep a few thousand, 2.2x an unbudgeted build. It now bands links
  by distance, keeps every band before the one the budget runs out in, and sorts only that one:
  142-168 ms on the real catalogue against 233-388 ms, 103 ms unbudgeted, returning early when all fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 19:03:11 +02:00
SenrokaiandClaude Opus 5 bd5e9d4b0d Budget the jump-link layer in pixels of line, nearest the view's centre first
With the drawn set following the view, the layer at 8 pc cost the integrated Radeon 503 ms a frame
at 30 pc from the Sun. Measured, the cost follows the length of line on screen (about 10 ms per
million pixels near the Sun), not the number of links: 100 000 links were 12 ms at the opening
view, 25 000 were 61 ms at 30 pc. So the budget is a length: a million pixels, turned into parsecs
at the depth the view is centred on, spent on the links nearest that centre by their nearer end.
Orbiting with links at 8 pc on the iGPU: 12-18 ms p50 at every pose measured, no long tasks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 18:23:55 +02:00
SenrokaiandClaude Opus 5 45d5209433 Merge pull request #27 from avalon-vanguard/feat/drawn-set-in-view
Draw what the camera shows: the budget goes to the stars in view

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 18:06:12 +02:00
SenrokaiandClaude Opus 5 977a4d8a92 Merge pull request #26 from avalon-vanguard/perf/drawn-set-one-walk
Choose the drawn stars in one walk of the brightness order

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 18:06:00 +02:00
SenrokaiandClaude Opus 5 3026cbde5f Merge pull request #25 from avalon-vanguard/perf/link-drawn-stars
Link only the stars that are drawn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 18:05:48 +02:00
SenrokaiandClaude Opus 5 c65a02e9bc Merge pull request #24 from avalon-vanguard/fix/routes-keep-input
Keep the Routes panel's entries across a trip to another tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 18:05:36 +02:00
SenrokaiandClaude Opus 5 9631ddf0a4 Answer the review: keep up with flights frame by frame, respect portrait frames, and let links follow an orbit
- Flights: the drawn stars were checked once a label pass, and a flight outruns that. Leaving a
  system jumps the camera to face another way, then zooms out forty-fold in a second: 74-83% of
  the stars that belong on screen were missing on the first frames back in parsec space, 33-50%
  before each re-choice on the way out. While the rig animates, the check now runs every frame.
  Probe on real flights (Gl 806, Barnard's Star, out): 0.90-1.00 of a fresh choice drawn on
  screen in flight, 1.00 on the frame of the jump; frame p95 12.2 ms, no long tasks. Choosing for
  the whole sky during flights was tried first and measured worse (0.15-0.18).
- Portrait frames: the turn and pan limits use the narrower half-extent, not the height.
- Links: a new drawn set arms the rebuild timer only when none is pending, so a continuous orbit
  gets a graph at most every 250 ms instead of never; an unchanged set asks for nothing.
- The centre-move test lets the first pass happen before moving the centre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 17:57:29 +02:00
Senrokai 9ad0815acd Merge branch 'perf/drawn-set-one-walk' into feat/drawn-set-in-view 2026-09-17 17:35:24 +02:00
Senrokai a769d70015 Merge branch 'perf/link-drawn-stars' into perf/drawn-set-one-walk 2026-09-17 17:35:21 +02:00
SenrokaiandClaude Opus 5 52c5d3b144 Answer the review: share a graph request asked again, by its range and its drawn list
Graph requests were never shared, on the grounds that the scene never asks for the same graph
twice. It does: turning the layer off and on while the worker is busy asks again for the graph
already waiting. The new request superseded the old one, and the old one's rejection handler,
which finds its request by range and drawn list, wiped the state of the new one: the layer stayed
on with no graph. An identical request now shares the outstanding promise, a graph being the same
when its range matches and its drawn list is the very same array.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 17:35:18 +02:00
SenrokaiandClaude Opus 5 6cd0666067 Draw what the camera shows: the budget goes to the stars in view
The drawn set was two spheres, around the view's centre and around the Sun, then the brightest
stars anywhere, so most of the budget sat behind or beside the camera: at 30 pc from the Sun
15.8% of the drawn stars were on screen, at 5 pc 9.1%, in a plan view zoomed to 10 pc 3.8%.

The same tiers are now taken only from the camera's frame, widened by a quarter
(VIEW_MARGIN), with the planet hosts in view drawn first after the pinned stars, so every ring
circles a star that can be clicked. The set is chosen again at the label cadence once the view
has turned, zoomed or moved half the margin, switched projection or been resized, and once for
the whole sky on the way out to the Galaxy. Drawn stars on screen: 74-76% at 30 pc, 73-75% at
5 pc, 66% in the zoomed plan view, 91.5% at the opening view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 17:04:08 +02:00
SenrokaiandClaude Opus 5 7064e34d02 Choose the drawn stars in one walk of the brightness order
Same stars in the same order, for less: one walk of the brightness index, reading positions laid
out in that order, sorts the view's neighbourhood, the Sun's and the rest as it goes, instead of
gathering both neighbourhoods in catalogue order and sorting them. A refocus in the page drops
from 11.3 ms to 4.6 ms (median; worst 19.1 to 7.1). This comes before the drawn set follows the
camera's turns, which makes refocusing far more frequent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 16:22:50 +02:00
SenrokaiandClaude Opus 5 c6206a8311 Link only the stars that are drawn
The jump-link graph linked the whole catalogue: 3.7 million links at 8 pc, 7.4-7.8 s in the
worker and a 443-515 ms frame on the main thread when they landed, and most of them between stars
that were neither drawn nor clickable. A graph request now carries the star field's drawn stars,
and the worker links only those, over an index of its own with cells as wide as the range. The
scene asks again once a new drawn set has held still for 250 ms.

The renderer is handed the graph's bounding sphere instead of computing it: three.js walked every
vertex on the main thread in the first frame that drew a new graph, 48-55 ms at 8 pc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 16:16:38 +02:00
SenrokaiandClaude Opus 5 c77d3ed1d9 Keep the Routes panel's entries across a trip to another tab
The panel was unmounted with its tab, so leaving it reset departure, destination and range. The
range reset was also a lie: the slider came back at 3 pc while the scene kept drawing the graph at
the range last chosen. The panel now stays mounted and is hidden while another tab is open, which
still replays the acquire wipe when it is shown again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:21:41 +02:00
SenrokaiandClaude Opus 5 96348161cc Merge pull request #23 from avalon-vanguard/star-map/perf/routing-worker
Plot routes and build the jump-link graph in a Web Worker

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:05:51 +02:00
SenrokaiandClaude Opus 5 962cb3f6bc Merge pull request #22 from avalon-vanguard/star-map/feat/drawn-set-follows-view
Draw the stars around wherever the view is, not only around the Sun

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:05:38 +02:00
SenrokaiandClaude Opus 5 e986f6bebf Merge pull request #21 from avalon-vanguard/star-map/perf/label-scan
Name the brightest stars by walking one order, instead of sorting 60 000 five times a second

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:05:27 +02:00
SenrokaiandClaude Opus 5 7efcd2e93a Merge pull request #20 from avalon-vanguard/star-map/feat/fast-routes
Route with A* over numeric cell keys, so a route can reach past the Sun's crowd

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:05:15 +02:00
SenrokaiandClaude Opus 5 e1c806d05b Merge pull request #19 from avalon-vanguard/star-map/feat/scale-tools
Give the map a scale bar, and rings that say how far from the Sun

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:05:01 +02:00
SenrokaiandClaude Opus 5 58656a5e34 Merge pull request #18 from avalon-vanguard/star-map/fix/hyg-gaia-distances
Draw each HYG star at Gaia's distance, and keep the ones Hipparcos misplaced

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:04:49 +02:00
SenrokaiandClaude Opus 5 f7a482b06a Merge pull request #17 from avalon-vanguard/star-map/guard/merge-quality
Fail the ETL on a merge that keeps the same star twice

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:04:37 +02:00
SenrokaiandClaude Opus 5 011ebe1356 Merge pull request #16 from avalon-vanguard/star-map/fix/host-sky-match
Match exoplanet hosts on the sky, at both epochs the archive might mean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:04:25 +02:00
SenrokaiandClaude Opus 5 fd5a24ce72 Merge pull request #15 from avalon-vanguard/star-map/fix/merge-epochs
Bring Gaia to HYG's epoch before merging, and keep a star's name when it matches

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-17 15:04:12 +02:00
SenrokaiandClaude Opus 5 f156e03822 Answer the review: send the worker one request at a time, keep only the latest, and never wait for a dead one
The adversarial review confirmed three defects in this PR, all reproduced in
the browser.

1. Superseded graphs queued up in front of routes. The worker answers
   messages one at a time and cannot drop one it has started. With the
   jump-link layer on, every pause on the range slider posted a full graph
   build, seconds of work at 6-8 pc. Answers no longer wanted were thrown
   away only once built. A route asked for afterwards waited behind every
   one of them: a one-jump route took 44 s.

   RoutingClient now holds requests and sends them one at a time. While one
   is out, only the latest of each kind waits: a newer graph replaces an
   older one before it is ever built, and the older promise is rejected with
   SupersededRequest. Routes go ahead of graphs. The same question asked
   again while outstanding shares the answer rather than being worked twice,
   as when the layer is turned off and on during a build.

   The same scenario in the browser (layer on, range stepped 5 -> 8 pc with
   400 ms pauses, then Sol to Proxima): the route came back in 110 ms. The
   worker was sent "links 3, links 5, route, links 8"; 6 and 7 were never
   built.

2. A worker that failed left the panel stuck. With no error handling, a
   worker that failed to load (a 404 on its chunk after a redeploy) or
   threw left "Plotting…" and a disabled button for good, and a graph at a
   range could not be asked for again.

   The worker now answers an exception with a 'failed' message, which
   rejects that request. A worker that fails to load or dies is abandoned,
   and what it left outstanding, and everything asked afterwards, is
   answered in place. The scene releases the panel when a route fails, and
   forgets a graph range that was never drawn so it can be asked for again.

3. Nothing type-checked the worker. The application builder never reads
   webWorkerTsConfig, and bundles the worker with esbuild, which strips
   types without checking them. tsconfig.app.json leaves the file out. A
   type error in the worker shipped.

   `npm run worker:typecheck` (tsc -p tsconfig.worker.json) now runs in CI
   beside the other project checks. webWorkerTsConfig is removed from
   angular.json, since it only suggested that something checked the worker.

Tests with a fake worker cover one request at a time, a waiting graph
replaced and a route sent ahead of it, a question shared, a failure rejected
and the next request sent, and a failed worker's requests answered in place.
A scene test covers the panel released after a failed route. Negative
controls, each caught: several requests sent at once, a waiting graph kept,
graphs ahead of routes, a question asked twice, a failure answered as a
success, a failed worker waited on, the panel left pending, and a type error
in the worker (caught by worker:typecheck).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 15:27:50 +02:00
SenrokaiandClaude Opus 5 965739e99e Merge branch 'feat/drawn-set-follows-view' into perf/routing-worker
The label and star-field review fixes arrive under the routing client: the
scene keeps constructing RoutingClient beside the neighbourhood, and builds
the brightness index where it built the order. Both sides' new scene tests
are kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 15:14:13 +02:00
SenrokaiandClaude Opus 5 86e143131e Answer the review: pin by the index the neighbourhood holds, and choose again only when it can matter
The adversarial review confirmed three costs this PR added, all reproduced in
the browser.

- The first pinned refocus stalled the first flight of a session. The
  renderer built its own id-to-index Map of 423 651 entries the first time a
  star was pinned, which is at the first selection, inside the approach
  flight. The worst frame was 47-103 ms, and the Map stayed as a second copy
  of a lookup the scene already had. The scene now pins by catalogue index,
  through the StarNeighbourhood it builds at load (new `indexOf`), and the
  renderer takes indices. First selection, measured in the browser: worst
  frame 18 ms.

- At galactic scale every label pass rewrote the drawn set. The view centre
  sweeps hundreds of parsecs a pass there, far past any star, so each pass
  chose the same 70 000 stars again and uploaded 2 MB to the GPU: 11 times
  on the flight out to the Galaxy. The scene no longer refocuses at galactic
  scale, where the whole catalogue is a few pixels, and the renderer leaves
  its buffers alone when the drawn set is unchanged. Flight to the Galaxy:
  2 refocuses, no frame over 50 ms.

- At load the same set was chosen twice: once by the renderer's constructor
  around the Sun, and again by the first label pass, centred on the Sun. The
  scene now records the constructor's choice as the current focus.

Tests: the buffers keep their version for an unchanged set, no refocus at
load, none at galactic scale, and pins arrive as indices. Proxima's id in the
scene spec now differs from its index, so a lookup by id cannot pass for one
by index. Negative controls, each caught: an unchanged set rewritten anyway, a
refocus at galactic scale, the boot choice not recorded, and pins passed as
ids.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 15:12:46 +02:00
SenrokaiandClaude Opus 5 c3fcb2e481 Merge branch 'perf/label-scan' into feat/drawn-set-follows-view
The label fix turns the brightness order into an index with positions and ids
laid out beside it. The star field only needs the order, so it is handed
`.order`. Both sides added scene tests in the same place; both are kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 15:11:29 +02:00
SenrokaiandClaude Opus 5 b071d87d8a Answer the review: walk the brightness order in memory order, and stop at the fifteenth label
The adversarial review confirmed a regression in this PR. Near the Sun, the
label pass became three to four times slower than the scan and sort it
replaced.

Within about 11 pc of the Sun, and in any plan view zoomed tighter than that,
the label radius clamps to 4 pc. That sphere holds a few dozen faint dwarfs
deep in the brightness order, so the walk rarely finds fifteen stars to name
and reads nearly the whole catalogue. Reading the star objects in brightness
order jumps all over memory, so a full walk took 19-25 ms against the old
5-6 ms.

The review also found that spreadLabels checked the label count at the top of
its loop. After placing the fifteenth label it asked for a sixteenth
candidate, which near the Sun can lie at the far end of the order.

brightnessIndex now lays each star's position and id out beside the
brightness order, in that order. The walk tests stars from those arrays in
sequence and reads a star object only when it yields one. spreadLabels breaks
straight after placing the fifteenth label.

Measured on the real catalogue with the label logic reduced to what decides
placement, camera at the given distance from the Sun (old sort / this PR as
first pushed / now):
  2 pc    4.9 / 24.7 / 2.5 ms
  5 pc    5.7 / 23.0 / 3.1 ms
  10 pc   6.3 / 18.2 / 0.95 ms
  307 pc  22  / 0.01 / 0.00 ms (the opening view)
The labels are identical in every case. Now faster than the old sort at every
distance.

A new scene test counts the candidates spreadLabels takes: exactly fifteen for
fifteen labels. Negative controls, each caught: positions one axis off, ids in
catalogue order, the selected star dropped, the radius edge excluded, and the
count checked before taking a candidate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 15:04:51 +02:00
SenrokaiandClaude Opus 5 7f8fb59f5d Merge main: another scheduled refresh ran with the pre-fix pipeline, keep this branch's data
The 2026-09-14 refresh (7f187e0) regenerated exoplanets.json on main with the
host matching this stack replaces, so the file conflicted again. Resolved by
keeping this branch's, for the same reason as last time: it is the output of
the reviewed pipeline, and what main's side adds is only archive rows
published since, which the next refresh re-fetches with the fixed code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 14:23:11 +02:00
SenrokaiandClaude Opus 5 8c69a7a8b2 Plot routes and build the jump-link graph in a Web Worker
Route plotting ran on the main thread, and so did the jump-link graph:

- the range search for a far target, HD 2626 at 236 pc, takes 4-5 s;
- the graph at 8 pc is 3.7 million links, 6-10 s to build, then as many
  link objects again to turn into vertices.

The map stopped for as long as either ran.

A Web Worker now does both. RoutingClient sends it the catalogue's ids and
positions once, and it keeps its own spatial index. A route question comes
back with the route, or with the range that would open one. A graph comes
back as one Float32Array of segment vertices, transferred rather than
copied.

On the scene side, only the latest route request is shown: an earlier
answer arriving later is dropped. Only the graph for the range last asked
for is drawn. The Routes panel says "Plotting…" and holds its button while
a request is out.

collectJumpLinks gave way to jumpLinkSegments, which writes the vertex pairs
straight into floats rather than building link objects first; the scene was
its only caller. The routing module (routing.ts) is the message protocol and
the one function answering it, so the worker is a dozen lines, and the same
answers are worked out in place where there is no Worker, as in the unit
tests' DOM. The worker is built with its own tsconfig, as the Angular
builder expects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 14:17:26 +02:00
SenrokaiandClaude Opus 5 2d997e41db Draw the stars around wherever the view is, not only around the Sun
The star field draws a budget of the catalogue: everything within 25 pc of
the Sun, then the brightest of the rest. That choice was made once, at load,
around the Sun, and never again. On the Gaia catalogue it left most of the
map empty wherever the view went:

- a region 150 pc out drew 49 of the 442 stars within 25 pc of it;
- a plotted route ran through stars no one could see or click. Sol to
  Almach at 8 pc passes 19 stars and drew 6, Sol to Mirfak 11 of 26;
- a search for a faint star flew the camera to an empty point.

The drawn set now follows the view. The scene chooses it again at the label
cadence, once the orbit target has moved more than 5 pc or the pinned stars
have changed. The budget goes, in order, to the selected star and the stars
of a plotted route, then everything within 25 pc of where the view is
centred, then the same around the Sun, then the brightest of the rest. The
instance buffers hold the budget and are rewritten in place.

Checked in Chromium on WebGPU, framing Mirfak from 12 pc: with the set
chosen around the Sun, 122 of the 649 stars within 25 pc were drawn;
following the view, all 649. At the opening view the drawn set is the same
as before.

A refocus takes 9 ms in the browser (5 ms of it choosing). The first version took
16-36 ms in the browser, a visible hitch during a flight. Most of that time
went on walking the 423 651-star brightness order once per neighbourhood,
out of catalogue order, and on recomputing 70 000 colours. Now both
neighbourhoods are gathered in one pass in catalogue order and sorted on
their own, and colours and sizes are computed once for the whole catalogue.
The brightness order itself sorts a typed copy of the magnitudes, taking
83 ms at load instead of 104-139 ms.

STAR_RENDER_BUDGET is now 70 000, and its comment gives the measurements
behind it rather than "currently set to the whole catalogue", which stopped
being true when Gaia landed. At 1920 x 1080 on a Ryzen 7700X:

- on the RTX 4080, the whole catalogue costs the same 6.1 ms a frame as the
  budget;
- on the processor's two-core Radeon, standing in for an entry-level laptop,
  every 100 000 stars costs about 4 ms: 112 fps at the budget, 44 at the
  whole catalogue, and the same under WebGL2;
- drawn whole, the opening view turns into a grey wash that buries the
  labels and the host rings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 14:11:42 +02:00
SenrokaiandClaude Opus 5 0a0b301807 Name the brightest stars by walking one order, instead of sorting 60 000 five times a second
The star labels are refreshed every 0.2 s. Each pass filtered the whole
catalogue to the stars within the label radius, sorted them by magnitude,
and turned every one into a label object, all to place at most fifteen.
At the opening view the radius holds about 60 000 stars, so each pass was a
55-70 ms task on the main thread. A CPU profile of the opening view, on a
Ryzen 7700X with an RTX 4080, counted 29 tasks over 50 ms in 6.7 s, one
every 230 ms; updateLabels took 23% of the main thread. That is the stutter
the frame-time bench measured on every GPU and every render budget.

The catalogue is now sorted by brightness once, when it loads.
brightestWithin walks that order and hands stars over lazily, and
spreadLabels already stopped once it had placed fifteen labels, so a pass
reads only the stars it looks at. The output is the same as before: the
same stars, in the same order, with ties in catalogue order, the selected
star named wherever it is, and a star exactly on the radius included. The
spec checks it against the filter-then-sort it replaces. Stars are no longer
scanned at all when the view is at galactic scale, where the result was
thrown away.

Profiled again on the same view: 0 tasks over 50 ms, and the scene's
per-frame work over the window dropped from 2 028 ms to 342 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-16 13:52:51 +02:00
github-actions[bot] 7f187e000e Refresh the astronomical catalogues
Scheduled re-run of the ETL against the live archives. Gated on the unit suite and a production build in this same run, because a GITHUB_TOKEN push triggers no CI of its own.
2026-09-14 10:54:47 +00:00
SenrokaiandClaude Opus 5 efe6667b00 Route with A* over numeric cell keys, so a route can reach past the Sun's crowd
The route search widened evenly from the departure, Dijkstra-style, with a
budget of 20 000 stars. On the Gaia catalogue those are all within about
40 pc of the Sun, so it found no route to anything farther at any range:
Sol to Mirfak (155 pc) failed at 3, 8, 15 and 30 pc alike. Every failure
then asked minimumRangeBetween what range would work. That search widened
the same way with a 30 pc ceiling, and it ran for up to a minute on the
main thread before giving up with nothing.

routeBetween is now an A* search. Each star is queued by the distance
travelled to it plus the straight line on to the destination, on a binary
heap rather than a linear scan of the frontier. It heads for the
destination instead of flooding the core around the departure.

minimumRangeBetween bisects the range, one routeBetween per step, because
whether a chain exists can only become truer as the range grows. Its
answer is always the longest hop of a route actually found, so a range it
names always opens one. Its ceiling is now the Routes panel's own
maximum, MAX_JUMP_RANGE_PC: a range the control cannot be set to is no
answer, and raiseTo already clamped any figure above it.

The spatial index keys its cells by one number packed from their three
indices instead of an "ix,iy,iz" string. A search visits up to 125 cells
for every star it expands, and building those strings was half of what a
route cost. forEachWithin hands neighbours over unsorted and uncollected,
which was most of the other half; within is now that, gathered and sorted.

The no-route line said nothing in the catalogue bridged the gap; it now
says no chain of jumps up to the panel's maximum reaches the star, which
is what was searched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-11 19:42:57 +02:00