Commit Graph
16 Commits
Author SHA1 Message Date
SenrokaiandClaude Opus 5.5 280159dcd2 Bring the figures the comments quote back to what the catalogue now holds
Later commits on this branch (0e9ab6f, ff744a0, 8c3a860, and this round's fold, positional lookup
and parallax fallback) changed counts that comments in build.ts, star-readouts.ts, star-catalog.ts
and fetchExoplanets.ts still quoted as "measured" or "today". Each is now the value measured on the
published catalogue after an ETL run from cache (decoded with decodeStarCatalog, or from the
validator log where it prints them):

- distances without a published error 439 -> 346, the Gliese ones 357 -> 264 (82 archive);
- parallax distances ranged 1 109 -> 1 116; HYG stars at Gaia's distance 8 129 -> 8 105, and the
  Gaia-placed stars HYG describes 62 002 -> 61 713 at Gaia's distance, 62 097 in all;
- HYG rows without a Gaia counterpart 11 554 -> 11 478: past 250 pc 8 307 (1 660 naked-eye), inside
  it 3 170 (1 204 brighter than V 8, 1 804 from 8 to 12, 162 fainter, 112 of them Gliese stars
  within 50 pc); the sentence also said those were every star past 250 pc, which the archive's
  hosts no longer let it be;
- planets with a host 6 327 -> 6 328, the 26 left having neither a distance nor a parallax;
- renamed hosts 575 -> 574; the composite table fills 100 of the 127 blank distances, not "the 100".

No ceiling or floor changes; comments only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 17:46:00 +02:00
SenrokaiandClaude Opus 5.5 5fb0d45623 Place an archive host by its parallax where the archive gives no distance, so mu2 Sco b has its star
The archive leaves sy_dist blank for mu2 Sco and publishes sy_plx 6.31 +- 0.86 mas. The matcher
only reads the parallax as a second chance beside a finite sy_dist, and the archive-star fallback
needs sy_dist too, so mu2 Sco b had no host while Pipirima (HIP 82545, V 3.56, B2 IV) sat on the
map 0.4" from the archive's direction at 145.3 pc. The build.ts comment and the step report said
the 27 hostless planets had "no distance in either archive table"; mu2 Sco was the one whose row
has a parallax.

archiveDistancePc takes sy_dist, or 1000 / sy_plx where it is blank (158.5 pc here, within the
ratio test of Pipirima's 145.3). An ETL run from cache changes exoplanets.json alone: mu2 Sco b
now has host 82294, Pipirima, and 6 328 of 6 354 planets have a host (26 without, none of whose
rows has a distance or a parallax). hostDistancePc stays the archive's own blank.

Control: ignoring the parallax fails "takes the archive's parallax where it gives no distance,
and nothing from a parallax that is none" alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:37:30 +02:00
SenrokaiandClaude Opus 5.5 23547defe0 Read an archive host's colour off the dwarf sequence at its temperature, and say it was not measured
For an archive-placed host with a temperature and no B magnitude, fetchExoplanets took B−V from
Ballesteros' blackbody fit, which runs 0.1 to 0.2 redder than Pecaut & Mamajek's dwarf sequence
below 3 800 K, and the luminosity then read its correction off that sequence at that colour: 3 500 K
came back as 3 102 K with a correction 1.15 magnitudes too large, anything under about 3 170 K was
clamped to B−V 2.00, and the card printed it as a measured "Colour B−V 2.00". CFBDSIR
J145829+101343, a 580 K brown dwarf, read "Spectral type ~M6, from colour".

temperatureToColorIndex now reads the table itself backwards, interpolating B−V between the two
types the temperature falls between, so the temperature and correction read back off the colour
are the table's at that temperature; it has no answer outside 2 420 to 31 400 K. The colour is
flagged colorFromTemperature, a fifth bit in the photometry byte (the format, README and the ETL's
round-trip check follow), and the card prints it "B−V 1.66, from its temperature", marked derived.

From cache: 57 archive stars change colour; 54 carry the flag and 3, CFBDSIR J145829+101343 among
them, now have none. For the 47 of them the archive gives a luminosity, the one derived from
magnitude and colour moves from a median 0.228 dex off it to 0.124; Kepler-445 (3 157 K) from
0.0282 L☉ to 0.0080 against the archive's 0.0079. Its colour goes from 2.00 to 1.67. The card shows
the archive's luminosity where it has one since earlier on this branch, so this is the figure used
for the rest and for their radii.

Controls, each failing its named test: the nearest hotter row taken without interpolating (2 of
803 failed), no refusal outside the table, the flag not encoded, and the card calling the colour
measured (1 of 803 each).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:29:09 +02:00
SenrokaiandClaude Opus 5.5 6d81c45f45 Carry the archive's positions back from Gaia DR2's J2015.5, where it publishes them, not J2016
fetchExoplanets placed each star it adds from the Exoplanet Archive by carrying the archive's
position back sixteen years, and 4c8e4a0 said the archive publishes at Gaia's J2016. It does not:
its positions are Gaia DR2's at J2015.5 although it names the DR3 source. Barnard's star in the
archive, 269.4486144, 4.7379808, equals DR2 4472832130942575872 at J2015.5 to 1e-7°, and sits 5.2″
from DR3's J2016 place; the review found 66 of the 67 archive-placed stars moving over 100 mas a
year within 0.21 mas of their DR2 position and none within 1 mas of DR3's. The matcher's comment
made the same claim of HD 133131 and TOI-2459, whose positions are DR2's too.

host-star-matching.ts now exports CATALOGUE_EPOCH and ARCHIVE_EPOCH = 2015.5, the matcher carries a
query back by their difference, and fetchExoplanets uses the same two, so the epoch is written
once. From cache: only the 3 277 archive-placed stars move, 2 287 of them by more than a
milliarcsecond, 74 by more than 50 and TOI-2406 most, by 203 mas, half a year of its 405 mas/yr;
no planet changes host. Invisible at map scale; the point is that the constant and its comments
now say what the archive does.

Control: ARCHIVE_EPOCH back to 2016.0 fails "carries Barnard's star from the archive's position to
where Gaia DR3's goes, to a few milliarcseconds" (1 of 802), which puts the two 5.2″ apart. The
GJ 15 A matching fixture is now built at J2015.5 too. fetchExoplanets' use of the constant has no
test of its own; the ETL has no test harness.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:20:26 +02:00
SenrokaiandClaude Opus 5.5 62f81f2c43 Number the stars the archive places after their host's name, so a refresh keeps each one's id
4c8e4a0 numbered each star it adds from the Exoplanet Archive by its place among the unmatched
hosts, in pl_name order, and the refresh workflow re-queries the archive every Monday. One host
added or dropped ahead of another renumbers it: in the review, removing a single planet row
renamed 3 276 of the 3 277 ids, and a bookmark kept on Kepler-186 (1070001620) opened Kepler-1860
under Kepler-186's stored name. HYG's and Gaia's ids do not move between refreshes; the merge's own
comment says ids are meant to hold.

archiveStarId (host-star-matching.ts, beside the matcher the ETL already imports from there) hashes
the host name with FNV-1a into the 3.7 million ids between 1 070 000 000 and 2^30, and moves a name
whose id is taken to the next free one. The added stars are sorted by id before they are appended,
so the published list stays in id order. Measured: all 4 237 archive-hosted planets change host id
once (Kepler-186 is now 1073671518), none of the others; one of the 3 277 names was probed past a
collision, and one new id falls in the old 1070000000-1070003276 range, so a bookmark saved on that
old id would open the wrong star once. Dropping the same row from a copy of the cached answer and
rerunning the exoplanet step in a scratch copy of the ETL now leaves 3 276 of 3 276 ids unchanged.
The validators pass: no duplicate id, all under 2^30.

Controls, each failing its named test: the id taken from the order of arrival, and no probing past
a taken id (1 of 801 each).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:12:00 +02:00
SenrokaiandClaude Opus 5.5 a81dd491ad Say which band each star was measured in, whose catalogue it is, and how sure its distance is
The readout named Hipparcos, Yale and Gliese while 378 775 of the 455 608 stars are described by
Gaia DR3, printed one "Magnitude" for G and V alike, and gave every distance to the parsec. The
band cannot be read off the star's source, which records whose position it has: 62 002 stars Gaia
places keep HYG's V and B-V, and the 575 hosts renamed after their planets are Gaia's, in G.

stars-meta.bin gains two byte columns, 14 to 16 bytes a star (6 378 512 to 7 289 728 bytes; gzip
-9 2 804 049 to 3 110 991). One holds the magnitude's band (V 76 555 stars, G 378 744, none 309,
whose magnitude is a stand-in), which colour the colour index is (B-V 75 203, BP-RP 376 703), and
whether the distance is Gaia's parallax. The other holds the distance's relative error as its
square root in 255ths: a step is 0.08 % of distance at 1 %, 0.35 % at 20 %, and 100 % is the top.
encode, decode, BYTES_PER_STAR_META and the build.ts round trip cover both; no workflow reads the
format.

Where the errors come from:
- Gaia rows keep the parallax_error their query already fetched: median 0.3 %, 90th percentile
  1.2 %, at most 20 %, the query's own cut.
- A HYG star at Gaia's distance takes the cross-match's parallax_over_error, and a star merged
  into a Gaia entry keeps that entry's error with its position.
- The 3 067 Hipparcos stars that keep their Hipparcos distance, Rigel, Deneb and Alnilam among
  them, take e_plx from van Leeuwen's 2007 reduction: a new cached query of
  public.hipparcos_newreduction on the ESA archive, whose 117 955 rows HYG's distances invert.
- The archive's stars take sy_disterr1/2 from pscomppars, in a query and cache file of their own
  so the composite rows already cached were not refetched.
- 439 distances have no published error: 357 Gliese rows and 82 archive hosts.

Of the errors, 392 786 are 1 % or less and are not printed; 61 156 print as "117 ± 12 pc" to the
distance's own digits; 1 196 between 20 and 100 %, and 30 past it, print as the range the
parallax gives, since a symmetric error in parallax is a lopsided one in distance.

The star card (measured on the dev server) now reads, for example:
- Rigel: 265 ± 23 pc, V 0.18, B-V -0.03, source HYG.
- Deneb: 433 ± 60 pc.
- Alnilam: "476 pc to 833 pc" (Hipparcos 1.65 ± 0.45 mas).
- Gaia DR3 5612323414549657984: 111 ± 2 pc, G 4.63, BP-RP -0.15, source Gaia DR3.
- Proxima Centauri: 1.30 pc, V 11.01, source "HYG, Gaia DR3 distance".
- TRAPPIST-1: G 15.62, BP-RP 4.90, source Gaia DR3.
- Kepler-186: V 15.14, source NASA Exoplanet Archive.
The neighbourhood's subtitle reads "Gaia DR3 378,775 · HYG 73,556 · NASA Exoplanet Archive
3,277", counted by the catalogue describing each star.

build.ts validateStars now fails a catalogue with more than 1 000 stars without a band (309
today) or without a distance error (439). Dropping G from the Gaia rows gave 379 040 without a
band, and dropping their parallax_error gave 441 216 without an error; both runs failed.

Decoding the catalogue in Node took a median 29 ms before and 24 ms after (nine runs each, within
noise). In the app, five cold boots gave a 654-786 ms long task after the data landed and the
HUD at 1.83-2.07 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 23:26:27 +02:00
SenrokaiandClaude Opus 5.5 4c8e4a02f3 Give every planet with a distance a star, from the archive where the catalogue has none
After matching, 4 237 planets still had no star: their hosts are too faint for either Gaia
query (fainter than G 12 past 50 pc) or too far (past 250 pc), Kepler-186 at 177.6 pc among
them. fetchExoplanets now adds one star per such host from the archive's own figures, 3 277
of them, whenever the archive gives a position and a distance:

- position carried back from J2016, where the archive publishes it (741 of the 746 matched
  hosts moving over 100 mas/yr sit nearer their star carried back, a median 0.11" against 3.47"
  as published), and placed at sy_dist;
- magnitude in V (2 999 hosts), else Gaia G (13), the band the catalogue's Gaia stars are
  already in; 265 have neither, 128 KMT, 95 OGLE and 32 MOA microlensing hosts at a median
  6.2 kpc among them, and take the ETL's faint stand-in of 15;
- colour as B-V from the archive's B and V, else from st_teff through a new
  temperatureToColorIndex (Ballesteros 2012, inverted; the Sun's 5 772 K gives 0.65), else
  left to st_spectype;
- ids from 1 070 000 000, past Gaia's two ranges and under the 2^30 validateStars enforces;
  source "exoplanet-archive".

Hosts past 250 pc are included: 399 of the 3 277 are within 250 pc, 1 962 between 250 pc and
1 kpc, 916 beyond. The drawn budget still chooses what is drawn (70 000 of 455 608).

Planets with a star: 2 090 -> 6 327 of 6 354. The other 27 have no distance in either table
(Luhman 16 A, mu2 Sco, PSR B1620-26 among them). Systems in the Solar Neighbourhood readout:
1 450 -> 4 736. validateExoplanets now refuses a catalogue where fewer than 99.5 % of planets
have a host (measured 99.58 %); dropping the added stars fails it at 2 090, and dropping the
composite fill at 6 227. No added star sits within an arcsecond of a catalogue star
(validateMerge's twins stay at 23); 5 of the 399 within 250 pc have one within a minute of arc,
VHS J125601.92-125723.9 (archive 12.7 pc) 5.8" from a Gaia entry at 21.2 pc the likeliest
duplicate.

In the app, searching TRAPPIST-1 or Kepler-186 and picking the star enters a system with its
seven and five planets drawn. gzip -9: stars.bin 5 030 741 -> 5 067 864 B, stars-meta.bin 2 784 614 -> 2 804 064,
stars-index.json 4 003 584 -> 4 015 882, exoplanets.json 342 467 -> 446 532 (host parameters,
both commits).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 22:09:16 +02:00
SenrokaiandClaude Opus 5.5 494fb5616b Find the hosts the catalogue already holds, and name them after their planets' star
The archive's default-parameter rows leave sy_dist blank for 100 planets, TRAPPIST-1's seven
among them, so the matcher could not place a host the catalogue had drawn since the Gaia
nearby query (Gaia DR3 2635476908753563008, 12.47 pc). fetchExoplanets now also reads the
Planetary Systems Composite table (pscomppars) and fills a default row's blank host cells from
it. Only host columns: a composite row takes each column from its own reference, so orbits
still come from the default row alone, one fit per planet. Where both tables give a distance or
a position they agree on all 6 225 and 6 352 rows.

Three hosts the catalogue holds by name failed the distance ratio test because the archive's
sy_dist, from TICv8, contradicts its own parallax: Lalande 21185 (GJ 411) 5.68 pc against
392 mas, Luyten's Star (GJ 273) 5.92 against 263 mas, Struve 2398 B (Gl 725 B) 6.84 against
285 mas. resolveHostStarId now takes the archive's parallax (sy_plx) as a second distance the
ratio test accepts. It is a second chance, not a replacement: 47 of 5 959 systems disagree past
the tolerance, and for faint far hosts the inverse parallax is the worse figure (K2-238: 538 pc
by sy_dist, 6 779 by parallax).

Planets on a catalogue star: 2 071 -> 2 090 of 6 354. The 19 gained are TRAPPIST-1 (7),
GJ 273 (2), GJ 411 (2), Gl 725 B, HD 62509 (Pollux), K2-65, TOI-2267 A and B, and two
brown-dwarf hosts, 2MASS J02192210-3925225 and DENIS-P J082303.1-491201. No planet lost or
changed its host.

A matched host whose catalogue name is a bare Gaia designation now takes the archive's host
name, so search finds TRAPPIST-1, Teegarden's Star, TOI-700, LP 791-18 and K2-18: 575 stars
renamed. validateExoplanets refuses a host that is still only a designation, and the star
assets are rewritten after the exoplanets for that reason; stars.bin and stars-meta.bin are
unchanged. TOI-2267 A and B both land on one Gaia entry, which takes the name TOI-2267 A.

Each planet also carries its host's radius, effective temperature and luminosity (10^st_lum),
and a mass for 6 344 planets instead of 5 474, from the default row where it gives one and the
composite table otherwise, for the star-physics step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 22:02:56 +02:00
SenrokaiandClaude Opus 5 037545d036 Answer the review: a name two stars answer to names neither, and NaN is not a proper motion
Three guards the matcher was missing, none of which changes a byte of the
regenerated data — the ETL re-run after them is identical — and all three
now have a test that fails without them.

A proper motion that is not a number poisoned every comparison rather than
one: NaN loses every `<` it appears in, so `cosine < minCosine` was false
for every star, each one reached the distance guard, and the last one in
catalogue order won — a confident wrong answer, order-dependent, where the
honest answer is "no match". The archive's own parser never produces one
(parseOptionalNumber maps a blank cell to undefined), but the matcher is
exported for offline re-cross-referencing and a caller reaching for bare
Number() is exactly the coercion the CSV helper documents as having caused
two prior bugs. An unusable motion now reads as no motion.

Normalizing a name strips the dot, so `Gl 55.2` and `Gl 552` — two stars
135 degrees apart — share one key, and the index kept whichever came last;
64 such groups exist in the catalogue, among them `Gl 84.1A`/`Gl 841A` and
`HD 96600` twice. A name that names two stars names neither, so ambiguous
keys are dropped and the query goes to the sky, where direction settles it.
No archive hostname lands on one today, which is why the data is unchanged.

And the cache is keyed by the whole request rather than the query alone,
here and in gaia.ts: fetchTextCached records only that some response
arrived, so an endpoint edit would have kept serving the old host's bytes —
the same silent staleness the query hash was added to close.

The tests now discriminate what the comments claim. Eight mutants, each
caught: judging only the published position, only the carried-back one,
judging each star on its worse epoch rather than its better, letting a
distance-rejected star claim best-so-far and shadow the true host behind
it, an unguarded proper motion, a last-wins name index, a fixed angular
tolerance instead of a transverse one, and no distance guard at all. The
GJ 887 test grew a decoy standing halfway along the star's own track: it is
nearer than Lacaille 9352 at the published position and nearer at the worse
of the two epochs, so it wins unless both epochs are tried and the better
one decides — the property the test's comment had been claiming untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-09-09 20:48:49 +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
Claude 9a5e41495b Make a cold-cache ETL run a pure function of the archives
Two order dependencies surfaced while designing the scheduled refresh, both
invisible on a developer machine with a warm cache and both guaranteed to
haunt a weekly CI run that starts cold.

The Exoplanet Archive query had no ORDER BY, so the archive was free to
return rows in any order it liked — and exoplanets.json preserves row order,
so a re-run could rewrite the file, and commit a diff, when nothing was
actually published. Ordered by pl_name, which is unique among default_flag=1
rows, so the order is total.

The deep-sky sort tie-broke with localeCompare, whose collation belongs to
the ICU build of whichever Node runs the ETL. Compared code points instead:
the file must not reorder because the runner's ICU disagrees with the machine
that wrote it last.

Both may reorder the committed files once, on the next real refresh. After
that, byte-identity between runs means what it should: the archives
published nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-17 14:05:41 +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 06cf7d2a15 Fix four defects a user hits in the first minute
Found by surveying the codebase against the plan; each was verified against the
committed assets or the running app before being touched.

TRAPPIST-1 was orbiting the Sun. The Exoplanet Archive leaves sy_dist blank for
some systems, and fetchExoplanets.ts read it with bare Number() — Number('') is
0, which is finite, so it slipped past the Number.isFinite guard in
resolveHostStarId, placed the host at the origin, and matched Sol at distance
exactly 0. 127 records shipped with hostStarId 0, all seven TRAPPIST-1 planets
among them, and the system view filters on that id, so drilling into Sol drew
127 alien worlds inside the real solar system.

Fixed in three places: resolveHostStarId now rejects a non-positive distance
(the robust guard, covering every caller), fetchExoplanets.ts uses the
parseOptionalNumber that already sat unused in that same file for ra/dec/dist,
and validateExoplanets asserts nothing ever resolves to the Sun again — the Sun
has no exoplanets, so that tripwire costs nothing and is permanent.

The archive's endpoint is blocked by this environment's egress policy, so the
ETL cannot be re-run here. The committed asset was corrected in place instead,
which is safe because the outcome is deterministic: the name path runs first and
none of the 127 resolve by name, so all of them reached id 0 positionally and
the fixed pipeline yields null for exactly that set. Cross-referenced hosts drop
from 761 to 634; record count is unchanged.

Dragging to rotate selected stars. Selection was bound to the raw click event,
which browsers fire on release however far the pointer travelled and which
OrbitControls does not suppress — so any drag ending over a star launched a
camera flight, and in system view routed away to /body/:id. Now tracks
pointerdown and ignores a release more than 5 px from it.

Ghost systems accumulated on every star-to-star hop. SystemOrbitsRenderer.dispose
released geometries and materials but never detached its group, so old orbit
lines stayed parented forever — still traversed and re-uploaded each frame with
disposed geometries, drawn over the new system and unpickable. dispose() now
detaches and clears.

Galaxy star labels stayed pinned inside the system view. They are CSS2D objects
parented to the scene rather than to galaxyGroup, so hiding the group left up to
15 parsec-space names clumped over the system's star. Cleared on entry. Also
gated the per-frame Kepler propagation on actually being in a system; it ran in
galaxy view too, because the renderer is never nulled on exit.

Tests: 116 passing, up from 112. 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 16:53:28 +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