/** * astroengine canonical mode -- integer, hashable, precision-honest output. * * The engine is already deterministic run-to-run: identical inputs give * bit-identical doubles on one machine. What floating point does NOT give is * cross-platform bit-stability (IEEE 754 leaves the transcendentals to libm, * so V8, JSC, and Python can differ in the last ulp), stable serialization * (float repr differs across languages), and boundary coherence (one ulp can * flip a sign, a house, or an aspect at its orb limit). Canonical mode fixes * all three by quantizing every continuous quantity to an integer on a * declared grid and re-deriving the discrete facts from the quantized values. * * Three commitments make it canonical rather than decoratively integer: * * 1. **One rounding rule.** `quantizeUnit` is floor(x * scale + 0.5) with a * guard for the IEEE edge where the addition itself crosses the half -- * the same bridge the skyview golden uses for JS-vs-Python rounding. * Ties therefore round toward +infinity, which is also the tie-break * everywhere discrete: a longitude exactly on a sign or house boundary * belongs to the LATER sign/house, a speed of exactly 0 is not * retrograde. * 2. **Quantize, then derive.** Sign, sign-degree, house, dignities, and * the aspect list are recomputed from the quantized longitudes in * integer arithmetic, so the displayed numbers and the derived facts can * never disagree ("29 deg 59' 60'' Pisces" showing beside sign: Aries is * impossible by construction). * 3. **Floats cannot leak.** {@link canonicalEncode} throws on any * non-integer number, so a digest over a canonical value proves the * whole payload was quantized. * * {@link canonicalDigest} (sha256 over the canonical encoding, implemented * dependency-free so the browser tier works) gives content-addressable * charts: stable cache keys, dedupe, provenance receipts -- and a * tolerance-free cross-language pin: the TS and Python digests must be * EQUAL, not close (canonical-golden). * * The `"accuracy"` grid is "validated, not asserted" applied to the output * format: each body quantizes no finer than its measured accuracy * (accuracy.json), so canonical output never states precision the * validation does not support. * * Mirrors python/astroengine/canonical.py; pinned by canonical-golden. */ import type { Chart } from "./chart.js"; /** Round half toward +infinity, guarded against the IEEE edge where * `x + 0.5` itself rounds across the half (mirrors Python `_js_round`). */ export declare function roundHalfUp(x: number): number; /** Quantize a value to integer units of `1/scale` (e.g. scale 3600 turns * degrees into arcseconds). The single rounding rule of canonical mode. */ export declare function quantizeUnit(x: number, scale: number): number; /** Angular resolution presets. `"dms"` shares the arcsecond grid but renders * angles as `[deg, min, sec]` triples -- the tradition's own integer form. * `"accuracy"` snaps each body to its measured validated accuracy. */ export type CanonicalGrid = "arcsec" | "milliarcsec" | "centideg" | "dms" | "accuracy"; /** * Per-body accuracy quantum in arcseconds for the `"accuracy"` grid, * mirroring the measured bounds in accuracy.json rounded up to a whole * arcsecond quantum (Sun-Saturn <=1", Uranus <=1.9", Neptune <=4.6", * Moon <=2.5", Pluto <=3.4" on the pack, true node <=1', true Lilith <=3', * Uranians <=2.3", intp_apog degree-scale vs SE by construction). A body * not named quantizes at the default 1". */ export declare const ACCURACY_QUANTUM_ARCSEC: Record; /** Integer milliseconds since the J2000.0 epoch (JD 2451545.0 UT). The * double ulp of a modern Julian Day is ~40 microseconds, well inside the * millisecond grid. */ export declare function canonicalTimeMs(jdUt: number): number; /** * Canonical JSON: keys sorted, no whitespace, and ONLY integers, strings, * booleans, null, arrays, and plain objects. Any non-integer number throws -- * that is the enforcement making a canonical digest trustworthy: it proves * every quantity in the payload was quantized. */ export declare function canonicalEncode(value: unknown): string; /** SHA-256 hex digest of a UTF-8 string. */ export declare function sha256Hex(input: string): string; /** sha256 over the canonical encoding: the content address of any canonical * value. Throws (via {@link canonicalEncode}) if a float leaked in. */ export declare function canonicalDigest(value: unknown): string; export interface CanonicalOptions { /** Angular grid; defaults to `"arcsec"`. */ grid?: CanonicalGrid; /** Aspect orb overrides (degrees), merged over DEFAULT_ORBS -- pass the * same options the chart was computed with so the re-derived aspect list * matches intent. */ orbs?: Record; /** Aspect angle table; defaults to the five Ptolemaic ASPECTS. */ aspects?: Record; /** Aspect separation mode used for re-derivation (default "longitude"). */ separation?: "longitude" | "spatial"; } export interface CanonicalBody { lon: number | [number, number, number]; lat: number | [number, number, number]; speed: number; latSpeed: number | null; /** Micro-AU integer, or null (nodes, Lilith points, intp_apog). */ distMicroAu: number | null; ra: number | [number, number, number]; dec: number | [number, number, number]; /** Derived FROM the quantized longitude (tie-break: a boundary value * belongs to the later sign). */ sign: string; signDeg: number | [number, number, number]; house: number; /** Quantized speed < 0 (exactly 0 is direct). */ retrograde: boolean; dignities: string[]; } export interface CanonicalAspect { a: string; b: string; aspect: string; /** Orb in grid units. */ orb: number; phase: "applying" | "separating" | "exact"; /** Closeness in per-mille (1000 = exact), integer-derived. */ strengthPerMille: number; } export interface CanonicalChart { format: "caelus-canonical"; version: 1; grid: CanonicalGrid; /** Self-describing units, bound into the digest. */ units: { angle: string; speed: string; time: string; dist: string; strength: string; }; timeMs: number; zodiac: string; houseSystem: string; houseSystemRequested: string; bodies: Record; unavailable: string[]; angles: Record; cusps: Array; aspects: CanonicalAspect[]; warnings: Array>; } /** * Project a {@link Chart} into canonical (integer) form on a declared grid. * Every continuous quantity quantizes under the single rounding rule; sign, * sign-degree, house, dignities, retrograde, and the aspect list are then * re-derived from the quantized values in integer arithmetic, so displayed * numbers and discrete facts cannot disagree and one-ulp platform drift * cannot flip anything. * * @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}. * @param opts Grid (default `"arcsec"`) and the aspect options the chart was * computed with. * @returns The {@link CanonicalChart}; feed it to {@link canonicalDigest} * for a content address. */ export declare function canonicalChart(chart: Chart, opts?: CanonicalOptions): CanonicalChart; /** Content address of a chart: sha256 over the canonical encoding of * {@link canonicalChart}. Equal digests mean equal charts at the grid's * resolution -- across machines, browsers, and languages. */ export declare function chartDigest(chart: Chart, opts?: CanonicalOptions): string; /** Quantize a list of event instants (Julian Days, UT) to integer * milliseconds since J2000 -- nulls pass through (a polar sun that never * sets stays null). */ export declare function canonicalTimesMs(jds: Array): Array; /** Grids that can serve as refinement targets. A remainder grid must be * strictly finer than the base grid. */ export type RemainderGrid = "arcsec" | "milliarcsec"; export interface RefineRemainders { format: "caelus-remainders"; version: 1; mode: "refine"; /** Digest of the base payload this sidecar belongs to; {@link composeRemainders} * refuses a mismatch. */ for: string; grid: CanonicalGrid; remainderGrid: RemainderGrid; units: { angle: string; speed: string; time: string; dist: string; }; /** Flat path -> integer residue, one entry per frontier leaf (zeros kept, * so completeness is checkable). Angle residues are the signed shortest * modular distance, so a longitude at the 360-degree wrap stays small. */ values: Record; } export interface BitsRemainders { format: "caelus-remainders"; version: 1; mode: "bits"; for: string; grid: CanonicalGrid; units: { encoding: string; angle: string; speed: string; time: string; dist: string; }; /** Flat path -> big-endian hex of the pre-quantization IEEE 754 double. */ values: Record; } export type CanonicalRemainders = RefineRemainders | BitsRemainders; export interface RemainderOptions extends CanonicalOptions { /** Refinement target grid, `"bits"` for exact doubles, or omitted for one * step finer than the base grid. */ remainder?: RemainderGrid | "bits"; } /** IEEE 754 binary64 bit pattern of a double, big-endian, 16 hex chars. */ export declare function doubleBitsHex(x: number): string; /** Inverse of {@link doubleBitsHex} -- exact round-trip, `-0` included. */ export declare function doubleFromBitsHex(hex: string): number; /** * Canonical payload plus its remainder set. In `"refine"` mode (default: * one grid finer than the base) each residue is the integer difference * between the finer-grid quantization and the base leaf lifted onto the * finer grid, so `finer = ratio * base + residue` exactly and * {@link composeRemainders} can rebuild the finer payload. Time residues are * integer microseconds, distance residues integer nano-AU, always. In * `"bits"` mode each value is the pre-quantization double as big-endian hex * (lossless, platform-local, engine-API-only). */ export declare function canonicalChartWithRemainders(chart: Chart, opts?: RemainderOptions): { payload: CanonicalChart; remainders: CanonicalRemainders; }; /** * Rebuild the finer-grid canonical payload from a base payload and its * refinement remainder set: every frontier leaf is reconstructed as * `ratio * base + residue` (modular for angles), then sign, house, * dignities, retrograde, and the aspect list are re-derived in integer * arithmetic exactly as {@link canonicalChart} would at that grid. Pass the * same `orbs`/`aspects`/`separation` options the base was built with. * * The result equals `canonicalChart(chart, { grid: remainders.remainderGrid })` * field for field -- the telescoping property the canonical-golden pins. * Time stays integer ms and distance micro-AU in the composed payload (both * are grid-independent); their microsecond / nano-AU residues are precision * escrow readable straight off the sidecar. * * Throws if the sidecar is a bits set, binds to a different payload, or its * key set does not exactly match the payload's frontier. */ export declare function composeRemainders(payload: CanonicalChart, remainders: CanonicalRemainders, opts?: CanonicalOptions): CanonicalChart; export interface BoundaryFlag { path: string; /** The residue itself, in sidecar sub-quanta. */ residue: number; /** The base-grid quantum expressed in sidecar sub-quanta (per-body on the * "accuracy" grid; 1000 for time and distance leaves). */ quantum: number; /** floor(1000 * (quantum - 2|residue|) / quantum): 0 = exactly on a * rounding boundary, 1000 = dead center of the quantum. */ marginPerMille: number; /** +1: the value sits just below a boundary (a platform computing it a * hair higher rounds UP); -1: just above one; 0: dead center. */ side: -1 | 0 | 1; } /** * Fragility report from a refinement remainder set: frontier leaves whose * value landed close to a base-grid rounding boundary, i.e. the places where * sub-quantum drift on another platform could flip the quantized leaf (and * with it a sign, a house, or an aspect at its orb limit -- which is exactly * a digest change). Pure integer arithmetic; needs only the sidecar. * * @param opts.maxMarginPerMille report leaves with margin at or under this * (default 10, i.e. within 1% of the quantum of a boundary). */ export declare function nearBoundary(remainders: RefineRemainders, opts?: { maxMarginPerMille?: number; }): BoundaryFlag[];