Skip to content

PR-12: parity geometry-GF (#483)#512

Open
charliecreates[bot] wants to merge 1 commit intocharlie/issue-454-v2-pr12-geometry-classic-group11from
charlie/issue-454-v2-pr13-geometry-gf-group12
Open

PR-12: parity geometry-GF (#483)#512
charliecreates[bot] wants to merge 1 commit intocharlie/issue-454-v2-pr12-geometry-classic-group11from
charlie/issue-454-v2-pr13-geometry-gf-group12

Conversation

@charliecreates
Copy link
Contributor

Stack step PR-12 in the issue #454 migration chain.

@github-actions
Copy link

orrery preview: https://pr-512.orrery-c4f.pages.dev/

Copy link
Contributor Author

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main risk in this diff is maintainability and cleanup correctness in the new cspice_runner.c geometry-gf cases, especially gfrepi, where the current goto done structure makes resource ownership fragile. The tspice dispatch additions are functionally fine but would benefit from explicit arity checks (for clearer errors) and some helper extraction to reduce index-heavy boilerplate. No obvious functional mismatches between the TS dispatch and the native runner were found for the added methods.

Additional notes (3)
  • Readability | packages/parity-checking/native/src/cspice_runner.c:893-911
    gfrefn now accepts boolean arguments via jsmn_parse_boolean, but other parts of the runner use parse_result-style return codes for richer error reporting (PARSE_INVALID vs PARSE_RANGE, etc.). Returning only true/false loses detail and makes debugging malformed inputs harder (e.g., non-primitive token vs primitive-but-not-true/false).

Also, the function currently only accepts lowercase true/false. That’s fine for strict JSON, but if upstream ever sends True/False (non-JSON but sometimes seen), the error will be generic. If strict JSON is the contract, keep it strict, but then error messages should clearly indicate it expects JSON booleans.

  • Readability | packages/parity-checking/src/runners/tspiceRunner.ts:1590-1614
    In the tspice dispatchers for geometry-gf.gfsstp, geometry-gf.gfstep, geometry-gf.gfstol, and geometry-gf.gfrefn, you validate types but do not validate the argument count. If a spec or caller passes fewer args, args[0] etc. will be undefined, which will correctly fail the type assertions—but the resulting error message will be about type mismatch rather than "missing arg".

This is minor, but it makes failures harder to triage, especially in a parity engine context where missing args often indicate spec bugs.

  • Maintainability | packages/parity-checking/src/runners/tspiceRunner.ts:1643-1737
    The new gfsep/gfdist dispatchers are very repetitive (9 string checks + numeric checks + two window recipes + handle prep). This is manageable now, but it’s already a lot of boilerplate and will get worse as more geometry-gf methods are added.

Given this PR is part of a migration chain, the repetition increases the chance of subtle copy/paste bugs (wrong index, wrong error label, wrong recipe index).

Summary of changes

What changed

  • Enabled geometry-gf parity coverage by removing all geometry-gf.* entries from the parity denylist:
    • packages/parity-checking/catalogs/parity-denylist.json now []
    • packages/parity-checking/catalogs/parity-denylist.ts now exports an empty PARITY_DENYLIST
  • Added CSPICE runner support for Geometry Finder APIs in packages/parity-checking/native/src/cspice_runner.c:
    • New JSON primitive parser: jsmn_parse_boolean()
    • New call IDs and dispatch for:
      • geometry-gf.gfsstp, geometry-gf.gfstep, geometry-gf.gfstol, geometry-gf.gfrefn, geometry-gf.gfrepi, geometry-gf.gfrepf, geometry-gf.gfsep, geometry-gf.gfdist
  • Added tspice runner dispatch entries for the same geometry-gf.* methods in packages/parity-checking/src/runners/tspiceRunner.ts, including argument validation and window handle lifecycle management.
  • Added method specs (v2) for the new geometry-gf methods under packages/parity-checking/specs/methods/geometry-gf/*@v2.yml.
  • Added shared legacy workflow: packages/parity-checking/workflows/legacy/geometry-gf.basic@v1.yml.
  • Updated completeness baselines & tests to reflect full coverage:
    • Canonical method coverage: 165 → 173
    • Denylist size: 8 → 0
    • Updated assertions in integration/e2e/spec coverage tests.

Comment on lines +9054 to +9152
case CALL_GEOMETRY_GF_GFREPI: {
if (tokens[argsTok].size < 3) {
write_error_json_ex("invalid_args", "geometry-gf.gfrepi expects args[0]=windowRecipe args[1]=begmss args[2]=endmss", NULL,
NULL, NULL, NULL);
goto done;
}

int windowTok = jsmn_get_array_elem(tokens, argsTok, 0, tokenCount);
int begmssTok = jsmn_get_array_elem(tokens, argsTok, 1, tokenCount);
int endmssTok = jsmn_get_array_elem(tokens, argsTok, 2, tokenCount);

if (begmssTok < 0 || begmssTok >= tokenCount || tokens[begmssTok].type != JSMN_STRING) {
write_error_json_ex("invalid_args", "geometry-gf.gfrepi expects args[1] to be a string", NULL,
NULL, NULL, NULL);
goto done;
}
if (endmssTok < 0 || endmssTok >= tokenCount || tokens[endmssTok].type != JSMN_STRING) {
write_error_json_ex("invalid_args", "geometry-gf.gfrepi expects args[2] to be a string", NULL,
NULL, NULL, NULL);
goto done;
}

RunnerCellRecipe windowRecipe;
char detail[256] = {0};
if (!parse_cells_windows_recipe(input, tokens, tokenCount, windowTok,
&windowRecipe, detail, sizeof(detail))) {
write_error_json_ex("invalid_args",
"geometry-gf.gfrepi expects args[0] to be a cell/window recipe",
detail[0] ? detail : NULL, NULL, NULL, NULL);
goto done;
}
if (windowRecipe.kind != RUNNER_CELL_RECIPE_WINDOW) {
write_error_json_ex("invalid_args",
"geometry-gf.gfrepi expects args[0] to be [\"window\",maxIntervals]",
NULL, NULL, NULL, NULL);
goto done;
}

SpiceCell *window = NULL;
bool isWindow = false;
if (!runner_alloc_cell_from_recipe(&windowRecipe, &window, &isWindow, detail,
sizeof(detail))) {
write_error_json_ex("invalid_args",
"geometry-gf.gfrepi window allocation failed",
detail[0] ? detail : NULL, NULL, NULL, NULL);
goto done;
}
(void)isWindow;

char *begmss = NULL;
char *endmss = NULL;

strDetail[0] = '\0';
jsmn_strdup_err_t begmssErr =
jsmn_strdup(input, &tokens[begmssTok], &begmss, strDetail, sizeof(strDetail));
if (begmssErr != JSMN_STRDUP_OK) {
runner_free_allocated_cell(window);
if (begmssErr == JSMN_STRDUP_INVALID) {
write_error_json_ex("invalid_request", "Invalid JSON string escape",
strDetail[0] ? strDetail : NULL, NULL, NULL, NULL);
} else {
write_error_json("Out of memory", NULL, NULL, NULL);
}
goto done;
}

strDetail[0] = '\0';
jsmn_strdup_err_t endmssErr =
jsmn_strdup(input, &tokens[endmssTok], &endmss, strDetail, sizeof(strDetail));
if (endmssErr != JSMN_STRDUP_OK) {
free(begmss);
runner_free_allocated_cell(window);
if (endmssErr == JSMN_STRDUP_INVALID) {
write_error_json_ex("invalid_request", "Invalid JSON string escape",
strDetail[0] ? strDetail : NULL, NULL, NULL, NULL);
} else {
write_error_json("Out of memory", NULL, NULL, NULL);
}
goto done;
}

gfrepi_c(window, begmss, endmss);
free(begmss);
free(endmss);
runner_free_allocated_cell(window);

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 gfrepi", shortMsg, longMsg, traceMsg);
goto done;
}

fputs("{\"ok\":true,\"result\":null}\n", stdout);
goto done;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

geometry-gf.gfrepi allocates a SpiceCell *window via runner_alloc_cell_from_recipe, but on the success path it is freed before checking failed_c(). If gfrepi_c() signals a SPICE failure, the function jumps to done after printing the error, but the allocated cell is already freed, so that part is fine.

The actual issue is the error paths before the call: you do free window when begmss duplication fails and when endmss duplication fails, but any earlier goto done after runner_alloc_cell_from_recipe (e.g., after (void)isWindow; there are no more early gotos until duplication) is currently safe. However, this pattern is fragile and easy to break when adding new validation later; the goto done style across this case makes resource ownership hard to reason about.

More importantly: this case uses a different cleanup strategy than gfsep/gfdist (which clean up in a single block), increasing the odds of future leaks/double-frees when this grows.

Suggestion

Refactor this case to use a single cleanup: label and local ownership flags/pointers (initialized to NULL) so every goto after allocation goes through one cleanup path. That makes future edits much safer.

Example pattern:

case CALL_GEOMETRY_GF_GFREPI: {
  SpiceCell *window = NULL;
  char *begmss = NULL;
  char *endmss = NULL;

  // ... validation ...
  if (!runner_alloc_cell_from_recipe(..., &window, ...)) {
    write_error_json_ex(...);
    goto cleanup;
  }

  if (jsmn_strdup(...) != JSMN_STRDUP_OK) { ...; goto cleanup; }
  if (jsmn_strdup(...) != JSMN_STRDUP_OK) { ...; goto cleanup; }

  gfrepi_c(window, begmss, endmss);

cleanup:
  free(begmss);
  free(endmss);
  if (window) runner_free_allocated_cell(window);

  if (failed_c() == SPICETRUE) { ... }
  fputs("{\"ok\":true,\"result\":null}\n", stdout);
  goto done;
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor.

Comment on lines +7 to +8
const BASELINE_CANONICAL_METHOD_COVERAGE = 173;
const MAX_BASELINE_DENYLIST_SIZE = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The completeness baselines were bumped (BASELINE_CANONICAL_METHOD_COVERAGE = 173, MAX_BASELINE_DENYLIST_SIZE = 0). This makes the guard much stricter and means any temporary denylisting will fail the guard. If denylisting is still expected as an escape hatch during migration chains, MAX_BASELINE_DENYLIST_SIZE = 0 may be too rigid and cause noisy failures for future PR stacks.

If the project intends to eliminate denylists entirely, this is fine—but then you should also ensure there’s a documented process for handling flaky/unsupported methods without denylisting (e.g., marking cases as skipped by capability detection).

Suggestion

If denylists are still a supported workflow during staged migrations, consider setting MAX_BASELINE_DENYLIST_SIZE to a small non-zero number (e.g., 2) and relying on tests to enforce it trends down over time, or introduce a separate guard that requires justification metadata for each denylisted method.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit adjusting the guard to allow a small denylist budget and/or require justification strings per entry.

@charliecreates charliecreates bot removed the request for review from CharlieHelps February 22, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant