From 65d2def00a3dae58bb8e3b78039ca4c5e1dd1d27 Mon Sep 17 00:00:00 2001 From: Hanzalah Ravat Date: Thu, 2 Apr 2026 21:59:22 +0100 Subject: [PATCH 1/2] fix(spk): correct de442s parsing and state chaining --- src/spk/index.ts | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/spk/index.ts b/src/spk/index.ts index 2991849..60ace0f 100644 --- a/src/spk/index.ts +++ b/src/spk/index.ts @@ -147,8 +147,8 @@ export class SpkKernel { const svSunSsb = evaluateSegment(this.buffer, sSunSsb, et, this.le) const svEmbSsb = evaluateSegment(this.buffer, sEmbSsb, et, this.le) const svEarthEmb = evaluateSegment(this.buffer, sEarthEmb, et, this.le) - // Earth/SSB = EMB/SSB - Earth/EMB - const earthSsb = subtractSV(svEmbSsb, svEarthEmb) + // Earth/SSB = EMB/SSB + Earth/EMB because the kernel stores 399:3. + const earthSsb = addSV(svEmbSsb, svEarthEmb) return subtractSV(svSunSsb, earthSsb) } } @@ -192,9 +192,11 @@ function parseDafFileRecord(buffer: ArrayBuffer): { } const ni = dv.getInt32(12, le) - const fward = dv.getInt32(256, le) - const bward = dv.getInt32(260, le) - const free = dv.getInt32(264, le) + // DAF file record layout stores the forward/backward/free record numbers + // directly after the 60-byte internal file name, not at byte 256. + const fward = dv.getInt32(76, le) + const bward = dv.getInt32(80, le) + const free = dv.getInt32(84, le) return { nd, ni, fward, bward, free, le } } @@ -279,9 +281,8 @@ export function evaluateType2( const intlen = dv.getFloat64(endOffset - 3 * BYTES_PER_DOUBLE, le) const init = dv.getFloat64(endOffset - 4 * BYTES_PER_DOUBLE, le) - // degree = (rsize - 2) / 3 (Type 2 stores 3 components) - const degree = Math.round((rsize - 2) / 3) - const nCoeffs = degree + 1 + // Type 2 record size is RSIZE = 3 * NCOEFF + 2. + const nCoeffs = Math.round((rsize - 2) / 3) let recIdx = Math.floor((et - init) / intlen) recIdx = Math.max(0, Math.min(Math.round(N) - 1, recIdx)) @@ -320,8 +321,8 @@ export function evaluateType3( const intlen = dv.getFloat64(endOffset - 3 * BYTES_PER_DOUBLE, le) const init = dv.getFloat64(endOffset - 4 * BYTES_PER_DOUBLE, le) - const degree = Math.round((rsize - 2) / 6) - const nCoeffs = degree + 1 + // Type 3 record size is RSIZE = 6 * NCOEFF + 2. + const nCoeffs = Math.round((rsize - 2) / 6) let recIdx = Math.floor((et - init) / intlen) recIdx = Math.max(0, Math.min(Math.round(N) - 1, recIdx)) @@ -374,6 +375,21 @@ function subtractSV(a: StateVector, b: StateVector): StateVector { } } +function addSV(a: StateVector, b: StateVector): StateVector { + return { + position: [ + a.position[0] + b.position[0], + a.position[1] + b.position[1], + a.position[2] + b.position[2], + ], + velocity: [ + a.velocity[0] + b.velocity[0], + a.velocity[1] + b.velocity[1], + a.velocity[2] + b.velocity[2], + ], + } +} + // ─── Leap-second kernel ─────────────────────────────────────────────────────── /** From ed221cfd409839eed0fdb5c8bae82afcb6dc4bd1 Mon Sep 17 00:00:00 2001 From: Hanzalah Ravat Date: Thu, 2 Apr 2026 21:59:26 +0100 Subject: [PATCH 2/2] test(spk): cover kernel-backed London sighting report --- test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test.mjs b/test.mjs index 1737102..3f1d40e 100644 --- a/test.mjs +++ b/test.mjs @@ -415,3 +415,18 @@ describe('Input validation', () => { assert.throws(() => getMoon(new Date(), 0, 200), /longitude/) }) }) + +describe('kernel-backed sighting pipeline', () => { + it('computes London sunset and moonset for 2025-03-29', async () => { + await initKernels() + + const observer = { lat: 51.5, lon: -0.1, elevation: 0 } + const report = await getMoonSightingReport(new Date('2025-03-29T00:00:00Z'), observer) + + assert.ok(report.sunsetUTC instanceof Date, `report.sunsetUTC=${report.sunsetUTC}`) + assert.ok(report.moonsetUTC instanceof Date, `report.moonsetUTC=${report.moonsetUTC}`) + assert.ok(report.bestTimeUTC instanceof Date, `report.bestTimeUTC=${report.bestTimeUTC}`) + assert.equal(report.yallop?.category, 'F') + assert.equal(report.odeh?.zone, 'D') + }) +})