solverforge-cli 3.0.0

CLI for scaffolding and managing SolverForge constraint solver projects
---
name: solverforge-modeling
description: Model a planning or optimization problem end-to-end with the `solverforge` CLI and produce a runnable SolverForge app. Use when a user describes a scheduling, assignment, routing, rostering, sequencing, packing, loading, knapsack, or resource-allocation problem and wants it turned into working code, or when they want to scaffold, extend, or fix a SolverForge project (facts, entities, scalar/list planning variables, constraints, score type, demo data, output shell). Covers the CLI scaffold workflow, output-shell choice (web / API / MCP / CLI), constraint authoring against the SolverForge stream API, and behavioral verification on shells that expose solving. Do not use for editing solverforge-cli itself.
---

# Modeling planning problems with solverforge-cli

`solverforge` scaffolds a neutral app shell, then grows it into a model through
managed commands. Your job: turn a user's problem statement into a **compiling,
solvable** SolverForge app, not a skeleton. For web/API, a model is finished only
after a real solve has completed without panicking and produced a score. The
generated CLI shell exposes demo-data only; add and test a solve command or state
that behavioral solve verification remains unavailable.

Read this file, then open the reference that matches the step you are on. Keep
`references/constraint-patterns.md` open while writing constraints: it contains
verified implementations for the common unary/reward/pair/join patterns and the
required source/collector rules for the grouped patterns.

## Prerequisites

- `solverforge` on `PATH` (`cargo install solverforge-cli`).
- Rust `1.95+`.
- Confirm the live contract before relying on version numbers:
  `solverforge --version`, `solverforge new --help`,
  `solverforge generate variable --help`.

This skill describes CLI `3.0.0` (scaffold runtime target `solverforge 0.19.4`,
UI `solverforge-ui 0.7.0`, maps `solverforge-maps 2.1.4`, MCP `rmcp 3.3.0`).
Re-derive specifics from the CLI if the version differs.

## Step 0 — Intake: ask the user before you build

Do not guess the model. Ask the user, in one message, for:

1. **The problem, in their words.** One paragraph is enough: what is being
   decided, what makes a plan good, and what makes a plan invalid. Restate it
   back as `facts / entities / variables / constraints` before scaffolding.
2. **The output shell** — the "type of output" they want:
   - `web` — end-to-end: JSON/SSE API **plus** the browser UI (default).
   - `api` — headless HTTP API only, no frontend assets.
   - `cli` — a terminal command-line scaffold, no Axum server or frontend. The
     generated command exposes demo data, not solving, unless you extend it.
   - `mcp` — an MCP server that exposes the retained solve lifecycle as
     schema-typed tools to any MCP-capable agent harness. stdio by default,
     stateless Streamable HTTP at `/mcp` with `--http`. Use it when the consumer
     is an agent/LLM rather than a browser or a human at a terminal.
3. **The constraints**, split into:
   - **Hard** constraints (must hold for a valid plan), and
   - **Soft** constraints (should be optimized).
   Ask explicitly; users usually name only a subset. For each, ask what
   triggers a violation and what data it reads.
4. **Score type** — default `HardSoftScore`. Only change it if the user needs a
   medium level (`HardMediumSoftScore`), decimals (`HardSoftDecimalScore`),
   soft-only (`SoftScore`), or a custom shape (`BendableScore<N, M>`).

Use a structured question with concrete choices for the shell question. Then use
`references/problem-modeling.md` to convert the answer into a concrete model and
echo that model back to the user for confirmation.

## Step 1 — Scaffold

```bash
solverforge new <name> --shell web   # or api / cli / mcp
cd <name>
```

- `<name>` starts with a letter; letters, digits, `-`, `_` only.
- `--skip-git` skips `git init`; `--skip-readme` skips the README.
- The scaffold is the same neutral model for every shell; the shell only changes
  which adapters (Axum routes, static UI, Clap, or MCP tools) are generated.

## Step 2 — Establish the solution identity first

If the user does not want the default `Plan` solution name, rename it **now**,
before adding any facts or entities:

```bash
solverforge generate solution schedule --score HardSoftScore
```

This is the one chance to replace the neutral scaffold automatically. After
facts/entities exist, `generate solution` refuses; you would have to
`destroy solution` (which empties the solution's collections and does **not**
re-wire them). See `references/gotchas.md`.

## Step 3 — Add facts and entities

Facts are immutable inputs; entities are the things the solver assigns.

```bash
solverforge generate fact resource --field capacity:i32 --field slot:usize
solverforge generate entity task --field demand:i32 --field ready_at:i64
```

- Names are `snake_case`; the CLI PascalCases the struct (`task` → `Task`) and
  naively pluralizes the collection (`task` → `tasks`).
- `--field name:Type` is repeatable. Every fact/entity gets `id: String` (and
  facts get `name: String`) automatically; do not declare them yourself.
- Watch irregular plurals: the collection name is used verbatim by variables
  (`--range`, `--elements`, data generation), so prefer regular nouns.

## Step 4 — Add planning variables

Scalar = one value per entity. List = an ordered sequence owned by an entity.

```bash
# scalar over a fact collection (value is an index into that collection)
solverforge generate variable resource_idx --entity Task --kind scalar \
  --range resources --allows-unassigned

# scalar over an integer range (value is the integer itself)
solverforge generate variable start_slot --entity Task --kind scalar --countable-range 0..24

# ordered list over a fact collection
solverforge generate variable stops --entity Route --kind list --elements visits
```

- Use `--allows-unassigned` only when `None` is a legal planning state or a
  specific runtime resource (such as an assignment scalar group) requires it.
  Scalar values are reusable unless a constraint enforces exclusivity, so entity
  count alone is not a reason. The field is `Option<usize>` either way.
- A scalar over a fact collection stores the **index** into that collection, not
  the fact itself.
- Ordered sequences and routes are **list** variables. Never model sequence
  topology as a scalar predecessor field.
- `--domain cvrp` selects the stock CVRP list profile. It owns its distance
  meters, route/savings hooks, metric class, and solution trait; those cannot be
  overridden alongside it, and the solution must satisfy the runtime's CVRP
  contract. For custom sequences, pass the specific list metadata flags instead
  (see `references/cli-workflow.md`).

## Step 5 — Author constraints (this is where models fail)

Generate one module per constraint with an **explicit pattern flag**, generate a
real implementation, and register nothing by hand.

```bash
solverforge generate constraint capacity --join --hard
solverforge generate constraint prefer_early --unary --soft
```

Always pass a pattern flag. Without one the CLI opens an interactive wizard that
fails in a non-interactive agent shell (`prompt error: IO error: not a
terminal`).

Patterns: `--unary --pair --join --balance --reward --runs --presence
--collect-vec --group-complement --projected-group`. `--hard` is the default;
`--soft` opts into a soft constraint. `--balance` and `--reward` imply soft.

### The localizing-source rule (read this even if you read nothing else)

The generated skeleton is a **compiling stub full of `panic!` placeholders**. Two
things must change before it is a real constraint:

1. Replace every placeholder function body with real domain logic.
2. **Use the generated collection accessor as the stream source.**

`#[planning_solution]` generates a public associated accessor for each
collection: `Plan::tasks()` for `#[planning_entity_collection] pub tasks:
Vec<Task>`, and `Plan::resources()` for `#[problem_fact_collection]`. These
carry the change-source metadata the incremental engine needs. The skeleton
instead defines `fn entity_items(solution: &Plan) -> &[Task]`, whose source is
`ChangeSource::Unknown`.

- `for_each`-only streams (unary, reward) happen to tolerate `Unknown`.
- **Joins, self-joins, `group_by`, complement, and projected streams do not.**
  They panic during the solve with:
  `constraint <name> received descriptor <n>, but source Unknown cannot localize entity indexes`.

So wire **every** stream through the generated accessor and delete the
hand-written extractor:

```rust
use crate::domain::{Plan, Resource, Task};
use solverforge::prelude::*;
use solverforge::stream::joiner::equal_bi;
use solverforge::IncrementalConstraint;

/// HARD: a task's demand must not exceed its assigned resource capacity.
pub fn constraint() -> impl IncrementalConstraint<Plan, HardSoftScore> {
    ConstraintFactory::<Plan, HardSoftScore>::new()
        .for_each(Plan::tasks())              // <- generated accessor, not entity_items
        .join((
            Plan::resources(),                // <- generated accessor, not fact_items
            equal_bi(entity_join_key, fact_join_key),
        ))
        .penalize(hard_weight(join_weight))
        .named("capacity")
}

fn entity_join_key(task: &Task) -> Option<usize> { task.resource_idx }
fn fact_join_key(resource: &Resource) -> Option<usize> { resource.slot.checked_sub(1) }

fn join_weight(task: &Task, resource: &Resource) -> HardSoftScore {
    if task.demand > resource.capacity {
        <HardSoftScore as Score>::one_hard()
    } else {
        <HardSoftScore as Score>::zero()
    }
}
```

Self-joins (entities sharing a key) use `joiner::equal(|e: &Task| key)`.
Entity↔fact joins use `joiner::equal_bi(entity_key, fact_key)`. Fact rows carry
no implicit index, so when you need to join to "the Nth fact" add an explicit
numeric field (`slot`/`index`) to the fact and read it in `fact_join_key`.

`references/constraint-patterns.md` has the full catalog with when-to-use and
verified implementations, plus the `hard_weight` / `one_hard` / `one_soft` /
`zero` weight vocabulary and the score-type caveats.

## Step 6 — Generate demo data

```bash
solverforge generate data --size standard
```

`src/data/data_seed.rs` is **compiler-owned**: domain-shape commands such as
`generate fact/entity/variable` re-render it from the current structs and
`--size` counts. Constraint-only changes do not. Do not hand-edit it; later
domain-shape changes overwrite it. Generated values
are structurally useful index-based samples, not realistic domain data. If the
user needs real data, load it from a new module or the stable `src/data/mod.rs`
wrapper rather than editing the seed.

## Step 7 — Verify (mandatory, in this order)

```bash
solverforge check          # structure, managed blocks, model resources, solver.toml
cargo check                # the code must actually compile
```

`check` passing does **not** prove the model works: it never runs the solver and
never sees placeholder `panic!`s. Run a real solve through web/API or through a
CLI solve entry point/integration test you add.

For `web`/`api`:

```bash
solverforge server --debug
# Or locate this loaded skill directory and run:
<skill-dir>/scripts/solve-smoke-test.sh .
```

Then drive the documented API (details in `references/verification.md`):

1. `GET /health`
2. `GET /demo-data` → `defaultId`
3. `GET /demo-data/<ID>` → the plan JSON
4. `POST /jobs` with that plan → `{ "id": ... }`
5. Poll `GET /jobs/<id>/status` until `lifecycleState` settles.

A passing result means: the server booted, the job reached `COMPLETED`, current
and best scores are non-null, and the log contains **no** constraint panic.
`SOLVING` after the timeout, `CANCELLED`, `FAILED`, or a panic is failure — fix
the model (often the localizing-source rule) and re-verify.

For `cli`, there is no generated solve command. Run `cargo run -- demo-data` to
verify compilation and serialization only. To claim a working optimizer, add a
solve subcommand backed by `src/solver/service.rs` (or a library integration
test that drives it) and verify a terminal score; otherwise report this
limitation explicitly.

For `mcp`, the generated binary is itself the MCP server. Boot the transport and
confirm it stays panic-free:

```bash
cargo run --release -- --http    # Streamable HTTP at http://127.0.0.1:7860/mcp
solverforge server               # equivalent; selects --http for mcp shells
```

`solverforge connect` prints ready-to-paste client configuration (Claude Code,
Claude Desktop, Cursor, VS Code); `solverforge connect --write vscode` merges
`.vscode/mcp.json`. The server exposes `list_demo_data`, `get_demo_data`,
`solve`, `get_status`, `get_best_solution`, `analyze_solution`, `get_telemetry`,
`get_candidate_trace`, `pause`, `resume`, `cancel`, and `delete`. `solve` is
task-backed for task-capable clients (retained `jobId` in result metadata) and
returns an immediate summary to others. The bundled smoke helper checks the HTTP
transport is up and panic-free; full protocol verification requires a real MCP
client (the CLI repo drives the generated server with `rmcp`).

## Step 8 — Report

Tell the user: the model you built, the shell, the score type, the constraints
(hard/soft) and what each does, the exact verification command you ran, the
observed score, and any remaining limitations. Record repository changes as
small conventional commits if the user asks for commits.

## Hard rules

- Never hand-wire managed blocks, `src/constraints/mod.rs`, or the solution's
  collections. Use the CLI commands.
- MCP-shell projects expose MCP tools, not Axum routes: do not expect `/health`,
  `/jobs`, or `solverforge routes` there.
- Always pass an explicit constraint pattern flag.
- Rename/replace the solution before adding facts and entities.
- `src/data/data_seed.rs` and web-shell `static/generated/ui-model.json` are
  compiler-owned; `solverforge.app.toml`, `solver.toml`, and the managed blocks
  are the CLI's surfaces.
- Ordered sequences are list variables; do not use scalar predecessor fields.
- Finish web/API only after a real solve completes. For MCP, verify the
  transport boots and, for a full claim, drive the tools with a real MCP client.
  For CLI, either implement and verify a solve entry point or explicitly report
  that the generated shell only proves data serialization.

## Reference index

| File | Use it for |
| --- | --- |
| `references/problem-modeling.md` | Converting a problem statement into facts, entities, variables, and hard/soft constraints; intake template. |
| `references/cli-workflow.md` | Exact command surface, flags, ordering, and shell-specific run steps. |
| `references/constraint-patterns.md` | Every constraint pattern and its source rules; verified code for unary/reward/pair/join. |
| `references/output-shells.md` | `web` vs `api` vs `cli` vs `mcp`, what each generates, and how to run each. |
| `references/verification.md` | `check` vs compile vs real solve; the API smoke flow and helper script. |
| `references/gotchas.md` | Managed blocks, compiler-owned files, ordering traps, score-type limits. |
| `references/advanced-resources.md` | Countable ranges, scalar hooks, list metadata, scalar groups, conflict repair, candidate traces. |