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:
- 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.
- 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.
- 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.
- 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.
- 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.
Background
pred createcurrently infers a user-facing construction interface fromProblemSchemaEntry.fields, but that metadata has two incompatible meanings across the catalog. Some entries mirror serialized Rust fields (for example LCS includes the internalmax_lengthfield), while others already describe constructor inputs (for exampleBicliqueCoverexposesleft_size,right_size, andedgeseven though its Rust struct storesgraphandk).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 inproblemreductions-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'sNewPostversusPost).Related work: #1132 introduces deferred Clap construction, but its centralized
FieldConstructionModeand 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
CreateSpecbeside the model and convert it to the model withTryFrom.Interface (Input → Output)
In:
Out:
VariantEntryconstructor callback;The intended flow is:
Unknown inputs, missing required inputs, invalid values, constructor failures, and schema collisions must return explicit errors. Unknown values must never be ignored.
Acceptance criteria
ProblemSchemaEntryhas one unambiguous construction-facing contract. Renamefields/FieldInfoif necessary so it cannot be mistaken for a serialized-struct schema.VariantEntryexposes a type-erased construction callback generated bydeclare_variants!or an equally local existing registration mechanism.CreateSpecbeside the model and a fallibleTryFrom<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.(problem, field) -> flagtable.dataandvariantvalues for the same normalized inputs.usize, matrix, edge list, arc list, graph type,One, etc.), but not on canonical problem names.CreateSpeconly in its own model module.CreateArgsor eagerly build every model's flags.Technical recommendations (non-binding)
ProblemSchemaEntryas the catalog-level declaration and add the executable constructor to the existing per-concrete-typeVariantEntry; do not create a second model registry.declare_variants!. Allow exceptional declarations to name a localCreateSpectype.problemreductions-macroscrate to generate static construction input metadata from named fields,Option<T>, doc comments, and explicit reusable codec attributes.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:The focused suite must demonstrate all of the following observable behavior:
strings = [[0,1],[1,2]]succeeds withoutmax_lengthand producesmax_length = 4. CLI--helpand MCP construction inputs do not exposemax_length.BicliqueCoverfromleft,right,biedges, andkproduces a serializedBipartiteGraphwith the stated partitions and edges through both CLI and MCP.max_lengthfor 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.biedgesfromBicliqueCoverreturns 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 4Together 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
build.rs; categorymod.rsdeclarations remain explicit.