Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ claude-output.log
*.json
.claude/worktrees/
docs/test-reports/
docs/superpowers/
10 changes: 10 additions & 0 deletions docs/agent-profiles/FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Features

- [Library API] — Define NP-hard problems, evaluate configurations, and reduce between problem types using Rust traits
- [CLI Tool (pred)] — Explore the reduction graph, create/inspect/reduce/solve problem instances from the terminal
- [MCP Server] — AI assistant integration via Model Context Protocol for graph queries and problem manipulation
- [Reduction Graph] — Automatic shortest-path search through registered reductions between problem types
- [BruteForce Solver] — Enumerate all configurations to find optimal or satisfying solutions
- [Variant System] — Graph/weight type parameterization with compile-time complexity registration
- [Overhead System] — Symbolic expressions describing how target problem size relates to source after reduction
- [Serialization] — JSON schema export and serde-based serialization for all problem types
13 changes: 13 additions & 0 deletions docs/agent-profiles/SKILLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Skills

- [issue-to-pr] — Convert a GitHub issue into a PR with an implementation plan
- [add-model] — Add a new problem model to the codebase
- [add-rule] — Add a new reduction rule to the codebase
- [review-implementation] — Review implementation completeness via parallel subagents
- [fix-pr] — Resolve PR review comments, CI failures, and coverage gaps
- [check-issue] — Quality gate for Rule and Model GitHub issues
- [check-rule-redundancy] — Check if a reduction rule is redundant via composite paths
- [write-model-in-paper] — Write or improve a problem-def entry in the Typst paper
- [write-rule-in-paper] — Write or improve a reduction-rule entry in the Typst paper
- [release] — Create a new crate release with version bump
- [meta-power] — Batch-resolve all open Model and Rule issues autonomously
24 changes: 24 additions & 0 deletions docs/agent-profiles/cli-tool-dr-sarah-chen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# cli-tool-dr-sarah-chen

## Target
CLI Tool (pred)

## Use Case
End-to-end workflow: create an MIS instance, reduce it to QUBO, solve the QUBO, and verify the mapped-back solution matches the known optimum.

## Expected Outcome
The full create → reduce → solve → evaluate pipeline completes successfully with correct optimal results. JSON output is well-formed and machine-readable at every stage.

## Agent

### Background
Dr. Sarah Chen is a computational physicist at a national lab who regularly uses optimization solvers (Gurobi, CPLEX, HiGHS) in her research on quantum annealing benchmarks. She evaluates new tools by running them against problems with known optimal solutions.

### Experience Level
Expert

### Decision Tendencies
Tests edge cases proactively — tries empty graphs, disconnected components, and large instances. Expects precise, actionable error messages when things go wrong. Compares solver output against independently computed optima. Will read --help but also try undocumented flag combinations.

### Quirks
Will try both file-based and piped workflows to check consistency. Inspects JSON output with jq for machine-readability. Gets impatient with vague error messages like "invalid input" — wants to know exactly what's wrong and where.
16 changes: 14 additions & 2 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
}
}
} else {
eprintln!("{canonical}\n");
eprintln!("No schema information available.");
bail!("{}", crate::problem_name::unknown_problem_error(canonical));
}

let example = example_for(canonical, graph_type);
Expand Down Expand Up @@ -551,6 +550,13 @@ fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> {
.as_deref()
.ok_or_else(|| anyhow::anyhow!("This problem requires --graph (e.g., 0-1,1-2,2-3)"))?;

if edges_str.trim().is_empty() {
bail!(
"Empty graph string. To create a graph with isolated vertices, use:\n \
pred create <PROBLEM> --random --num-vertices N --edge-prob 0.0"
);
}

let edges: Vec<(usize, usize)> = edges_str
.split(',')
.map(|pair| {
Expand All @@ -560,6 +566,12 @@ fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> {
}
let u: usize = parts[0].parse()?;
let v: usize = parts[1].parse()?;
if u == v {
bail!(
"Self-loop detected: edge {}-{}. Simple graphs do not allow self-loops",
u, v
);
}
Ok((u, v))
})
.collect::<Result<Vec<_>>>()?;
Expand Down
9 changes: 9 additions & 0 deletions problemreductions-cli/src/commands/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ pub fn evaluate(input: &Path, config_str: &str, out: &OutputConfig) -> Result<()
);
}

for (i, (val, dim)) in config.iter().zip(dims.iter()).enumerate() {
if *val >= *dim {
anyhow::bail!(
"Config value {} at position {} is out of range: variable {} has {} possible values (0..{})",
val, i, i, dim, dim - 1
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The error message computes dim - 1 for display. If dim is ever 0, this will underflow (usize) and produce a bogus upper bound (or panic in debug builds). Prefer printing an exclusive range like 0..{dim} (or handle dim == 0 explicitly) so the message can’t underflow.

Suggested change
val, i, i, dim, dim - 1
val, i, i, dim, dim

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This new out-of-range validation path in pred evaluate doesn’t appear to be covered by the CLI integration tests. Adding a test that passes a config value >= dim and asserts the command fails with the new message would lock in the intended UX and prevent accidental removal.

Suggested change
val, i, i, dim, dim - 1
val,
i,
i,
dim,
dim.saturating_sub(1)

Copilot uses AI. Check for mistakes.
);
}
}

let result = problem.evaluate_dyn(&config);

let text = result.to_string();
Expand Down
78 changes: 48 additions & 30 deletions problemreductions-cli/src/commands/graph.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::output::OutputConfig;
use crate::problem_name::{aliases_for, parse_problem_spec, resolve_variant};
use anyhow::{Context, Result};
use problemreductions::{asymptotic_normal_form, Expr};
use problemreductions::registry::collect_schemas;
use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalDirection};
use problemreductions::types::ProblemSize;
Expand Down Expand Up @@ -124,7 +125,10 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
crate::output::fmt_problem_name(&format!("{}{}", spec.name, slash))
);
if let Some(c) = graph.variant_complexity(&spec.name, v) {
text.push_str(&format!("{label} complexity: {c}\n"));
text.push_str(&format!(
"{label} complexity: {}\n",
big_o_of(&Expr::parse(c))
));
Comment on lines 127 to +131
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Expr::parse(c) will panic if a registered complexity string has invalid syntax. This is a behavior regression for pred show (previously it printed the raw string) and makes the CLI vulnerable to crashing when a new model registers a malformed complexity. Consider adding/exposing a fallible parser in problemreductions (e.g., Expr::try_parse/parse_expr returning Result) and using that here with a graceful fallback to the original complexity string when parsing fails.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +131
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The new Big-O formatting in pred show/pred path output isn’t covered by existing CLI tests (current tests only assert headers like "Variants"/"Path"). Adding assertions that the output includes complexity: O( and overhead entries like field = O( would prevent regressions in the normalization/formatting logic.

Suggested change
text.push_str(&format!(
"{label} complexity: {}\n",
big_o_of(&Expr::parse(c))
));
let mut complexity_str = big_o_of(&Expr::parse(c));
if !complexity_str.contains("O(") {
complexity_str = format!("O({})", complexity_str);
}
text.push_str(&format!("{label} complexity: {complexity_str}\n"));

Copilot uses AI. Check for mistakes.
} else {
text.push_str(&format!("{label}\n"));
}
Expand Down Expand Up @@ -172,12 +176,7 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
crate::output::fmt_outgoing("\u{2192}"),
fmt_node(&graph, e.target_name, &e.target_variant),
));
let oh_parts: Vec<String> = e
.overhead
.output_size
.iter()
.map(|(field, poly)| format!("{field} = {poly}"))
.collect();
let oh_parts = fmt_overhead_parts(&e.overhead.output_size);
if !oh_parts.is_empty() {
text.push_str(&format!(" ({})", oh_parts.join(", ")));
}
Expand All @@ -195,29 +194,18 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
crate::output::fmt_outgoing("\u{2192}"),
fmt_node(&graph, e.target_name, &e.target_variant),
));
let oh_parts: Vec<String> = e
.overhead
.output_size
.iter()
.map(|(field, poly)| format!("{field} = {poly}"))
.collect();
let oh_parts = fmt_overhead_parts(&e.overhead.output_size);
if !oh_parts.is_empty() {
text.push_str(&format!(" ({})", oh_parts.join(", ")));
}
text.push('\n');
}

let edge_to_json = |e: &problemreductions::rules::ReductionEdgeInfo| {
let overhead: Vec<serde_json::Value> = e
.overhead
.output_size
.iter()
.map(|(field, poly)| serde_json::json!({"field": field, "formula": poly.to_string()}))
.collect();
serde_json::json!({
"source": {"name": e.source_name, "variant": e.source_variant},
"target": {"name": e.target_name, "variant": e.target_variant},
"overhead": overhead,
"overhead": overhead_to_json(&e.overhead.output_size),
})
};
let variants_json: Vec<serde_json::Value> = variants
Expand All @@ -227,6 +215,11 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
serde_json::json!({
"variant": v,
"complexity": complexity,
"big_o": if complexity.is_empty() {
String::new()
} else {
big_o_of(&Expr::parse(complexity))
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This JSON branch also uses Expr::parse(complexity), which can panic and crash pred show -o ... if a complexity string is malformed. Using a fallible parse here would keep JSON export robust and allow you to omit/empty the big_o field (or fall back to the original string) on parse errors.

Suggested change
big_o_of(&Expr::parse(complexity))
std::panic::catch_unwind(|| big_o_of(&Expr::parse(complexity)))
.unwrap_or_else(|_| String::new())

Copilot uses AI. Check for mistakes.
},
})
})
.collect();
Expand All @@ -248,6 +241,37 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
out.emit_with_default_name(&default_name, &text, &json)
}

/// Format an expression as Big O notation using asymptotic normalization.
/// Falls back to wrapping the original expression if normalization fails.
fn big_o_of(expr: &Expr) -> String {
match asymptotic_normal_form(expr) {
Ok(norm) => format!("O({})", norm),
Err(_) => format!("O({})", expr),
}
}

/// Format overhead fields as `field = O(...)` strings.
fn fmt_overhead_parts(output_size: &[(&'static str, Expr)]) -> Vec<String> {
output_size
.iter()
.map(|(field, poly)| format!("{field} = {}", big_o_of(poly)))
.collect()
}

/// Convert overhead fields to JSON entries with Big O notation.
fn overhead_to_json(output_size: &[(&'static str, Expr)]) -> Vec<serde_json::Value> {
output_size
.iter()
.map(|(field, poly)| {
serde_json::json!({
"field": field,
"formula": poly.to_string(),
"big_o": big_o_of(poly),
})
})
.collect()
}

/// Convert a variant BTreeMap to slash notation showing ALL values.
/// E.g., {graph: "SimpleGraph", weight: "i32"} → "/SimpleGraph/i32".
pub(crate) fn variant_to_full_slash(variant: &BTreeMap<String, String>) -> String {
Expand Down Expand Up @@ -299,7 +323,7 @@ fn format_path_text(
));
let oh = &overheads[i];
for (field, poly) in &oh.output_size {
text.push_str(&format!(" {field} = {poly}\n"));
text.push_str(&format!(" {field} = {}\n", big_o_of(poly)));
}
}

Expand All @@ -308,7 +332,7 @@ fn format_path_text(
let composed = graph.compose_path_overhead(reduction_path);
text.push_str(&format!("\n {}:\n", crate::output::fmt_section("Overall")));
for (field, poly) in &composed.output_size {
text.push_str(&format!(" {field} = {poly}\n"));
text.push_str(&format!(" {field} = {}\n", big_o_of(poly)));
}
}

Expand All @@ -330,19 +354,13 @@ fn format_path_json(
"from": {"name": pair[0].name, "variant": pair[0].variant},
"to": {"name": pair[1].name, "variant": pair[1].variant},
"step": i + 1,
"overhead": oh.output_size.iter().map(|(field, poly)| {
serde_json::json!({"field": field, "formula": poly.to_string()})
}).collect::<Vec<_>>(),
"overhead": overhead_to_json(&oh.output_size),
})
})
.collect();

let composed = graph.compose_path_overhead(reduction_path);
let overall: Vec<serde_json::Value> = composed
.output_size
.iter()
.map(|(field, poly)| serde_json::json!({"field": field, "formula": poly.to_string()}))
.collect();
let overall = overhead_to_json(&composed.output_size);

serde_json::json!({
"steps": reduction_path.len(),
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub mod prelude {

// Re-export commonly used items at crate root
pub use error::{ProblemError, Result};
pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError};
pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError, Expr};
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Re-exporting Expr from the crate root makes the expression AST and its parsing API part of the public surface area. Since Expr::parse is documented to leak memory for variable names and panics on invalid syntax, consider whether you also want to expose a fallible parsing API (and/or clearer docs) so downstream consumers can parse complexity strings without risking panics in long-running processes.

Suggested change
pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError, Expr};
pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError};

Copilot uses AI. Check for mistakes.
pub use registry::{ComplexityClass, ProblemInfo};
pub use solvers::{BruteForce, Solver};
pub use traits::{OptimizationProblem, Problem, SatisfactionProblem};
Expand Down
Loading