Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/full-data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
description: Authorized URL for the SHA-pinned production dataset
required: false
type: string
fagan_bradley_dataset_url:
description: Authorized URL for the historical website dataset (SHA 06d4c26e...)
required: false
type: string
regenerate:
description: Acquire official JPL inputs and regenerate twice
required: true
Expand Down Expand Up @@ -42,6 +46,20 @@ jobs:
bun install --frozen-lockfile
bun run build:wasm
bun run verify "$RUNNER_TEMP/production.bin" "$RUNNER_TEMP/conventions-wasm.json"
cargo build --locked --release --manifest-path ../../Cargo.toml -p dataset-builder
bun prepare-segments.ts "$RUNNER_TEMP/production.bin" "$RUNNER_TEMP/segments"
bun verify-segments.ts "$RUNNER_TEMP/production.bin" "$RUNNER_TEMP/segments"
- if: inputs.fagan_bradley_dataset_url != ''
env:
DATASET_URL: ${{ inputs.fagan_bradley_dataset_url }}
run: |
curl --fail --location --proto '=https' "$DATASET_URL" --output "$RUNNER_TEMP/fagan-bradley.bin"
FAGAN_BRADLEY_DATASET="$RUNNER_TEMP/fagan-bradley.bin" cargo test --locked --test fagan_bradley_legacy -- --include-ignored
cargo install wasm-bindgen-cli --version 0.2.122 --locked
cd examples/configurable-chart
bun install --frozen-lockfile
bun run build:wasm
bun verify-fagan-bradley.ts "$RUNNER_TEMP/fagan-bradley.bin"
- if: inputs.regenerate
run: |
python -m pip install --require-hashes -r tools/jpl/requirements.lock
Expand Down
35 changes: 35 additions & 0 deletions docs/website-regressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Regression ownership after the website migration

The library owns numerical calculations, native/Wasm consistency and extraction
parity. Website CI owns its adapter presets and state, segment selection/loading,
course/UI code and packaged asset delivery. Website releases do not run this
repository's verification workflows.

`tests/true_sky_source.rs` preserves the original True Sky source fixture from the
website. It runs with `cargo test --locked --workspace` without external data.

The eight historical Fagan-Bradley charts are preserved byte-for-byte in
`tests/fixtures/fagan-bradley-71350f0.json`. They were captured from engine revision
`71350f0889c3b5e1585935d2017c97efe0bb5848`, before the chart API migration.
Their dataset SHA-256 is
`06d4c26e589207bf479dbd217fafb3e4d6c559626425b156912e39a4d859487b`.
This differs from the dataset used by `tests/fixtures/conventions.json`.
Use authorized local bytes or the explicit full-data workflow's separate
`fagan_bradley_dataset_url` input; no private website credential belongs here.

```sh
FAGAN_BRADLEY_DATASET=/absolute/path/cheb.bin cargo test --locked --test fagan_bradley_legacy -- --include-ignored
cd examples/configurable-chart
bun install --frozen-lockfile
bun run build:wasm
bun verify-fagan-bradley.ts /absolute/path/cheb.bin
```

Both runtimes compare numbers within an absolute tolerance of 1e-12 and compare
all other captured values and ordering exactly. Tiny arithmetic rounding is
allowed; fixtures must never be regenerated to hide an implementation change.
Comparator negative controls reject changes above tolerance and reordered bodies.

The explicit full-data workflow also prepares segments and runs
`verify-segments.ts` against its production dataset. Full-data checks remain
manual, separate from normal source CI and from independent astronomical accuracy.
37 changes: 37 additions & 0 deletions examples/configurable-chart/verify-fagan-bradley.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Jack Asher
// SPDX-License-Identifier: MPL-2.0
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import init, { calculate } from './engine/chart.js';

const hash = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex');
const compare = (actual: unknown, expected: unknown): void => {
if (typeof actual === 'number' && typeof expected === 'number') {
assert.ok(Number.isFinite(actual) && Math.abs(actual - expected) <= 1e-12, `${actual} != ${expected}`);
} else if (actual !== null && expected !== null && typeof actual === 'object' && typeof expected === 'object') {
assert.equal(Array.isArray(actual), Array.isArray(expected));
if (Array.isArray(actual) && Array.isArray(expected)) assert.equal(actual.length, expected.length);
assert.deepEqual(Object.keys(actual).sort(), Object.keys(expected).sort());
for (const key of Object.keys(expected)) compare((actual as Record<string, unknown>)[key], (expected as Record<string, unknown>)[key]);
} else assert.equal(actual, expected);
};
compare(29.288207831506444, 29.288207831506437);
assert.throws(() => compare(1, 1.00000001));
assert.throws(() => compare({'0': 1}, [1]));
assert.throws(() => compare(['SUN', 'MOON'], ['MOON', 'SUN']));
const path = process.argv[2];
if (!path) throw new Error('Usage: bun verify-fagan-bradley.ts /path/to/historical-cheb.bin');
const fixture = await readFile(new URL('../../tests/fixtures/fagan-bradley-71350f0.json', import.meta.url));
assert.equal(hash(fixture), 'd70c88d0452efce0f0725a8baf8109992915b4221beed2ec1d75840bf3eb3c8a');
const source = JSON.parse(fixture.toString()) as {engineRevision: string; datasetSha256: string; cases: {input: [string, number, number, string]; chart: unknown}[]};
assert.equal(source.engineRevision, '71350f0889c3b5e1585935d2017c97efe0bb5848');
assert.equal(source.cases.length, 8);
const bytes = await readFile(path);
assert.equal(hash(bytes), source.datasetSha256, 'Use the historical dataset, not the conventions dataset');
await init({module_or_path: await readFile(new URL('./engine/chart_bg.wasm', import.meta.url))});
for (const {input: [utc, latitude, longitude, house_system], chart: expected} of source.cases) {
const chart = JSON.parse(calculate(bytes, JSON.stringify({instant: {scale:'utc', value:utc}, latitude, longitude, house_system, configuration: {reference:'fagan-bradley', divisions:'equal', ophiuchus:null}}))) as {bodies: {name: string; longitude: number; speed: number; declination: number}[]; cusps: number[]; reference_offset_degrees: number; house_system: string};
compare({bodies: chart.bodies.map(({name, longitude, speed, declination}) => ({name, longitude, speed, declination})), cusps: chart.cusps, ayanamsa_degrees: chart.reference_offset_degrees, house_system: chart.house_system}, expected);
}
console.log(JSON.stringify({runtime:'Wasm', cases:8, datasetSha256:source.datasetSha256, tolerance:1e-12}));
103 changes: 103 additions & 0 deletions tests/fagan_bradley_legacy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Jack Asher
// SPDX-License-Identifier: MPL-2.0

use astrology_engine::{calculate_chart, ChartInput, Ephemeris, Location, ZodiacConfiguration};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};

fn compare(actual: &Value, expected: &Value) {
match (actual, expected) {
(Value::Number(a), Value::Number(b)) => {
let delta = (a.as_f64().unwrap() - b.as_f64().unwrap()).abs();
assert!(
delta <= 1e-12,
"numeric regression: {a} != {b} (delta {delta})"
);
}
(Value::Array(a), Value::Array(b)) => {
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b) {
compare(a, b);
}
}
(Value::Object(a), Value::Object(b)) => {
assert_eq!(a.keys().collect::<Vec<_>>(), b.keys().collect::<Vec<_>>());
for (key, b) in b {
compare(&a[key], b);
}
}
_ => assert_eq!(actual, expected),
}
}

#[test]
fn comparison_bounds_rounding_without_hiding_regressions() {
compare(
&json!({"cusps": [29.288207831506444]}),
&json!({"cusps": [29.288207831506437]}),
);
assert!(std::panic::catch_unwind(|| compare(&json!(1.0), &json!(1.00000001))).is_err());
assert!(
std::panic::catch_unwind(|| compare(&json!(["SUN", "MOON"]), &json!(["MOON", "SUN"])))
.is_err()
);
}

#[test]
#[ignore = "requires FAGAN_BRADLEY_DATASET with the historical website dataset"]
fn historical_fagan_bradley_charts() {
let fixture = include_bytes!("fixtures/fagan-bradley-71350f0.json");
assert_eq!(
format!("{:x}", Sha256::digest(fixture)),
"d70c88d0452efce0f0725a8baf8109992915b4221beed2ec1d75840bf3eb3c8a"
);
let source: Value = serde_json::from_slice(fixture).unwrap();
assert_eq!(
source["engineRevision"],
"71350f0889c3b5e1585935d2017c97efe0bb5848"
);
let bytes =
std::fs::read(std::env::var("FAGAN_BRADLEY_DATASET").expect("set FAGAN_BRADLEY_DATASET"))
.unwrap();
assert_eq!(
format!("{:x}", Sha256::digest(&bytes)),
source["datasetSha256"].as_str().unwrap()
);
let ephemeris = Ephemeris::parse(bytes).unwrap();
let cases = source["cases"].as_array().unwrap();
assert_eq!(cases.len(), 8);
for case in cases {
let input = &case["input"];
let chart = calculate_chart(
&ephemeris,
ChartInput {
epoch: input[0].as_str().unwrap().parse().unwrap(),
location: Location {
latitude: input[1].as_f64().unwrap(),
longitude: input[2].as_f64().unwrap(),
},
house_system: serde_json::from_value(input[3].clone()).unwrap(),
zodiac: serde_json::from_value::<ZodiacConfiguration>(
json!({"reference":"fagan-bradley", "divisions":"equal", "ophiuchus":null}),
)
.unwrap(),
},
)
.unwrap();
let bodies: Vec<_> = chart
.bodies
.iter()
.map(|body| {
json!({
"name": body.values.name, "longitude": body.values.longitude,
"speed": body.values.speed, "declination": body.values.declination,
})
})
.collect();
compare(
&json!({"bodies": bodies, "cusps": chart.cusps, "ayanamsa_degrees": chart.reference_offset_degrees, "house_system": chart.house_system}),
&case["chart"],
);
}
println!("Compared 8 historical Fagan-Bradley charts at 1e-12 degree tolerance");
}
Loading
Loading