Tier 1 data enrichment: catalogue, brightness, rich detail panel - #7
Conversation
Build an enrichment pipeline (CelesTrak SATCAT + GCAT + McCants qs.mag) that produces a same-origin, lazily-joined catalogue, and surface it in the app as a rich detail panel, a brightness badge, and a new Catalogue browser. Pipeline (scripts/enrich/, Node, no deps): - SATCAT is the load-bearing base set; GCAT adds mass/dims/shape/orbit/ operator/status; mmccants adds intrinsic magnitude (vendored — upstream qsmag.zip 404s; freeware licence). - Per-field precedence with _sources provenance stamping; 12-month decay window (SATCAT DECAY_DATE only — GCAT DDate also marks assembly/renaming). - GCAT joins on its Satcat=NORAD column (98.3% join); outputs a lean index plus NORAD/1000 buckets, a manifest, and generated SOURCES.md. - Fail-safe validation gate aborts before deploy on broken input. Client: - src/enrichment.js: lazy bucket fetch + brightness bucketing. - Selection panel gains a Catalogue section, brightness badge, source line. - New Catalogue tab: search + type/brightness filters over ~36k objects, detail view, and Show-in-3D for loaded objects. Deploy: enrich.yml rebuilds daily and redeploys fresh data, sharing static.yml's pages concurrency group; committed data/ is the seed. Bump static.yml checkout v4 -> v6. See WORKLOG.md and docs/ for findings and open questions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The collapse button hid .panel-body/.panel-section generically, but reentry mode force-shows #reentry-section via an id-bearing rule (body.mode-reentry #reentry-section) that outranks it — so collapsing only hid the id-less "Find a satellite" section. Add a matching-specificity rule so the watch list collapses too, leaving just the header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mmccants measures almost none of the large constellations (0 Starlink/OneWeb magnitudes in qs.mag), leaving the brightness badge blank for most of what users click. Add a fallback that assigns one representative magnitude per constellation bus, applied ONLY when there is no measured value. Estimates are always surfaced as estimates: magSource 'estimate', magBasis naming the constellation, a "~" prefix and dashed badge, an "Est. magnitude" readout row, and "estimate" in the sources line. Starlink is launch-date-split to reflect the brightness drop after the mid-2020 VisorSat / dielectric coatings: pre-visor v0.9/v1.0 (before 2020-06) get ~4.5, coated sats ~5.5. Covers 12,842 objects (11,381 Starlink); 179 early pre-visor Starlink remain and get the brighter estimate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughAdds a Node-based enrichment pipeline that combines satellite sources, writes NORAD-bucketed data, and deploys the site through GitHub Pages. Adds lazy catalogue loading, brightness classification, catalogue search, filters, object details, source attribution, documentation, and generated enrichment records. ChangesEnrichment catalogue
Estimated code review effort: 4 (Complex) | ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 256e177d13
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!s) return null; | ||
| const t = s.trim().replace(/[?~*]/g, ''); | ||
| if (!t || t === '-') return null; | ||
| const v = parseFloat(t); |
There was a problem hiding this comment.
Treat zero-valued GCAT physical fields as missing
When GCAT uses 0 for an unknown physical measurement, num() accepts it as real and the nullish fallback also stops at that zero. In the committed catalogue this produces 12,635 objects with a displayed mass of 0 kg and 12,417 with dimensions such as L 0 m · Ø 0 m · span 0 m, rather than omitting unavailable values; it can also prevent a useful Mass or DryMass value from being selected when TotMass is zero.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 861d7f8. Added a positive-only posNum() and use it for mass/length/diameter/span, so GCAT's 0 sentinel is treated as missing — no more 0 kg/Ø 0 m, and the ?? fallback now falls through a zero TotMass to a real Mass/DryMass.
Generated by Claude Code
| const rec = await getEnrichment(norad); | ||
| if (!rec) { detail.innerHTML = '<p class="subtle">No catalogue record for this object.</p>'; return; } |
There was a problem hiding this comment.
Discard stale catalogue detail responses
When a user quickly selects rows from different uncached buckets, the requests can resolve out of order, and this response is rendered without confirming that its NORAD ID is still selected. A slower first request can therefore overwrite the second row's detail and make “Show in 3D” target the wrong object while the active highlight remains on the second row; add the same request/selection guard already used by showEnrichment().
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 861d7f8. selectCatRow() now sets a catReqNorad request token and bails with if (catReqNorad !== norad) return; after the fetch resolves — the same guard showEnrichment() uses — so a stale response can no longer overwrite a newer row's detail or mis-target "Show in 3D".
Generated by Claude Code
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (32)
data/enrichment/69.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject the contradictory lifecycle record for NORAD
69729.
decayDateis set to"2026-06-26"andopsStatusis"decayed", butstatusremains"in-orbit". Since the decay is before the current date, this record should be treated as decayed; normalise these fields during merge/validation, fail the build when they disagree, and regenerate this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/69.json` at line 1, Update the enrichment merge/validation logic for NORAD 69729 so a past decayDate such as 2026-06-26 normalizes opsStatus to decayed and status to the decayed state instead of in-orbit. Add validation that fails the build when lifecycle fields remain contradictory, then regenerate data/enrichment/69.json with the corrected record.data/enrichment/9.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject records with inconsistent launch dates before publishing.
Line 1 contains conflicting dates:
- NORAD
9703: COSPAR1973-086GB,launchDate1977-12-21.- NORAD
9799: COSPAR1976-126Z,launchDate1971-02-25.- NORAD
9954: COSPAR1976-126AU,launchDate1971-02-25.Fix the source join or the upstream records. Add validation that the COSPAR year matches the
launchDateyear before generating the shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/9.json` at line 1, Reject records whose COSPAR year differs from the launchDate year before publishing the enrichment shard. Add validation to the shard-generation flow that parses the year from cospar and launchDate, excludes inconsistent records such as NORAD 9703, 9799, and 9954, and then corrects the source join or upstream data so valid records remain included.scripts/enrich/http.mjs-26-56 (1)
26-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a timeout to the
fetch()call.
getText()callsfetch(url, { headers, redirect: 'follow' })with no timeout. If CelesTrak or GCAT hangs or responds slowly, this call can block indefinitely.
satcat.mjsuses this helper for the load-bearing source. A hang here stalls the entire scheduled enrichment build, since there is no time limit set on the call.Add a bounded timeout with
AbortSignal.timeout(...)so a slow upstream fails fast instead of hanging the job.⏱️ Proposed fix to bound the fetch call
- const res = await fetch(url, { headers, redirect: 'follow' }); + const res = await fetch(url, { headers, redirect: 'follow', signal: AbortSignal.timeout(30_000) });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/enrich/http.mjs` around lines 26 - 56, Update the fetch call in getText to include a bounded AbortSignal.timeout(...) option alongside the existing headers and redirect settings. Use the project’s intended timeout duration if already defined; otherwise introduce an appropriate finite timeout so slow upstream requests fail promptly while preserving the current response and caching flow.scripts/enrich/write.mjs-51-65 (1)
51-65: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winWrite new bucket files before deleting stale ones.
At lines 54-56, every existing
*.jsonfile indata/enrichment/is deleted before the new bucket files are written at lines 63-65. If the process is interrupted between these two loops,catalog-index.json(already written at line 50) will reference NORAD ids whose bucket file was deleted but not yet rewritten. Client lookups for those ids will then fail even though the index lists them.Write the new bucket files first, and delete only the files that are not part of the new bucket set, afterwards. This removes the window where the index and the bucket files disagree.
🗂️ Proposed fix: write before delete
- // 2. Full records, bucketed by NORAD/1000. Clear stale buckets first so a - // shrunk catalogue never leaves orphans behind. - for (const f of await readdir(enrichDir).catch(() => [])) { - if (f.endsWith('.json')) await rm(join(enrichDir, f)); - } const buckets = new Map(); for (const r of all) { const b = bucketOf(r.norad); if (!buckets.has(b)) buckets.set(b, {}); buckets.get(b)[r.norad] = r; } + const keep = new Set([...buckets.keys()].map((b) => `${b}.json`)); for (const [b, obj] of [...buckets].sort((a, z) => a[0] - z[0])) { await writeFile(join(enrichDir, `${b}.json`), stringify(obj)); } + // Clear stale buckets only after every new bucket has been written, so a + // shrunk catalogue never leaves orphans behind and a crash mid-run never + // leaves the index pointing at a missing file. + for (const f of await readdir(enrichDir).catch(() => [])) { + if (f.endsWith('.json') && !keep.has(f)) await rm(join(enrichDir, f)); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/enrich/write.mjs` around lines 51 - 65, Update the bucket-writing flow after the bucket map is built to write all new bucket JSON files before removing any existing files. Track the generated bucket filenames, then delete only existing JSON files not present in that new set; keep catalog-index.json and the existing bucket ordering unchanged.scripts/enrich/sources/gcat.mjs-67-75 (1)
67-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winFix the GCAT
Status→ lifecycle mapping for deep-space transition codes.
lifecycle()maps anyDprefix to'decayed', but GCAT treats theStatuscolumn as a phase-status code and uses deep-space codes such asDSOandDSAfor objects that leave the Earth-bound catalog for heliocentric or Lagrange trajectories. Records such as IMAP, SWFO-L1, CARRUTHERS, and their rocket bodies now carry"status":"decayed"despite"opsStatus":"operational", which contradicts their real trajectory.Narrow the
Dbranch to genuine re-entry/decay phase codes, and handle deep-space transition codes separately rather than folding them into'decayed'.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/enrich/sources/gcat.mjs` around lines 67 - 75, Update lifecycle() so D-prefixed deep-space transition statuses such as DSO and DSA are handled separately and do not return 'decayed'. Restrict the decayed mapping to genuine re-entry or decay phase codes, while preserving the existing orbit, landed, empty, and unknown-status behavior.data/enrichment/66.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftFix the cross-source join for NORAD IDs
66898and66903.These records identify
STARLINK-36065andSTARLINK-36077with COSPAR IDs2025-283Wand2025-283AB, but uselaunchDate: "2026-07-07", ownersAXELandENDUR, and non-Starlink dimensions and masses. This mixes catalogue identity with fields from another payload.Correct the source join or adapter, then regenerate the shard. Do not patch this generated file manually. Add validation for COSPAR year, launch date, owner, dimensions, and mass consistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/66.json` at line 1, Fix the cross-source join or adapter producing NORAD records 66898 and 66903 so STARLINK-36065 (2025-283W) and STARLINK-36077 (2025-283AB) retain consistent Starlink catalogue fields rather than unrelated launch dates, owners, dimensions, or masses. Add validation covering COSPAR year, launch date, owner, dimensions, and mass consistency, then regenerate the affected shard instead of editing the generated JSON directly.data/enrichment/67.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftCorrect the launch-date outliers before publishing.
Records
67250and67251uselaunchDate:"2025-11-28", but the surrounding2025-313*records fromVOSTOuse2025-12-28. These records share the same COSPAR launch designator, so the two dates are inconsistent and will expose incorrect catalogue data. Correct the source records, regeneratedata/enrichment/67.json, and add a launch-group consistency check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/67.json` at line 1, Correct the source records for NORAD entries 67250 and 67251 to use launchDate 2025-12-28, then regenerate data/enrichment/67.json so the generated records reflect the corrected dates. Add a consistency check that validates matching COSPAR launch groups, including the surrounding 2025-313* VOSTO records, share the same launch date before publishing.data/enrichment/58.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject lifecycle-inconsistent records before publishing this shard.
Records
58015,58049,58050,58307,58752, and58964are markedstatus:"decayed"withoutdecayDate, so their 12-month decay window cannot be checked. Record58049also has bothstatus:"decayed"andopsStatus:"operational".Update the validation stage to require
decayDateon decoded records and reject contradictory lifecycle states, then regenerate this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/58.json` at line 1, Update the shard validation stage for decoded records to require a valid decayDate whenever status is "decayed", and reject contradictory lifecycle combinations such as status "decayed" with opsStatus "operational". Apply this validation before publishing the shard, then regenerate data/enrichment/58.json so records 58015, 58049, 58050, 58307, 58752, and 58964 are corrected or excluded.data/enrichment/10.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftExclude stale decayed records from the published shard.
Line 1 contains old records such as NORAD
10017and10509withstatus:"decayed"but nodecayDate.scripts/enrich/merge.mjstherefore does not apply the 12-month filter to them, so they remain in the catalogue despite the active/recently-decayed policy. Update the merge contract to exclude or separately classify decayed records without a valid current decay date, then regenerate this shard and its manifest.This follows the PR objective for a 12-month decay window and the supplied
scripts/enrich/merge.mjs:33-106contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/10.json` at line 1, Update the merge contract in scripts/enrich/merge.mjs, particularly the filtering/classification logic between the existing decay-date handling boundaries, so records with status "decayed" and no valid current decayDate are excluded or separately classified rather than published. Preserve inclusion only for active records and decayed records within the 12-month policy, then regenerate data/enrichment/10.json and its manifest from the corrected merge output.data/enrichment/11.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject mismatched COSPAR and launch-date years before publication.
scripts/enrich/validate.mjsonly checks row counts and GCAT join fraction. As written,scripts/enrich/merge.mjslets GCATlaunchDateoverride SATCAT dates when joined on the same NORAD id, so records such as11006(1978-079C/1979-12-07) and11792(1966-096C/1979-12-24) can be published with inconsistent launch metadata. Add a validation gate that rejects records where GCAT-sourcedlaunchDatehas a different year from SATCAT-sourcedcosparunless the source evidence explicitly supports the correction.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/11.json` at line 1, Add a validation gate in scripts/enrich/validate.mjs for merged records produced by scripts/enrich/merge.mjs: compare the year from GCAT-sourced launchDate with the year encoded in SATCAT-sourced cospar, and reject mismatches before publication. Permit an exception only when the record’s source evidence explicitly supports the corrected launch date, while preserving existing row-count and GCAT join validations.data/enrichment/12.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftEnforce the decay window before writing this shard.
Records
12255,12732,12927, and12938havestatus:"decayed"but nodecayDate. The build cannot prove that these records fall within the stated 12-month decay window. They can remain in the catalogue and generated record counts indefinitely.Reject decayed records without a valid
decayDate, apply the cutoff to that date, then regenerate the shard and manifest. Add validation fixtures for missing and expired decay dates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/12.json` at line 1, The enrichment shard includes decayed records without valid, window-checked decay dates. Update the shard-generation validation to reject records with status "decayed" when decayDate is missing or invalid, exclude records whose decayDate falls outside the 12-month cutoff, then regenerate this shard and its manifest and add fixtures covering missing and expired decay dates.data/enrichment/13.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftEnforce the 12-month decay window for decayed objects.
merge.mjscurrently only usesdecayDateto filter decayed records. Records such as13104,13105,13152,13413,13580, and13901have"status":"decayed"but nodecayDate, so they are not checked against the documented 12-month retention rule and can remain in the catalogue. Update the merge/build validation to reject decayed objects without a trusteddecayDate, or apply an explicit documented fallback before regenerating the shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/13.json` at line 1, Update the merge/build validation in merge.mjs to enforce the 12-month retention rule for every object with status decayed, including records lacking decayDate. Reject such records unless they have a trusted decayDate within the allowed window, or implement the project’s explicitly documented fallback before regenerating the enrichment shard.data/enrichment/61.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winCorrect the cross-assigned Galileo alternate names.
NORAD
61182is namedGSAT0232 (GALILEO 32)but listsGSAT0226. NORAD61183is namedGSAT0226 (GALILEO 31)but listsGSAT0232. This will make catalogue search and detail views identify the wrong object.Swap the two
GSAT...aliases and recheck the remaining Galileo aliases against the source before regenerating the shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/61.json` at line 1, Correct the cross-assigned GSAT aliases for NORAD entries 61182 and 61183: ensure GSAT0232 is associated with GALILEO 32 and GSAT0226 with GALILEO 31. Verify the remaining Galileo aliases against the source, then regenerate the enrichment shard while preserving its existing schema and metadata.data/enrichment/62.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject contradictory lifecycle states before publishing the shard.
data/enrichment/62.jsoncontains incompatible lifecycle values:
62460and62717havestatus:"decayed"andopsStatus:"operational".62643hasopsStatus:"decayed"andstatus:"in-orbit".62485is anotherstatus:"decayed"withopsStatus:"operational"case.This pattern also appears across the enrichment shards, so add validation or correct precedence in the enrichment pipeline, then regenerate the outputs rather than editing this generated file manually.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/62.json` at line 1, The enrichment pipeline must reject or resolve contradictory lifecycle states before publishing generated shard outputs. Update the validation or precedence logic handling status and opsStatus so decayed objects cannot remain operational or in-orbit, including records like 62460, 62717, 62643, and 62485; then regenerate all affected enrichment shards instead of editing data/enrichment/62.json manually.data/enrichment/64.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject contradictory lifecycle states before publishing the shard.
Line 1 contains conflicting records for NORAD 64197 (
TIANWEN-2) and NORAD 64527 (COSMOS 2590). Both records setstatusto"decayed"butopsStatusto"operational"and omitdecayDate.This can show an object as both decayed and operational in catalogue filters and detail views. Fix the merge or validation logic, then regenerate
data/enrichment/64.json. Do not patch only this generated file.#!/bin/bash set -euo pipefail for file in data/enrichment/*.json; do jq -e ' [ to_entries[] | .value | select( .status == "decayed" and (.opsStatus != "decayed" or (.decayDate | not)) ) ] | length == 0 ' "$file" >/dev/null done🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/64.json` at line 1, Update the merge or validation logic that produces enrichment shard records so any object with status "decayed" also has opsStatus "decayed" and a decayDate, rejecting contradictory records before publication. Ensure this applies to records such as TIANWEN-2 and COSMOS 2590, then regenerate the generated enrichment shard rather than editing it directly.data/enrichment/18.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject cross-source launch-date and COSPAR mismatches before publishing.
Line 1 contains records where the COSPAR year differs from
launchDate:18257(1987-006Cvs1985-09-04),18290(1986-019RYvs1975-01-22), and18608(1985-077Kvs1987-06-09). Their_sourcesvalues showcosparfromsatcatandlaunchDatefromgcat, which can corrupt date-based filters and launch-time calculations. Add validation for this invariant, fix the join/precedence logic, and regenerate the shard. Allow only documented schema exceptions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/18.json` at line 1, The enrichment data includes records whose COSPAR year conflicts with launchDate, notably NORAD records 18257, 18290, and 18608. Add validation to reject cross-source COSPAR/launchDate mismatches before publishing, correct the join or field-precedence logic so both values come from a consistent source, preserve only documented schema exceptions, and regenerate the affected shard.data/enrichment/4.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject historical decayed objects without
decayDate.
data/enrichment/4.jsonincludes objects such as4040,4041,4226,4366, and4368with"status":"decayed"but nodecayDate, so the build cannot place them in the 12-month window. Fail closed in the merge/validation step whendecayDateis missing and regenerate the shard set, rather than publishing stale records indata/enrichment/*.json.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/4.json` at line 1, Reject records with status "decayed" when decayDate is missing during the merge/validation step, failing the build rather than emitting them into enrichment shards. Apply this validation to all historical objects, then regenerate the complete data/enrichment shard set so stale decayed records such as 4040, 4041, 4226, 4366, and 4368 are removed.data/enrichment/40.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftApply the 12-month decay window to records without
decayDate.This shard contains records such as
40319,40320,40321,40322,40323,40390, and40391with"status":"decayed"but nodecayDate. The build cannot prove that these records fall within the configured 12-month window, so they can remain in the deployed catalogue indefinitely. Update merge validation to exclude decayed records with an unknown date, or define a bounded fallback, then regenerate this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/40.json` at line 1, Update the merge-validation logic that filters decayed records so entries with status "decayed" and no decayDate are excluded from the deployed catalogue, or use a bounded fallback date within the configured 12-month window. Preserve inclusion of records with a known decayDate only when they satisfy the existing window, then regenerate the affected enrichment shard.data/enrichment/44.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject contradictory lifecycle states before generating the shard.
Records
44432,44441, and44625have"status":"decayed"with"opsStatus":"operational"and no"decayDate", while other decayed records in the shard have"opsStatus":"decayed"plus a decay date. This makes the catalogue show objects as both decayed and operational.Fix the merge or validation rule that produces these records. Require
"decayDate"for decayed records and resolve conflicting operational status values before regeneratingdata/enrichment/44.json.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/44.json` at line 1, Reject contradictory lifecycle states during the enrichment merge or validation step before shard generation: records with status “decayed” must include decayDate and use a non-operational opsStatus consistent with other decayed records. Update the responsible merge/validation rule, then regenerate data/enrichment/44.json so records 44432, 44441, and 44625 no longer remain operational without decay dates.data/enrichment/22.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftCorrect the shifted
altNamesmapping before publishing this shard.Line 1 assigns
IO-26to NORAD22827(HEALTHSAT 1). NORAD22826isITAMSAT (IO-26), but itsaltNamesvalue is missing even though_sources.altNamesisgcat. CelesTrak mapsIO-26to NORAD22826, not22827. (celestrak.org)Fix the source or merge assignment, add an invariant test for alternate-name ownership, and regenerate
data/enrichment/22.json. Do not edit the generated shard manually.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/22.json` at line 1, Correct the source or merge assignment that currently places “IO-26” on NORAD 22827 so it belongs to NORAD 22826, ITAMSAT, while preserving its _sources.altNames value from gcat. Add an invariant test ensuring alternate names are assigned to the correct NORAD object, then regenerate data/enrichment/22.json through the established generation workflow rather than editing the generated shard directly.data/enrichment/45.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject conflicting lifecycle states before publishing this shard.
Line 1 contains records such as
45110,45167,45169,45170,45171,45610,45918, and45935withopsStatus: "operational"butstatus: "decayed". These records also omitdecayDate. The catalogue can therefore show the same object as operational and decayed, with no timestamp to resolve the conflict.Define the contract between
opsStatusandstatus, then reconcile the source values or reject the record during validation. Regeneratedata/enrichment/45.jsonfrom the corrected merge output. Do not edit this generated file manually.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/45.json` at line 1, Define and enforce the lifecycle-state consistency contract in the enrichment merge/validation pipeline so records cannot publish opsStatus "operational" with status "decayed", especially when decayDate is absent. Reconcile conflicting source values or reject invalid records before shard generation, then regenerate data/enrichment/45.json from the corrected merge output rather than editing it manually.data/enrichment/46.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winResolve the conflicting lifecycle state for NORAD 46113.
MEV-2is markedstatus:"decayed"with nodecayDate, whileopsStatus:"operational". This inconsistent lifecycle record can appear in the generated catalogue. MakestatusandopsStatusconsistent, or reject this shape during generation, then regenerate the shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/46.json` at line 1, Resolve the NORAD 46113 record for MEV-2 so its lifecycle fields are consistent: do not retain status "decayed" without a decayDate alongside opsStatus "operational". Update the source/generation data or validation responsible for this record, then regenerate the affected shard and verify the corrected entry.data/enrichment/26.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftCorrect the generated state for NORAD 26400 before deployment.
Record
26400identifiesISS (ZVEZDA)asopsStatus:"operational"butstatus:"decayed", with nodecayDate. NASA updates dated April 16, 2026 and May 27, 2026 still describe Zvezda as part of the operational ISS. (nasa.gov)Fix the source precedence or validation rule, then regenerate
data/enrichment/26.json. Do not patch this generated shard manually.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/26.json` at line 1, The generated record for NORAD 26400 has the invalid combination of operational opsStatus and decayed status without a decayDate. Update the source precedence or validation rule governing generated enrichment records, using the NORAD record-generation and validation symbols, so ISS (ZVEZDA) remains operational; then regenerate data/enrichment/26.json rather than editing the shard directly.Source: MCP tools
data/enrichment/28.json-1-1 (2)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftResolve the conflicting lifecycle state for NORAD
28646.This record contains
"status":"decayed"and"opsStatus":"operational"._sourcesshows that the conflict comes from GCAT and SATCAT. Reconcile the values during merging, or fail validation before regenerating the shard. Otherwise, the catalogue can present a decayed object as operational.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/28.json` at line 1, Resolve the conflicting lifecycle fields for NORAD 28646 before finalizing the enrichment shard: do not retain status decayed alongside opsStatus operational. Reconcile the GCAT and SATCAT values using the merge/validation flow, or reject the record during validation so regeneration cannot publish contradictory lifecycle state.
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject decayed records without
decayDatebefore writing the shard.
merge.mjsonly applies the 12-month decay cutoff whendecayDateis present, so NORAD objects markedstatus: "decayed"without that date still pass intodata/enrichment/28.json; examples include28159,28646,28788, and28928. Either reject these records during validation/build or explicitly classify their decay window, then regenerate the shard and manifest.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/28.json` at line 1, Reject records with status "decayed" when decayDate is missing during the validation or build flow before shard output is written; update the relevant merge/validation logic rather than modifying individual records. Preserve the existing 12-month decay cutoff for records that include decayDate, then regenerate data/enrichment/28.json and its manifest.data/enrichment/50.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject contradictory decay metadata before publishing the shard.
Records
50463and50464havestatus: "decayed"but nodecayDate. Record50463also hasopsStatus: "operational". These values cannot reliably support the stated 12-month decay window and can show an object as decayed without a usable decay date.Update the merge or validation stage to require a valid
decayDatefor decayed records and to resolve the50463status conflict. Then regenerate this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/50.json` at line 1, Reject or correct contradictory decay metadata in the enrichment merge/validation stage before publishing the shard: any record with status “decayed” must have a valid decayDate, and record 50463 must no longer combine status “decayed” with opsStatus “operational.” Apply the resolved metadata consistently, then regenerate the shard data.data/enrichment/52.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject decayed records without a
decayDate.NORAD
52914(CAPSTONE) and NORAD52916(LUNAR PHOTON) have"status":"decayed"but nodecayDate. The 12-month decay filter cannot determine whether these records are within the retention window. NORAD52914also reports"opsStatus":"operational", which creates contradictory catalogue data.Require a valid
decayDateduring merge or validation, or encode and handle a documented exception for non-Earth-orbit objects before publishing this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/52.json` at line 1, Reject or correct decayed records lacking a valid decayDate during the enrichment merge or validation flow, specifically NORAD 52914 (CAPSTONE) and 52916 (LUNAR PHOTON), before publishing this shard. Resolve NORAD 52914’s contradictory opsStatus value as part of the same handling, or add a documented non-Earth-orbit exception that the 12-month decay filter explicitly supports.data/enrichment/29.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject undated decayed records before writing enrichment shards.
scripts/enrich/merge.mjsonly applies the 12-month decay cutoff whendecayDateexists. This shard contains decayed records withoutdecayDate, including29244,29321,29326,29406,29431,29474,29510,29511, and29652. If build/validation does not explicitly reject or exclude them, they can still be written todata/enrichment/29.jsonand enter the published catalogue.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/29.json` at line 1, Update the enrichment merge flow in scripts/enrich/merge.mjs to reject or exclude decayed records lacking decayDate before shard output is written. Ensure records such as 29244, 29321, 29326, 29406, 29431, 29474, 29510, 29511, and 29652 cannot enter data/enrichment/29.json, while preserving the existing 12-month cutoff for records with decayDate.data/enrichment/32.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject the contradictory decay status for NORAD
32474.This record has
decayDate: "2026-07-25"butstatus: "in-orbit". July 25, 2026 is before the current date, August 1, 2026. The catalogue can therefore display a decayed object as in orbit.Make the merge or validation stage force
status: "decayed"whendecayDateis in the past, or reject the record. Regenerate this shard instead of editing the generated JSON directly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/32.json` at line 1, The generated shard contains a contradictory NORAD 32474 record whose past decayDate is paired with status "in-orbit". Update the upstream merge or validation stage to force status "decayed" whenever decayDate is before the current date, or reject the record, then regenerate this shard rather than editing the JSON directly.data/enrichment/33.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReconcile
statuswithdecayDatebefore publishing enrichment shards.
data/enrichment/33.jsoncontains records such as33901wheredecayDateis2026-07-29butstatusremainsin-orbit. Records with present or past decay dates should not be reported as in orbit. Apply this rule acrossdata/enrichment/*.json, update the lifecycle derivation/merge step, regenerate the shards, and reject inconsistent records before merge.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/33.json` at line 1, Reconcile lifecycle fields in the enrichment derivation/merge step so any record with a present decayDate on or before the applicable reference date cannot retain status “in-orbit”; derive the appropriate decayed status consistently across all enrichment shards. Add validation that rejects inconsistent records before merge, then regenerate every data/enrichment shard, including 33.json, with corrected values.data/enrichment/35.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not ship
status:"decayed"records withoutdecayDate.
data/enrichment/35.jsonincludes decay-cycle inconsistencies:35315is bothstatus:"decayed"andopsStatus:"operational"with nodecayDate, while35539,35568,35787,35939, and35952are decayed with missing decay dates. The 12-month window needs a date to validate those retained records. Make the build fail closed, exclude decayed records without a validated date, or reconcile the lifecycle source before writing this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/35.json` at line 1, The enrichment shard contains records marked status "decayed" without a validated decayDate, including conflicting lifecycle values. Before writing data/enrichment/35.json, fail closed or exclude every decayed record lacking a valid decayDate, and reconcile conflicting status/opsStatus values such as record 35315 so no invalid decayed records are shipped.data/enrichment/38.json-1-1 (1)
1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftReject decayed records without a decay date.
Record
38104has"status":"decayed"but nodecayDateor decay-date provenance. As at 1 August 2026, the 12-month retention rule requires a date on or after 1 August 2025. Require a validdecayDateduring validation, or exclude38104, then regenerate this shard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/enrichment/38.json` at line 1, Remove record 38104 from the enrichment shard, or provide a valid decayDate with corresponding _sources.decayDate provenance meeting the 12-month retention cutoff of 1 August 2025, then regenerate the shard while preserving the existing validation rules.
🧹 Nitpick comments (2)
index.html (1)
43-43: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
role="tab"on a dialog-opening button is a semantic mismatch.
#mode-cataloguesits inrole="tablist"alongside#mode-tracker/#mode-reentry, which switch content in place. This button instead opens a modal dialog (aria-haspopup="dialog") and itsaria-selectedstate is toggled independently inmain.js. Assistive technology announcing it as a tab in a 3-tab set is misleading, since selecting it does not select a tab panel — it opens an overlay on top of everything. Use a plain button role witharia-controlspointing at the dialog instead.♻️ Proposed fix
- <button id="mode-catalogue" role="tab" aria-selected="false" aria-haspopup="dialog">Catalogue</button> + <button id="mode-catalogue" type="button" aria-haspopup="dialog" aria-controls="catalogue">Catalogue</button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@index.html` at line 43, Update the `#mode-catalogue` button to remove role="tab" and its tab-selection semantics, and add aria-controls referencing the catalogue dialog element. Preserve its existing dialog-opening behavior and ensure the referenced dialog ID matches the actual dialog markup.src/main.js (1)
460-469: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCatalogue dialog has no focus trap or focus-return.
The dialog sets
aria-modal="true"(index.html), but nothing here enforces the modal focus contract. Keyboard users can Tab out of the open dialog into the (visually hidden, but still focusable) page behind it, and focus is not returned to#mode-cataloguewhencloseCatalogue()runs. OnlyEscapeis handled.Extend the
keydownhandler to trapTab/Shift+Tabwithin#catalogue, and restore focus to the opener button on close.♻️ Proposed focus-trap addition
document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && catalogueOpen) closeCatalogue(); + if (!catalogueOpen) return; + if (e.key === 'Escape') { closeCatalogue(); return; } + if (e.key === 'Tab') { + const focusables = Array.from($('catalogue').querySelectorAll( + 'a[href], button:not([disabled]), input, select, [tabindex]:not([tabindex="-1"])', + )); + if (!focusables.length) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } + else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } + } });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main.js` around lines 460 - 469, Extend the catalogue keydown handler to keep Tab and Shift+Tab cycling among focusable elements within `#catalogue` while catalogueOpen, preventing focus from reaching the background page. Update closeCatalogue to return focus to `#mode-catalogue` after closing, while preserving the existing Escape behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 12250f9f-5291-4807-ab37-73a0bacb20e8
📒 Files selected for processing (94)
.github/workflows/enrich.yml.github/workflows/static.yml.gitignoreSOURCES.mdWORKLOG.mddata/catalog-index.jsondata/enrichment/0.jsondata/enrichment/1.jsondata/enrichment/10.jsondata/enrichment/100.jsondata/enrichment/11.jsondata/enrichment/12.jsondata/enrichment/13.jsondata/enrichment/14.jsondata/enrichment/15.jsondata/enrichment/16.jsondata/enrichment/17.jsondata/enrichment/18.jsondata/enrichment/19.jsondata/enrichment/2.jsondata/enrichment/20.jsondata/enrichment/21.jsondata/enrichment/22.jsondata/enrichment/23.jsondata/enrichment/24.jsondata/enrichment/25.jsondata/enrichment/26.jsondata/enrichment/27.jsondata/enrichment/28.jsondata/enrichment/29.jsondata/enrichment/3.jsondata/enrichment/30.jsondata/enrichment/31.jsondata/enrichment/32.jsondata/enrichment/33.jsondata/enrichment/34.jsondata/enrichment/35.jsondata/enrichment/36.jsondata/enrichment/37.jsondata/enrichment/38.jsondata/enrichment/39.jsondata/enrichment/4.jsondata/enrichment/40.jsondata/enrichment/41.jsondata/enrichment/42.jsondata/enrichment/43.jsondata/enrichment/44.jsondata/enrichment/45.jsondata/enrichment/46.jsondata/enrichment/47.jsondata/enrichment/48.jsondata/enrichment/49.jsondata/enrichment/5.jsondata/enrichment/50.jsondata/enrichment/51.jsondata/enrichment/52.jsondata/enrichment/53.jsondata/enrichment/54.jsondata/enrichment/55.jsondata/enrichment/56.jsondata/enrichment/57.jsondata/enrichment/58.jsondata/enrichment/59.jsondata/enrichment/6.jsondata/enrichment/60.jsondata/enrichment/61.jsondata/enrichment/62.jsondata/enrichment/63.jsondata/enrichment/64.jsondata/enrichment/65.jsondata/enrichment/66.jsondata/enrichment/67.jsondata/enrichment/68.jsondata/enrichment/69.jsondata/enrichment/7.jsondata/enrichment/8.jsondata/enrichment/9.jsondata/manifest.jsondocs/data-enrichment-schema.mddocs/enrichment-build-job.mdindex.htmlscripts/enrich/build.mjsscripts/enrich/constellations.mjsscripts/enrich/http.mjsscripts/enrich/merge.mjsscripts/enrich/sources/gcat.mjsscripts/enrich/sources/mmccants.mjsscripts/enrich/sources/satcat.mjsscripts/enrich/validate.mjsscripts/enrich/vendor/qs.magscripts/enrich/write.mjssrc/enrichment.jssrc/main.jsstyles/main.css
…detail - gcat: treat GCAT's 0-valued physical fields (mass/length/diameter/span) as missing via a positive-only `posNum()`. GCAT stores 0 as a "no measurement" sentinel, so `num()` was surfacing "0 kg" / "Ø 0 m" and, through the ?? fallback, blocking a real value in a later column. - main: guard `selectCatRow()` against out-of-order detail responses with a `catReqNorad` request token, matching `showEnrichment()`. A slow first fetch could otherwise overwrite a newer row's detail and point "Show in 3D" at the wrong object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V39eEU8VDuM2WpzKhNgBg
PR #7's review fixed gcat.mjs to treat GCAT's 0-valued physical fields as "no measurement" (posNum), but the committed seed data/ was not regenerated — so the checked-in catalogue still showed "0 kg" / "Ø 0 m" for 12,635 records until the next enrich.yml cron run. Regenerate it now. - massKg===0: 12,635 -> 0; dimensions containing a 0: 12,544 -> 0 - record count unchanged (36,174); real values preserved (ISS 20,351 kg) - diff is data/ enrichment buckets + manifest only (index carries no mass/dims) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a satellite enrichment catalogue — richer per-object data than orbital position alone — served same-origin and joined lazily by NORAD id, plus the Tier 1 UI to browse and read it.
What's here
Build pipeline (
scripts/enrich/, Node, zero deps)qs.magadds intrinsic magnitude (vendored — upstreamqsmag.zip404s; recovered from Wayback; freeware licence)._sourcesprovenance stamping; rolling 12-month decay window.Satcat=NORAD column (98.3% join); emits a lean index, NORAD/1000 buckets, a manifest, and a generatedSOURCES.md.Per-constellation magnitude fallback (
constellations.mjs)~prefix, dashed badge, "Est. magnitude",estimatein sources).Client (Tier 1 UI)
Deploy
enrich.ymlrebuilds daily and redeploys fresh data, sharingstatic.yml'spagesconcurrency group; committeddata/is the seed. Bumpedstatic.ymlcheckout@v4 → v6.Also in this PR
Verified
First real build: 36,174 records, GCAT 98.3%, magnitudes 3,015 (+12,842 estimated); index 4.8 MB → 276 KB gzip. Both UI surfaces verified in-browser (ISS full record incl. mag −2.5 "Naked eye"; Starlink degrades cleanly / shows estimated badge).
Notes for review
docs/data-enrichment-schema.mdanddocs/enrichment-build-job.mdare the design;WORKLOG.mdrecords findings and decisions made during the build (GCAT join key, the ISS decay-date bug, magnitude sourcing).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation