a81dd49 added a query of public.hipparcos_newreduction on the ESA archive, cached as
hipparcos-errors-<hash>.csv, and fetchStars requires it: a failed or short answer throws. The
refresh workflow carries only tools/etl/.cache/gaia-dr3-*.csv between runs, a glob that file does
not match, so every weekly run fetched its 117 955 rows live from the archive the cache exists to
spare, and an ESA outage would have failed the refresh even with every Gaia answer cached. Before
a81dd49 a warm cache meant no request to that archive at all.
The cache step now lists both globs. Its key gains "-hipparcos": actions/cache keys are immutable,
and an entry already saved under the old key would be restored without the new file and never
saved again. Checked with Node's path.matchesGlob against the local cache (gaia-dr3-54fdbc7a,
gaia-dr3-hip-785b92fc and hipparcos-errors-f846b045 match, the archive's own files do not) and by
parsing the workflow with PyYAML. A workflow file has no unit test to fail without the change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 adversarial review confirmed three defects in this PR, all reproduced in
the browser.
1. Superseded graphs queued up in front of routes. The worker answers
messages one at a time and cannot drop one it has started. With the
jump-link layer on, every pause on the range slider posted a full graph
build, seconds of work at 6-8 pc. Answers no longer wanted were thrown
away only once built. A route asked for afterwards waited behind every
one of them: a one-jump route took 44 s.
RoutingClient now holds requests and sends them one at a time. While one
is out, only the latest of each kind waits: a newer graph replaces an
older one before it is ever built, and the older promise is rejected with
SupersededRequest. Routes go ahead of graphs. The same question asked
again while outstanding shares the answer rather than being worked twice,
as when the layer is turned off and on during a build.
The same scenario in the browser (layer on, range stepped 5 -> 8 pc with
400 ms pauses, then Sol to Proxima): the route came back in 110 ms. The
worker was sent "links 3, links 5, route, links 8"; 6 and 7 were never
built.
2. A worker that failed left the panel stuck. With no error handling, a
worker that failed to load (a 404 on its chunk after a redeploy) or
threw left "Plotting…" and a disabled button for good, and a graph at a
range could not be asked for again.
The worker now answers an exception with a 'failed' message, which
rejects that request. A worker that fails to load or dies is abandoned,
and what it left outstanding, and everything asked afterwards, is
answered in place. The scene releases the panel when a route fails, and
forgets a graph range that was never drawn so it can be asked for again.
3. Nothing type-checked the worker. The application builder never reads
webWorkerTsConfig, and bundles the worker with esbuild, which strips
types without checking them. tsconfig.app.json leaves the file out. A
type error in the worker shipped.
`npm run worker:typecheck` (tsc -p tsconfig.worker.json) now runs in CI
beside the other project checks. webWorkerTsConfig is removed from
angular.json, since it only suggested that something checked the worker.
Tests with a fake worker cover one request at a time, a waiting graph
replaced and a route sent ahead of it, a question shared, a failure rejected
and the next request sent, and a failed worker's requests answered in place.
A scene test covers the panel released after a failed route. Negative
controls, each caught: several requests sent at once, a waiting graph kept,
graphs ahead of routes, a question asked twice, a failure answered as a
success, a failed worker waited on, the panel left pending, and a type error
in the worker (caught by worker:typecheck).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
HYG and Gaia were both cut at 250 pc, each on its own distance. A star
Hipparcos put at 200 pc and Gaia at 300 was kept by the first, never
downloaded from the second, and drawn at 200. That is where 83% of the
9 691 mid-magnitude HYG stars without a Gaia counterpart came from, and at
the median Hipparcos had them a third too close. The mirror case, Hipparcos
outside and Gaia inside, dropped the HYG row and left its Gaia entry
anonymous.
Gaia's own Hipparcos cross-match (hipparcos2_best_neighbour, a fixed DR3
table of 99 525 rows) gives a usable Gaia distance for 97 751 of them.
placementDistancePc keeps a star either survey puts inside the cutoff, and
draws every kept star at the better measurement, inside the cutoff or not.
57 121 HYG stars now sit at Gaia's distance. 6 833 of them are past 250 pc:
Zet Per 230 -> 259 pc, 35 Ori 137 -> 330, 44 Cnc 223 -> 613, and the
farthest, HIP 69445, at 8.7 kpc. 3 666 stars that Hipparcos put outside are
now kept, and 3 656 of them give a Gaia entry its name.
The cross-match is required rather than skipped when unreachable. Without
it, every one of those stars would move back to its Hipparcos distance, and
the published map would flip with the archive's availability. The ESA TAP
answered it with a 500 at first and in 102 s on the next try. So fetches
now retry 5xx and network failures twice, after 30 s and 120 s, in the
fetch every source goes through. The refresh job also carries the Gaia DR3
responses from run to run in the Actions cache: the release is frozen, and
a live re-fetch has already reproduced stars.bin byte for byte.
423 651 stars (+10), 61 168 HYG rows folded into Gaia entries (+3 656),
351 597 unnamed designations (-3 656). 10 886 HYG survivors and 23 unmerged
pairs under an arcsecond, both inside the merge gate's ceilings. The same
1 972 exoplanets have a host; KELT-4 A b and MWC 758 c now sit on their
named star.
The HUD's "Radius" becomes "Survey radius": 250 pc is where Gaia is
surveyed to, and no longer the edge of the map.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
With the merge gate in place, a Gaia DR3 outage no longer ships a HYG-only
catalogue: the ETL skips the unreachable source, and validateMerge then fails
on the survivor count. That is the right outcome and the wrong message: "68 000
HYG stars found no Gaia counterpart" sends the reader looking at the merge.
Gaia contributing nothing is now checked first, by name.
Two comments said Gaia was best-effort, in data-refresh.yml and on the merge
in fetchStars. They now say what happens instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
Junie has been reviewing pull requests here since the key was added, and eight
of the last eight runs succeeded. What it was not, was configured like the
sibling repositories, and three of those differences are worth closing.
The action was referenced as `@v1`. It is the only third-party action in this
repository and the only one handed a repository secret, so its definition
should not be able to change under us. `v1` and `v1.7.5` resolve to the same
commit today — the pin is not about which code runs now, it is about who gets
to decide that later.
There was no timeout. A run that goes wrong hangs rather than stops, and the
pull request shows a pending check until Actions gives up on its own six hours
later. Forty-five minutes, not the thirty the siblings use: the longest review
this repository has actually had ran thirty-five, on the largest diff so far,
and a ceiling that cuts a successful review short is worse than none.
And a pull request closed and reopened had had no review since it was closed.
Left alone deliberately: the absent-key step, which says so in the run summary
instead of failing a pull request for a reason that has nothing to do with its
code, and the lack of a `branches:` filter — work here stacks feature onto
feature, and filtering on main would skip every pull request in a chain but the
last.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
Mondays 05:23 UTC: re-run the ETL cold against the live archives, and if the
output differs by a byte, gate it on the unit suite and a production build,
commit it to main, and dispatch the Pages deploy. If nothing changed, say so
in the run summary and touch nothing.
The gates run inside this workflow because they cannot run after it: a push
made with GITHUB_TOKEN fires no push workflows at all — GitHub's recursion
guard — so an unguarded push would deploy nothing and be checked by nothing.
The same guard is why the deploy and a visible CI record are dispatched
explicitly afterwards; dispatch events do go through where push events do
not. ci.yml gains a workflow_dispatch trigger for exactly that call.
Also corrects pages.yml's claim that configure-pages enables Pages on first
run. It cannot: the action's `enablement` input requires an admin-scoped
token, which GITHUB_TOKEN is not. If the site has never been enabled, the
first deploy fails at that step and the one-time fix is Settings → Pages →
Source: GitHub Actions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
Builds on a push to main and deploys the result, so the thing is reachable
without checking it out and running a dev server.
Two details a project site needs that a root deployment does not:
The base href is set from the repository name at build time. The app is served
from a subdirectory there, and `DataLoaderService` fetches its catalogues with
relative URLs — those resolve against `<base>` rather than the current path, so
without it a deep link would ask for /body/assets/data/stars.bin.
Pages serves a static tree with no rewrite rules, so /body/mars has no file
behind it and returns 404. Answering that 404 with the app lets the router
render the route. The status stays 404, which crawlers will notice and readers
will not; the alternative is hash URLs, which everyone notices.
Verified against a server that mimics both behaviours: the root loads the star
field with all six catalogue assets at 200, and /body/mars boots through
404.html and renders Mars at 3,390 km with its assets still resolving.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
`develop` became the integration branch when #3 merged into it, but CI's push
trigger still named only `main` — so the merge commit itself ran nothing. A
pull request is checked before the merge, not after, which leaves the state of
the branch people actually build from unverified whenever two green pull
requests conflict semantically.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
The workflow as written failed on ddf805e: with no JUNIE_API_KEY secret the
input expands to empty and the action exits on "Missing required input",
marking the pull request failed for a reason that has nothing to do with its
code. I called it inert without the key. It was not inert; it was red, and it
would have been red on every pull request until someone added the secret.
So gate the steps on the key's presence and write the reason into the run
summary instead. `secrets` is not a context a step's `if` can read and neither
`secrets` nor `env` is available to a job-level `if`, so the presence is
resolved once into a job-level env var, which steps can read.
The action step also gets continue-on-error: an outage or a rate limit at
JetBrains' end is worth seeing in the log, but this workflow is meant to be an
opinion beside CI rather than a gate in front of it, and a failure to obtain
that opinion should not hold a pull request whose tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
CI says whether the code works. Nothing says whether it reads well, and the
one review this repository has had so far arrived by hand.
Kept as a separate workflow rather than a third job in ci.yml so a review can
never turn the build red — the two answer different questions and should be
able to disagree.
It skips drafts, and skips pull requests from forks: GitHub withholds secrets
from `pull_request` runs on a forked head, so the job would fail on a missing
JUNIE_API_KEY rather than say anything about the code. Each push supersedes
the previous review rather than stacking another comment beside it.
Requires a JUNIE_API_KEY repository secret, generated at
junie.jetbrains.com/cli. Without it the workflow is inert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
Nothing has ever checked this project automatically. There are no workflows,
so the pull request's green tick means only that the checks were run by hand
on one machine, and nothing would catch a regression pushed later.
The lockfile had to come first. `npm ci` is the only install that guarantees
CI tests the dependency tree that is actually committed, and it refuses to
run without package-lock.json — which was gitignored. Un-ignoring it also
pins the 617 packages this was built and verified against; without it, a
transitive release could change what CI runs from one day to the next with
no commit to point at. Checked before committing: every entry resolves to
registry.npmjs.org, and it carries no credentials.
Two jobs rather than one, run in parallel. The typecheck/unit/build job is
fast and deterministic; the end-to-end job drives a real headless browser
through WebGL2 software rendering and is the one that will be slow and, if
anything here is going to be flaky, flaky. Keeping them apart means a
browser timeout cannot hide a failing unit test behind it.
Between the four steps, all four TypeScript projects are compiled: the ETL
and end-to-end configs explicitly, since nothing else ever builds them, and
the spec and app configs by `ng test` and `ng build` respectively.
One thing this cannot verify from here: the runner installs Chromium to
match the pinned Playwright, where this container ships an older build. The
suite was run locally against that older browser instead, and passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G