PR-11: parity geometry classic (#482)#511
Conversation
|
orrery preview: https://pr-511.orrery-c4f.pages.dev/ |
There was a problem hiding this comment.
The native runner additions are functional but have two high-risk correctness/robustness problems: (1) repeated memory leaks on CSPICE failure paths due to goto done after failed_c() without freeing allocations, and (2) unsafe reliance on SpicePlane struct layout by directly reading/writing plane.normal and plane.constant, which can break across CSPICE versions. Separately, the new v2 method specs define empty result schemas (properties: {}), reducing the ability of parity to catch output-shape regressions. Addressing these will make the new geometry coverage much more stable and maintainable.
Additional notes (1)
- Maintainability |
packages/parity-checking/src/runners/tspiceRunner.ts:120-120
assertNumberArgonly checkstypeof value === "number", but many SPICE calls will behave poorly (or fail in surprising ways) withNaN/Infinity. This diff adds several new geometry entry points that acceptetand other numeric inputs; accepting non-finite numbers increases the risk of hard-to-debug parity failures.
You already enforce Number.isFinite in assertSpkPackedDescriptor; geometry should be held to the same standard.
Summary of changes
What changed
- Enabled classic geometry method parity coverage by removing these methods from the parity denylist:
geometry.subpnt,geometry.subslr,geometry.sincpt,geometry.ilumin,geometry.illumg,geometry.illumf,geometry.occult,geometry.nvc2pl,geometry.pl2nvc
- Extended the native CSPICE runner (
packages/parity-checking/native/src/cspice_runner.c):- Added new
CallIdenum entries andparse_call_idmappings for the geometry calls. - Implemented JSON arg parsing, CSPICE invocation, and JSON result serialization for each new geometry call.
- Added new
- Added v2 method specs under
packages/parity-checking/specs/methods/geometry/*@v2.ymlfor the new geometry APIs. - Added a legacy workflow definition:
workflows/legacy/geometry.basic@v1.yml. - Updated completeness baselines & tests to reflect increased coverage:
- Canonical covered methods:
156 → 165 - Denylist size:
17 → 8 - Updated assertions in
validateCompletenessand multiple test suites.
- Canonical covered methods:
- Updated
tspiceRunnerdispatch to support new geometry methods and added argument validators (assertPlane4).
| case CALL_GEOMETRY_NVC2PL: { | ||
| if (tokens[argsTok].size < 2) { | ||
| write_error_json_ex( | ||
| "invalid_args", | ||
| "geometry.nvc2pl expects args[0]=normal args[1]=konst", | ||
| NULL, | ||
| NULL, | ||
| NULL, | ||
| NULL); | ||
| goto done; | ||
| } | ||
|
|
||
| int normalTok = jsmn_get_array_elem(tokens, argsTok, 0, tokenCount); | ||
| int konstTok = jsmn_get_array_elem(tokens, argsTok, 1, tokenCount); | ||
|
|
||
| SpiceDouble normal[3]; | ||
| if (!jsmn_parse_vec3(input, tokens, normalTok, tokenCount, normal)) { | ||
| write_error_json_ex( | ||
| "invalid_args", | ||
| "geometry.nvc2pl expects args[0] to be a length-3 array of numbers", | ||
| NULL, | ||
| NULL, | ||
| NULL, | ||
| NULL); | ||
| goto done; | ||
| } | ||
|
|
||
| SpiceDouble konst = 0.0; | ||
| if (konstTok < 0 || konstTok >= tokenCount || jsmn_parse_double(input, &tokens[konstTok], &konst) != PARSE_OK) { | ||
| write_error_json_ex("invalid_args", "geometry.nvc2pl expects args[1] to be a number", NULL, | ||
| NULL, NULL, NULL); | ||
| goto done; | ||
| } | ||
|
|
||
| SpicePlane plane; | ||
| nvc2pl_c(normal, konst, &plane); | ||
|
|
||
| if (failed_c() == SPICETRUE) { | ||
| char shortMsg[1841]; | ||
| char longMsg[1841]; | ||
| char traceMsg[1841]; | ||
| capture_spice_error(shortMsg, sizeof(shortMsg), longMsg, sizeof(longMsg), traceMsg, | ||
| sizeof(traceMsg)); | ||
| write_error_json("SPICE error in nvc2pl", shortMsg, longMsg, traceMsg); | ||
| goto done; | ||
| } | ||
|
|
||
| SpiceDouble outPlane[4]; | ||
| outPlane[0] = plane.normal[0]; | ||
| outPlane[1] = plane.normal[1]; | ||
| outPlane[2] = plane.normal[2]; | ||
| outPlane[3] = plane.constant; | ||
|
|
||
| fputs("{\"ok\":true,\"result\":", stdout); | ||
| json_print_double_array(outPlane, 4); | ||
| fputs("}\n", stdout); | ||
| goto done; | ||
| } | ||
|
|
||
| case CALL_GEOMETRY_PL2NVC: { | ||
| if (tokens[argsTok].size < 1) { | ||
| write_error_json_ex( | ||
| "invalid_args", | ||
| "geometry.pl2nvc expects args[0]=plane", | ||
| NULL, | ||
| NULL, | ||
| NULL, | ||
| NULL); | ||
| goto done; | ||
| } | ||
|
|
||
| int planeTok = jsmn_get_array_elem(tokens, argsTok, 0, tokenCount); | ||
| SpiceDouble planeData[4]; | ||
| if (!jsmn_parse_double_array_fixed(input, tokens, planeTok, tokenCount, 4, planeData)) { | ||
| write_error_json_ex( | ||
| "invalid_args", | ||
| "geometry.pl2nvc expects args[0] to be a length-4 array of numbers", | ||
| NULL, | ||
| NULL, | ||
| NULL, | ||
| NULL); | ||
| goto done; | ||
| } | ||
|
|
||
| SpicePlane plane; | ||
| plane.normal[0] = planeData[0]; | ||
| plane.normal[1] = planeData[1]; | ||
| plane.normal[2] = planeData[2]; | ||
| plane.constant = planeData[3]; | ||
|
|
||
| SpiceDouble normal[3]; | ||
| SpiceDouble konst = 0.0; | ||
| pl2nvc_c(&plane, normal, &konst); | ||
|
|
||
| if (failed_c() == SPICETRUE) { | ||
| char shortMsg[1841]; | ||
| char longMsg[1841]; | ||
| char traceMsg[1841]; | ||
| capture_spice_error(shortMsg, sizeof(shortMsg), longMsg, sizeof(longMsg), traceMsg, | ||
| sizeof(traceMsg)); | ||
| write_error_json("SPICE error in pl2nvc", shortMsg, longMsg, traceMsg); | ||
| goto done; | ||
| } | ||
|
|
||
| fputs("{\"ok\":true,\"result\":{\"normal\":", stdout); | ||
| json_print_double_array(normal, 3); | ||
| fputs(",\"konst\":", stdout); | ||
| fprintf(stdout, "%.17g", (double)konst); | ||
| fputs("}}\n", stdout); | ||
| goto done; | ||
| } |
There was a problem hiding this comment.
CALL_GEOMETRY_NVC2PL serializes a SpicePlane by reading plane.normal[] and plane.constant directly. In CSPICE, SpicePlane is an opaque-ish type whose internal representation is not part of the public contract; accessing fields directly risks ABI/implementation breakage across CSPICE versions/build flags.
This is especially brittle because you later reconstruct a SpicePlane in pl2nvc by writing to those same fields, doubling down on the assumption of layout.
Suggestion
Avoid direct struct field access for SpicePlane. Prefer CSPICE helper APIs to convert between representations:
- For
nvc2pl: you already havenormalandkonst; just return those instead of the struct internals, or use a supported accessor (e.g.,pl2nvc_c) on the produced plane to extractnormal/konstfor serialization. - For
pl2nvc: accept{ normal, konst }(or two args) and callnvc2pl_cthenpl2nvc_c, or (better) update the wire format so you never have to materialize a plane from undocumented internals.
Concretely, safest is to define the JSON plane representation as { "normal": [..], "konst": n } and use only nvc2pl_c/pl2nvc_c to cross the boundary.
Reply with "@CharlieHelps yes please" if you'd like me to add a commit that changes the serialization format and updates the TS runner/specs accordingly.
| schemaVersion: 2 | ||
| manifest: | ||
| id: methods/geometry/sincpt@v2 | ||
| kind: method | ||
| contract: | ||
| contractMethod: geometry.sincpt | ||
| canonicalMethod: geometry.sincpt | ||
| aliases: [] | ||
| result: | ||
| type: object | ||
| properties: {} | ||
| setup: | ||
| kernels: | ||
| - $FIXTURES/basic-time | ||
| - $FIXTURES/ephemeris-de440s | ||
| defaults: | ||
| compare: | ||
| tolAbs: 1e-12 | ||
| tolRel: 1e-12 | ||
| workflow: | ||
| steps: | ||
| - op: invokeLegacyCall | ||
| cases: | ||
| - id: geometry.sincpt.ellipsoid-ray | ||
| args: | ||
| - ELLIPSOID | ||
| - MOON | ||
| - 0 | ||
| - IAU_MOON | ||
| - NONE | ||
| - EARTH | ||
| - J2000 | ||
| - - 1 | ||
| - 0 | ||
| - 0 | ||
| expect: | ||
| ok: true |
There was a problem hiding this comment.
The method specs declare contract.result with type: object and properties: {} for all newly added geometry calls. That effectively removes any schema-level checking of the result shape and can hide regressions (e.g., missing fields like found, spoint, etc.). Given these are parity tests, being explicit about expected result structure is valuable.
At minimum, include the top-level shape and required properties for stable outputs (and handle conditional outputs like sincpt's found).
Suggestion
Tighten the contract.result schemas for the new geometry specs. Example for sincpt:
contract:
result:
oneOf:
- type: object
required: [found]
properties:
found: { const: false }
- type: object
required: [found, spoint, trgepc, srfvec]
properties:
found: { const: true }
spoint:
type: array
minItems: 3
maxItems: 3
items: { type: number }
trgepc: { type: number }
srfvec:
type: array
minItems: 3
maxItems: 3
items: { type: number }Do similarly for subpnt/subslr (object with spoint, trgepc, srfvec), ilumin/illumg/illumf (numbers + arrays + booleans for illumf), occult (integer), and plane methods.
Reply with "@CharlieHelps yes please" if you'd like me to add a commit updating the YAML contracts for these methods.
| "geometry.pl2nvc": (backend, args) => { | ||
| assertPlane4(args[0], "geometry.pl2nvc args[0]"); | ||
| return backend.pl2nvc(args[0]); | ||
| }, |
There was a problem hiding this comment.
For geometry.pl2nvc, the runner accepts a length-4 array and treats it as a SpicePlane (normal[3] + constant). SPICE plane structures often assume the normal is a unit vector (or at least non-zero); passing a zero normal or denormalized vector can produce undefined/degenerate results.
Since the TS side now has assertPlane4, it’s a good place to prevent obviously-invalid planes before invoking native code.
Suggestion
Harden assertPlane4 by rejecting a zero normal vector (and optionally non-finite elements):
function assertPlane4(value: unknown, label: string): asserts value is Plane4 {
// existing checks...
const [nx, ny, nz] = value as unknown as number[];
const n2 = nx * nx + ny * ny + nz * nz;
if (!Number.isFinite(n2) || n2 === 0) {
invalidArgs(`${label} expects a non-zero normal vector (got ${formatValue(value)})`);
}
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
Stack step PR-11 in the issue #454 migration chain.