Skip to content

Deterministic truncated path sets: lib-level canonical ReductionPath order for pred path --all (CLI + MCP) #1087

Description

@isPANN

Background

pred path <S> <T> --all (and the MCP find_path tool) enumerate reduction paths, sort them, truncate to --max-paths (default 20), and return the survivors as both text and JSON. Two independent problems make the returned set non-deterministic across builds:

  1. Length-only sort leaves a build-dependent order. find_paths_up_to discovery order depends on inventory/link iteration, so paths of equal length keep an unstable order.

  2. Truncation picks a discovery-dependent subset. Both consumers fetch only find_paths_up_to(..., max_paths + 1), then sort, then truncate. When the total path count exceeds the cap, which paths are fetched — hence which survive — depends on discovery order regardless of how the fetched set is later sorted. Example: KSatisfiability → QUBO has 108 paths; the default cap returns 20, and which 20 is not reproducible across builds.

This undermines the milestone's determinism requirement (docs/design/symbolic-growth-domain.md, Quality requirements: "identical output across platforms … golden files").

Objective

Make the truncated path set returned by pred path --all and the MCP find_path tool reproducible across builds, from one shared definition of canonical path order.

Interface (Input → Output)

  • New lib API on ReductionPath (src/rules/graph.rs): a canonical total-order key, e.g. pub fn canonical_key(&self) -> Vec<(&'static str, String)> built from every step's name + its (already-ordered) variant BTreeMapnot Display/type_names (both drop or collapse variants). Optionally impl Ord for ReductionPath on (len, canonical_key).
  • path_all (CLI) and the MCP find_path handler both: fetch the complete path set (bounded — see recommendations), sort by (len, canonical_key), then truncate. Delete the CLI-local path_signature closure and the MCP length-only sort_by_key.
  • The pred path --all OOMs/hangs: big_o_normal_form exponentially expands composed overhead expressions #1069 test selector switches to canonical_key; regenerate the golden fixture if it changes.

Technical recommendations (non-binding)

  • To make the subset (not just its order) reproducible, sort-then-truncate must run over the full candidate set, so fetch all paths before truncating. Guard blowup on hub nodes with the existing bounded enumeration (find_paths_up_to with a high internal ceiling, or find_all_paths with the same MAX_INTERMEDIATE_NODES bound used by find_dominated_rules); document the ceiling and log/note when it is hit rather than silently truncating by discovery order.
  • Keep each test < 5 s.

Verification

  1. cargo test green with a new test that builds the KSat→QUBO candidate set, sorts+truncates it, then does the same to a reversed copy of the input, and asserts the two truncated results are identical (name+variant sequences equal) — proving order-independence of both the ordering and the surviving subset.
  2. grep -n 'sort_by_key(|p| p.len())' problemreductions-cli/src/mcp/tools.rs returns nothing, and grep -n 'canonical_key' src/rules/graph.rs returns the new API — proving the length-only sibling was replaced by the shared key.
  3. pred path KSat QUBO --all --json and the MCP find_path tool return the same 20-path set (by name+variant sequence) for the same inputs.

Negative control: the reversed-input test in (1), run against a length-only sort (the current MCP code), fails (different truncated subset) — proving the test detects the non-determinism the canonical key fixes.

Dependencies

Depends on #1079 (introduced the CLI tiebreak this generalizes). Milestone: Symbolic Growth Domain & Pareto Search.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions