Commit Graph
20 Commits
Author SHA1 Message Date
SenrokaiandClaude Opus 5.5 dadae7b8ca Give Haumea's card its three semi-axes beside its mean radius
Haumea is triaxial, 1161 x 852 x 513 km (Ortiz et al. 2017, Nature 550, 219), and is drawn as the
sphere of its volume, 797.6 km. Its card listed "Radius 798 km" under Measured and said nothing of
its shape, which only a code comment and a commit message gave: its long semi-axis is 1.46 times
that radius and its short one 0.64.

BodyRecord takes semiAxesKm, set by the ETL from Haumea's spec, and the card then reads "Mean
radius 798 km" and "Semi-axes 1,161 x 852 x 513 km". Every other body keeps its one Radius row. The
ETL checks that a body's radius is the mean of its semi-axes, the radius of the sphere of the same
volume, to 0.1 per cent (797.6 against 797.62).

Measured on :4301, Haumea's page: "Mean radius 798 km, Semi-axes 1,161 x 852 x 513 km". Test: the
card of Haumea as shipped in bodies.json. Guarded mutants: the semi-axes dropped from the spec (run
through the solar ETL), the row, or the view model, or the label left as Radius, each fail it and
only it; a radius that is not their mean fails the ETL ("Haumea's radius, 1161 km, is not the mean
of its semi-axes").

Horizons gives triaxial radii for Phobos, Deimos, Miranda and Ariel too, which the page parser
already reads into their mean; they are not carried here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:12:30 +02:00
SenrokaiandClaude Opus 5.5 fc7677e715 Turn Nereid in the 11.594 hours Kepler measured, where it was drawn still
Nereid's Horizons page states no spin, and the ETL, finding none, left it still; the validator's
comment read that as "Nereid has no spin". Its rotation is measured: Kepler's K2 light curve gives
11.594 +/- 0.017 hours, confirming earlier ground-based periods (Kiss et al. 2016, MNRAS 457, 2908;
arXiv:1601.02395). Its spec now carries that day, as Eris's carries Bernstein et al.'s, and with no
known pole it turns about its orbit normal, as Eris, Haumea and Makemake do. The free-spinner check
accepts it (11.594 hours against a 360-day orbit).

A new validator: a moon without a lock must have a day unless it tumbles, and only Hyperion
("Rotational period = Chaotic") does. Nereid, left without one, fails it: "Moon nereid is drawn not
turning, and is not known to tumble". The renderer spec's example of a body left still was Titan,
said to have no period on Horizons, though it carries its orbit's; it is Hyperion now, and
BodyRecord.rotationPeriodHours says where each kind of period comes from.

Measured: the solar ETL passes; in bodies.json only Hyperion has no rotationPeriodHours; live on
:4301 Nereid's marker turns 60.000 degrees in a sixth of its day. Test: Nereid, as shipped, turns 60
degrees in 1.93 hours. Guarded mutant, the day removed from its spec and run through the solar
ETL: the validator fails, and the suite on the data it wrote fails that test and only it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:02:30 +02:00
SenrokaiandClaude Opus 5.5 3b4fd1af6c Turn each locked moon at its orbit's rate and Iapetus's pole round its orbit, so they face their planets at every date the clock reaches
The IAU gives a locked moon's W the mean motion of whichever orbit its authors had, and JPL's table
has another. Near the present the difference is nothing; over the clock's AD 1 to 3000 it turned
Proteus's far side to Neptune at AD 1 (146 degrees), Mimas 52 degrees from Saturn and Miranda 23.
Iapetus was worse for another reason: its IAU pole is a straight line, 3.9 degrees a century in
right ascension, through its orbit normal's 3 439-year circle round the Laplace pole, which by AD 1
has run past the celestial pole (Dec 97.9), 11 degrees off the orbit, with the face 87 degrees
from Saturn. Mimas and Iapetus carry mission maps, so a wrong hemisphere was drawn facing Saturn.
The ETL's lock check sampled only 1950-2100, so none of it failed.

tools/etl/lib/locked-spin.ts, lockedToOrbit, called for every locked moon:
- W's rate becomes the orbit's own mean motion, its constant moved so W is unchanged on
  2025-01-01; the pole and every periodic term stay the IAU's. A W with a quadratic is left
  (Phobos's orbit already takes it; the Moon's is its tidal slowing, 0.75 degrees at AD 1). The
  kernel's rate must be within 1e-5 of the orbit's first (at most 3.4e-6, Iapetus).
- Iapetus (poleFollowsOrbit): the pole follows its orbit normal, as a moon in a Cassini state does,
  in the IAU's own form: sines of the node's angle and four harmonics on right ascension, cosines
  on declination, fitted to the normal's circle and pinned to the IAU pole at the present; W takes
  sines of the same angles, fitted to hold the face where it is today.

build.ts samples the lock over AD 1 to 3000 (8 114 dates, every 135 days) instead of 1950-2100, and
subPlanetLongitudeDeg moved to the lib, shared by both. Measured on the real catalogue: at most
5.36 degrees (Titan) but the Moon 7.62 (its eccentricity, and W's quadratic at AD 1: a new named
ceiling of 8), Mimas 8.94 (ceiling 11 -> 9.5) and Iapetus 15.95 (19 -> 16.5, 9.4 of it its row's
lag); Proteus's own ceiling of 9 is gone, at 2.66. Iapetus's axis stays within 0.74 degrees of its
orbit normal (11.06 before) and its pole is the IAU's at the present to 1e-4 degrees.

Live on :4301, the longitude facing the planet at AD 1 / 1000 / 2025 / 2999: Proteus 2.6 / 2.6 /
2.6 / 2.6 (was -146.5 / -72.9 / 2.6 / 74.5), Iapetus -15.9 / -15.4 / -15.3 / -9.3 (-87.0 / -50.9 /
-15.3 / 22.8), Mimas 4.1 / 6.8 / 5.7 / 8.4 (48.6 / 29.3 / 5.7 / -13.0), Miranda -0.1 / 2.2 / 0.0 /
1.4 (-23.4 / -9.6 / 0.0 / 12.6); the present is unchanged.

The renderer spec now takes the rotational elements from bodies.json too, so no hand copy is left,
and a new test turns Proteus, Miranda, Mimas and Iapetus to their planets at AD 1 and AD 3000.
Guarded mutants, each run through the solar ETL and then the full suite on what it wrote:
lockedToOrbit bypassed (validator: Mimas 52.30, ceiling 9.5; the new test fails), Iapetus on the
IAU's straight pole (98.48), and its pole round the orbit without W's terms (73.13); each fails the
new test and only it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:58:04 +02:00
SenrokaiandClaude Opus 5.5 48fd6fd0e5 Stop telling the reader that the hundred directly imaged exoplanets were never imaged
Every exoplanet card without a map ended "Not an observation — no image of this world exists.",
and 395b613 wrote that rule into a comment ("Only an exoplanet has never been imaged") and a test.
The NASA Exoplanet Archive flags 102 planets as detected by imaging (ima_flag), all 102 of them in
exoplanets.json: HR 8799 b to e (Marois et al. 2008), bet Pic b, 51 Eri b, AF Lep b, and bet Pic c
and eps Ind A b, found by radial velocity and imaged since. The review read the sentence on the
live pages of HR 8799 b, 51 Eri b and bet Pic b.

The ETL now asks the archive for those names in a query of its own, cached apart from the main
table so the other 6 252 planets stay on the snapshot they were built from, and carries
`imaged: true` on the matching records. exoplanets.json changes by that field on 102 records and
nothing else (compared record by record). Their cards now end "Not an observation — it has been
imaged only as a point of light beside its star, and no map of it exists."; the rest keep "no
image of this world exists". The provenance comment and the texture catalogue's comment say which
is which.

Checked live on :4301: HR 8799 b and eps Ind A b carry the new sentence, Kepler-22 b the old one.

Tests: body-view-model.spec 'says a directly imaged exoplanet was seen as a point of light, not
that no image of it exists'; the old test is renamed 'says an exoplanet the archive does not flag
as imaged has no image'. Unit controls, each failing that test only (1 failed, 840 passed): the
provenance ignoring the flag; the view model dropping it. build.ts now requires at least 95
imaged planets (measured 102); control, the full ETL with the join made on the host's name
instead of the planet's, fails with "Only 0 exoplanets are marked as imaged".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:50:04 +02:00
SenrokaiandClaude Opus 5.5 8e3a494fe9 Print Hyperion's eccentricity as JPL measures it now, 0.105, not the archived row's 0.023
The card listed Hyperion's eccentricity under "Measured" as 0.023: the archived JPL satellite row
the orbit is drawn from gives 0.0232. JPL's current table (SAT441) gives 0.105, and Horizons'
osculating orbit ranges 0.074 to 0.132 from 1980 to 2100 (0.1099 on 2025-01-01). The row stays
the orbit: with 0.105 put into it, Hyperion is further from Horizons, not nearer (median 9.6
degrees against 7.8 over 1980-2100, as a reviewer measured), so only the card changes.

BodyRecord.measuredEccentricity carries the figure the card prints where it is not the orbit's
own; the ETL sets it for Hyperion, and buildBodyViewModel prints it. build.ts now checks every
card's eccentricity against Horizons' osculating one on 2025-01-01, within 0.03: measured at most
0.0151 (Phoebe, and the Moon, whose eccentricity swings) once Hyperion prints 0.105, where the
row put it 0.0867 out. The live Hyperion page reads "ECCENTRICITY 0.105".

Controls: the ETL with Hyperion on its row's figure fails ("Hyperion's card gives an eccentricity of
0.0232, where Horizons' osculating orbit has 0.1099"); the view model ignoring the field fails
"prints the eccentricity measured for a moon whose orbit keeps an older one".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 19:17:06 +02:00
SenrokaiandClaude Opus 5.5 1c86584642 Carry every body's IAU rotational elements, read from NAIF's kernel of the 2015 report
bodies.json now holds, for 33 of the 38 bodies, the pole right ascension and declination and the
prime meridian W of the IAU WGCCRE 2015 report (Archinal et al. 2018), with their rates and the
periodic terms. They are read from NAIF's pck00011.tpc, which carries the report in a form a
program can read, periodic terms and their angles included. Hyperion (chaotic), Nereid, Eris,
Haumea and Makemake have no model in the report.

The parser, src/app/shared/astro/rotational-elements.ts, sits beside the other source readers so
the unit suite covers it. It reads data blocks only where \begindata stands alone on a line, as
the kernel's own prose mentions the token mid-sentence. It reads the Fortran exponent (the Moon's
-1.4D-12 d² term) and the degree-2 angles of the Mars system, where Phobos's tidal acceleration
lives. NAIF numbers a small body 2 000 000 past its catalogue number, so Ceres is 2000001.

Periodic terms under 0.01 degrees are left out. 0.01 degrees moves a point by 0.11 px on the
largest body ever drawn (Jupiter at 641 px of radius). That drops 32 terms:
- Mercury: 4 (0.0011 degrees and less)
- the Moon: 8 of 13 (0.0072 and less)
- Mars: 13 (0.00024 and less); its three 0.42-1.59 degree long-period terms stay
- Phobos: 1 (0.0063)
- Jupiter: 5 (0.0022 and less)
- Europa: 1 (0.009)
Kept, among others: Mimas's 44.85-degree libration, Triton's 32-degree precession, Miranda's 4.4
and Phobos's 1.14-degree libration.

build.ts now checks the elements against Horizons on the real catalogue:
- Every body but those five carries elements, and they do not.
- The IAU day, 360 over W's rate, is within 1e-4 of Horizons' period. Measured: at most 1.8e-5
  (Jupiter). Neptune gets a 0.01 ceiling: 0.89 per cent, because the report takes Karkoschka's
  15.9663 h where Horizons keeps Voyager's 16.11.
- The spin axis, the pole turned end for end where W runs backwards, is within 0.1 degrees of
  Horizons' obliquity. Measured: at most 0.058 (Venus, 177.358 against 177.3); Uranus 97.771,
  Pluto 119.610, Earth 23.435.
Full npm run etl passes. Three mutants each fail it on the named check:
- W's sign dropped: "Venus's IAU spin axis is 2.642 degrees".
- Ceres looked up by catalogue number: "Body ceres has no IAU rotational elements".
- W's rate read per century: "Mercury's IAU day ... 3.65e+4".

Nothing is drawn from these yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 22:46:14 +02:00
SenrokaiandClaude Opus 5.5 7b65ab4812 Draw a planet and a heavy moon going round their barycentre, as Pluto and Charon do
Standish's "Pluto" is the Pluto-Charon barycentre, and Charon is an eighth of Pluto's mass, so
that point lies 2 131 km from Pluto's centre, 943 km above its surface. Drawn the usual way, with
Pluto at its row's position and Charon going round it, Pluto sits where nothing is and Charon's
orbit is 2 131 km too wide on one side.

A moon record can now carry massRatio, its mass over its planet's. For such a moon the renderer
keeps the pivot at the planet's elements, which is the barycentre, and each tick puts the planet
massRatio / (1 + massRatio) of the relative separation back from it and the moon the rest out.
Both orbits are the relative ellipse scaled, the moon's by 1 / (1 + q) and the planet's by
-q / (1 + q), turned with the moon's node every tick: Charon's spans 17 460 km of radius and
Pluto's 2 131, round the same point, and neither passes through Pluto. Only Charon will carry it;
every other moon's barycentre is inside its planet.

Checked against Horizons in the unit suite, on JPL's records for the two: Pluto (999) from the
Pluto-system barycentre (9) in 2100 is 2 131.24 km out, and the renderer puts it within 5 km of
that length and 0.5 degrees of that direction, exactly opposite Charon at the inverse of their
mass ratio; Charon from Pluto is within 0.5 degrees of Horizons in 2100 (measured 0.37). The same
table adds Titania, against Uranus's equator 120 years from its 1980 epoch, within 0.75 (measured
0.62).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:49:47 +02:00
SenrokaiandClaude Opus 5.5 48319c3fe2 Move the solar system on JPL's mean elements, so it stays right as the clock runs
Every body carried one set of osculating elements from Horizons at 2025-01-01, run forward by
Kepler with a GM from a table of mass ratios. That set is exact at its instant and drifts from
then on, and the clock now runs a month a second: the Moon, with Earth's mass ratio lacking its
own and the osculating axis, went round in 27.70 days instead of 27.32, 66 degrees out after a
year, and its locked face was spun at the same wrong rate.

Planets and Pluto now take Standish's Table 2a/2b ("Keplerian Elements for Approximate
Positions of the Major Planets"): elements against the J2000 ecliptic, their rates per century,
and the b, c, s, f terms of Jupiter to Pluto, fit for 3000 BC to AD 3000. Table 1 is closer near
the present (Saturn 0.23 degrees at worst 1950-2100, against 0.32 here) but is only fit for
1800-2050, and by AD 3000 has Saturn 4.3 degrees out where Table 2 holds every planet within 0.3.
The moons take JPL SSD's satellite mean elements: sidereal mean motion n to ten figures, the
periods of their node and periapsis, and each one's local Laplace plane by its pole. They
propagate with n itself, never a GM: gmForParent and its mass table are gone. Horizons still
gives size, spin and obliquity.

Both tables are read from the Internet Archive's copy of JPL's pages, pinned to one capture: the
live approx_pos page has dropped Pluto, and the live sats/elem page has dropped n and rounds the
period to four or five figures (Phobos 0.3187 d, a revolution out within a decade).

What the tables leave implicit, measured against Horizons before it was accepted:
- The precession periods are magnitudes. A node regresses on a prograde orbit and advances on a
  retrograde one; a periapsis advances except where a resonance forces the eccentricity. Io's
  and Europa's follow their conjunction line backwards at 2 n(Europa) - n(Io) = 0.74 degrees a
  day, which is exactly the 1.625- and 1.394-year periods in the table. Read as advancing, Io
  was 0.9 degrees out and Europa 2.1.
- On a retrograde orbit the node's turning is added back to the mean anomaly. Taken off, Triton
  drifted a degree a year, 105 degrees by 2100.
- The Laplace frame's x axis is where the plane rises through the ICRF equator, RA of the pole
  plus 90. Read against the ecliptic, Io was 2.8 degrees out, Phobos 54 and Titan 127.

Orbit lines are now drawn in their own plane and turned by a quaternion each tick, so a turning
node carries the line with the body: fixed at one date, the Moon's line would be up to 69 000 km
off it nine years on. The Earth row is the Earth-Moon barycentre, 4 700 km from Earth, 0.002
degrees from the Sun. A tidally locked moon's day is now 360 / n, its sidereal period (the Moon
27.321662 d), so it stays locked to the orbit it is drawn on.

Angular error against Horizons VECTORS (ICRF, TDB; heliocentric for planets, planet-centred for
moons), degrees, read from the live renderer's markers in the running app:

body       1950-01-01 1975-01-01 1987-07-23 2000-01-01 2025-01-01 2037-03-06 2050-01-01 2075-01-01 2100-01-01   max
mercury         0.004      0.002      0.003      0.002      0.002      0.001      0.000      0.002      0.000  0.004
venus           0.003      0.007      0.003      0.004      0.004      0.004      0.003      0.004      0.004  0.007
earth           0.003      0.008      0.002      0.005      0.004      0.009      0.003      0.002      0.003  0.009
mars            0.009      0.010      0.008      0.024      0.009      0.012      0.009      0.011      0.028  0.028
jupiter         0.063      0.030      0.171      0.135      0.013      0.020      0.056      0.041      0.075  0.171
saturn          0.080      0.064      0.018      0.320      0.066      0.114      0.044      0.164      0.177  0.320
uranus          0.018      0.169      0.068      0.050      0.101      0.015      0.141      0.017      0.114  0.169
neptune         0.070      0.028      0.004      0.021      0.036      0.037      0.013      0.029      0.072  0.072
pluto           0.045      0.054      0.041      0.033      0.019      0.020      0.023      0.027      0.026  0.054
moon            0.486      1.928      0.127      0.631      1.407      1.086      0.720      0.339      1.180  1.928
phobos          2.068      0.294      0.881      1.113      0.313      0.636      2.089      5.862     11.099 11.099
deimos          0.077      0.043      0.310      0.066      0.164      0.068      0.034      0.468      0.044  0.468
io              0.021      0.015      0.010      0.019      0.009      0.035      0.006      0.011      0.022  0.035
europa          0.036      0.039      0.053      0.064      0.078      0.032      0.006      0.034      0.044  0.078
ganymede        0.132      0.103      0.018      0.007      0.023      0.054      0.091      0.118      0.044  0.132
callisto        0.040      0.019      0.023      0.019      0.038      0.008      0.060      0.119      0.056  0.119
titan           0.003      0.019      0.023      0.023      0.027      0.028      0.048      0.008      0.014  0.048
triton          0.051      0.029      0.009      0.021      0.052      0.048      0.063      0.089      0.137  0.137

Three miss what was hoped for, and why:
- Jupiter 0.17, Saturn 0.32, Uranus 0.17 against the 0.1 hoped for: short-period perturbations
  of the giants by one another, which no Keplerian fit carries. Standish states his own Table 2
  errors as 600, 1 000 and 2 000 arcseconds (0.17, 0.28, 0.56 degrees). Out to AD 3000, measured
  at 1800, 2200, 2400, 2600 and 3000, every planet stays within 0.3.
- The Moon, 1.9: evection (1.27) and variation (0.66), which a mean ellipse leaves out.
- Phobos, 2.1 until 2050, then 5.9 in 2075 and 11.1 in 2100, growing as the square of the time:
  its tidal acceleration, which the table has no column for. Its elements are MAR080's, epoch
  1950. The map's dates are also UTC where the elements are TDB, 69 s today,
  which is 0.9 degrees of Phobos and nothing for anything else.

Held in place by:
- build.ts: each body's mean elements against Horizons' own osculating elements on the ETL's
  2025-01-01, at most 0.25 degrees for a planet and 2.5 for a moon (measured: Uranus 0.101, the
  Moon 1.407; a regressing Triton node reads 10.24 and fails), and every moon's day equal to its
  sidereal period (a 1% error fails).
- Unit tests freezing nine Horizons vectors (Earth 2100, Jupiter 1950, Saturn 2075, Pluto 1975,
  the Moon 2050, Io and Europa 1950, Titan and Triton 2100) through SystemOrbitsRenderer, the
  Moon kept on its own turning line, the retrograde rule, the Standish terms, the Laplace frame,
  and both table parsers. Nine mutants each fail the test named for them, and the two
  validators each refuse a mutated build of the real catalogue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 20:18:57 +02:00
SenrokaiandClaude Opus 5.5 c38a42cbcb Fix what the review of this branch found, starting with the pick rule it only claimed
The off-screen rule for clicks was described in 3f0abf8 and in the pull request, but only its
comment was committed: pickAt still let the slop reach past the frame. The mutant that was said
to catch it matched nothing, and an unrelated flaky test failed instead. The frame test is now in
pickAt, before the slop, and its test fails without it (star at NDC 1.01, click at 0.995).

Venus, Uranus and Pluto turned forwards: Horizons states a retrograde spin twice, by a negative
rate and by an obliquity over 90 degrees, and both were applied. The period's sign is now used
only when no obliquity is known. Measured on the live markers, spin axis against orbit normal is
cos(obliquity) for each: Venus -0.999, Uranus -0.135, Pluto -0.494, Earth 0.917.

Moons listed as rates rather than "Synchronous" drifted about 5 degrees an orbit and Titan did not
turn: every moon is now locked at its Kepler period. Pluto's obliquity comes from IAU WGCCRE 2015,
Horizons gives none.

Also:
- the star's light is white at pi, not a warm 2.2 that left the photographs dim;
- procedural textures are 128x64, not 512x256 that froze the main thread ~60 ms a body;
- Io, Pluto, Titan and Deimos lose their "maps", which were disc photographs with black sky;
- an exoplanet with only a mass gets a radius from it (M^0.55, capped at Jupiter), not Earth's;
- the clock knows when it has left the present even once back at real time, so the date and
  "Back to now" stay up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 18:50:07 +02:00
SenrokaiandClaude Opus 5 225d676ab0 Turn each body at its own rate, from Horizons' own figures
The view had one rotation in it — the planet on the detail page, at 0.08 rad/s,
a number with no source. Nothing in the system view turned at all.

The data was already on disk: every cached Horizons page carries how its body
spins, in one of five forms. The rate in radians per second is preferred where
it appears, because it is signed — that is how Venus and Uranus are known to
turn backwards — then a period in hours or days, then the `9h 55m 29.711 s`
the giant planets use, and finally the word every major moon here carries
instead of a number: Synchronous. A tidally locked moon's day is its orbit, so
Kepler supplies it from the elements already parsed and the parent it goes
round.

Seventeen of the eighteen bodies come out within 1% of their published period —
Earth 23.934 h, Jupiter 9.925 h, Venus -5832.5 h, Io 42.5 h, Callisto 400.5 h.
Titan is the exception: its page states no period at all, so it is left still
rather than turned at an invented rate.

The axis is the orbit normal tilted by the obliquity about the orbit's
ascending node, which is where an obliquity is measured from and the only line
in the orbit the elements name. The phase at the epoch is published for none of
these bodies, so the face turned toward the camera is not a claim; the rate and
the direction are.

At true rates nothing is visible moving — Earth turns 15 degrees an hour. A
clock the reader can run faster is the next piece, and the audit asks for it
anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-21 23:12:02 +02:00
SenrokaiandClaude Fable 5 080bbe16dc Match exoplanet hosts on the sky, at both epochs the archive might mean
The host cross-reference matched in 3D, nearest star within half a parsec.
That is the wrong space for the same reason the star merge learned it: a
direction is measured, a distance is inferred. At 170 pc half a parsec is a
ten-arcminute cone, wide enough to hand the planets of stars our catalogue
does not carry to whatever bright star floats nearest — HATS-6 b sat on
HD 39500, seventy arcseconds away. At 60 pc it is tighter than the routine
disagreement between the archive's Hipparcos distances and our Gaia ones,
which is how four bright giants (7 CMa, HD 81688, omi UMa, xi Aql) lost
their planets and GJ 15 A's landed on a neighbouring entry.

Hosts are now resolved like stars are merged: by name first, then the
nearest star on the sky within a transverse budget — angle times the
archive's distance, 0.01 pc — whose distance does not flatly contradict the
archive's (the merge's own 50 % ratio). The budget is transverse because the
dominant error is proper motion over an epoch difference, a physical
displacement that is the same in parsecs at every distance: as an angle it
is 60" for Proxima and 2" for a host at 100 pc. Measured on the 504 hosts
whose archive name matches a catalogue name outright, true pairs reach
3.4e-3 pc; shifting every host a quarter of a degree finds nothing else
within 0.01 but Proxima's own entry, whose budget at 1.3 pc is wider than
the shift.

The archive never says which epoch a position is for, and they are mixed:
alf Tau and GJ 273 publish J2000, HD 133131 and TOI-2459 publish Gaia's
J2016. So the query asks for sy_pmra/sy_pmdec too, tries each position at
both ends of those sixteen years, and judges a star on whichever is closer.
Guess one epoch and a fast star's planets land on a companion: J2016 puts
Aldebaran's on Gl 171.1B, J2000 puts GJ 15 A's on a Gaia entry 15.9" out.

1 972 of 6 354 planets now sit on a host, 1 548 before: 432 gained, 26 on a
better star (GJ 15 A to Groombridge 34, GJ 676 A off its companion,
HD 19994 to 94 Cet), 8 lost — six false 3D matches to stars the catalogue
never contained, and GJ 273 b/c, whose archive row says 5.92 pc for
Luyten's Star at 3.79: a distance in flat contradiction is exactly what the
ratio guard exists to refuse, and the number to fix is upstream.

The 2 pc "rematch" apparatus is gone. build.ts recomputed every match after
fetchExoplanets had already written the file — at a different tolerance, so
the log reported a match count the data did not contain — and the offline
entry point that persisted it had no caller. One matcher, one set of
constants, used once. The archive cache is now keyed by a hash of the TAP
query, so a response cached before the proper-motion columns cannot serve
rows without them, where a missing cell would quietly read as "does not
move"; the row's astrometry is stored with each planet, which is what made
these tolerances measurable offline in the first place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-09 14:14:05 +02:00
SenrokaiandClaude Opus 5 7a112e4bb3 Answer the review: HYG's own last-resort name is a designation too
Junie: a HYG star that fell all the way through the ETL's naming chain --
no proper name, Bayer, Flamsteed, HD, Gliese or HIP -- is called "HYG <id>",
and with `source: 'hyg'` the predicate was looking for a lower-case "hyg "
prefix and calling it named. None in the current catalogue, but the path is
in `tools/etl/fetchStars.ts` and a refresh could walk it.

Fixed in the table rather than in the predicate: `hyg: 'HYG'` next to
`gaia: 'Gaia DR3'`, so the encoder, the decoder and the predicate all read the
one rule. The sourceless case reads the same entry instead of repeating it.

npm test 609/609, build and ETL typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fcUfL82nvyh9VebX1Fz6w
2026-08-27 20:16:11 +02:00
SenrokaiandClaude Opus 5 f14e252b19 Name the neighbours that have a name, before the ones that only have a number
The neighbour ring exists to say where you are. Since the catalogue refresh it
has been spending one of its four places in Sol on "Gaia DR3 5853498713190525696"
-- a nineteen-digit survey id for the star printed beside it as Proxima
Centauri, the same star twice -- and that duplicate row pushed Barnard's Star
off the ring altogether. 91.9% of the refreshed catalogue is named that way.

Named stars now come first, and survey designations fill in only where fewer
than four named ones are in reach. The line between the two is the one the
catalogue format already draws: a name is a designation when it is what the
star's source would generate for it. Judged by the prefix rather than by
rebuilding "prefix id" from the row, because the number after "Gaia DR3" is the
survey's own id, which the 32-bit row id cannot hold -- a round trip through
the id would have called every one of those stars named.

The preference lives on the index as `nearestPreferring`: the preferred pass
exhausts the search before the fill runs, so a named star is never outranked by
a nearer unnamed one. That is the whole point of asking.

The end-to-end spec names Barnard's Star again, on purpose. The four nearest
named stars to the Sun are a fact about space, not about which catalogue was
refreshed last, and without this change that is exactly the label that
vanished -- checked by running the spec with the preference stashed: it fails
on that line, and passes with it back.

npm test 609/609, npx playwright test 16/16 under CI=true --workers=2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fcUfL82nvyh9VebX1Fz6w
2026-08-27 20:06:04 +02:00
Claude 7b0f32f71a Assemble a body's readouts once, not once per panel
Follow-up to review on #3. The card and the detail page each built their own Measured/Derived
split, kind label and provenance sentence — the drift buildBodyViewModel exists to prevent,
re-forked one layer up, and the drift would have been in which side of the measured/derived line
a quantity falls on, which is the distinction those panels exist to draw. One bodyReadouts(body)
now returns both blocks and the sentence, and both templates iterate it. The two surfaces render
identical rows as a result, and the card gains the inclination the detail page already showed.

Also from that review:

- KIND_LABELS was duplicated between the two panels; it now lives beside bodyReadouts. The third
  copy the review pointed at is a different union (search results are star/body/exoplanet, and
  label a body "Body"), so it stays where it is.
- CardRow was HudReadout renamed. Both are now Readout, which HudReadout extends with its
  derived flag.
- The enterable-systems count was a 21-line lazy memo over arrays that are already in hand; it is
  one expression where those arrays are assigned.
- buildBodyViewModel now carries hostStarId, so the detail scene stops rescanning both catalogues
  for something the builder had already resolved.
- heliocentricPeriodDays was called twice for the same body.
- The superscript helper was a split/map/join; it is a replace.
- info-panel had five computed() each wrapping one pure call with a non-null assertion, beside a
  template that inlined the same kind of call directly. They are gone with the shared readouts.
- Dropped a tautological test that compared a pure function to itself. Replaced with one that
  asserts the host star id the builder now carries.
- Removed the orphaned doc comment left behind when formatParsecs/formatAu moved out.

And one the review raised as out of scope but is worth taking: formatRadiusKm grouped thousands
above its decimal threshold and not below it, so 69,911 km sat beside a bare 6371 km. Both are
grouped now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-06 03:52:11 +00:00
Claude efa9e4084a Draw the whole catalogue, and build the aggregation the rest would need
Two things, one verified and one that cannot be.

The render budget is now the whole catalogue: 68388 stars, one instanced
draw call, which is what a GPU should be asked to do. The budget itself
stays, because the catalogue is meant to grow past what any machine should
draw at once — Gaia alone could contribute a million — and at that point
the selection is what keeps the field legible rather than a grey wash. A
`?stars=` override handles the machines that cannot, including the
software rasterizer the end-to-end suite runs against, whose frame rate is
two orders of magnitude below a real GPU's and which was measuring the
rasterizer rather than the app.

The aggregation is the second thing, and none of it has run. Every ESA,
NOIRLab, SDSS and Euclid endpoint is unreachable from here — only GitHub
raw is, which is why HYG and OpenNGC are the current sources. So this is
infrastructure and a Gaia query written against the published DR3 schema,
not data.

What the framework encodes is that these surveys are not interchangeable.
The distinction is not size but whether a catalogue knows how far away its
objects are, because a 3D map cannot place a star it only has a direction
for. Gaia is the only one of the five that can add stars here, because it
is the only one that measures parallaxes. DECaPS2 has fifty times Gaia's
object count and photometry alone — not one of its 3.32 billion objects
can be placed in depth. Euclid's bulge is 8 kpc away, where a parallax is
microarcseconds; its contribution would be imagery. SDSS-V and SAGA are
keyed to stars something else already places, so they enrich rather than
extend. Those roles are recorded as data the ETL prints, not as prose that
can drift.

Overlapping catalogues are reconciled on direction rather than on 3D
proximity, which is the one non-obvious part. Two surveys agree on a
star's direction to within an arcsecond and disagree on its distance by
tens of per cent, so a star at 200 pc is 50 pc from itself between
catalogues while being unmistakably the same object. Matching in 3D would
need a tolerance so loose it swallowed real neighbours. The better
parallax wins where both reach; where only one does, the star stays.

Names become dense-with-holes with a source dictionary, because a survey
catalogue has no proper names — writing "Gaia DR3 4472832130942575872"
once per star would cost 25 MB per million to repeat what two adjacent
fields already say. An empty entry costs three bytes and is regenerated on
load. The Sun needed its own case in the merge: it sits at the origin, has
no direction to compare, and appears in every catalogue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-05 08:51:54 +00:00
Claude 29fd92d118 Widen the star catalogue, and separate what is drawn from what is known
The map held 8750 stars within 50 pc and rendered 371 systems. Both were
lower than they needed to be, for different reasons.

The star catalogue was capped by its own encoding as much as by the
cutoff: one JSON object per star, eight key names repeated each time, 157
bytes a star. At the range HYG actually reaches that is 17 MB to download
and parse before the first frame. So the numbers move into two binary
column stores — positions in stars.bin, which the GPU is handed verbatim,
and id/magnitude/colour/spectral index in stars-meta.bin — and the JSON
keeps only the strings, with 2600 distinct spectral classifications
collapsed to a dictionary. The layout is defined once, in star-catalog.ts,
and the ETL and the app both use it, so the writer and the reader cannot
drift.

The cutoff then goes to 250 pc: 68388 stars, 7.8x as many for 1.7x the
bytes. That is where HYG's measurements stop rather than a round number —
98.6% of its rows are Hipparcos, whose parallaxes are good to about a
milliarcsecond, so beyond 250 pc it would be plotting noise.

Drawing all of them is a separate question from knowing them, and it is
answered separately. The field draws a budget: every star inside 25 pc,
because the nearest are faint red dwarfs and Proxima Centauri is magnitude
11, then the brightest of everything beyond. Search, navigation and the
planet cross-reference still see the whole catalogue. A real GPU would
draw all 68388 without noticing; the budget is for the machines that would
not, and it is one constant.

Systems were limited by something else entirely. The archive data already
shipped named 4735 host stars and only 388 resolved, because the rest lay
outside a 50 pc catalogue — and the cross-reference kept only its own
result, so redoing it meant re-downloading an archive that is not
reachable from here. Host coordinates are now stored with each planet, and
the match is re-resolved at build time against whatever catalogue the run
produced. Even name matching alone, which needs no coordinates and so
works on the records already shipped, rescues 335 planets across 238
systems: 371 renderable systems become 609.

Two selection rules were tuned for a 50 pc bubble and no longer fit.
Tethers followed the Sun's nearest neighbours, which are a speck at this
range, and now follow the brightest; labels were ranked by proximity,
which named whatever sat nearest the middle of the screen, and are now
ranked by brightness — so the view names Canopus, Achernar and Spica
rather than a clump of catalogue designations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-05 08:35:41 +00:00
Claude 8d8c65bdb2 Propagate exoplanets with their real orbital period
Every exoplanet was propagated with gmForParent(undefined) — the Sun's
gravitational parameter — so the whole catalogue orbited as though each host
were exactly one solar mass. Most hosts are red dwarfs far lighter than that,
and a heavier central mass pulls harder and shortens the period, so their
planets were whirling round much too fast: TRAPPIST-1 is 0.09 solar masses, and
its planets were completing an orbit in roughly a third of the true time.

pl_orbper was already in the TAP query and was being discarded on the way into
the record. It is now kept, along with st_mass. A period and a semi-major axis
together pin the host's gravitational parameter exactly, via GM = n^2 a^3 — no
stellar model, no assumption, just the inverse of the orbitalPeriodDays helper
that was already there.

resolveGravitationalParameter picks the best available source: the measured
period, else the published host mass, else one solar mass as before. A derived
value implying something outside 0.01-150 solar masses is rejected and falls
through, since a period and axis taken from disagreeing solutions would
otherwise send a planet spinning at a visibly absurd rate.

Note the direction of the error, which is the opposite of what it looks like:
assuming a *heavier* host than reality makes a planet orbit *faster*. A test
pins it, and caught me stating it backwards first.

The NASA Exoplanet Archive is unreachable from this environment (egress policy
returns 403 on CONNECT), so exoplanets.json cannot be regenerated here and still
carries no periods. Behaviour is therefore unchanged until `npm run etl` is run
somewhere with archive access, at which point every planet with a published
period starts moving correctly with no further code changes. build.ts reports
how many records gained a period, and rejects non-positive ones.

Tests: 171 passing, up from 151, including a new end-to-end check that
TRAPPIST-1 b with its real period completes exactly one orbit in 1.51088 days
and sits a full diameter away at half that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-04 11:19:45 +00:00
Claude 4aca223027 Regenerate the star catalogue, fixing 2331 names and 875 colours
Two ETL bugs, both fixed at the source and then re-run against HYG. Star ids,
ordering and positions are all unchanged, so stars.bin is byte-identical and
every exoplanet cross-reference still resolves.

Names. HYG's `gl` column already carries its own catalogue prefix ("Gl 581",
"GJ 3512"), unlike the bare numbers in `hd` and `hip`, so prefixing it again
produced 2331 of 8750 stars named "Gl GJ 1076". That corrupted three surfaces at
once: search, the on-screen labels, and exoplanet host-star name matching, which
compares normalised names and could never match "glgj1076" to "gj1076".

Colours. `Number(row['ci']) || 0` cannot tell a blank cell from a real zero, and
0 is a real B-V colour index meaning a hot blue-white A-type star. All 875
affected stars turned out to be blanks — the catalogue contains no genuine zero
inside the distance cutoff — so several hundred red dwarfs were rendering
blue-white. colorIndex is now `number | null` rather than defaulted, because any
numeric default is indistinguishable from a measurement.

Consumers resolve the gap from the spectral type instead. That needs real
parsing: HYG's `spect` column runs to 134 distinct spellings among the affected
stars alone, including a bare lowercase "m" for 354 of them, plus "k-m" ranges,
"dM4" luminosity prefixes and "K:" uncertainty flags. 622 of the 875 recover a
class this way — 497 of them M-class — and the remaining 253, which carry no
classification at all, fall back to neutral white.

The parse is anchored at the start of the string rather than scanning it. A scan
is the obvious implementation and is quietly wrong: the ETL writes the literal
"Unknown" for unclassified stars, that contains a K, and every one of those 253
would have been classified as an orange K-type. A test covers it.

Also lifts parseOptionalNumber out of fetchExoplanets into lib/csv, where both
fetchers now use it, and gives magnitude a faint default instead of 0 — no
current star is affected, but 0 would mean "as bright as Vega" and render an
unphotometered star as one of the largest points on the map.

Tests: 145 passing, up from 116, including the first coverage of
StarFieldRenderer. Build, both typechecks and the Playwright suite are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-04 10:56:30 +00:00
Claude 3a859360ba Add the deep-sky backdrop, the last unbuilt piece of the plan
The design doc scopes deep-sky objects as a galaxy-view backdrop and lists
fetchDeepSky.ts, deepsky.json and deepsky.model.ts, but none of it existed —
it was the only part of the plan with no implementation behind it.

ETL: fetchDeepSky.ts pulls the OpenNGC catalog, classifies each object as a
galaxy/nebula/cluster, and keeps the ~460 worth drawing (everything Messier,
everything with a common name, and anything brighter than magnitude 9) out of
~12,000 mostly-anonymous rows. build.ts runs it and validates the output.

Distances are the hard part: OpenNGC has no distance column, and both fallbacks
fail for the best-known objects. M31, M33 and M42 are Local Group members whose
redshift is negative or absent, and a galaxy's catalog parallax comes from a
cross-matched foreground star — 6 mas for M31 would put a 780 kpc galaxy at
167 pc. So records store a unit direction on the celestial sphere rather than a
position (the line of sight is always known precisely, and the objects are drawn
on a fixed backdrop shell where true distance is unusable anyway), and distance
is optional metadata carrying its own provenance. Parallax is trusted only for
galactic objects, redshift only above z=0.003 where expansion outweighs peculiar
velocity. 330 of 463 get a distance; the rest honestly report none.

Rendering: DeepSkyRenderer paints the objects as soft additive billboards on a
2500 pc shell — clear of the 50 pc star field, beyond the camera's 2000 pc orbit
limit, and inside its 5000 pc far plane. Size comes from real angular extent, so
Andromeda is six times wider than the full Moon, clamped at both ends. Sprites
rather than points because the WebGPU backend caps point primitives at one pixel;
materials are shared per kind and brightness band, so 460 objects cost nine of
them. The brightest dozen get permanent labels, which needed the label overlay to
accept string ids alongside numeric star ids. The backdrop is decorative, so a
failure to load its dataset is logged and the star field comes up regardless.

Also documents the app in the README, which until now covered only the plugin
marketplace.

Tests: 112 passing, up from 54. Build, both typechecks and the Playwright suite
are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-03 15:19:39 +00:00
Senrokai d7e8ea1d4d @
Add star-map Angular app, ETL pipeline, and caveman plugin

Angular 3D star map (galaxy/system/body views, Three.js rendering,
navigation store) plus the NASA ETL tooling that builds the star,
exoplanet and solar-system datasets, Playwright e2e suite, and the
cs:caveman Claude Code plugin (command, agent, skill).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
2026-08-03 16:50:10 +02:00