Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Current scaffold policy:
- `templates/scalar/generic` is the embedded neutral scaffold used by `solverforge new`; `templates/list/generic` is not a public `new` selector
- the neutral `solver.toml` declares no `[[phases]]`, so a fresh app solves through the runtime's model-aware default pipeline; `generate scalar-group` refuses a user-authored scalar construction phase that reaches its assignment-owned target
- `generate`, `destroy`, and `new` rustfmt every Rust file they write (rustfmt is optional; a missing one leaves files as generated), fresh scaffolds pass `cargo fmt --check`, and managed constraint `mod` lines stay in rustfmt's sorted order so `cargo fmt` never breaks `check`
- `mod.rs` and `lib.rs` files, in this repo and in every scaffold, hold only module declarations and re-exports (plus the domain `planning_model!` manifest); constraint assembly lives in `src/constraints/assemble.rs`, and `solverforge check` rejects code in any module root
- `solverforge check` is structural and says it did not compile the project; generated constraint skeletons compile but their placeholders panic with the function and file to edit, so a solve that reaches one ends `FAILED`
- generated constraint analysis DTOs carry `weightMultiplier`; selector telemetry DTOs carry no `conflictRepair*` counters (the run-level telemetry keeps them)
- generated `Cargo.toml` files must carry `rust-version = "1.95"` plus the current explicit dependency baselines for the selected shell from the template/materialization code
Expand Down
11 changes: 10 additions & 1 deletion WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ Required managed surfaces:
- solution files: `solution-imports`, `solution-collections`,
`solution-constructor-params`, and `solution-constructor-init`
- entity files: `entity-variables` and `entity-variable-init`
- `src/constraints/mod.rs`: `constraint-modules` and `constraint-calls`
- `src/constraints/mod.rs`: the `constraint-modules` block, `mod assemble;`, and
`pub use assemble::create_constraints;`, with no code
- `src/constraints/assemble.rs`: `create_constraints` and the
`constraint-calls` block, which calls each module as
`super::<id>::constraint()`
- `solver.toml`: one generated model-resource region marked with
`# @solverforge:begin solver-config` and
`# @solverforge:end solver-config`; each generated phase has an exact-ID
Expand All @@ -204,6 +208,11 @@ Required managed surfaces:
Commands that add or remove generated resources should fail clearly when those
current markers are missing or duplicated.

Every `mod.rs` and `lib.rs` under `src/` holds only module declarations, `use`
items, and (in `src/domain/mod.rs`) the `planning_model!` manifest.
`solverforge check` rejects any function, type, impl, constant, or inline module
body in those files.

`constraint-modules` holds `mod` lines in rustfmt's sorted order and matches
them to calls by name; `constraint-calls` keeps insertion order because it is
the constraint set's evaluation order. `generate`, `destroy`, and `new` pipe
Expand Down
4 changes: 2 additions & 2 deletions skills/solverforge-modeling/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Re-derive specifics from the CLI if the version differs.
Decide this before anything else; it changes which parts of this skill apply.

- **Scaffolded project** — produced by `solverforge new`, so the CLI owns the
model through `src/domain/mod.rs`, `src/constraints/mod.rs`, managed blocks,
model through `src/domain/mod.rs`, `src/constraints/{mod,assemble}.rs`, managed blocks,
`solverforge.app.toml`, and `solver.toml`. Follow Steps 0–8 below.
- **Existing model** — a hand-written app that already depends on `solverforge`
directly (the
Expand Down Expand Up @@ -397,7 +397,7 @@ small conventional commits if the user asks for commits.
- Explain a plan with framework analysis (`analyze()`, `analyze_snapshot`,
`evaluate_detailed`), not by re-deriving constraint predicates in application
code.
- Never hand-wire managed blocks, `src/constraints/mod.rs`, or the solution's
- Never hand-wire managed blocks, `src/constraints/{mod,assemble}.rs`, or the solution's
collections. Use the CLI commands. (Scaffolded projects only; an existing
hand-written model is edited as ordinary Rust — see
`references/existing-model.md`.)
Expand Down
4 changes: 2 additions & 2 deletions skills/solverforge-modeling/references/constraint-patterns.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Constraint patterns

Every `solverforge generate constraint <id> --<pattern> [--hard|--soft]` writes
`src/constraints/<id>.rs` and wires it into `src/constraints/mod.rs` (managed
blocks) and, for web shells, `static/sf-config.json`. Do not wire constraints by
`src/constraints/<id>.rs` and wires it into `src/constraints/mod.rs` and
`src/constraints/assemble.rs` (managed blocks) and, for web shells, `static/sf-config.json`. Do not wire constraints by
hand.

The generated file is a **stub**: a real `ConstraintFactory` call streaming from
Expand Down
3 changes: 2 additions & 1 deletion skills/solverforge-modeling/references/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ compilation, and real runtime failures.

## 1. `solverforge check` — structural

Validates `src/domain/mod.rs`, `src/constraints/mod.rs`, the domain model,
Validates `src/domain/mod.rs`, `src/constraints/mod.rs`,
`src/constraints/assemble.rs`, that no `mod.rs` or `lib.rs` holds code, the domain model,
solution, entities, model resources, and `solver.toml` refs. It does **not**
compile Rust and does **not** run the solver, so it cannot see placeholder
`panic!`s or a broken stream source. Exit code 0 means the managed structure is
Expand Down
33 changes: 23 additions & 10 deletions src/commands/check.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::fs;
use std::path::Path;

use crate::commands::generate_constraint::{parse_domain, validate_constraint_mod_source};
use crate::commands::generate_constraint::{
parse_domain, validate_constraint_mod_source, ConstraintSources, CONSTRAINTS_ASSEMBLE_PATH,
CONSTRAINTS_MOD_PATH,
};
use crate::commands::generate_domain::{find_file_for_type, validate_domain_mod_source};
use crate::error::{CliError, CliResult};
use crate::managed_block;
use crate::model_contract::{ContractSeverity, ModelContract};
use crate::module_manifest;
use crate::output;
use crate::solver_config;

Expand Down Expand Up @@ -55,29 +59,38 @@ pub fn run() -> CliResult {

let constraints_dir = Path::new("src/constraints");
if constraints_dir.exists() {
output::print_status("check", "src/constraints/mod.rs");
let mod_path = constraints_dir.join("mod.rs");
let constraints_mod_src = read_path(&mod_path, &mut errors);
if let Some(src) = constraints_mod_src.as_deref() {
if validate_managed_source(&mod_path, src, &mut errors, validate_constraint_mod_source)
{
if let Ok(module_names) = validate_constraint_mod_source(src) {
output::print_status("check", CONSTRAINTS_MOD_PATH);
output::print_status("check", CONSTRAINTS_ASSEMBLE_PATH);
let module = read_path(Path::new(CONSTRAINTS_MOD_PATH), &mut errors);
let assemble = read_path(Path::new(CONSTRAINTS_ASSEMBLE_PATH), &mut errors);
if let (Some(module), Some(assemble)) = (module, assemble) {
match validate_constraint_mod_source(&ConstraintSources { module, assemble }) {
Ok(module_names) => {
for mod_name in module_names {
let file = constraints_dir.join(format!("{mod_name}.rs"));
if !file.exists() {
errors.push(format!(
"Constraint module '{}' declared in src/constraints/mod.rs but file not found",
mod_name
"Constraint module '{mod_name}' declared in {CONSTRAINTS_MOD_PATH} but file not found"
));
}
}
}
Err(err) => errors.push(err),
}
}
} else {
warnings.push("src/constraints/ directory not found".to_string());
}

output::print_status("check", "src/**/{mod,lib}.rs");
for root in module_manifest::module_roots(Path::new("src")) {
if let Some(src) = read_path(&root, &mut errors) {
if let Err(err) = module_manifest::ensure_declarations_only(&src) {
errors.push(format!("{}: {err}", output::display_path(&root)));
}
}
}

for (path, required_blocks) in [
(ENTITY_TEMPLATE_PATH, managed_block::ENTITY_REQUIRED_BLOCKS),
(
Expand Down
113 changes: 35 additions & 78 deletions src/commands/check_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,14 @@ impl Plan {
.expect("failed to corrupt plan");
fs::write(
"src/constraints/mod.rs",
r#"pub use self::assemble::create_constraints;

mod assemble {
use super::*;

pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
()
}
}
"#,
"mod assemble;\n\npub use assemble::create_constraints;\n",
)
.expect("failed to corrupt constraints mod");
fs::write(
"src/constraints/assemble.rs",
"pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {\n ()\n}\n",
)
.expect("failed to corrupt constraint assembly");

let err = run().expect_err("malformed managed files should fail check");
let err = err.to_string();
Expand Down Expand Up @@ -171,29 +167,7 @@ pub use plan::Plan;
"#,
)
.expect("failed to rewrite domain mod");
fs::write(
"src/constraints/mod.rs",
r#"use crate::domain::Plan;
use solverforge::prelude::*;

pub use self::assemble::create_constraints;

// @solverforge:begin constraint-modules
mod coverage_gap;
// @solverforge:end constraint-modules

mod assemble {
use super::*;

pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
// @solverforge:begin constraint-calls
()
// @solverforge:end constraint-calls
}
}
"#,
)
.expect("failed to rewrite constraints mod");
test_support::write_constraint_sources("mod coverage_gap;\n", " ()\n");
fs::write(
"src/constraints/coverage_gap.rs",
"pub fn constraint() {}\n",
Expand Down Expand Up @@ -318,6 +292,29 @@ impl Task {
);
}

#[test]
fn check_rejects_code_in_module_roots() {
let _cwd_guard = test_support::lock_cwd();
let tmp = tempfile::tempdir().expect("failed to create temp dir");
let _dir_guard = CwdGuard::enter(tmp.path());
write_project_layout();
run().expect("the generated layout holds no code in module roots");

fs::create_dir_all("src/api").expect("failed to create api dir");
fs::write(
"src/api/mod.rs",
"mod routes;\n\npub fn router() -> usize {\n 1\n}\n",
)
.expect("failed to write api mod");

let err = run().expect_err("a function in a mod.rs should fail check");
let err = err.to_string();
assert!(
err.contains("1 error(s), 0 warning(s)"),
"unexpected summary: {err}"
);
}

#[test]
fn check_rejects_stale_scalar_group_variable_targets() {
let _cwd_guard = test_support::lock_cwd();
Expand Down Expand Up @@ -760,29 +757,10 @@ fn check_rejects_constraint_named_value_that_differs_from_module_id() {
"#,
)
.expect("failed to write constraint");
fs::write(
"src/constraints/mod.rs",
r#"use crate::domain::Plan;
use solverforge::prelude::*;

pub use self::assemble::create_constraints;

// @solverforge:begin constraint-modules
mod unary_placeholder;
// @solverforge:end constraint-modules

mod assemble {
use super::*;

pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
// @solverforge:begin constraint-calls
unary_placeholder::constraint()
// @solverforge:end constraint-calls
}
}
"#,
)
.expect("failed to write constraints mod");
test_support::write_constraint_sources(
"mod unary_placeholder;\n",
" super::unary_placeholder::constraint()\n",
);

let err = run().expect_err("constraint ID drift should fail check");
let err = err.to_string();
Expand Down Expand Up @@ -901,27 +879,6 @@ impl Plan {
"#,
)
.expect("failed to write plan");
fs::write(
"src/constraints/mod.rs",
r#"use crate::domain::Plan;
use solverforge::prelude::*;

pub use self::assemble::create_constraints;

// @solverforge:begin constraint-modules
// @solverforge:end constraint-modules

mod assemble {
use super::*;

pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
// @solverforge:begin constraint-calls
()
// @solverforge:end constraint-calls
}
}
"#,
)
.expect("failed to write constraints mod");
test_support::write_constraint_sources("", " ()\n");
fs::write("solver.toml", "# test config\n").expect("failed to write solver.toml");
}
29 changes: 10 additions & 19 deletions src/commands/destroy.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::fs;
use std::path::Path;

use crate::commands::generate_constraint::remove_constraint_from_source;
use crate::commands::generate_constraint::{domain::DomainModel, parse_domain};
use crate::commands::generate_constraint::{
is_constraint_file_stem, remove_constraint_from_source, ConstraintSources,
CONSTRAINTS_ASSEMBLE_PATH, CONSTRAINTS_MOD_PATH,
};
use crate::commands::generate_domain::{find_file_for_type, snake_to_pascal};
use crate::commands::generate_domain::{
remove_domain_mod_entry_source, sync_demo_data_module, unwire_collection_from_solution_source,
Expand Down Expand Up @@ -370,7 +373,7 @@ fn ensure_type_is_unreferenced_in_source(
let constraint_id = (file.parent() == Some(constraints_dir))
.then(|| file.file_stem().and_then(|stem| stem.to_str()))
.flatten()
.filter(|stem| *stem != "mod");
.filter(|stem| is_constraint_file_stem(stem));
match constraint_id {
Some(id) => constraint_ids.push(id.to_string()),
None => other_files.push(slash_path(file)),
Expand Down Expand Up @@ -562,7 +565,6 @@ pub fn run_variable(field: &str, entity: &str, skip_confirm: bool) -> CliResult
pub fn run_constraint(name: &str, skip_confirm: bool) -> CliResult {
let constraint_id = ConstraintId::parse(name)?;
let file_path = Path::new("src/constraints").join(format!("{}.rs", constraint_id.as_str()));
let mod_path = Path::new("src/constraints/mod.rs");

if !file_path.exists() {
return Err(CliError::ResourceNotFound {
Expand All @@ -576,24 +578,12 @@ pub fn run_constraint(name: &str, skip_confirm: bool) -> CliResult {
return Ok(());
}

if !mod_path.exists() {
return Err(CliError::NotInProject {
missing: "src/constraints/mod.rs",
});
}

let mod_src = fs::read_to_string(mod_path).map_err(|e| CliError::IoError {
context: "failed to read src/constraints/mod.rs".to_string(),
source: e,
})?;
let new_mod_src = remove_constraint_from_source(&mod_src, constraint_id.as_str())
let sources = ConstraintSources::read(Path::new("."))?;
let rewritten = remove_constraint_from_source(&sources, constraint_id.as_str())
.map_err(CliError::general)?;
model_contract::ensure_constraint_has_no_conflict_repair_refs(constraint_id.as_str())?;

fs::write(mod_path, new_mod_src).map_err(|e| CliError::IoError {
context: "failed to update src/constraints/mod.rs".to_string(),
source: e,
})?;
rewritten.write(Path::new("."))?;
fs::remove_file(&file_path).map_err(|e| CliError::IoError {
context: format!("failed to delete {}", file_path.display()),
source: e,
Expand All @@ -603,7 +593,8 @@ pub fn run_constraint(name: &str, skip_confirm: bool) -> CliResult {
sync_demo_data_module()?;

output::print_remove(&file_path.display().to_string());
output::print_update("src/constraints/mod.rs");
output::print_update(CONSTRAINTS_MOD_PATH);
output::print_update(CONSTRAINTS_ASSEMBLE_PATH);
Ok(())
}

Expand Down
Loading
Loading