Skip to content

ux(reshape): composite score ranking for cross-family alternatives (closes #153) - #836

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/153-wave11
Jun 6, 2026
Merged

cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/153-wave11

Conversation

@cristim

@cristim cristim commented May 28, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Replace the single-metric EffectiveMonthlyCost sort in fillAlternativesFromRecs with a compositeScore function that blends five weighted signals (cost 60%, family-gen 15%, arch 10%, CE confidence 10%, NF proximity 5%).
  • Add SavingsAbs *float64 and RecommendationCount int to OfferingOption; populate them in recommendationToOffering from RecommendationRecord so the confidence signal flows through without the exchange package importing internal config types.
  • Absent signals return a neutral 0.5 so missing data never penalises an offering (nullable-not-zero rule).

Signal weights (named consts in reshape.go)

Signal Weight Rationale
scoreWeightCost 0.60 Price remains dominant; non-price signals nudge but cannot flip a large cost gap
scoreWeightFamilyGen 0.15 Same-prefix generation jump (m5->m6i) signals shape compatibility
scoreWeightArch 0.10 x86->ARM swap requires AMI validation users may not have done
scoreWeightConfidence 0.10 High CE confidence (large fleet + large savings) is a strong signal
scoreWeightNFProximity 0.05 Exact NF match avoids over/under-capacity surprises

Test plan

  • TestCompositeScore_SameGenOutranksTermMismatch - near-perfect m6i outranks cheaper r5 with no family match
  • TestCompositeScore_SameArchOutranksCrossArch - x86->x86 outranks x86->ARM when family-gen is equal
  • TestCompositeScore_HighConfidenceOutranksLow - high-confidence CE rec beats low-confidence at equal cost
  • TestCompositeScore_AbsentSavingsIsNeutral - nil SavingsAbs treated as neutral (0.5), not low-confidence (0.0)
  • TestCompositeScore_FamilyPrefixDetection - familyPrefix strips generation digits correctly
  • TestCompositeScore_ARMDetection - isARMFamily classifies Graviton vs x86/AMD families
  • TestAnalyzeReshapingWithRecs_CompositeScoreOrdersAlternatives - snapshot integration: m6i > m6g > c5

Closes #153

Summary by CodeRabbit

  • Improvements
    • Enhanced the ranking and ordering of alternative instance offerings to better align with AWS Cost Explorer recommendations and architectural compatibility, resulting in more relevant alternatives presented to users.

)

Replace the ascending-by-EffectiveMonthlyCost sort in
fillAlternativesFromRecs with a compositeScore function that blends
five weighted signals:

  cost (60%): dominant term, cheaper offerings score higher
  familyGen (15%): same-prefix generation jump (m5->m6i) bonus
  arch (10%): cross-architecture (x86->ARM) penalty
  confidence (10%): CE savings+count confidence heuristic
  nfProximity (5%): normalized-capacity proximity bonus

Absent signals return 0.5 neutral to avoid penalising offerings for
missing data (per nullable-not-zero guideline).

Add SavingsAbs *float64 and RecommendationCount int fields to
OfferingOption; populate them in recommendationToOffering from
RecommendationRecord.Savings and .Count.

Add 25 new tests: unit tests for each score component helper plus an
integration snapshot that asserts m6i > m6g > c5 ordering.
@cristim cristim added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/s Hours type/feat New capability labels May 28, 2026
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12cd61a9-efb7-4c7e-952a-0d034fcba952

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements composite-score ranking for EC2 instance reshaping alternatives. The OfferingOption struct gains optional Cost Explorer metadata fields (SavingsAbs, RecommendationCount), which are populated from AWS recommendations. A new scoring system combines price with family-generation fit, architecture alignment, and CE confidence to replace cost-only sorting, with deterministic tie-breaking by monthly cost and instance type.

Changes

Composite Score Ranking for Cross-Family Alternatives

Layer / File(s) Summary
Cost Explorer metadata in OfferingOption
pkg/exchange/reshape.go
OfferingOption struct is extended with optional SavingsAbs (pointer to float64) and RecommendationCount (int) fields to carry AWS Cost Explorer recommendation data.
Recommendation to offering conversion with CE fields
internal/api/exchange_lookup.go
recommendationToOffering builds an OfferingOption instance step-by-step, populating RecommendationCount and conditionally setting SavingsAbs pointer when savings exceed zero, enriching the offering with Cost Explorer signals.
Composite scoring system with helper functions
pkg/exchange/reshape.go
New compositeScore function and supporting helpers (costComponent, familyGenComponent, architectureComponent with ARM/Intel detection, confidenceComponent bucketing, normalizationFactorComponent) combine price, family-generation penalty, architecture mismatch penalty, CE confidence boost, and normalization proximity.
Alternative ranking by composite score
pkg/exchange/reshape.go
fillAlternativesFromRecs documentation and sorting logic are updated: alternatives are now ordered by descending composite score with deterministic tie-breaking by ascending EffectiveMonthlyCost and then InstanceType.
Unit tests for scoring component logic
pkg/exchange/reshape_crossfamily_test.go
Tests validate composite-score component behavior: same-generation outranks cross-generation, same-architecture outranks cross-architecture, high-confidence outranks low-confidence at parity cost, nil savings are neutral. Helper tests verify familyPrefix extraction and isARMFamily classification.
Integration and determinism tests for alternative ranking
pkg/exchange/reshape_crossfamily_test.go
Tests ensure deterministic ordering when composite score and monthly cost are identical (lexicographic tie-break by InstanceType), and validate end-to-end ordering through AnalyzeReshapingWithRecs with three alternatives ranked by descending composite score.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

priority/p2, severity/medium, impact/many, effort/m

Poem

🐰 A rabbit hops through ranking schemes,
Where cost meets gen in composite dreams,
ARM-safe and certain, no cross-arch woes—
The scores align, and better choices grow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'ux(reshape): composite score ranking for cross-family alternatives (closes #153)' accurately describes the main change: implementing composite score-based ranking for cross-family alternatives, and references the closed issue.
Linked Issues check ✅ Passed All primary objectives from #153 are met: composite score ranking implemented with five weighted signals (cost, family-gen, architecture, confidence, capacity), SavingsAbs and RecommendationCount fields added to OfferingOption, and comprehensive unit and integration tests added to validate behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #153 objectives: scoring logic, supporting fields, and test coverage for cross-family alternative ranking. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/153-wave11

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 30, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Rate Limit Exceeded

@cristim have exceeded the limit for the number of chat messages per hour. Please wait 39 minutes and 27 seconds before sending another message.

… tie-break (refs #153)

isARMFamily used strings.HasPrefix(suffix, "g") which produced false
positives for the Intel Dense Storage/Network families is4gen and im4gn:
both have a suffix that starts with "g" after stripping letters+digits,
but they are x86-only processors. Fix: exclude the "im" and "is" prefixes
explicitly (reusing familyPrefix), then require an exact match against the
known Graviton suffix set {"g", "gd", "gn"} instead of a prefix check.

The alternatives sort.Slice had a two-key comparator (compositeScore desc,
EMC asc); two offerings with equal score and equal EMC could order
non-deterministically. Fix: add InstanceType as the final lexicographic
tie-breaker.

Tests: extend TestCompositeScore_ARMDetection with is4gen, im4gn (x86 ->
false), g4dn, g5 (NVIDIA GPU -> false). Add
TestFillAlternativesFromRecs_DeterministicOrder: delivers two equal-score
equal-EMC offerings in reverse-lexical order and asserts lexicographic
output -- fails pre-fix, passes post-fix.
@cristim

cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/exchange/reshape_crossfamily_test.go (1)

455-459: ⚡ Quick win

Misleading test name: "TermMismatch" should be "CrossFamily".

The test name TestCompositeScore_SameGenOutranksTermMismatch suggests it's testing RI term duration mismatches (1y vs 3y), but the test actually validates that same-family-prefix alternatives (m5→m6i) outrank cross-family alternatives (m5→r5). In the RI context, "term" conventionally refers to commitment duration, not family lineage.

📝 Suggested name fix
-// TestCompositeScore_SameGenOutranksTermMismatch asserts that a
+// TestCompositeScore_SameGenOutranksCrossFamily asserts that a
 // near-perfect alternative (same family prefix, exact NF, high confidence)
 // outranks an alternative with identical savings but no family match and
 // no confidence signal.
-func TestCompositeScore_SameGenOutranksTermMismatch(t *testing.T) {
+func TestCompositeScore_SameGenOutranksCrossFamily(t *testing.T) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/exchange/reshape_crossfamily_test.go` around lines 455 - 459, Rename the
misleading test TestCompositeScore_SameGenOutranksTermMismatch to reflect
cross-family behavior (e.g., TestCompositeScore_SameGenOutranksCrossFamily) and
update any references to this function name; specifically change the test
function name declaration and any calls or mentions
(TestCompositeScore_SameGenOutranksTermMismatch) and the comment header to match
the new name so it clearly documents that it validates same-family-prefix
alternatives outranking cross-family alternatives.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/exchange/reshape.go`:
- Around line 308-334: The confidenceComponent function currently coerces
RecommendationCount == 0 to 1 which can incorrectly penalize offers when the
count is actually missing; change the logic in confidenceComponent (which takes
an OfferingOption) so that if off.RecommendationCount == 0 you treat it as
"missing" and return 0.5 (neutral) instead of coercing to 1 — otherwise use the
actual count for the existing savings thresholds (savings >= 200 && count >= 3
-> 1.0, savings >= 50 -> 0.5, else 0.0).

---

Nitpick comments:
In `@pkg/exchange/reshape_crossfamily_test.go`:
- Around line 455-459: Rename the misleading test
TestCompositeScore_SameGenOutranksTermMismatch to reflect cross-family behavior
(e.g., TestCompositeScore_SameGenOutranksCrossFamily) and update any references
to this function name; specifically change the test function name declaration
and any calls or mentions (TestCompositeScore_SameGenOutranksTermMismatch) and
the comment header to match the new name so it clearly documents that it
validates same-family-prefix alternatives outranking cross-family alternatives.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ffe1ccf-c756-4c65-ae26-6fe871998513

📥 Commits

Reviewing files that changed from the base of the PR and between 4956d66 and 9a8cd6a.

📒 Files selected for processing (3)
  • internal/api/exchange_lookup.go
  • pkg/exchange/reshape.go
  • pkg/exchange/reshape_crossfamily_test.go

Comment thread pkg/exchange/reshape.go
Comment on lines +308 to +334
// confidenceComponent maps SavingsAbs + RecommendationCount to a [0,1]
// confidence score using the same thresholds as the API layer:
//
// high ($200+ savings, 3+ instances) -> 1.0
// medium ($50+ savings) -> 0.5
// low (otherwise) -> 0.0
//
// When SavingsAbs is nil (not supplied) the component returns 0.5 (neutral)
// so that an absent field doesn't tilt the ranking.
func confidenceComponent(off OfferingOption) float64 {
if off.SavingsAbs == nil {
return 0.5
}
savings := *off.SavingsAbs
count := off.RecommendationCount
if count < 1 {
count = 1
}
switch {
case savings >= 200 && count >= 3:
return 1.0
case savings >= 50:
return 0.5
default:
return 0.0
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle missing RecommendationCount as neutral in confidence scoring.

At Line 323, missing/zero RecommendationCount is coerced to 1, which can still produce a low-confidence penalty when count is actually absent. That conflicts with the neutral-missing contract documented at Lines 94-95.

Proposed fix
 func confidenceComponent(off OfferingOption) float64 {
-	if off.SavingsAbs == nil {
+	if off.SavingsAbs == nil || off.RecommendationCount <= 0 {
 		return 0.5
 	}
 	savings := *off.SavingsAbs
 	count := off.RecommendationCount
-	if count < 1 {
-		count = 1
-	}
 	switch {
 	case savings >= 200 && count >= 3:
 		return 1.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// confidenceComponent maps SavingsAbs + RecommendationCount to a [0,1]
// confidence score using the same thresholds as the API layer:
//
// high ($200+ savings, 3+ instances) -> 1.0
// medium ($50+ savings) -> 0.5
// low (otherwise) -> 0.0
//
// When SavingsAbs is nil (not supplied) the component returns 0.5 (neutral)
// so that an absent field doesn't tilt the ranking.
func confidenceComponent(off OfferingOption) float64 {
if off.SavingsAbs == nil {
return 0.5
}
savings := *off.SavingsAbs
count := off.RecommendationCount
if count < 1 {
count = 1
}
switch {
case savings >= 200 && count >= 3:
return 1.0
case savings >= 50:
return 0.5
default:
return 0.0
}
}
// confidenceComponent maps SavingsAbs + RecommendationCount to a [0,1]
// confidence score using the same thresholds as the API layer:
//
// high ($200+ savings, 3+ instances) -> 1.0
// medium ($50+ savings) -> 0.5
// low (otherwise) -> 0.0
//
// When SavingsAbs is nil (not supplied) the component returns 0.5 (neutral)
// so that an absent field doesn't tilt the ranking.
func confidenceComponent(off OfferingOption) float64 {
if off.SavingsAbs == nil || off.RecommendationCount <= 0 {
return 0.5
}
savings := *off.SavingsAbs
count := off.RecommendationCount
switch {
case savings >= 200 && count >= 3:
return 1.0
case savings >= 50:
return 0.5
default:
return 0.0
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/exchange/reshape.go` around lines 308 - 334, The confidenceComponent
function currently coerces RecommendationCount == 0 to 1 which can incorrectly
penalize offers when the count is actually missing; change the logic in
confidenceComponent (which takes an OfferingOption) so that if
off.RecommendationCount == 0 you treat it as "missing" and return 0.5 (neutral)
instead of coercing to 1 — otherwise use the actual count for the existing
savings thresholds (savings >= 200 && count >= 3 -> 1.0, savings >= 50 -> 0.5,
else 0.0).

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • pkg/exchange/reshape.go

Commit: 0a4b21da9660f0cc23c77ae6932818f093eb3c2c

The changes have been pushed to the fix/153-wave11 branch.

Time taken: 4m 46s

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@cristim
cristim merged commit f3f5616 into feat/multicloud-web-frontend Jun 6, 2026
4 checks passed
@cristim
cristim deleted the fix/153-wave11 branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/feat New capability urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant