"typecheck" enchaîne etl:typecheck, e2e:typecheck et worker:typecheck : les trois
projets TypeScript que rien d'autre ne compile, et que la CI vérifiait en trois
étapes séparées. C'est le nom que lance le workflow partagé
avalon-vanguard/ci (node.yml).
App et specs n'y sont pas ajoutés : `ng build` vérifie déjà les types de
tsconfig.app.json et `ng test` ceux de tsconfig.spec.json. Vérifié en ajoutant une
erreur de type (TS2322) dans src/app/app.ts puis dans src/app/app.spec.ts : le
build, puis les tests, échouent avec [plugin angular-compiler].
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Corrige les 23 erreurs restantes, sans changer le comportement :
- no-unused-vars (18)
- galaxy-system-scene.component.spec.ts : les 5 faux `links` déclaraient des
paramètres _rangePc, _drawn, _budget seulement pour typer mock.calls ; ils sont
typés par vi.fn<LinkScene['routing']['links']>(), même signature.
- body-detail-scene : `const viewModel = this.viewModel()` jamais lu (lecture de
signal hors contexte réactif, sans effet).
- galaxy-system-scene : `const camera = this.engine.getCamera()` jamais lu dans
swapToSystemSpace et swapToGalaxySpace. getCamera() ne lève que si le moteur
n'est pas initialisé, or ces fonctions ne tournent qu'en rappel de rig.flyTo,
piloté par le tick du moteur qui appelle déjà getCamera() à chaque image.
- grid-plane, star-field-renderer : imports cités seulement dans un {@link} de
JSDoc (SUN_HEIGHT_ABOVE_MIDPLANE_PC, REFERENCE_VIEWPORT_HEIGHT_PX). Les modules
restent importés pour leurs autres exports.
- no-useless-assignment (4) : valeurs initiales jamais lues (u, v, s de gaussian(),
affectés dans le do avant toute lecture ; raw de BookmarksStore.read(), affecté
dans le try dont le catch retourne). Les déclarations gardent leur type.
- no-unused-expressions (1) : `this.display().jumpLinks;` dans l'effect des liens
de saut est une lecture voulue (abonnement au signal) ; écrite
`void this.display().jumpLinks;`, même lecture.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Corrige les 14 erreurs @angular-eslint/prefer-inject (EngineService,
BodyDetailSceneComponent, InfoPanelComponent, GalaxySystemSceneComponent,
SearchComponent). Chaque paramètre devient un champ `inject()` de même nom, même
visibilité (navigationStore reste public : le template le lit) et dans le même
ordre, placé avant les autres champs : les dépendances restent résolues avant tout
autre initialiseur, comme l'étaient les paramètres. Les corps de constructeur
(effects, buildIndex) ne changent pas.
engine.service.spec.ts construisait EngineService avec `new` et un faux NgZone :
il le fait maintenant dans runInInjectionContext, avec le même faux NgZone fourni
par un Injector.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- eslint.config.js reprend l'exemple du README du paquet (préfixe app, comme dans
angular.json), plus les sorties ignorées par .gitignore (out-tsc, rapports
Playwright, cache de l'ETL…) : ESLint ne lit pas .gitignore, et un
playwright-report/index.html local serait sinon lu comme un template Angular.
- devDependencies aux mêmes plages que web-site (main) : eslint ^10.10.0,
@eslint/js ^10.0.1, typescript-eslint ^8.70.0, angular-eslint ^22.5.0. Le paquet
partagé les déclare en pairs optionnels : chaque dépôt qui l'importe les garde.
- Script "lint" : eslint .
`npm run lint` signale 37 erreurs existantes (14 prefer-inject, 18 no-unused-vars,
4 no-useless-assignment, 1 no-unused-expressions), corrigées dans les commits
suivants.
package-lock.json : angular-eslint tire @angular-devkit/core 22.2, qui épingle
picomatch 4.0.7 ; npm remonte donc picomatch 4.0.4 → 4.0.7 (et retire la copie
4.0.5 de vite), @jridgewell/sourcemap-codec 1.5.5 → 1.6.0, et remonte ora.
Vérifié depuis un `npm ci` propre contre main 822c699 : les 33 fichiers du
`ng build` de production ont le même sha256, et prettier --check, les 5 typechecks,
`ng test` (729 tests verts) et `playwright test --list` (17 tests) sont identiques.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- .npmrc : le scope @avalon-vanguard pointe vers le registre npm de Gitea (lecture
anonyme, aucune ligne _authToken).
- devDependency @avalon-vanguard/config ^1.0.0 (résolu depuis git.avalonvanguard.com).
- .prettierrc : "@avalon-vanguard/config/prettier/angular" (mêmes printWidth 100,
singleQuote et parser angular pour *.html ; les défauts de Prettier 3 deviennent
explicites).
- tsconfig.json étend @avalon-vanguard/config/tsconfig/angular.json ; files et
references restent ici. Les sous-configs (app, spec, worker, e2e, etl) étendent
toujours ../tsconfig.json, sans changement.
- .editorconfig : copie du fichier du paquet (seuls trois commentaires s'ajoutent).
Aucun changement de résultat, vérifié depuis un `npm ci` propre de chaque côté
(Node 24.15, rm -rf dist .angular/cache) contre main 822c699 :
- `ng build` de production : les 33 fichiers de dist/ ont le même sha256.
- `prettier --check .` : sortie identique (les mêmes 135 fichiers déjà signalés).
- etl, e2e et worker typecheck, tsc app et spec : propres avant et après ;
`tsc --showConfig` sémantiquement identique pour les 5 projets.
- `ng test` : 729 tests sur 41 fichiers, tous verts des deux côtés.
- `playwright test --list` : identique (17 tests, 8 fichiers).
- node_modules : seul @avalon-vanguard/config s'ajoute.
Note : index.html embarque la CSS de Google Fonts téléchargée au moment du build ;
deux builds de main à quelques minutes d'écart peuvent donc différer sur ce seul
fichier. Les deux builds comparés ici ont été faits à la suite.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
npm 11.12.1 retire de lui-même ces entrées dès la première installation
(`npm install --package-lock-only` sur main donne exactement ce diff) : @babel/*,
istanbul-lib-*, rollup et ses binaires de plateforme, etc. Ce sont des pairs
optionnels (dev + optional + peer) de @angular/build, @angular/compiler-cli et
vitest, que `npm ci` n'installe pas : node_modules est identique avant et après.
Commit à part pour que le diff de l'adoption de @avalon-vanguard/config reste lisible.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
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.
Both sides added a test beside the other in the dock's spec: the panel's own
departure guard here, the give-up wording on main. Both kept, and the offer test
carries the `least` the route answer now has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
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
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
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
`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
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
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
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
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
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
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
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
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
- 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
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
- 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
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
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
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
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
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