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
This commit is contained in:
@@ -17,9 +17,29 @@ export interface HostStarQuery {
|
||||
pmDecMasPerYear?: number;
|
||||
}
|
||||
|
||||
/** Builds a lookup of normalized star name -> star, for fast repeated name matching. */
|
||||
/**
|
||||
* Builds a lookup of normalized star name -> star, for fast repeated name matching.
|
||||
*
|
||||
* A name two stars answer to names neither: normalizing strips the dot, so `Gl 55.2` and
|
||||
* `Gl 552` — 135° apart, and 64 such groups exist in the catalogue — collide on `gl552`, and a
|
||||
* map would silently keep whichever came last. Ambiguous keys are dropped instead, which sends
|
||||
* the query to the sky, where direction settles it.
|
||||
*/
|
||||
export function buildStarNameIndex(stars: readonly StarRecord[]): Map<string, StarRecord> {
|
||||
return new Map(stars.map((star) => [normalizeStarName(star.name), star]));
|
||||
const index = new Map<string, StarRecord>();
|
||||
const ambiguous = new Set<string>();
|
||||
for (const star of stars) {
|
||||
const key = normalizeStarName(star.name);
|
||||
if (index.has(key)) {
|
||||
ambiguous.add(key);
|
||||
} else {
|
||||
index.set(key, star);
|
||||
}
|
||||
}
|
||||
for (const key of ambiguous) {
|
||||
index.delete(key);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +75,10 @@ export const HOST_TRANSVERSE_TOLERANCE_PC = 0.01;
|
||||
const CATALOGUE_EPOCH = 2000.0;
|
||||
const ARCHIVE_LATEST_EPOCH = 2016.0;
|
||||
|
||||
function knownMotion(masPerYear: number | undefined): number {
|
||||
return Number.isFinite(masPerYear) ? (masPerYear as number) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-references an exoplanet host star to the star catalogue: first by (normalized) name,
|
||||
* then on the sky — the nearest star within {@link HOST_TRANSVERSE_TOLERANCE_PC} whose distance
|
||||
@@ -96,8 +120,11 @@ export function resolveHostStarId(
|
||||
const carriedBack = propagateProperMotion(
|
||||
query.raDeg,
|
||||
query.decDeg,
|
||||
query.pmRaMasPerYear ?? 0,
|
||||
query.pmDecMasPerYear ?? 0,
|
||||
// A proper motion that is not a number must read as "stands still", not poison the
|
||||
// comparison: one NaN makes every star's cosine NaN, and `NaN < min` is false, so every
|
||||
// star would pass the direction test and the last one in array order would win.
|
||||
knownMotion(query.pmRaMasPerYear),
|
||||
knownMotion(query.pmDecMasPerYear),
|
||||
CATALOGUE_EPOCH - ARCHIVE_LATEST_EPOCH
|
||||
);
|
||||
const carried = raDegDecDistanceToXyz(carriedBack.raDeg, carriedBack.decDeg, 1);
|
||||
|
||||
Reference in New Issue
Block a user