Skip to content

Make problem construction model-owned and shared by CLI and MCP #1133

Description

@isPANN

Background

pred create currently infers a user-facing construction interface from ProblemSchemaEntry.fields, but that metadata has two incompatible meanings across the catalog. Some entries mirror serialized Rust fields (for example LCS includes the internal max_length field), while others already describe constructor inputs (for example BicliqueCover exposes left_size, right_size, and edges even though its Rust struct stores graph and k).

As a result, construction semantics have leaked into central CLI code: derived-field lists, model-specific flag renames, composite graph expansion, and duplicated derivation formulas live in problemreductions-cli/src/commands/create/schema_support.rs. MCP independently maintains model-name dispatch in problemreductions-cli/src/mcp/tools.rs. Adding a model whose constructor differs from its serialized representation can therefore require edits in the model, registry, CLI, and MCP.

Rust and Serde already provide the appropriate conceptual boundary: deserialize a dedicated input type and convert it fallibly with TryFrom. Mature Rust libraries also use separate creation/input types when the write interface differs from the stored/read model (for example Diesel's NewPost versus Post).

Related work: #1132 introduces deferred Clap construction, but its centralized FieldConstructionMode and model-name matches should be replaced rather than extended.

Objective

Establish one model-owned, transport-neutral construction contract per problem and register its type-erased constructor through the existing problem variant registry. Both CLI and MCP must discover inputs and construct instances through this contract, with no central dispatch keyed by canonical model names.

Ordinary models whose construction input equals their serialized data must require no extra input DTO. Models with derived state, renamed inputs, optional inference, or composite inputs must define a small typed CreateSpec beside the model and convert it to the model with TryFrom.

Interface (Input → Output)

In:

  • canonical problem name and resolved variant;
  • a model-owned list of construction inputs: name, concrete value type, required/optional status, description, and reusable input format/codec;
  • normalized input values supplied by CLI or MCP.

Out:

  • a validated concrete Rust problem instance through a VariantEntry constructor callback;
  • the canonical serialized problem JSON produced from that instance.

The intended flow is:

CLI raw values ─┐
                ├─> normalized construction values
MCP JSON ───────┘                │
                                 v
                      VariantEntry constructor
                                 │
                                 v
                         typed CreateSpec
                                 │ TryFrom
                                 v
                           Rust problem

Unknown inputs, missing required inputs, invalid values, constructor failures, and schema collisions must return explicit errors. Unknown values must never be ignored.

Acceptance criteria

  • ProblemSchemaEntry has one unambiguous construction-facing contract. Rename fields/FieldInfo if necessary so it cannot be mistaken for a serialized-struct schema.
  • VariantEntry exposes a type-erased construction callback generated by declare_variants! or an equally local existing registration mechanism.
  • Direct models use a zero-boilerplate direct construction path.
  • A custom model may define a typed CreateSpec beside the model and a fallible TryFrom<CreateSpec> conversion. Static input metadata must be generated from that type or otherwise have one source of truth; do not hand-maintain the same field list twice.
  • Derived fields are absent from construction inputs and are computed by the model conversion/constructor. There is no centralized derived-field list.
  • Renamed and composite inputs are declared by the model's construction input type. There is no centralized (problem, field) -> flag table.
  • CLI and MCP invoke the same registered constructor and produce identical canonical data and variant values for the same normalized inputs.
  • Shared code may dispatch on reusable value types/codecs (usize, matrix, edge list, arc list, graph type, One, etc.), but not on canonical problem names.
  • Delete superseded model-specific creation branches from CLI and MCP; do not retain aliases, compatibility lookup, or fallback dispatch.
  • Update the repository's add-model instructions so a new ordinary model never requires changes in CLI or MCP. A special model adds its CreateSpec only in its own model module.
  • Preserve deferred selected-model Clap expansion; do not reintroduce a monolithic derived CreateArgs or eagerly build every model's flags.

Technical recommendations (non-binding)

  • Keep ProblemSchemaEntry as the catalog-level declaration and add the executable constructor to the existing per-concrete-type VariantEntry; do not create a second model registry.
  • Generate direct constructor callbacks from declare_variants!. Allow exceptional declarations to name a local CreateSpec type.
  • Use a small derive/helper macro in the existing problemreductions-macros crate to generate static construction input metadata from named fields, Option<T>, doc comments, and explicit reusable codec attributes.
  • Keep Schemars out of the core model crate. It describes JSON serialization/deserialization well, but it does not encode the CLI's compact graph/matrix/list formats and would add derive work to the full model catalog.
  • Use model constructors or TryFrom<CreateSpec> for derived values and invariant validation. Do not reproduce constructor formulas in the CLI.

Verification

Add a catalog-level test module whose tests share the prefix construction_contract_, then run:

cargo test -p problemreductions-cli --features mcp construction_contract -- --nocapture
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features --quiet

The focused suite must demonstrate all of the following observable behavior:

  1. Registry-only discovery: register a test-only problem variant and construction spec in test support, without adding its name to CLI or MCP source. Creating it through both frontends succeeds and yields the same canonical JSON. This fails if either frontend still requires a central model-name match.
  2. Derived field: creating SCS from strings = [[0,1],[1,2]] succeeds without max_length and produces max_length = 4. CLI --help and MCP construction inputs do not expose max_length.
  3. Composite input: creating BicliqueCover from left, right, biedges, and k produces a serialized BipartiteGraph with the stated partitions and edges through both CLI and MCP.
  4. Negative control: supplying max_length for SCS or an unknown field to the test-only construction spec is rejected by both frontends. The test must fail if unknown inputs are silently dropped.
  5. Missing-input control: omitting biedges from BicliqueCover returns an explicit missing-input error rather than constructing an empty graph.

A reviewer-friendly manual check must also pass:

cargo run -q -p problemreductions-cli -- create SCS --strings '0,1;1,2'

The printed JSON must contain alphabet_size: 3, max_length: 4, and the two input strings. This negative command must fail with an unexpected-argument error:

cargo run -q -p problemreductions-cli -- create SCS --strings '0,1;1,2' --max-length 4

Together these checks prove that the construction contract is dynamically discovered, used by both transports, computes derived state through model-owned construction, and rejects fields outside that contract.

Out of scope

  • Changing the persisted problem JSON envelope or canonical serialization format.
  • Automatically discovering Rust module files with build.rs; category mod.rs declarations remain explicit.
  • Migrating paper definitions or canonical examples to inventory-based collection, except for updates directly required by changed construction metadata.
  • Adding compatibility aliases for removed CLI or MCP input names.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions