From 003b92f64a60af2f4681863f974a777bdc89ea2e Mon Sep 17 00:00:00 2001 From: VG31OP Date: Tue, 13 Oct 2026 16:57:55 +0530 Subject: [PATCH 1/3] fix(satellites): reject TLE pairs with mismatched catalog numbers (#906, #936) --- CHANGELOG.md | 7 ++++--- src/sources/tle.js | 15 +++++++++++---- src/tools/queries/space.test.mjs | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24ac95ed..83b32baaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,9 +95,10 @@ ## [Unreleased] -- Keep later satellites when a TLE catalog entry is missing an orbital line. - The parser skips the incomplete set and resumes at the next complete named - set, instead of losing the rest of the group (#906). +- Keep later satellites when a TLE catalog entry is missing an orbital line or + pairs lines with mismatched NORAD catalog numbers. The parser skips corrupt sets + and resumes at the next complete named set, instead of losing the rest of the + group (#906, #936). - Add an experimental ChatGPT/Codex OAuth option for cloud voice in Provider Settings. API-key voice remains the default. Local sign-in can start from diff --git a/src/sources/tle.js b/src/sources/tle.js index ac7aedc2c..8f97e4eda 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -13,7 +13,14 @@ export function parseTleText(text) { const name = lines[i]; const line1 = lines[i + 1]; const line2 = lines[i + 2]; - if (line1.startsWith('1 ') && line2.startsWith('2 ')) { + const cat1 = tleCatalogNumber(line1); + const cat2 = tleCatalogNumber(line2); + if ( + line1.startsWith('1 ') && + line2.startsWith('2 ') && + cat1 !== null && + cat1 === cat2 + ) { result.push({ name, line1, line2 }); i += 2; } @@ -21,8 +28,8 @@ export function parseTleText(text) { return result; } -/** The NORAD catalog number from TLE line 1, or null. */ -export function tleCatalogNumber(line1) { - const number = Number.parseInt(String(line1).slice(2, 7), 10); +/** The NORAD catalog number from a TLE line, or null. */ +export function tleCatalogNumber(line) { + const number = Number.parseInt(String(line).slice(2, 7), 10); return Number.isInteger(number) ? number : null; } diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index f6c2a8df8..2b2586b45 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -51,6 +51,23 @@ for (const missingLine of [1, 2]) { }); } +test('TLE parsing rejects pairs with mismatched catalog numbers and resumes', () => { + const text = `SAT A +1 25544U 98067A 24001.50000000 .00016717 00000-0 30270-3 0 9994 +2 48274 41.4697 175.8913 0005643 312.8370 141.6893 15.61287398150000 +SAT B +1 48274U 21035A 24001.50000000 .00020137 00000-0 24481-3 0 9993 +2 48274 41.4697 175.8913 0005643 312.8370 141.6893 15.61287398150000 +`; + const entries = parseTleText(text); + assert.deepEqual( + entries.map((entry) => entry.name), + ['SAT B'], + ); + assert.equal(tleCatalogNumber(entries[0].line1), 48274); + assert.equal(tleCatalogNumber(entries[0].line2), 48274); +}); + test('the next pass defaults to the ISS and reports times, peak and direction', async () => { const catalog = composeCatalog({ tools: coreTools, From 40442ab85444d526588b59d67764b796404c1ebb Mon Sep 17 00:00:00 2001 From: VG31OP Date: Sat, 17 Oct 2026 17:01:13 +0530 Subject: [PATCH 2/3] style: fix whitespace formatting in tle.js and space.test.mjs --- src/sources/tle.js | 1 - src/tools/queries/space.test.mjs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/sources/tle.js b/src/sources/tle.js index feeb2f94b..e46266244 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -60,4 +60,3 @@ export function tleCatalogNumber(line) { const number = Number.parseInt(field, 10); return Number.isInteger(number) ? number : null; } - diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index ad7287004..dcde81cba 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -85,7 +85,6 @@ SAT ALPHA5 assert.equal(tleCatalogNumber(entries[0].line2), 100000); }); - test('the next pass defaults to the ISS and reports times, peak and direction', async () => { const catalog = composeCatalog({ tools: coreTools, From df5ad4ca4a9e1606334ae787bfddc3ddddf7b36b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 17 Oct 2026 13:46:00 -0700 Subject: [PATCH 3/3] Pair TLE lines through the shared decoder and add the changelog fragment --- changelog.d/1101-tle-catalog-number-match.md | 3 +++ docs/CURRENT-STATE.md | 5 ++++- src/tools/queries/space.test.mjs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1101-tle-catalog-number-match.md diff --git a/changelog.d/1101-tle-catalog-number-match.md b/changelog.d/1101-tle-catalog-number-match.md new file mode 100644 index 000000000..d1e2aa407 --- /dev/null +++ b/changelog.d/1101-tle-catalog-number-match.md @@ -0,0 +1,3 @@ +- Skip TLE sets whose line 1 and line 2 carry different catalog numbers, so a + spliced catalog cannot pair one satellite's elements with another's; the next + valid set still parses (VG31OP, #1101). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 458c44c6e..6d066a475 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1474,7 +1474,10 @@ access. Callers retain validation, transport and response policy. The shared TLE text parser skips incomplete named sets and resumes at the next complete set. A missing orbital line does not discard the later satellites in -the catalog; satellite layers and next-pass tools use the same parser. +the catalog. A set whose two lines carry different catalog numbers, or a +malformed catalog number, is skipped the same way, so a spliced catalog cannot +pair one satellite's elements with another's. Satellite layers and next-pass +tools use the same parser. ## Build configuration and local provider boundaries diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index 5b0fa5837..0eb5ce346 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -90,6 +90,21 @@ SAT ALPHA5 assert.equal(tleCatalogNumber(entries[0].line2), 100000); }); +test('TLE parsing rejects a pair whose catalog fields are malformed and resumes', () => { + // Read as digits up to the first non-digit, both fields would be 2554. + const text = `SAT MALFORMED +1 2554AU 98067A 24001.50000000 .00016717 00000-0 30270-3 0 9994 +2 2554B 51.6416 247.4627 0006703 130.5360 325.0288 15.50377579432414 +SAT HALF MALFORMED +1 2554AU 98067A 24001.50000000 .00016717 00000-0 30270-3 0 9994 +2 02554 51.6416 247.4627 0006703 130.5360 325.0288 15.50377579432414 +${TLE}`; + assert.deepEqual( + parseTleText(text).map((entry) => entry.name), + ['ISS (ZARYA)', 'CSS (TIANHE)'], + ); +}); + test('Alpha-5 catalog numbers decode, skipping I and O', () => { for (const [field, id] of [ ['A0000', 100000],