@@ -14,7 +14,8 @@ use super::contract::{
1414} ;
1515use super :: source_set:: { ManifestNativeRust , PackageSource , load_package} ;
1616use super :: {
17- Manifest , PackageDiff , PackageInterfaceChange , PackageInterfaceChangeKind ,
17+ Manifest , PackageCapabilityChange , PackageCapabilityChangeKind , PackageDiff ,
18+ PackageInterfaceChange , PackageInterfaceChangeKind ,
1819 PackageManifestChange , PackageReviewAwaitBoundary , PackageReviewAwaitSite ,
1920 PackageReviewFileKind , PackageRisk , feature_values_label,
2021 package_feature_may_change_boundary_risk, package_identity, package_risk_label,
@@ -71,10 +72,23 @@ pub fn diff_package_dirs(old_dir: &Path, new_dir: &Path) -> Result<PackageDiff,
7172 old_review. await_sites . as_slice ( ) ,
7273 new_review. await_sites . as_slice ( ) ,
7374 ) ;
75+ let capability_changes =
76+ diff_package_capabilities ( & old_review. capabilities , & new_review. capabilities ) ;
77+
7478 let mut reasons = package_diff_reasons ( & manifest_changes, & interface_changes) ;
7579 if await_sites_changed {
7680 reasons. push ( "await site review metadata changed" . to_string ( ) ) ;
7781 }
82+ for change in & capability_changes {
83+ if change. change == PackageCapabilityChangeKind :: Added
84+ && change. risk == crate :: CapabilityRisk :: High
85+ {
86+ reasons. push ( format ! (
87+ "new high-risk capability `{}` via {}" ,
88+ change. category, change. binding_symbol
89+ ) ) ;
90+ }
91+ }
7892 if old_review. risk != new_review. risk {
7993 reasons. push ( format ! (
8094 "package risk changed from {} to {}" ,
@@ -99,11 +113,66 @@ pub fn diff_package_dirs(old_dir: &Path, new_dir: &Path) -> Result<PackageDiff,
99113 reasons,
100114 manifest_changes,
101115 interface_changes,
116+ capability_changes,
102117 old_review : old_review. summary ,
103118 new_review : new_review. summary ,
104119 } )
105120}
106121
122+ /// Distinct capabilities (by category + binding symbol) added or removed between
123+ /// two package versions, high-risk first.
124+ fn diff_package_capabilities (
125+ old : & [ crate :: package:: types:: PackageReviewCapability ] ,
126+ new : & [ crate :: package:: types:: PackageReviewCapability ] ,
127+ ) -> Vec < PackageCapabilityChange > {
128+ use std:: collections:: BTreeMap ;
129+ fn distinct (
130+ capabilities : & [ crate :: package:: types:: PackageReviewCapability ] ,
131+ ) -> BTreeMap < ( String , String ) , crate :: CapabilityRisk > {
132+ let mut map = BTreeMap :: new ( ) ;
133+ for capability in capabilities {
134+ map. entry ( ( capability. category . clone ( ) , capability. binding_symbol . clone ( ) ) )
135+ . or_insert ( capability. risk ) ;
136+ }
137+ map
138+ }
139+ let old_set = distinct ( old) ;
140+ let new_set = distinct ( new) ;
141+ let mut changes = Vec :: new ( ) ;
142+ for ( ( category, binding_symbol) , risk) in & new_set {
143+ if !old_set. contains_key ( & ( category. clone ( ) , binding_symbol. clone ( ) ) ) {
144+ changes. push ( PackageCapabilityChange {
145+ change : PackageCapabilityChangeKind :: Added ,
146+ category : category. clone ( ) ,
147+ binding_symbol : binding_symbol. clone ( ) ,
148+ risk : * risk,
149+ } ) ;
150+ }
151+ }
152+ for ( ( category, binding_symbol) , risk) in & old_set {
153+ if !new_set. contains_key ( & ( category. clone ( ) , binding_symbol. clone ( ) ) ) {
154+ changes. push ( PackageCapabilityChange {
155+ change : PackageCapabilityChangeKind :: Removed ,
156+ category : category. clone ( ) ,
157+ binding_symbol : binding_symbol. clone ( ) ,
158+ risk : * risk,
159+ } ) ;
160+ }
161+ }
162+ let rank = |risk : crate :: CapabilityRisk | match risk {
163+ crate :: CapabilityRisk :: High => 0u8 ,
164+ crate :: CapabilityRisk :: Medium => 1 ,
165+ crate :: CapabilityRisk :: Low => 2 ,
166+ } ;
167+ changes. sort_by ( |a, b| {
168+ rank ( a. risk )
169+ . cmp ( & rank ( b. risk ) )
170+ . then_with ( || a. category . cmp ( & b. category ) )
171+ . then_with ( || a. binding_symbol . cmp ( & b. binding_symbol ) )
172+ } ) ;
173+ changes
174+ }
175+
107176fn compare_package_identity (
108177 old : & Manifest ,
109178 new : & Manifest ,
@@ -647,3 +716,56 @@ fn manifest_change(
647716 risk,
648717 }
649718}
719+
720+ #[ cfg( test) ]
721+ mod capability_diff_tests {
722+ use super :: * ;
723+ use crate :: CapabilityRisk ;
724+ use crate :: package:: types:: PackageReviewCapability ;
725+
726+ fn cap ( binding : & str , category : & str , risk : CapabilityRisk ) -> PackageReviewCapability {
727+ PackageReviewCapability {
728+ function : binding. to_string ( ) ,
729+ binding_symbol : binding. to_string ( ) ,
730+ category : category. to_string ( ) ,
731+ risk,
732+ provider : None ,
733+ service : None ,
734+ action : None ,
735+ resource : None ,
736+ call_chain : vec ! [ binding. to_string( ) ] ,
737+ span : None ,
738+ unknown_reason : None ,
739+ }
740+ }
741+
742+ #[ test]
743+ fn reports_added_and_removed_capabilities_high_risk_first ( ) {
744+ let old = vec ! [ cap( "Db.read" , "database.read" , CapabilityRisk :: Medium ) ] ;
745+ let new = vec ! [
746+ cap( "Db.read" , "database.read" , CapabilityRisk :: Medium ) ,
747+ cap( "Db.write" , "database.write" , CapabilityRisk :: High ) ,
748+ cap( "Net.get" , "network.client" , CapabilityRisk :: High ) ,
749+ ] ;
750+ let changes = diff_package_capabilities ( & old, & new) ;
751+ // Two high-risk additions, sorted high-first by category.
752+ assert_eq ! ( changes. len( ) , 2 ) ;
753+ assert ! ( changes. iter( ) . all( |c| c. change == PackageCapabilityChangeKind :: Added ) ) ;
754+ assert_eq ! ( changes[ 0 ] . category, "database.write" ) ;
755+ assert_eq ! ( changes[ 0 ] . risk, CapabilityRisk :: High ) ;
756+ assert_eq ! ( changes[ 1 ] . category, "network.client" ) ;
757+ }
758+
759+ #[ test]
760+ fn dedups_per_binding_and_detects_removal ( ) {
761+ let old = vec ! [
762+ cap( "Db.write" , "database.write" , CapabilityRisk :: High ) ,
763+ cap( "Db.write" , "database.write" , CapabilityRisk :: High ) ,
764+ ] ;
765+ let new: Vec < PackageReviewCapability > = vec ! [ ] ;
766+ let changes = diff_package_capabilities ( & old, & new) ;
767+ assert_eq ! ( changes. len( ) , 1 ) ;
768+ assert_eq ! ( changes[ 0 ] . change, PackageCapabilityChangeKind :: Removed ) ;
769+ assert_eq ! ( changes[ 0 ] . binding_symbol, "Db.write" ) ;
770+ }
771+ }
0 commit comments