From 47b006f03e7ed2c4afdde70f1812877d32fda7b1 Mon Sep 17 00:00:00 2001 From: Vittorio Distefano Date: Thu, 5 Nov 2026 12:06:19 +0100 Subject: [PATCH] feat(scoring): document ConstraintWeight and export it at the crate roots ConstraintWeight bounds the weight of every public penalize and reward but was #[doc(hidden)], so those methods named a trait their documentation did not show. It is now documented: what score and is_hard mean, every implementing family (built-in scores, FixedWeight, closures, HardWeight), and a doctest implementing it for a custom weight scored through penalize. It is exported beside FixedWeight and HardWeight at the scoring and facade roots, and the wireframes list it. Closes #214. --- crates/solverforge-scoring/WIREFRAME.md | 4 +- crates/solverforge-scoring/public-api.txt | 3 + crates/solverforge-scoring/src/lib.rs | 4 +- crates/solverforge-scoring/src/stream/mod.rs | 4 +- .../src/stream/weighting_support.rs | 64 ++++++++++++++++++- crates/solverforge/WIREFRAME.md | 1 + crates/solverforge/src/lib.rs | 8 +-- 7 files changed, 77 insertions(+), 11 deletions(-) diff --git a/crates/solverforge-scoring/WIREFRAME.md b/crates/solverforge-scoring/WIREFRAME.md index a2bc6a91b..ebdc91bdb 100644 --- a/crates/solverforge-scoring/WIREFRAME.md +++ b/crates/solverforge-scoring/WIREFRAME.md @@ -377,7 +377,7 @@ pub use api::analysis::{ // Fluent Stream API pub use stream::{ fixed_weight, hard_weight, BiConstraintBuilder, BiConstraintStream, ConstraintFactory, - FixedWeight, GroupedConstraintBuilder, GroupedConstraintStream, HardWeight, + ConstraintWeight, FixedWeight, GroupedConstraintBuilder, GroupedConstraintStream, HardWeight, Projection, ProjectionSink, UniConstraintBuilder, UniConstraintStream, }; @@ -738,6 +738,8 @@ Construct with `new(entities)` or `with_description(entities, description)`. **`ConstraintAnalysis<'a, Sc: Score>`** — `{ constraint_ref: &'a ConstraintRef, weight_multiplier: u32, score: Sc, matches: Vec>, is_hard: bool }`; `new(constraint_ref, weight_multiplier, score, matches, is_hard)` +**`ConstraintWeight`** — The weight bound of every `penalize`/`reward`: `score(&self, args) -> Sc` scores one match from its borrowed row tuple (`(&A,)` through `(&A, &B, &C, &D, &E)`, or key and result for grouped streams), and `is_hard(&self) -> bool` decides the constraint's hardness. Implemented for the built-in score types (fixed per match, hard when the first level is non-zero), `FixedWeight`, closures over the matched rows (soft), and `HardWeight` (hard); implement it for a custom weight type. + **`FixedWeight` / `fixed_weight(score)`** — Public zero-erasure wrapper for custom fixed score weights. Use `penalize(fixed_weight(custom_score))` or `reward(fixed_weight(custom_score))` when `Sc` is user-defined. **`HardWeight` / `hard_weight(weight)`** — Public zero-erasure wrapper that forces hard constraint metadata while delegating scoring to the wrapped fixed or dynamic weight. diff --git a/crates/solverforge-scoring/public-api.txt b/crates/solverforge-scoring/public-api.txt index caa9e0453..4a5888d4f 100644 --- a/crates/solverforge-scoring/public-api.txt +++ b/crates/solverforge-scoring/public-api.txt @@ -506,6 +506,9 @@ trait director::score_director::WeightMode: pub trait WeightMode: Mode + Send + trait director::shadow_aware::SolvableSolution: pub trait SolvableSolution: PlanningSolution { // Required methods fn descriptor() -> SolutionDescriptor; fn entity_count(solution: &Self, descriptor_index: usize) -> usize; } director::shadow_aware::SolvableSolution: fn descriptor() -> SolutionDescriptor director::shadow_aware::SolvableSolution: fn entity_count(solution: &Self, descriptor_index: usize) -> usize +trait stream::ConstraintWeight: pub trait ConstraintWeight: Send + Sync { // Required methods fn score(&self, args: Args) -> Sc; fn is_hard(&self) -> bool; } + stream::ConstraintWeight: fn score(&self, args: Args) -> Sc + stream::ConstraintWeight: fn is_hard(&self) -> bool trait stream::ExistenceTarget: pub trait ExistenceTarget where EA: CollectionExtract, FA: UniFilter,{ type Output; // Required method fn apply( self, mode: ExistenceMode, extractor_a: EA, filter_a: FA, ) -> Self::Output; } stream::ExistenceTarget: type Output stream::ExistenceTarget: fn apply( self, mode: ExistenceMode, extractor_a: EA, filter_a: FA, ) -> Self::Output diff --git a/crates/solverforge-scoring/src/lib.rs b/crates/solverforge-scoring/src/lib.rs index 463973e73..42149e008 100644 --- a/crates/solverforge-scoring/src/lib.rs +++ b/crates/solverforge-scoring/src/lib.rs @@ -70,6 +70,6 @@ Fluent Constraint Stream API pub use stream::{ fixed_weight, hard_weight, BiConstraintBuilder, BiConstraintStream, ConstraintFactory, - FixedWeight, GroupedConstraintBuilder, GroupedConstraintStream, HardWeight, Projection, - ProjectionSink, UniConstraintBuilder, UniConstraintStream, + ConstraintWeight, FixedWeight, GroupedConstraintBuilder, GroupedConstraintStream, HardWeight, + Projection, ProjectionSink, UniConstraintBuilder, UniConstraintStream, }; diff --git a/crates/solverforge-scoring/src/stream/mod.rs b/crates/solverforge-scoring/src/stream/mod.rs index e6764af26..a2a37249d 100644 --- a/crates/solverforge-scoring/src/stream/mod.rs +++ b/crates/solverforge-scoring/src/stream/mod.rs @@ -157,6 +157,4 @@ pub use tri_stream::{TriConstraintBuilder, TriConstraintStream}; #[doc(hidden)] pub use unassigned::UnassignedEntity; pub use uni_stream::{UniConstraintBuilder, UniConstraintStream}; -#[doc(hidden)] -pub use weighting_support::ConstraintWeight; -pub use weighting_support::{fixed_weight, hard_weight, FixedWeight, HardWeight}; +pub use weighting_support::{fixed_weight, hard_weight, ConstraintWeight, FixedWeight, HardWeight}; diff --git a/crates/solverforge-scoring/src/stream/weighting_support.rs b/crates/solverforge-scoring/src/stream/weighting_support.rs index 1c6502e96..adcc989c1 100644 --- a/crates/solverforge-scoring/src/stream/weighting_support.rs +++ b/crates/solverforge-scoring/src/stream/weighting_support.rs @@ -31,10 +31,72 @@ pub const fn hard_weight(weight: W) -> HardWeight { HardWeight { weight } } -#[doc(hidden)] +/// The weight a `penalize` or `reward` call scores each match with. +/// +/// `Args` is the tuple of borrowed rows one match carries, `(&A,)` for a uni +/// stream up to `(&A, &B, &C, &D, &E)` for a penta stream; grouped streams +/// pass the key and the collector result. Implemented for: +/// +/// - every built-in score type (`SoftScore`, `HardSoftScore`, +/// `HardMediumSoftScore`, `HardSoftDecimalScore`, `BendableScore`), scoring +/// each match with that fixed value; +/// - [`FixedWeight`], wrapping any other fixed score; +/// - closures `Fn(&A, ...) -> Sc` over the matched rows; +/// - [`HardWeight`], wrapping another weight and marking it hard. +/// +/// Implement it for a custom weight type to compute a match's score from +/// state of your own. +/// +/// # Examples +/// +/// ``` +/// use solverforge_core::score::SoftScore; +/// use solverforge_scoring::api::constraint_set::IncrementalConstraint; +/// use solverforge_scoring::stream::{ +/// source, ChangeSource, ConstraintFactory, ConstraintWeight, +/// }; +/// +/// /// Scores each late minute past a deadline. +/// struct MinutesLate { +/// deadline: i64, +/// } +/// +/// impl<'a> ConstraintWeight<(&'a i64,), SoftScore> for MinutesLate { +/// fn score(&self, (end,): (&'a i64,)) -> SoftScore { +/// SoftScore::of((*end - self.deadline).max(0)) +/// } +/// +/// fn is_hard(&self) -> bool { +/// false +/// } +/// } +/// +/// #[derive(Clone)] +/// struct Plan { +/// ends: Vec, +/// } +/// +/// fn ends(plan: &Plan) -> &[i64] { +/// plan.ends.as_slice() +/// } +/// +/// let constraint = ConstraintFactory::::new() +/// .for_each(source(ends as fn(&Plan) -> &[i64], ChangeSource::Descriptor(0))) +/// .penalize(MinutesLate { deadline: 10 }) +/// .named("late"); +/// +/// let plan = Plan { ends: vec![8, 13, 15] }; +/// assert_eq!(constraint.evaluate(&plan), SoftScore::of(-8)); +/// assert!(!constraint.is_hard()); +/// ``` pub trait ConstraintWeight: Send + Sync { + /// The score one match with rows `args` contributes, before the + /// constraint's penalty or reward sign is applied. fn score(&self, args: Args) -> Sc; + /// Whether the constraint this weight scores is hard. Built-in fixed + /// scores are hard when their first (hard) level is non-zero; closures + /// are soft unless wrapped in [`HardWeight`]. fn is_hard(&self) -> bool; } diff --git a/crates/solverforge/WIREFRAME.md b/crates/solverforge/WIREFRAME.md index 046dd50dc..299fbc4ab 100644 --- a/crates/solverforge/WIREFRAME.md +++ b/crates/solverforge/WIREFRAME.md @@ -101,6 +101,7 @@ Each module groups re-exports from one layer crate, so every type a facade signa - `hard_weight` - `FixedWeight` - `HardWeight` +- `ConstraintWeight` (trait: the weight bound of `penalize`/`reward`) - `ConstraintSet` (trait) - `ConstraintMetadata<'a>` (borrowed constraint identity view), `ConstraintResult` - `SharedNodeDiagnostics`, `SharedNodeId`, `SharedNodeOperation` diff --git a/crates/solverforge/src/lib.rs b/crates/solverforge/src/lib.rs index 88345facf..fcd977837 100644 --- a/crates/solverforge/src/lib.rs +++ b/crates/solverforge/src/lib.rs @@ -200,10 +200,10 @@ Constraint API */ pub use solverforge_scoring::{ - fixed_weight, hard_weight, ConstraintMetadata, ConstraintResult, ConstraintSet, FixedWeight, - HardWeight, IncrementalConstraint, IncrementalUniConstraint, ListPrecedenceMakespanConstraint, - Projection, ProjectionSink, SharedNodeDiagnostics, SharedNodeId, SharedNodeOperation, - SingleConstraint, + fixed_weight, hard_weight, ConstraintMetadata, ConstraintResult, ConstraintSet, + ConstraintWeight, FixedWeight, HardWeight, IncrementalConstraint, IncrementalUniConstraint, + ListPrecedenceMakespanConstraint, Projection, ProjectionSink, SharedNodeDiagnostics, + SharedNodeId, SharedNodeOperation, SingleConstraint, }; /* ============================================================================