Reliable and flexible Java contracts can only be achieved through semantic versioning of packages. The OSGi Alliance semantically versions all its packages allowing users of the OSGi specifications to reliably pick something that will work, even when it's not the version they built with. The JCP does not version the packages it defines, although it does try to preserve backwards compatibility from release to release. Versions, in the context of the JCP, are specification versions. These are not semantic, they're marketing versions, just like product versions. These specification versions are not available to the runtime and so there's no way to reason about compatibility.
A number of projects have tried to add version information for the javax packages, but with no direction from the JCP, the approaches taken have been inconsistent. Some projects use specification versions (e.g. servlet 2.5, servlet 3.0), some choose a specification version as a 'baseline' and then semantically version from there (e.g. servlet 2.5, servlet 2.6), and some have even done both, exporting the same package at multiple versions.
So, back to the question posed at the start, "what do you do when the party providing the contract doesn't provide enough information to be certain they're providing what you want?". Well, as is customary with software engineering problems of this type, the answer is, you add another layer of indirection. In this case, the indirection is achieved using the "osgi.contract" namespace defined in the OSGi Enterprise R5 specification. Instead of depending on specific package versions, a consumer depends on a specific osgi.contract capability and then the provider (or some other third-party) provides a contract definition that corresponds to the package versions that the provider exports. That's probably as clear as mud, so let's see an example.
The following is a slightly updated version of the Servlet 2.5 example taken from the Enterprise R5 specification.
Provide-Capability: osgi.contract; osgi.contract=JavaServlet; version:Version=2.5; uses:="javax.servlet,javax.servlet.http"
Import-Package: javax.servlet; version="[2.5,3)", javax.servlet.http; version="[2.5,3)"
In this example a servlet container is providing the contract called "JavaServlet" versioned at 2.5 (i.e. it implements the Servlet 2.5 specification). The contract provider also imports the container's servlet packages in a version range appropriate to the package versions the container exports. The 'uses' directive in the contract definition ensures that anyone wiring to this contract will also be wired to the same provider of the servlet packages.
On the consumer side, anyone wanting to use Servlet 2.5 via this contract would specify the following:
Require-Capability: osgi.contract; filter:="(&(osgi.contract=JavaServlet)(version>=2.5))",
Import-Package: javax.servlet, javax.servlet.http
The Require-Capability says this bundle requires the JavaServlet contract, or later (it's assuming backwards compatibility). Note, the packages it imports are unversioned. The osgi.contract requirement ensures compatibility and the unversioned imports means the consuming bundle does not depend on a particular package versioning scheme of the provider (semantic or spec version).
The OSGi Alliance has started maintaining a set of contracts for the Java EE specifications. There is also a json representation of the contracts for use by tools to help them identify when a contract is not being used and make recommendations or provide quick-fixes.
One final note, it is only a good practice to use osgi.contracts when you're not able to rely on the package versions provided. Where at all possible, it's much better to use semantic versioning on your Import- and Export-Package statements.