From 124c8cb8926e874011853e6c04b7a242c7ac5da4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 02:53:49 +0800 Subject: [PATCH 1/4] Add plan for #714: [Model] KClique --- docs/plans/2026-03-21-kclique-model.md | 231 +++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 docs/plans/2026-03-21-kclique-model.md diff --git a/docs/plans/2026-03-21-kclique-model.md b/docs/plans/2026-03-21-kclique-model.md new file mode 100644 index 000000000..9f854653f --- /dev/null +++ b/docs/plans/2026-03-21-kclique-model.md @@ -0,0 +1,231 @@ +# KClique Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `KClique` decision model end-to-end so issue #714 can unblock the pending rule issues that target or source Clique as a satisfaction problem. + +**Architecture:** Implement `KClique` as a graph-based `SatisfactionProblem` with one binary variable per vertex and a runtime threshold `k: usize`. Reuse the same clique-validity semantics as `MaximumClique`, but expose a boolean metric, graph-only registry variants, and constructor-facing schema/CLI fields `graph` and `k`. Use the issue’s 5-vertex house-graph instance with witness `[0, 0, 1, 1, 1]` and `k = 3` as the canonical example for tests, `pred create --example`, and the paper. + +**Tech Stack:** Rust, serde, inventory registry, Clap CLI, MCP create helpers, Typst paper, `make test`, `make clippy`, `make paper`. + +--- + +## Issue Context + +- **Issue:** #714 `[Model] KClique` +- **Good label:** present +- **Existing PR:** none (`action = create-pr`) +- **Associated rule issues verified from the issue + open rule search:** #229, #231, #201, #206 +- **References to honor:** `@garey1979`, `@karp1972`, `@xiao2017` +- **Important modeling decision:** this is a dedicated decision problem, not a variant of `MaximumClique`, because the metric is `bool` and the instance carries a threshold `k` + +## Required Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `KClique` | +| 2 | Mathematical definition | Given an undirected graph `G = (V, E)` and integer `k`, determine whether there exists `V' ⊆ V` with `|V'| ≥ k` such that every distinct pair in `V'` is adjacent | +| 3 | Problem type | Satisfaction (`Metric = bool`) | +| 4 | Type parameters | `G: Graph` | +| 5 | Struct fields | `graph: G`, `k: usize` | +| 6 | Configuration space | `vec![2; graph.num_vertices()]` | +| 7 | Feasibility check | Selected vertices must all be pairwise adjacent and the selected set size must be at least `k` | +| 8 | Objective function | `bool` only: `true` iff the configuration is a clique witness of size at least `k` | +| 9 | Best known exact algorithm | `O*(1.1996^n)` via complement to MIS / maximum clique, encoded as `"1.1996^num_vertices"` | +| 10 | Solving strategy | Brute force already works through `SatisfactionProblem`; no ILP work in this issue | +| 11 | Category | `src/models/graph/` | +| 12 | Expected outcome | On the issue’s 5-vertex graph with edges `{0,1},{0,2},{1,3},{2,3},{2,4},{3,4}` and `k = 3`, witness `[0, 0, 1, 1, 1]` is satisfying and is the unique satisfying assignment | + +## Batch 1: Model, Registry, CLI, Example DB, Tests + +### Task 1: Write the model tests first + +**Files:** +- Create: `src/unit_tests/models/graph/kclique.rs` + +**Step 1: Write the failing tests** + +Add focused tests that encode the issue semantics before any implementation exists: +- `test_kclique_creation` for constructor/getters/dims +- `test_kclique_evaluate_yes_instance` using the issue witness +- `test_kclique_evaluate_rejects_non_clique` +- `test_kclique_evaluate_rejects_too_small_clique` +- `test_kclique_solver_finds_unique_witness` +- `test_kclique_serialization_round_trip` +- `test_kclique_paper_example` + +Use helper builders in the test file for the issue graph and witness so the same instance is reused across tests. + +**Step 2: Run the focused test to verify RED** + +Run: `cargo test kclique --lib` + +Expected: fail because `KClique` and its test module do not exist yet. + +### Task 2: Implement the core `KClique` model + +**Files:** +- Create: `src/models/graph/kclique.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the model file** + +Create `src/models/graph/kclique.rs` with: +- `inventory::submit!` schema entry +- `KClique` struct deriving `Debug, Clone, Serialize, Deserialize` +- constructor `new(graph, k)` with a guard that `k <= num_vertices` +- getters `graph()`, `k()`, `num_vertices()`, `num_edges()` +- `is_valid_solution(&self, config: &[usize]) -> bool` +- internal clique-check helper shared by `evaluate` +- `Problem` impl with `NAME = "KClique"`, `Metric = bool`, graph-only `variant()`, binary `dims()`, and boolean `evaluate()` +- `SatisfactionProblem` impl +- `declare_variants! { default sat KClique => "1.1996^num_vertices" }` +- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue instance +- linked test module + +Do not add a weight dimension. `k` is instance data, not a variant. + +**Step 2: Register exports** + +Wire the new model through: +- `src/models/graph/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` prelude exports + +Also add `canonical_model_example_specs()` into the graph example chain in `src/models/graph/mod.rs`. + +**Step 3: Run the focused tests to verify GREEN** + +Run: `cargo test kclique --lib` + +Expected: the new `kclique` model tests pass. + +### Task 3: Add CLI and MCP creation support + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/mcp/tools.rs` + +**Step 1: Add failing create-path tests** + +In the existing `problemreductions-cli/src/commands/create.rs` test module, add at least: +- a positive `pred create KClique --graph ... --k 3` test that deserializes to `KClique` +- a negative test that rejects missing `--k` or `k > |V|` + +Prefer using the same issue example graph for the positive case. + +**Step 2: Run the focused create tests to verify RED** + +Run: `cargo test create_kclique --package problemreductions-cli` + +Expected: fail because create support does not exist yet. + +**Step 3: Implement create support** + +Update `problemreductions-cli/src/commands/create.rs` to: +- add `KClique` to the problem help examples table +- accept `KClique` in the graph problem dispatch +- parse `--graph` plus required `--k` +- construct `KClique::new(graph, k)` for normal create +- support random graph creation with `--random --num-vertices ... --k ...` + +Update `problemreductions-cli/src/cli.rs` help text so `KClique` appears in the “Flags by problem type” list with `--graph, --k`. + +Update `problemreductions-cli/src/mcp/tools.rs` in the mirrored create paths and supported-problem text so the MCP interface can also create `KClique` instances. + +**Step 4: Run focused CLI verification** + +Run: +- `cargo test create_kclique --package problemreductions-cli` +- `cargo test kclique --package problemreductions-cli` + +Expected: the new create-path tests pass. + +### Task 4: Verify example-db and registry integration + +**Files:** +- No new files beyond the ones above unless a small central regression test proves necessary + +**Step 1: Run focused integration checks** + +Run: +- `cargo test example_db --lib` +- `cargo test schema --lib` +- `cargo test problem_size --lib` + +If one of these suites exposes a missing assertion for `KClique`, add the minimal regression in the existing central test file that failed. Do not preemptively edit unrelated test files unless the failure requires it. + +**Step 2: Run batch-1 verification** + +Run: +- `cargo test kclique --all-targets` +- `cargo test create_kclique --package problemreductions-cli` +- `cargo test example_db --lib` + +Expected: all implementation-batch tests pass before touching the paper. + +## Batch 2: Paper Entry and Paper-Example Consistency + +### Task 5: Add the Typst paper entry after the model is stable + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Add the display name** + +Register: +- `"KClique": [$k$-Clique],` + +near the `display-name` dictionary. + +**Step 2: Add the `problem-def("KClique")` entry** + +Place it near the other graph problem definitions. The entry should: +- use the example-db fixture via `load-model-example("KClique")` +- introduce `G = (V, E)` and threshold `k` +- explain the decision semantics (`|K| >= k`) +- cite the historical context with `@karp1972` / `@garey1979` +- cite the exact-algorithm bound with `@xiao2017` +- present the issue’s house-graph example and witness `[0,0,1,1,1]` +- explicitly state why the witness is satisfying and why no 4-clique exists in that graph + +Avoid adding the chordal-graph claim unless you also add a proper bibliography entry and use it consistently. + +**Step 3: Re-run the paper-specific model test** + +Run: `cargo test kclique_paper_example --lib` + +Expected: pass against the same instance used in the paper entry. + +### Task 6: Final verification + +**Files:** +- No new files + +**Step 1: Run repository verification commands** + +Run: +- `cargo test kclique --all-targets` +- `cargo test create_kclique --package problemreductions-cli` +- `make paper` +- `make test` +- `make clippy` + +If `make paper` regenerates ignored exports under `docs/src/reductions/`, leave them unstaged. + +**Step 2: Inspect git status** + +Run: `git status --short` + +Expected: only the intended tracked source/doc changes remain. + +**Step 3: Implementation summary points for the PR comment** + +Capture for the final PR comment: +- model file(s) added and registrations updated +- CLI/MCP create support added +- canonical example + paper entry added +- any deviations from the plan, or `None` From 1041d2e4894ba12bcc11f873e29d761599c9231e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 03:10:54 +0800 Subject: [PATCH 2/4] Implement #714: [Model] KClique --- docs/paper/reductions.typ | 27 ++++ problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 105 +++++++++++++- problemreductions-cli/src/mcp/tools.rs | 43 +++++- src/lib.rs | 2 +- src/models/graph/kclique.rs | 145 +++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 4 +- src/unit_tests/models/graph/kclique.rs | 77 ++++++++++ 9 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 src/models/graph/kclique.rs create mode 100644 src/unit_tests/models/graph/kclique.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 79b4fbdea..bb895168d 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -76,6 +76,7 @@ "IsomorphicSpanningTree": [Isomorphic Spanning Tree], "KthBestSpanningTree": [Kth Best Spanning Tree], "KColoring": [$k$-Coloring], + "KClique": [$k$-Clique], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], "TravelingSalesman": [Traveling Salesman], @@ -1416,6 +1417,32 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } +#{ + let x = load-model-example("KClique") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.edges + let k = x.instance.k + let sol = (config: x.optimal_config, metric: x.optimal_value) + let K = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1))) + [ + #problem-def("KClique")[ + Given an undirected graph $G = (V, E)$ and an integer $k$, determine whether there exists a subset $K subset.eq V$ with $|K| >= k$ such that every pair of distinct vertices in $K$ is adjacent. + ][ + $k$-Clique is the classical decision version of Clique, one of Karp's original NP-complete problems @karp1972 and listed as GT19 in Garey and Johnson @garey1979. Unlike Maximum Clique, the threshold $k$ is part of the input, so this formulation is the natural target for decision-to-decision reductions such as $3$SAT $arrow.r$ Clique. The best known exact algorithm matches Maximum Clique via the complement reduction to Maximum Independent Set and runs in $O^*(1.1996^n)$ @xiao2017. + + *Example.* Consider the house graph $G$ with $n = #nv$ vertices, $|E| = #ne$ edges, and threshold $k = #k$. The set $K = {#K.map(i => $v_#i$).join(", ")}$ is a valid witness because all three pairs #clique-edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ") are edges, so $|K| = 3 >= #k$ and this is a YES instance. This witness is unique, and no $4$-clique exists because every vertex outside $K$ misses at least one edge to the other selected vertices. + + #figure({ + let hg = house-graph() + draw-edge-highlight(hg.vertices, hg.edges, clique-edges, K) + }, + caption: [The house graph with satisfying witness $K = {#K.map(i => $v_#i$).join(", ")}$ for $k = #k$. The selected vertices and their internal clique edges are highlighted in blue.], + ) + ] + ] +} #{ let x = load-model-example("MaximumClique") let nv = graph-num-vertices(x.instance) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 5c3bae2d1..b271a9cc9 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -222,6 +222,7 @@ Flags by problem type: QUBO --matrix SpinGlass --graph, --couplings, --fields KColoring --graph, --k + KClique --graph, --k MinimumMultiwayCut --graph, --terminals, --edge-weights PartitionIntoTriangles --graph GraphPartitioning --graph diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 85fb53849..4fb70ff60 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -371,6 +371,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5", _ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1", }, + "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", "MinimumCutIntoBoundedSets" => { @@ -1291,6 +1292,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { util::ser_kcoloring(graph, k)? } + "KClique" => { + let usage = "Usage: pred create KClique --graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let k = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?; + (ser(KClique::new(graph, k))?, resolved_variant.clone()) + } + // SAT "Satisfiability" => { let num_vars = args.num_vars.ok_or_else(|| { @@ -3261,6 +3269,21 @@ fn ser(problem: T) -> Result { util::ser(problem) } +fn parse_kclique_threshold( + k_flag: Option, + num_vertices: usize, + usage: &str, +) -> Result { + let k = k_flag.ok_or_else(|| anyhow::anyhow!("KClique requires --k\n\n{usage}"))?; + if k == 0 { + bail!("KClique: --k must be positive"); + } + if k > num_vertices { + bail!("KClique: k must be <= graph num_vertices"); + } + Ok(k) +} + fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } @@ -4278,6 +4301,21 @@ fn create_random( } } + "KClique" => { + let edge_prob = args.edge_prob.unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + bail!("--edge-prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); + let usage = + "Usage: pred create KClique --random --num-vertices 5 [--edge-prob 0.5] [--seed 42] --k 3"; + let k = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?; + ( + ser(KClique::new(graph, k))?, + variant_map(&[("graph", "SimpleGraph")]), + ) + } + // MinimumCutIntoBoundedSets (graph + edge weights + s/t/B/K) "MinimumCutIntoBoundedSets" => { let edge_prob = args.edge_prob.unwrap_or(0.5); @@ -4530,7 +4568,7 @@ fn create_random( _ => bail!( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ - MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \ + MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, TravelingSalesman, \ SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)" ), }; @@ -5172,4 +5210,69 @@ mod tests { let err = create(&args, &out).unwrap_err().to_string(); assert!(err.contains("out of bounds for left partition size 4")); } + + #[test] + fn test_create_kclique() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::KClique; + + let mut args = empty_args(); + args.problem = Some("KClique".to_string()); + args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); + args.k = Some(3); + + let output_path = + std::env::temp_dir().join(format!("kclique-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "KClique"); + assert_eq!( + created.variant.get("graph").map(String::as_str), + Some("SimpleGraph") + ); + + let problem: KClique = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.k(), 3); + assert_eq!(problem.num_vertices(), 5); + assert!(problem.evaluate(&[0, 0, 1, 1, 1])); + + let _ = std::fs::remove_file(output_path); + } + + #[test] + fn test_create_kclique_requires_valid_k() { + let mut args = empty_args(); + args.problem = Some("KClique".to_string()); + args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string()); + args.k = None; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err(); + assert!( + err.to_string().contains("KClique requires --k"), + "unexpected error: {err}" + ); + + args.k = Some(6); + let err = create(&args, &out).unwrap_err(); + assert!( + err.to_string().contains("k must be <= graph num_vertices"), + "unexpected error: {err}" + ); + } } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index e35a076f9..0e242bf51 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -2,7 +2,7 @@ use crate::util; use problemreductions::models::algebraic::QUBO; use problemreductions::models::formula::{CNFClause, Satisfiability}; use problemreductions::models::graph::{ - MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, + KClique, MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumSumMulticenter, MinimumVertexCover, SpinGlass, TravelingSalesman, }; use problemreductions::models::misc::Factoring; @@ -68,7 +68,7 @@ pub struct CreateProblemParams { )] pub problem_type: String, #[schemars( - description = "Problem parameters as JSON object. Graph problems: {\"edges\": \"0-1,1-2\", \"weights\": \"1,2,3\"}. SAT: {\"num_vars\": 3, \"clauses\": \"1,2;-1,3\"}. QUBO: {\"matrix\": \"1,0.5;0.5,2\"}. KColoring: {\"edges\": \"0-1,1-2\", \"k\": 3}. Factoring: {\"target\": 15, \"bits_m\": 4, \"bits_n\": 4}. Random graph: {\"random\": true, \"num_vertices\": 10, \"edge_prob\": 0.3}. Geometry graphs (use with MIS/KingsSubgraph etc.): {\"positions\": \"0,0;1,0;1,1\"}. UnitDiskGraph: {\"positions\": \"0.0,0.0;1.0,0.0\", \"radius\": 1.5}" + description = "Problem parameters as JSON object. Graph problems: {\"edges\": \"0-1,1-2\", \"weights\": \"1,2,3\"}. SAT: {\"num_vars\": 3, \"clauses\": \"1,2;-1,3\"}. QUBO: {\"matrix\": \"1,0.5;0.5,2\"}. KColoring: {\"edges\": \"0-1,1-2\", \"k\": 3}. KClique: {\"edges\": \"0-1,0-2,1-3,2-3,2-4,3-4\", \"k\": 3}. Factoring: {\"target\": 15, \"bits_m\": 4, \"bits_n\": 4}. Random graph: {\"random\": true, \"num_vertices\": 10, \"edge_prob\": 0.3}. Geometry graphs (use with MIS/KingsSubgraph etc.): {\"positions\": \"0,0;1,0;1,1\"}. UnitDiskGraph: {\"positions\": \"0.0,0.0;1.0,0.0\", \"radius\": 1.5}" )] pub params: serde_json::Value, } @@ -406,6 +406,16 @@ impl McpServer { util::ser_kcoloring(graph, k)? } + "KClique" => { + let (graph, _) = parse_graph_from_params(params)?; + let k_flag = params.get("k").and_then(|v| v.as_u64()).map(|v| v as usize); + let k = parse_kclique_threshold(k_flag, graph.num_vertices())?; + ( + ser(KClique::new(graph, k))?, + variant_map(&[("graph", "SimpleGraph")]), + ) + } + // SAT "Satisfiability" => { let num_vars = params @@ -613,6 +623,22 @@ impl McpServer { util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?; util::ser_kcoloring(graph, k)? } + "KClique" => { + let edge_prob = params + .get("edge_prob") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + anyhow::bail!("edge_prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, seed); + let k_flag = params.get("k").and_then(|v| v.as_u64()).map(|v| v as usize); + let k = parse_kclique_threshold(k_flag, graph.num_vertices())?; + ( + ser(KClique::new(graph, k))?, + variant_map(&[("graph", "SimpleGraph")]), + ) + } "MinimumSumMulticenter" => { let edge_prob = params .get("edge_prob") @@ -644,7 +670,7 @@ impl McpServer { _ => anyhow::bail!( "Random generation is not supported for {}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ - MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, \ + MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, \ TravelingSalesman, MinimumSumMulticenter)", canonical ), @@ -1230,6 +1256,17 @@ fn parse_graph_from_params(params: &serde_json::Value) -> anyhow::Result<(Simple Ok((SimpleGraph::new(num_vertices, edges), num_vertices)) } +fn parse_kclique_threshold(k_flag: Option, num_vertices: usize) -> anyhow::Result { + let k = k_flag.ok_or_else(|| anyhow::anyhow!("KClique requires 'k'"))?; + if k == 0 { + anyhow::bail!("KClique: 'k' must be positive"); + } + if k > num_vertices { + anyhow::bail!("KClique: k must be <= graph num_vertices"); + } + Ok(k) +} + /// Parse `weights` field from JSON params as vertex weights (i32), defaulting to all 1s. fn parse_vertex_weights_from_params( params: &serde_json::Value, diff --git a/src/lib.rs b/src/lib.rs index 34ca7f66a..b35413fa6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,7 @@ pub mod prelude { pub use crate::models::graph::{ BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, - GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, + GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, KClique, KthBestSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, }; diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs new file mode 100644 index 000000000..d16e2911c --- /dev/null +++ b/src/models/graph/kclique.rs @@ -0,0 +1,145 @@ +//! KClique problem implementation. +//! +//! KClique is the decision version of Clique: determine whether a graph +//! contains a clique of size at least `k`. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "KClique", + display_name: "k-Clique", + aliases: &["Clique"], + dimensions: &[VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"])], + module_path: module_path!(), + description: "Determine whether a graph contains a clique of size at least k", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "k", type_name: "usize", description: "Minimum clique size threshold" }, + ], + } +} + +/// The k-Clique decision problem. +/// +/// Given a graph `G = (V, E)` and a positive integer `k`, determine whether +/// there exists a subset `K ⊆ V` of size at least `k` such that every pair of +/// distinct vertices in `K` is adjacent. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KClique { + graph: G, + k: usize, +} + +impl KClique { + /// Create a new k-Clique problem instance. + pub fn new(graph: G, k: usize) -> Self { + assert!(k > 0, "k must be positive"); + assert!(k <= graph.num_vertices(), "k must be <= graph num_vertices"); + Self { graph, k } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the clique-size threshold. + pub fn k(&self) -> usize { + self.k + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Check whether a configuration is a valid witness. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + is_kclique_config(&self.graph, config, self.k) + } +} + +impl Problem for KClique +where + G: Graph + crate::variant::VariantParam, +{ + const NAME: &'static str = "KClique"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + is_kclique_config(&self.graph, config, self.k) + } +} + +impl SatisfactionProblem for KClique where G: Graph + crate::variant::VariantParam {} + +fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { + if config.len() != graph.num_vertices() { + return false; + } + + let selected: Vec = match config + .iter() + .enumerate() + .map(|(index, &value)| match value { + 0 => Ok(None), + 1 => Ok(Some(index)), + _ => Err(()), + }) + .collect::, _>>() + { + Ok(values) => values.into_iter().flatten().collect(), + Err(()) => return false, + }; + + if selected.len() < k { + return false; + } + + for i in 0..selected.len() { + for j in (i + 1)..selected.len() { + if !graph.has_edge(selected[i], selected[j]) { + return false; + } + } + } + true +} + +crate::declare_variants! { + default sat KClique => "1.1996^num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "kclique_simplegraph", + instance: Box::new(KClique::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + 3, + )), + optimal_config: vec![0, 0, 1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/kclique.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 274d9b76c..ffd86472f 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -12,6 +12,7 @@ //! - [`MinimumCutIntoBoundedSets`]: Minimum cut into bounded sets (Garey & Johnson ND17) //! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) +//! - [`KClique`]: Clique decision problem with threshold k //! - [`KthBestSpanningTree`]: K distinct bounded spanning trees (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles @@ -49,6 +50,7 @@ pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; +pub(crate) mod kclique; pub(crate) mod kcoloring; pub(crate) mod kth_best_spanning_tree; pub(crate) mod length_bounded_disjoint_paths; @@ -88,6 +90,7 @@ pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; +pub use kclique::KClique; pub use kcoloring::KColoring; pub use kth_best_spanning_tree::KthBestSpanningTree; pub use length_bounded_disjoint_paths::LengthBoundedDisjointPaths; @@ -127,6 +130,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SimpleGraph { + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) +} + +fn issue_witness() -> Vec { + vec![0, 0, 1, 1, 1] +} + +#[test] +fn test_kclique_creation() { + let problem = KClique::new(issue_graph(), 3); + + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 6); + assert_eq!(problem.k(), 3); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 6); + assert_eq!(problem.dims(), vec![2; 5]); +} + +#[test] +fn test_kclique_evaluate_yes_instance() { + let problem = KClique::new(issue_graph(), 3); + + assert!(problem.evaluate(&issue_witness())); + assert!(problem.is_valid_solution(&issue_witness())); +} + +#[test] +fn test_kclique_evaluate_rejects_non_clique() { + let problem = KClique::new(issue_graph(), 3); + + assert!(!problem.evaluate(&[1, 0, 1, 1, 0])); + assert!(!problem.is_valid_solution(&[1, 0, 1, 1, 0])); +} + +#[test] +fn test_kclique_evaluate_rejects_too_small_clique() { + let problem = KClique::new(issue_graph(), 3); + + assert!(!problem.evaluate(&[1, 0, 1, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 1, 1, 0])); +} + +#[test] +fn test_kclique_solver_finds_unique_witness() { + let problem = KClique::new(issue_graph(), 3); + let solver = BruteForce::new(); + + assert_eq!(solver.find_satisfying(&problem), Some(issue_witness())); + assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); +} + +#[test] +fn test_kclique_serialization_round_trip() { + let problem = KClique::new(issue_graph(), 3); + let json = serde_json::to_string(&problem).unwrap(); + let restored: KClique = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.graph().edges(), problem.graph().edges()); + assert_eq!(restored.k(), 3); + assert!(restored.evaluate(&issue_witness())); +} + +#[test] +fn test_kclique_paper_example() { + let problem = KClique::new(issue_graph(), 3); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&issue_witness())); + assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); +} From fd859fccdab7b96f08a97483e32d7688897f3f59 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 03:10:58 +0800 Subject: [PATCH 3/4] chore: remove plan file after implementation --- docs/plans/2026-03-21-kclique-model.md | 231 ------------------------- 1 file changed, 231 deletions(-) delete mode 100644 docs/plans/2026-03-21-kclique-model.md diff --git a/docs/plans/2026-03-21-kclique-model.md b/docs/plans/2026-03-21-kclique-model.md deleted file mode 100644 index 9f854653f..000000000 --- a/docs/plans/2026-03-21-kclique-model.md +++ /dev/null @@ -1,231 +0,0 @@ -# KClique Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `KClique` decision model end-to-end so issue #714 can unblock the pending rule issues that target or source Clique as a satisfaction problem. - -**Architecture:** Implement `KClique` as a graph-based `SatisfactionProblem` with one binary variable per vertex and a runtime threshold `k: usize`. Reuse the same clique-validity semantics as `MaximumClique`, but expose a boolean metric, graph-only registry variants, and constructor-facing schema/CLI fields `graph` and `k`. Use the issue’s 5-vertex house-graph instance with witness `[0, 0, 1, 1, 1]` and `k = 3` as the canonical example for tests, `pred create --example`, and the paper. - -**Tech Stack:** Rust, serde, inventory registry, Clap CLI, MCP create helpers, Typst paper, `make test`, `make clippy`, `make paper`. - ---- - -## Issue Context - -- **Issue:** #714 `[Model] KClique` -- **Good label:** present -- **Existing PR:** none (`action = create-pr`) -- **Associated rule issues verified from the issue + open rule search:** #229, #231, #201, #206 -- **References to honor:** `@garey1979`, `@karp1972`, `@xiao2017` -- **Important modeling decision:** this is a dedicated decision problem, not a variant of `MaximumClique`, because the metric is `bool` and the instance carries a threshold `k` - -## Required Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `KClique` | -| 2 | Mathematical definition | Given an undirected graph `G = (V, E)` and integer `k`, determine whether there exists `V' ⊆ V` with `|V'| ≥ k` such that every distinct pair in `V'` is adjacent | -| 3 | Problem type | Satisfaction (`Metric = bool`) | -| 4 | Type parameters | `G: Graph` | -| 5 | Struct fields | `graph: G`, `k: usize` | -| 6 | Configuration space | `vec![2; graph.num_vertices()]` | -| 7 | Feasibility check | Selected vertices must all be pairwise adjacent and the selected set size must be at least `k` | -| 8 | Objective function | `bool` only: `true` iff the configuration is a clique witness of size at least `k` | -| 9 | Best known exact algorithm | `O*(1.1996^n)` via complement to MIS / maximum clique, encoded as `"1.1996^num_vertices"` | -| 10 | Solving strategy | Brute force already works through `SatisfactionProblem`; no ILP work in this issue | -| 11 | Category | `src/models/graph/` | -| 12 | Expected outcome | On the issue’s 5-vertex graph with edges `{0,1},{0,2},{1,3},{2,3},{2,4},{3,4}` and `k = 3`, witness `[0, 0, 1, 1, 1]` is satisfying and is the unique satisfying assignment | - -## Batch 1: Model, Registry, CLI, Example DB, Tests - -### Task 1: Write the model tests first - -**Files:** -- Create: `src/unit_tests/models/graph/kclique.rs` - -**Step 1: Write the failing tests** - -Add focused tests that encode the issue semantics before any implementation exists: -- `test_kclique_creation` for constructor/getters/dims -- `test_kclique_evaluate_yes_instance` using the issue witness -- `test_kclique_evaluate_rejects_non_clique` -- `test_kclique_evaluate_rejects_too_small_clique` -- `test_kclique_solver_finds_unique_witness` -- `test_kclique_serialization_round_trip` -- `test_kclique_paper_example` - -Use helper builders in the test file for the issue graph and witness so the same instance is reused across tests. - -**Step 2: Run the focused test to verify RED** - -Run: `cargo test kclique --lib` - -Expected: fail because `KClique` and its test module do not exist yet. - -### Task 2: Implement the core `KClique` model - -**Files:** -- Create: `src/models/graph/kclique.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the model file** - -Create `src/models/graph/kclique.rs` with: -- `inventory::submit!` schema entry -- `KClique` struct deriving `Debug, Clone, Serialize, Deserialize` -- constructor `new(graph, k)` with a guard that `k <= num_vertices` -- getters `graph()`, `k()`, `num_vertices()`, `num_edges()` -- `is_valid_solution(&self, config: &[usize]) -> bool` -- internal clique-check helper shared by `evaluate` -- `Problem` impl with `NAME = "KClique"`, `Metric = bool`, graph-only `variant()`, binary `dims()`, and boolean `evaluate()` -- `SatisfactionProblem` impl -- `declare_variants! { default sat KClique => "1.1996^num_vertices" }` -- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue instance -- linked test module - -Do not add a weight dimension. `k` is instance data, not a variant. - -**Step 2: Register exports** - -Wire the new model through: -- `src/models/graph/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` prelude exports - -Also add `canonical_model_example_specs()` into the graph example chain in `src/models/graph/mod.rs`. - -**Step 3: Run the focused tests to verify GREEN** - -Run: `cargo test kclique --lib` - -Expected: the new `kclique` model tests pass. - -### Task 3: Add CLI and MCP creation support - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/mcp/tools.rs` - -**Step 1: Add failing create-path tests** - -In the existing `problemreductions-cli/src/commands/create.rs` test module, add at least: -- a positive `pred create KClique --graph ... --k 3` test that deserializes to `KClique` -- a negative test that rejects missing `--k` or `k > |V|` - -Prefer using the same issue example graph for the positive case. - -**Step 2: Run the focused create tests to verify RED** - -Run: `cargo test create_kclique --package problemreductions-cli` - -Expected: fail because create support does not exist yet. - -**Step 3: Implement create support** - -Update `problemreductions-cli/src/commands/create.rs` to: -- add `KClique` to the problem help examples table -- accept `KClique` in the graph problem dispatch -- parse `--graph` plus required `--k` -- construct `KClique::new(graph, k)` for normal create -- support random graph creation with `--random --num-vertices ... --k ...` - -Update `problemreductions-cli/src/cli.rs` help text so `KClique` appears in the “Flags by problem type” list with `--graph, --k`. - -Update `problemreductions-cli/src/mcp/tools.rs` in the mirrored create paths and supported-problem text so the MCP interface can also create `KClique` instances. - -**Step 4: Run focused CLI verification** - -Run: -- `cargo test create_kclique --package problemreductions-cli` -- `cargo test kclique --package problemreductions-cli` - -Expected: the new create-path tests pass. - -### Task 4: Verify example-db and registry integration - -**Files:** -- No new files beyond the ones above unless a small central regression test proves necessary - -**Step 1: Run focused integration checks** - -Run: -- `cargo test example_db --lib` -- `cargo test schema --lib` -- `cargo test problem_size --lib` - -If one of these suites exposes a missing assertion for `KClique`, add the minimal regression in the existing central test file that failed. Do not preemptively edit unrelated test files unless the failure requires it. - -**Step 2: Run batch-1 verification** - -Run: -- `cargo test kclique --all-targets` -- `cargo test create_kclique --package problemreductions-cli` -- `cargo test example_db --lib` - -Expected: all implementation-batch tests pass before touching the paper. - -## Batch 2: Paper Entry and Paper-Example Consistency - -### Task 5: Add the Typst paper entry after the model is stable - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Add the display name** - -Register: -- `"KClique": [$k$-Clique],` - -near the `display-name` dictionary. - -**Step 2: Add the `problem-def("KClique")` entry** - -Place it near the other graph problem definitions. The entry should: -- use the example-db fixture via `load-model-example("KClique")` -- introduce `G = (V, E)` and threshold `k` -- explain the decision semantics (`|K| >= k`) -- cite the historical context with `@karp1972` / `@garey1979` -- cite the exact-algorithm bound with `@xiao2017` -- present the issue’s house-graph example and witness `[0,0,1,1,1]` -- explicitly state why the witness is satisfying and why no 4-clique exists in that graph - -Avoid adding the chordal-graph claim unless you also add a proper bibliography entry and use it consistently. - -**Step 3: Re-run the paper-specific model test** - -Run: `cargo test kclique_paper_example --lib` - -Expected: pass against the same instance used in the paper entry. - -### Task 6: Final verification - -**Files:** -- No new files - -**Step 1: Run repository verification commands** - -Run: -- `cargo test kclique --all-targets` -- `cargo test create_kclique --package problemreductions-cli` -- `make paper` -- `make test` -- `make clippy` - -If `make paper` regenerates ignored exports under `docs/src/reductions/`, leave them unstaged. - -**Step 2: Inspect git status** - -Run: `git status --short` - -Expected: only the intended tracked source/doc changes remain. - -**Step 3: Implementation summary points for the PR comment** - -Capture for the final PR comment: -- model file(s) added and registrations updated -- CLI/MCP create support added -- canonical example + paper entry added -- any deviations from the plan, or `None` From a304248fa2cf95971db9e0beb3d810a15b0aa2cf Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 11:58:27 +0800 Subject: [PATCH 4/4] chore: add gh retry logic and enforce read-only agentic tests in review-pipeline - pipeline_board.py: retry transient gh CLI failures (3 attempts with backoff) - review-pipeline SKILL.md: explicitly instruct agentic-test subagent to skip fix mode Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/review-pipeline/SKILL.md | 2 +- scripts/pipeline_board.py | 34 +++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.claude/skills/review-pipeline/SKILL.md b/.claude/skills/review-pipeline/SKILL.md index f8511f88f..9849a5432 100644 --- a/.claude/skills/review-pipeline/SKILL.md +++ b/.claude/skills/review-pipeline/SKILL.md @@ -186,7 +186,7 @@ Invoke `/review-quality` (file: `.claude/skills/review-quality/SKILL.md`) with t - Classify as: `confirmed` / `not reproducible in current worktree` - For confirmed issues, note severity and recommended fix -**Do NOT fix any issues.** Only report them. +**Do NOT fix any issues.** Only report them. When dispatching the agentic-test subagent, explicitly instruct it: "This is a read-only review run. Do NOT offer to fix issues, do NOT select option (a) 'Review together and fix', and do NOT modify any files. Report findings only and stop after generating the report." ### 2. Compose Combined Review Comment diff --git a/scripts/pipeline_board.py b/scripts/pipeline_board.py index f75a3c9b5..4f4e86cd4 100644 --- a/scripts/pipeline_board.py +++ b/scripts/pipeline_board.py @@ -61,8 +61,38 @@ FAILURE_LABELS = {"PoorWritten", "Wrong", "Trivial", "Useless"} -def run_gh(*args: str) -> str: - return subprocess.check_output(["gh", *args], text=True) +def run_gh(*args: str, retries: int = 3, retry_delay: float = 5.0) -> str: + """Run a ``gh`` CLI command, retrying on transient failures. + + The ``gh project`` subcommands occasionally fail with cryptic errors + like "unknown owner type" due to transient API issues or token + refresh races (see cli/cli#7985, cli/cli#8885). Retrying after a + short delay resolves these reliably. + """ + last_exc: subprocess.CalledProcessError | None = None + for attempt in range(retries): + try: + return subprocess.check_output( + ["gh", *args], text=True, stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as exc: + last_exc = exc + stderr = (exc.stderr or "").strip() + if attempt < retries - 1: + print( + f"[run_gh] attempt {attempt + 1}/{retries} failed " + f"(rc={exc.returncode}, stderr={stderr!r}), " + f"retrying in {retry_delay}s…", + file=sys.stderr, + ) + time.sleep(retry_delay) + else: + print( + f"[run_gh] all {retries} attempts failed " + f"(stderr={stderr!r})", + file=sys.stderr, + ) + raise last_exc # type: ignore[misc] def _graphql_board_query(project_id: str, page_size: int, cursor: str | None) -> str: