Skip to content

Fix Maven VEX false attestation through profiles, properties and coordinates - #2005

Open
Mikola Lysenko (mikolalysenko) wants to merge 40 commits into
mainfrom
fix/maven-discovery-false-attestation
Open

Mikola Lysenko (mikolalysenko) wants to merge 40 commits into
mainfrom
fix/maven-discovery-false-attestation

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

VEX attested hosted (and pre-v5 vendored) Maven pins that the build may not resolve: a profile declaring the GA at another version, an executable classifier copy, a .mvn/maven.config -D that moves the pin's ${property}, profile <properties> that move it (alone or combined, through nested properties), or ones that retarget a ${…} groupId / artifactId. Hosted scan wrote some of those pins too.

Fix

  • formats/maven: one property resolver (properties.rs): Maven's lookup order, nested interpolation (≤16), profile combinations (≤8, else fail closed). PomDep has version (as spelled) and build_version (-D first), plus version_alternatives and coordinate_alternatives.
  • VEX discovery: bypasses() keeps the ref but withholds it: MavenClassifierUnpatched, new MavenProfileUnpatched and MavenConfigOverride. A Socket version only maven.config supplies is a diagnostic, not a ref.
  • Hosted rewriter: refuses, writing nothing, any pin VEX would withhold: new redirect_maven_profile_coordinates_unpatched; the classifier and profile codes now refuse whenever a version may be off the pin.
  • Vendored reactor: same resolver and combinations; coordinates_overridden covers profiles.
  • CLI: notes vex_maven_profile_unpatched / vex_maven_config_override; CLI_CONTRACT rows.

Owner decisions

Where to look

  1. formats/maven/properties.rs: lookup_property, profile_combinations, the interpolators.
  2. formats/maven/mod.rs: PomScope::resolve_all_in_build / coordinate_alternatives / build_versions.
  3. vex/discover/maven.rs: bypasses, profile_override, unpatched_classifier_copy, retargeted.
  4. patch/redirect/maven_index.rs (retargeted, profile_bypass, unserved_classifier_copy) and their order in rewrite_maven_pom.
  5. vendor/jvm/maven_reactor.rs: profile_override, warn_property_coordinates.

Testing

Each behavior fix has a test that failed before it. Locally: core maven / vex:: / patch::redirect:: (1965), redirect_golden, CLI vex_sources, vex, e2e_vex, cli_parse_vex, vendor_jvm_cli; workspace clippy and fmt clean.

Folds #2020, #2026.

Review history

Fixed:

  • Profile declarations / management and classifier copies (live, profile, managed) withhold.
  • maven.config override keeps the ref unattested; maven.config-only pin is a diagnostic; -Dproject.* ignored.
  • Profile <properties>, nested and combined, move versions; model refs move with them.
  • Attributed / spaced / duplicate (last-wins) property tags read.
  • Unreadable or >8 profiles fail closed for property pins only.
  • Profile ids from the direct <id>, default when absent.
  • Unresolved parent property keeps its profile alternatives.
  • Retargeted coordinates: classifier copies, test-jar, type + classifier, ${…} type, empty type.
  • Profile declaration reads its own profile's coordinates.
  • Hosted back-ports: coordinates, profile versions, classifier copies.
  • Vendored: one resolver, combinations, retargeted classifier warns.
  • Simplify: shared copy/key/definition helpers, dead code, docs, test helpers.

Declined:

  • Over-refusals (fail-safe): hosted retargeted ignores management, legacy same-GAV served, 8-profile cap, unterminated ${.
  • Classifier copy at the exact suffixed pin pins: it fails resolution loudly.
  • Hosted/discovery semantic gaps (profile mgmt with no version, ${parent} classifier, ${…} type) need a behavior PR first.
  • Performance: per-lookup Vecs, per-dep combination reuse, eager index work.
  • direct_child_text > in attributes: detail text only.

🤖 Generated with Claude Code

VEX discovery attested two Maven pins the build may not resolve:

1. Profile declarations (J1). Discovery dropped every dependency inside
   <profiles> from the version grouping and diagnosed only suffixed
   profile versions. A profile declaring the pinned GA at the base
   version (or any other version) left the hosted ref attested, yet
   whenever that profile is active Maven resolves the unpatched jar. The
   hosted rewriter leaves profile declarations as written
   (redirect_maven_profile_dependency_unpatched) and vendored mode treats
   every profile as live, so discovery now agrees with the writers: a
   profile declaration of the pinned GA (main jar in <dependencies> or
   <dependencyManagement>, a declaration an undefined coordinate property
   may make that GA, or an executable classifier copy) at any version
   other than the pin keeps the ref but marks it unattested with the new
   UnattestedKind::MavenProfileUnpatched (CLI note
   vex_maven_profile_unpatched). A profile declaration with no <version>
   takes the live pin and does not withhold it. The pre-v5 single-pom
   vendored wiring gets the same check against its vendored version.

2. .mvn/maven.config user properties (J4). A ${prop} <version> was
   resolved from the pom's <properties> only, though coordinates already
   read maven.config's -D user properties. Maven looks user properties up
   first, so <version>${v}</version> with <v>1.0-socket.x</v> in the pom
   and -Dv=1.0 in maven.config builds the base release while discovery
   attested the pin. PomScope now lays the user properties over the root
   <properties> once, and both version resolution and coordinate
   interpolation read that map. The vendored reactor's interpolator is a
   method on the multi-pom Reactor, so it is not reused here; this is the
   minimal change in formats/maven.

Tests (each fails on origin/main):
- formats::maven::tests::version_property_reads_user_properties_first
- vex::discover::maven::tests::version_property_reads_maven_config_overrides
- vex::discover::maven::tests::profile_declaration_off_the_pin_is_not_attested
- vex::discover::maven::tests::vendored_ref_under_a_profile_version_is_not_attested
No existing test asserted the old behavior; the golden corpus is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the profile / user-property attestation fix:

- A profile <dependencyManagement> entry was flagged even when the live
  pin is a direct literal <version>, which beats management; likewise a
  profile-managed classifier entry no declaration takes. Profile
  management now withholds the pin only when management governs it (no
  live direct version). The old test case managed(Some("1.7.36")) beside
  a direct pin asserted the wrong outcome and now expects an attested
  pin; the governing case (managed live pin) expects the opposite.
- A version-less profile classifier copy was always flagged. One shared
  classifier check now covers live and profile copies and resolves a
  version-less copy from the classifier-keyed management (live, with a
  profile's entry overriding it; none in this pom fails closed). Profile
  classifier copies report MavenClassifierUnpatched, like live ones.
- The same check now runs for the pre-v5 vendored wiring too, where any
  executable classifier copy comes from the next repository (the vendored
  tree holds only the main jar). formats::maven gains
  NON_EXECUTABLE_CLASSIFIERS / is_executable_classifier, worded as in
  #2006 so the two merge cleanly.
- A pom-typed (or java-source / javadoc) profile declaration carries no
  code and no longer counts against the pin. Other types (test-jar, ejb)
  still do: they put a jar on a classpath.
- PomDep now keeps both the version as the pom spells it (`version`, what
  hosted writes and rolls back) and as the build resolves it
  (`build_version`, -D user properties first). Pins are found from the
  former, so a pin .mvn/maven.config moves stays a ref (list / rollback /
  remove and liveness keep the wiring) but is marked unattested with the
  new UnattestedKind::MavenConfigOverride (CLI note
  vex_maven_config_override). A Socket version only maven.config
  supplies is diagnosed (DIAG_REF_UNATTRIBUTABLE naming
  .mvn/maven.config), never a pom.xml ref, so nothing edits pom.xml for
  it.
- The build view leaves project.* / pom.* to the model: -Dproject.version
  no longer resolves a ${project.version} dependency version.
- Cleanup: main_jar_declarations / classifier_declarations take a scope
  predicate and back ga_versions, the classifier check and the profile
  check; group_by_ga is gone (live_gas lists the pin candidates);
  trusted_checksums takes the maven.config text extract already read.

Tests: version_property_reads_maven_config_overrides,
profile_declaration_off_the_pin_is_not_attested and
vendored_ref_bypassed_by_a_profile_or_classifier_is_not_attested
(renamed from vendored_ref_under_a_profile_version_is_not_attested) were
extended with a case per finding; each new case failed on the previous
commit. formats::maven gains model_reference_version_ignores_user_properties.
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes the two Maven false-attestation gaps #2005 left open in VEX
discovery.

1. Profile <properties> redefining a pin's property. A live
   <version>${v}</version> that the root <properties> resolve to the
   Socket pin resolves something else whenever an active profile's
   <properties> redefine v (or a property v names): Maven merges active
   profile properties over the pom's own before interpolating. Vendored
   mode refuses to pin over such a property (version_override); discovery
   attested it. PomScope now reads each profile's <properties>, and every
   PomDep carries version_alternatives: the other versions its
   build_version takes under any profile or set of profiles active
   together (user properties and project.* model references stay fixed; a
   value some set leaves unresolved, cyclic or too deep counts as the text
   as written). profile_override now walks live main-jar declarations too
   (same main_jar_declarations scope and management-governs filter): one
   whose build version is the pin but has an alternative off it marks the
   ref unattested with the existing UnattestedKind::MavenProfileUnpatched
   (CLI note and CLI_CONTRACT.md updated), in hosted and the pre-v5
   vendored wiring alike. Profile declarations and the shared classifier
   check (unpatched_classifier_copy) count every alternative as a version
   the declaration may resolve. A <profiles> declaration's own ${v} reads
   its own profile's <properties> first.

2. Property versions resolved one level deep. ${a} with <a>${b}</a> now
   resolves through every property it names, in both views PomDep keeps:
   `version` (as the pom spells it: model, own profile, root properties)
   and `build_version` (.mvn/maven.config user properties first at every
   level; project.* stays the model's). Limits are the vendored reactor's:
   at most 16 levels, and a cycle, undefined name or unterminated ${ never
   resolves (the version stays as written, which is never a pin). A partly
   literal version (${a}.${b}-x) resolves too, and ${project.version}
   resolves from the model in both views.

The vendored reactor's interpolator (interpolate_by and the duplicate
body of Reactor::interpolate_with) moves to formats/maven as the pure
interpolate_strict, built on interpolate_each, which enumerates the
values a name may take (capped at 16 results). Both now detect cycles
explicitly and bound work (1024 lookups, 4096 bytes), so a pom whose
properties each name the next many times over fails closed instead of
expanding exponentially. Reactor::interpolate drops its always-zero
depth argument. Vendored behavior is otherwise unchanged.

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_redefining_a_pin_property_are_not_attested
- vex::discover::maven::tests::profile_declaration_reads_its_own_profile_properties
- vex::discover::maven::tests::version_property_resolves_through_nested_properties
  (16-deep chain resolves, 17 does not; two cycles; nested user overrides)
- vex::discover::maven::tests::vendored_ref_under_profile_properties_is_not_attested
- formats::maven::tests::{version_properties_resolve_recursively_and_fail_closed,
  profile_properties_give_version_alternatives, interpolation_is_bounded}
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the profile / nested property resolution (#2020):

1. Model references moved by properties (false attestation). The model
   (${project.version} from <version>${revision}</version>) was bound
   already resolved from the root properties, and profiles were assumed
   unable to change it. Model references are now bound raw and resolved
   wherever they are read, with that read's user properties and active
   profiles: a profile <revision> moves ${project.version} (unattested),
   and a -Drevision=<pin> only .mvn/maven.config supplies makes the
   spelled and built versions differ, so it is diagnosed, never a ref.
2. Attributed / spaced property tags (false attestation). read_properties
   took the whole tag as the name, so <v combine.self="override"> and
   <v > were skipped. The name now ends at whitespace or '/', closing tags
   may carry whitespace, and <v/> defines an empty value.
3. Nested references in a profile value resolved against the root. Each
   combination of profiles is now resolved as one property view (the
   profiles merged over the pom's, a later one winning), so a profile's
   ${w} reads its own <w> first.
4. Profile <properties> parse errors failed the whole scope, including
   the hosted rewriter's. PomScope::with_user_properties no longer reads
   profile properties; parse_pom_with calls read_profiles(), and an
   unreadable one withholds every ${...} pin (MovedBy::Unreadable) while
   the rest of the pom is read.
5. The shared cross-product budget collapsed ordinary multi-profile poms.
   interpolate_each and its 16-result / 1024-lookup product are gone:
   profile_combinations walks the names reachable from the version,
   keeps only the profiles that define one (at most 8, else fail closed),
   and every combination of those is resolved on its own budget.
6. The detail named the lexicographically first alternative. Alternatives
   are listed smallest combination first and carry the profile ids
   (MovedBy), and the detail names them and the version they give.
7. Two precedence implementations. formats::maven::properties now holds
   lookup_property (Maven's order: model, user properties, each pom's
   properties with its active profiles merged in, bare ${version}),
   PomLayer and profile_combinations; PomScope and the vendored
   Reactor::lookup_with / version_override both use them, so vendoring
   now weighs the same combinations VEX does (it previously tried one
   profile at a time) and fails closed past the same bound.
8. Two interpolators. The round-based coordinate interpolate is gone:
   coordinates and model references go through the same expansion as
   versions, in a keep-unresolved mode (interpolate_lenient). Coordinate
   alternatives stay for #2026; PomScope::resolve_in_build takes the
   interpolation so it can reuse it.
9. Cleanups: resolutions() drops its from-profile flag (the classifier
   check derives it), PomScope::fixed and the profile-set guard are gone,
   and readers that never weigh profiles do not scan them.

Tests (each failed before this commit):
- vex::discover::maven::tests::model_references_move_with_the_properties_they_are_spelled_through
- vex::discover::maven::tests::profile_properties_with_attributes_are_read
- vex::discover::maven::tests::profile_values_resolve_through_their_own_profile
- vex::discover::maven::tests::unreadable_profile_properties_withhold_only_property_pins
- vex::discover::maven::tests::many_profiles_that_keep_the_pin_are_attested
- vex::discover::maven::tests::profile_property_detail_names_the_profile_and_version
- vendor::jvm::maven_reactor::tests::profiles_that_move_the_version_only_together_are_weighed
plus formats::maven::properties unit tests and extended
formats::maven::tests::profile_properties_give_version_alternatives.
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes the last Maven property false-attestation gap in VEX discovery
after #2020: a <groupId> / <artifactId> written as ${prop} that a
profile's <properties> redefine.

Coordinates were read only with no profile active, so discovery
attested two shapes the build may not resolve:

(a) The declaration carrying the pin names another package whenever a
    profile redefining its coordinate property is active; the pin then
    no longer applies to the patched GA, which resolves from its other
    declarations or transitively, unpinned.
(b) Another declaration (live, or inside a profile whose own properties
    retarget it) becomes the patched GA under a profile, at its own
    version off the pin.

Both readers now resolve coordinates through #2020's shared helpers
(formats::maven::properties): the groupId and artifactId are joined
with COORDINATE_SEPARATOR (U+0001, which XML 1.0 forbids) so one
profile_combinations walk and one interpolation per combination read
both under the same active profiles, through interpolate_keeping (the
keep-unresolved interpolate_lenient as an Interpolate), and
split_coordinates splits the result.

formats/maven: PomScope::coordinate_alternatives runs the joined text
through resolve_in_build; PomDep carries group_text / artifact_text and
coordinate_alternatives (CoordinateAlternative: group, artifact,
MovedBy), every pair some combination gives other than the no-profile
reading (ScopedDep.group / artifact, unchanged), plus a profile
declaration's own-profile reading. More than 8 relevant profiles or an
unreadable profile fail closed (the text as written, which may_name
reads as any text).

vex/discover/maven: bypasses() runs coordinate_override after
profile_override, reported as MavenProfileUnpatched (detail names the
profiles), for (a) the live main-jar declaration carrying the pin
(build_version == pin) with any alternative, and (b) any declaration
with an alternative that may be the GA's main jar (literal or
may_shadow; pom / sources / javadoc types skipped) at a version other
than the pin among its resolutions(), or literally an executable
classifier copy off the pin or with no version of its own. As in
profile_override, a managed declaration counts only where management
governs the pin, and a versionless main jar takes the live pin. CLI
note and CLI_CONTRACT.md updated.

vendor/jvm/maven_reactor: the v5 planner had the same gap.
interpolate_with and profile_override take the Interpolate to use
(interpolate_strict for versions, as before), and coordinates_elsewhere
reads the joined coordinates in each inheritor and under each profile
combination on the chain through them. (a) The existing
coordinates_overridden check now weighs profiles, not only inheritors.
(b) New warn_moved_coordinates: a declaration that names another
artifact in its pom but may be the GA in an inheritor or under a profile
combination is warned (coordinates_overridden) and, when versioned,
unpins its root and is uncertain, so attestable_checked no longer
attests it. This also covers the inheritor form of (b).

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_moving_the_pinned_coordinates_are_not_attested
- vex::discover::maven::tests::profile_properties_turning_a_declaration_into_the_pinned_ga_are_not_attested
- vendor::jvm::maven_reactor::tests::a_profile_moving_the_declared_coordinates_is_left_alone
- vendor::jvm::maven_reactor::tests::a_profile_turning_a_declaration_into_the_patched_artifact_is_weighed
- formats::maven::tests::profile_properties_give_coordinate_alternatives
Guard: vex::discover::maven::tests::unrelated_coordinate_properties_do_not_withhold.
User-property variants (a chain through -D, -D beating the profile) are
in each. The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecks

/code-review high findings on #2026.

Discovery (vex/discover/maven.rs): coordinate_override is gone. Each
declaration's coordinate_alternatives now carry the version it
resolves under the same profile combination, and bypasses() weighs
every such reading as a declaration of its own (retargeted(): read only
under those profiles, marked retargeted_by) through the existing
unpatched_classifier_copy and profile_override checks; only the pinned
declaration moving off the GA keeps its own small check
(pin_retargeted). So:
- (1) a classifier copy retargeted onto the GA is weighed with the
  Wiring: under the pre-v5 vendored repository (main jar only) it
  withholds even at the vendored version;
- (6) a profile that moves coordinates and version onto the pin
  together no longer withholds;
- (8) the reading's <type> maps as Maven's does: test-jar is the tests
  classifier copy, any other non-jar type (war, pom, ...) is not the
  GA's jar.

formats/maven: resolve_in_build is a wrapper over resolve_all_in_build,
which reads several texts of one declaration under the same profile
combinations but each through its own interpolation and work budget;
profile_combinations takes all of them. (5) The U+0001 joined text is
gone: an unterminated ${ in one coordinate stays in that one, and a
reading always names a non-empty profile set.

Vendored (maven_reactor.rs): profile_override / read_elsewhere read
several texts under one combination, each on its own budget. (7)
warn_unresolved_coordinates and warn_moved_coordinates are one pass,
warn_property_coordinates, classifying each declaration as unresolved
or moved with a shared warn-and-unpin tail. (2)(4) Text that can never
read the patched GA (may_interpolate_to) is ruled out before any
inheritor or profile is read, so a >8-profile fail-closed walk no
longer unpins roots for ${g}:guava. (3) An executable classifier copy
an inheritor or profile retargets onto the GA warns
classifier_declared, as a literal one does.

Hosted (9, back-port): PomIndex reads profile <properties> and each
declaration's coordinate_alternatives, and rewrite_maven_pom refuses a
dep whose pinned declaration a profile moves off the GA, or that
another declaration becomes at another version (or as an executable
classifier copy), with nothing written:
redirect_maven_profile_coordinates_unpatched (CLI_CONTRACT.md). The
vex_maven_profile_unpatched note no longer suggests re-patching such a
pin with vendored mode.

Tests (each fails with the fix disabled):
- vex::discover::maven::tests::vendored_ref_under_a_retargeted_classifier_copy_is_not_attested
- vendor::jvm::maven_reactor::tests::many_profiles_on_an_unrelated_coordinate_are_not_weighed
- vendor::jvm::maven_reactor::tests::a_profile_retargeting_a_classifier_copy_warns
- patch::redirect::tests::maven_pom_profile_moved_coordinates_refuse_the_dep
- new cases in profile_properties_turning_a_declaration_into_the_pinned_ga_are_not_attested
  (test-jar withheld; war and coordinates+version onto the pin attested)
  and formats::maven::tests::profile_properties_give_coordinate_alternatives
  (an unterminated ${ in one coordinate).
Redirect and VEX goldens unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Back-port rule: hosted must not write a pin VEX can never attest.
Hosted scan already refuses a live ${property} version, so the shape
left was a versioned <profiles> declaration of the GA beside a live
one: hosted warned redirect_maven_profile_dependency_unpatched but
still pinned the live declaration, and discovery's profile_override
withheld that pin forever. That covers a profile literal at the base or
another release, a profile ${property} (undefined, or one its own or
another profile's <properties> move off the pin, alone or combined),
a profile declaration through an undefined coordinate property, and a
profile <dependencyManagement> entry where management governs the pin
(the <dependencyManagement> pin hosted adds when no live direct
declaration carries a version).

PomIndex now records each declaration's managed flag and every version
the build may resolve for it (PomScope::resolve_in_build with
interpolate_strict, the same data discovery reads), and
PomIndex::profile_bypass mirrors discovery's profile check. When it
finds one, rewrite_maven_pom refuses the dep with nothing written,
under the existing redirect_maven_profile_dependency_unpatched code
(detail: the declaration, the version it resolves, the suffixed pin).
A versionless profile declaration, one resolving the pin, or profile
management under a direct pin still lets the pin land; the old
informational warning now fires only when nothing live is pinned or on
the legacy same-GAV path. CLI_CONTRACT.md and the
vex_maven_profile_unpatched note say so.

Tests (each fails with the check disabled):
- patch::redirect::tests::maven_profile_versions_are_weighed_before_pinning (new)
- patch::redirect::tests::maven_versioned_profile_declarations_are_never_pinned_over
  and maven_pom_undefined_coordinate_property_in_a_profile_is_reported,
  which asserted the old pin-beside-a-profile behavior.
Redirect golden (profile-literal keeps its warning code) and the VEX
golden corpus are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Back-port rule: hosted must not write a pin VEX can never attest.
Hosted scan skipped a dep only when an executable classifier copy
(not sources / javadoc) named the patched release; one at another
release, through a ${property}, or with no version in the pom let the
main jar's pin land, and discovery's unpatched_classifier_copy then
withheld it forever (vex_maven_classifier_unpatched).

PomIndex::unserved_classifier_copy mirrors that check with the shared
is_executable_classifier and the per-declaration build_versions (every
version the build may resolve, profile <properties> included): a
non-managed executable copy of the GA, live or inside <profiles>, whose
own versions, or else its classifier-keyed <dependencyManagement>
entries' (live; a profile's too; none live = no version in this pom),
include anything but the suffixed pin. rewrite_maven_pom then refuses
the dep with nothing written under the existing
redirect_maven_classifier_unsupported code (detail: the copy, the
version it resolves, the suffixed pin). sources / javadoc copies still
only warn, and the legacy same-GAV path keeps its release check. A
test-jar typed declaration has no classifier and stays refused as an
unsupported packaging (live) or a profile bypass. CLI_CONTRACT.md says
so in both the hosted and the vex_maven_classifier_unpatched sections.

Test (fails with the check disabled):
- patch::redirect::tests::maven_executable_classifier_off_the_pin_blocks_the_redirect
Redirect golden and the VEX golden corpus are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

…classifiers

The "only through .mvn/maven.config" diagnostic fired whenever the build
resolved a different Socket version than the pom spelled, even when the
pom spelling was itself a Socket pin. That pin is pom.xml wiring: pin
detection keeps its ref and reports MavenConfigOverride, so the
diagnostic contradicted it. Skip it when the pom spelling is a pin.

The executable-classifier scan matched only a literal groupId/artifactId,
so a classifier copy declared through an undefined ${property}
coordinate was ignored and the main-jar pin could still be attested
while Maven might put that unpatched classifier jar on the classpath.
Match coordinates with may_name, as main-jar grouping already does.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 6, 2026 12:35
A version-less executable classifier copy took its version from any
managed classifier entry `may_name` matched, including one whose groupId
or artifactId is an undefined property. That entry may manage another
GA, so when it was the only one and sat at the pin, discovery attested
while Maven could resolve the copy from outside this pom, unpatched.
Such entries still count as possible versions, but without a live entry
that names the GA literally the copy's version is treated as unknown.
Addresses Bugbot's classifier-management finding on this PR.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 6, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 2af00b2b.

  • CI: 29/31 non-skipped checks green on 2af00b2b. The only failures are the main-wide e2e … pdm:: --ignored leg (hishel 2; also red on main; Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes it) and the ci-ok aggregate that follows from it. The full coverage set on the previous head 64a07dae was green apart from that same leg.
  • Bugbot:
    • Fixed in 2af00b2b: "Classifier management over-match attests unpatched copies". A version-less executable classifier copy whose only managed entry reached the GA through an undefined coordinate property inherited that entry's version. That entry may manage another GA, so it no longer decides the version on its own: without a literal live entry the version is treated as unknown (fail closed). A regression row in executable_classifier_copy_off_the_pin_is_not_attested fails without the fix.
    • Bugbot re-reviewed 2af00b2b: no new issues, no unresolved threads.
  • What a reviewer should look at: unpatched_classifier_copy in vex/discover/maven.rs (the literal-vs-property distinction), and the profile / user-property bypass checks from the earlier commits.
  • Slack: not announced. The Slack send action isn't available in this run, so this comment has no slack-announced marker and the next run will retry.

Generated by Claude Code

…property-overrides

Resolve the classifier-copy conflict by keeping both sides: this branch's
per-resolution profile flags (flagged/resolutions) and the base's rule
that a managed entry naming the GA only through an undefined property
never stands in for the copy's version alone.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does: Maven VEX discovery stops attesting pins the build may not use. (1) If a <profiles> entry declares the pinned GA's main jar at another version, or an executable classifier copy (live or in a profile) resolves another version, the pin stays a ref but is not attested. (2) ${prop} versions now resolve .mvn/maven.config -D user properties before pom <properties>, as Maven does. A pin that maven.config moves is not attested; a Socket version that only maven.config supplies becomes a diagnostic, not a pom.xml ref.

Risk: medium. This is the attestation path, and it is about 800 lines of rule logic. But every change withholds or narrows an attestation, never adds one, and the golden corpus output is unchanged.

Look here:

Verified:

  • Read the full diff, with an independent second review.
  • cargo test -p socket-patch-core --lib -- vex::discover:: formats::maven vendor::jvm::: 708 passed.
  • The new tests cover each change and would fail without it.
  • maven.config parsing handles -Dk=v, -D k=v, --define, quoting, comments and CRLF.
  • CHANGELOG.md is untouched. Bugbot is clean on 2af00b2. No unresolved threads.
  • CI: 29 passed, 19 skipped. The 2 red (ci-ok and the pdm e2e) are the e2e failure on every PR, which Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes.

Changes I made: none.

Open questions (none block merging):

  1. Profile <properties> that redefine a property a pin is spelled through are still not read. For example, root <slf4j.version>1.7.36-socket.x</slf4j.version> with a profile setting 1.7.36 is still attested. The PR's "Not covered" section says so; hosted mode writes literal versions, and draft Resolve Maven pin properties through profiles and nesting #2020 follows up on it.
  2. MavenConfigOverride also fires for vendored refs, but the docs and CLI note say "hosted Maven pin". In one rare case it withholds a vendored ref the build does use: the pom spells ${v}, the pom doesn't define v, and maven.config sets -Dv to the vendored version. Small follow-up.
  3. The config-only diagnostic prints the version after property resolution, not the ${…} text the pom actually spells.

Approving now doesn't merge it yet: auto-merge is not armed because ci-ok is red from the main-wide pdm failure. I'll arm it once CI is green after #2028 lands.


Generated by Claude Code

…rrides' into fix/maven-discovery-coordinate-overrides

# Conflicts:
#	crates/socket-patch-core/src/vex/discover/maven.rs
Second review round on Maven discovery false attestation.

- `MavenConfigOverride` compared the build-resolved versions with the
  pom-spelled ones, so a property that only `.mvn/maven.config` defines
  AT the pinned version (a pre-v5 vendored `${commons.version}`) was
  reported as an override. It is one only when the build resolves
  something other than the pin; the detail lists versions plainly
  instead of a debug-printed set.
- A version-less profile `<dependencyManagement>` entry for a classifier
  copy (scope or exclusions only) counted as an unpatched copy; it
  overrides no version and is skipped, as `profile_override` already
  does for the main jar.
- The config-only Socket-version diagnostic is emitted once per GA and
  version, not once per declaration.
- The hosted rewriter uses the shared `is_executable_classifier`; the
  contract rows, `UnattestedKind` doc and CLI notes say pre-v5 vendored
  refs get these kinds too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation' into fix/maven-discovery-property-overrides

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-core/src/vex/discover/maven.rs
Second review round on Maven property resolution.

- `read_properties` kept the FIRST definition of a duplicated property;
  Maven's model reader keeps the last (`Properties.put`). A pom defining
  `slf4j.version` as the Socket pin and then as the base release was
  attested while Maven built the base. Last wins now, for the root, the
  profiles and the reactor alike.
- A profile declaration's `<groupId>` / `<artifactId>` were interpolated
  with no profile active while its version read the profile's own
  `<properties>`, so a profile dependency naming the pinned GA only
  through a profile property was missed by `profile_override`.
  Coordinates now read the declaration's own profile first.
- A profile was named by the first `<id>` anywhere under it (a nested
  `<repository>`'s); it is named by its own direct `<id>` child.
- An undetermined profile alternative (unreadable profile properties,
  too many profiles) no longer reads as "nothing it can resolve
  (${v})" but as a version this reader cannot work out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rrides' into fix/maven-discovery-coordinate-overrides

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-core/src/formats/maven/mod.rs
#	crates/socket-patch-core/src/patch/redirect/mod.rs
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b6d0178. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: not labeled Ready for review. Added agent:needs-human.

At b6d0178, CI is 31/31 green and Bugbot found nothing. One human review finding is still open and real (thread on vex/discover/maven.rs:944): a profile's <properties> can redefine the property a ${…} pin is spelled through, and that still attests here. The stacked draft #2020 fixes exactly this.

Question for a maintainer: should #2020 be folded into this PR before review, or should #2005 land as it is, with #2020 following right after? (Until #2020 lands, main would carry this false-attestation gap.)


Generated by Claude Code

…property-overrides

The base merged main, which brought in #2006 (shared Maven variant
helpers, classifier-and-type keyed management) and #2008/#2007
(new unattested kinds in CLI_CONTRACT). Kept both sides:

- CLI_CONTRACT: base's eight-case list plus this branch's
  property-interpolation paragraph for profile declarations.
- formats/maven: keep base's is_variant / is_main_jar /
  is_executable_variant helpers; is_model_reference already lives in
  maven/properties.rs on this branch.
- vex/discover/maven: unpatched_classifier_copy keys management by
  classifier and type (base) and still checks every profile-property
  resolution (flagged/resolutions, this branch); profile_override walks
  live and profile main-jar declarations (this branch) through base's
  is_main_jar filter.
- vendor/jvm/maven_reactor: base's new variant checks call this
  branch's two-argument interpolate.

Co-Authored-By: Claude <noreply@anthropic.com>
…-coordinate-overrides

The base took main's variant refactor (#2006: a <type> such as test-jar
counts like the classifier it implies). Conflicts resolved keeping both:

- redirect/mod.rs: the variant loop labels and weighs copies through
  variant_label / is_executable_variant, and still defers suffixed pins
  to unserved_classifier_copy.
- maven_index.rs: unserved_classifier_copy now weighs every executable
  variant (typed ones too), with management keyed by classifier and
  type, so a test-jar copy off the pin still blocks the redirect.
- maven_reactor.rs: version_moved kept as a wrapper over this PR's
  read_elsewhere (main's callers use it); the property-coordinate filter
  keeps this PR's executable-classifier rule.
- discover/maven.rs: this PR's retargeted helpers with the base's
  variant doc and imports.
- CLI_CONTRACT.md: both sides' additions (invalid_hash checksum, NuGet
  and sbt cases, typed variants, Go workspaces) plus this PR's text.

Co-Authored-By: Claude <noreply@anthropic.com>
Two readings of a classifier copy that a profile's <properties> move
disagreed with VEX discovery:

- Hosted: `retargeted` counted a moved classifier copy as the patched
  GA only when its interpolated coordinates equal it exactly. Discovery
  reads them through `may_name`, so a coordinate the profile leaves on
  a property the pom does not define may still be the GA. Scan could
  write a pin that vex then withholds (`vex_maven_classifier_unpatched`).
  Use `may_name` here too.
- Vendored: `warn_property_coordinates` dropped every declaration whose
  <type> is not `jar`, test-jar included. Discovery and hosted map
  test-jar to the `tests` classifier copy, so a profile that turns one
  into the patched GA now gets `classifier_declared` like a
  <classifier>tests</classifier> copy does.

Addresses Bugbot 4501092184 and 4501092187 on #2026.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aePC6sLFkqRM2XPWq4P
Main's #2010 added the pnpm branch-lock and self-patched npm/Bun
unattested kinds in the same places this PR adds the Maven profile and
.mvn/maven.config kinds (UnattestedKind, vex_sources.rs notes, and the
CLI_CONTRACT.md bullet and table). Kept both sides, kept this PR's
reworded Maven classifier sentence and row, and counted the cases as
ten (six before, two from each side).

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged main (32e1630, #2010) to fix the merge conflict. New head ab71a53.

#2010 added the pnpm branch-lock and self-patched npm/Bun unattested kinds in the same places this PR adds the Maven profile and .mvn/maven.config kinds: UnattestedKind, the vex_sources.rs notes, and the CLI_CONTRACT.md bullet and table. I kept both sides, kept this PR's reworded Maven classifier sentence and row, and changed the case count to "Ten cases" (six before, plus two from each side).

Local checks: cargo check -p socket-patch-core -p socket-patch-cli --all-targets passes, core vex:: tests pass (791), and cli vex tests pass (38). The agent:needs-human question above is still open, so I left the label.


Generated by Claude Code

Claude (claude) and others added 3 commits November 9, 2026 01:31
…property-overrides

Resolve CLI_CONTRACT.md: the base grew the unattested-references list
to ten cases (pnpm branch locks, self-patched npm/Bun packages); keep
that text and re-insert this PR's sentence on profile-redefined
${property} pin versions in the Maven profile declarations case.

Co-Authored-By: Claude <noreply@anthropic.com>
…-coordinate-overrides

Resolve CLI_CONTRACT.md's Unattested references paragraph with a
sentence-level three-way merge: keep the base's ten-case text (pnpm
branch locks, self-patched npm/Bun) and this PR's coordinate-override
wording.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Owner decision on the agent:needs-human question ("fold #2020 into this PR, or land as is with #2020 right after?"): land the stack in order, back-to-back through the merge queue. Don't fold. This PR goes first, then #2020, then #2026. The one open thread (profile <properties> override at vex/discover/maven.rs) is fixed in #2020 and is resolved here on that basis. Removing agent:needs-human.


Generated by Claude Code

/code-review high on #2020:

- A live `${prop}` this pom leaves unresolved (a parent pom defines it)
  dropped every profile alternative, so a pre-v5 vendored ref spelled that
  way stayed attested while a profile's `<properties>` moved the build to
  a public release. The profile values are now kept, and the live check
  weighs an unresolved declaration as one that may be the pin.
- A profile without `<id>` is named `default`, as Maven and the vendored
  reactor name it (it was numbered by `<properties>` block).
- Docs: read_properties is last-wins; ScopedDep coordinates read a
  profile declaration's own `<properties>` only once read_profiles ran,
  which the hosted rewriter does not do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ScopedDep coordinates here are the no-profile value, with every profile
reading carried by coordinate_alternatives, and the hosted index reads
profile <properties> too: restate both docs #2020's round-3 merge brought
in for its own branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/code-review high on #2026:

- A <profiles> declaration's ${...} groupId / artifactId was read with no
  profile active, its own profile's reading only an alternative. A
  coordinate only its own <properties> define stayed "${g}", which
  may_name reads as any text, so VEX withheld and hosted scan refused a
  valid pin beside a profile dependency its own profile names as another
  group (a regression from #2020). Coordinates read the declaration's own
  profile again, as its version does; other profiles stay alternatives.
- The vendored reactor's warn_property_coordinates skipped a ${...}
  <type>, which discovery and the hosted index read as possibly a jar:
  it is weighed now, so vendored mode warns instead of pinning a root
  VEX then never attests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

[agent] Approving: no regressions found by two independent passes (Claude Opus, codex); all Bugbot threads are fixed at head.

Non-blocking:

  • Profile <properties> changing a ${…} pin is still attested here (pre-existing on main); #2020 closes it, so land it soon after.
  • Fail-safe over-refusals codex noted: a profile-local ${v} equal to the pin (maven.rs:955), a profile's direct version vs root management (maven.rs:845), and a classifierless <type>ejb</type> vendored dep treated as an unserved variant (maven.rs:870).
  • PR body is stale: #2006 has landed and the deferred test-jar item is effectively closed.

Behavior-preserving cleanup of the folded #2005/#2020/#2026 diff:

- Share the copies the three PRs each grew: `classpath_copy` (which copy
  a profile-moved declaration reads as) for discovery's and hosted's
  `retargeted` and the reactor's coordinate warning; `management_key`
  for both classifier-copy checks; `Definitions::fixed` for the
  model-reference / user-property prefix PomScope and the reactor built
  by hand; one `named` helper for "(which may be g:a)" details.
- Drop dead code: resolve_in_build's always-strict `Interpolate`
  parameter (hosted reads `resolve_version` instead), the reactor's
  literal re-binding of already-literal coordinates, a redundant filter
  in the -D diagnostic, a re-checked `suffixed_version`.
- Cheaper: literal coordinates skip the profile scan, `bypasses` borrows
  the pom's declarations unless a profile moves one, hosted computes
  build versions only for the profile / classifier declarations that are
  read, and its detail text is built only when returned.
- Docs: no review-history narration; the UnattestedKind and note docs
  reflowed and pointed at one another; the discovery module doc defers
  to `bypasses`.
- Tests: shared builders and assertion helpers (decl / mgmt /
  assert_withheld / vendored_project_with, rewrite_pom / assert_refused
  / assert_pinned, reactor / with_corp_profile, user_props) replace
  per-test copies; no case removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hosted build_versions are computed for every declaration again, so an
  empty list only ever means "no <version>"; the index reads a new
  PomScope::build_versions (the build half of resolve_version) instead
  of resolving the pom-spelled version it discarded.
- assert_withheld takes the case it checks, so a table-driven failure
  names the failing case again.
- The discovery module doc lists the bypasses it withholds for (classifier
  copies, profile declarations and management, profile properties moving
  a version or coordinates, -D overrides) and the interpolation bounds.
- The reactor's classifier comment says where its type rule is narrower.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `${…}` groupId / artifactId declaration with an empty `<type></type>`
gave no reading once a profile's <properties> moved it: classpath_copy
treated the empty type as "no jar on the classpath", while Maven (and
is_main_jar / is_variant everywhere else) reads it as the default jar.
So a declaration a profile turned into the pinned GA at another version
went unweighed: hosted scan wrote the pin and VEX attested it (a false
attestation, carried over from the folded #2026). An empty type now maps
like `jar`. Tests: a withheld case in
profile_properties_turning_a_declaration_into_the_pinned_ga_are_not_attested
and a refused case in maven_pom_profile_moved_coordinates_refuse_the_dep,
both failing before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Stop attesting Maven pins a profile or user property overrides Fix Maven VEX false attestation through profiles, properties and coordinates Nov 9, 2026
…alse-attestation

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Merged main (6d17873, with #2018 and #1893) into this PR as de7254b, to clear the conflict. No other code changed.

One conflict, in CLI_CONTRACT.md: the single line that holds the Unattested references list. Both sides are kept:

  • From main (Never patch a pnpm, yarn or vlt dependency the user patched #2018): the self-patched npm/Bun text ("of any tarball"; a key naming the pinned tarball is still omitted), and the new Packages patched with pnpm, Yarn or patch-package paragraph. Main left the case count at "Eight" because that paragraph shares the vex_npm_patched_dependency tag.
  • From this PR: the Maven classifier / profile declaration / user property entries, and the count "Ten … the tenth" (the two Maven cases this PR adds).

vex/discover/mod.rs (UnattestedKind) and vex_sources.rs (the notes) merged without conflicts.

Local runs on de7254b:

  • core maven / vex:: / patch::redirect::: 1971 passed;
  • redirect_golden, CLI vex_sources, vex, e2e_vex, cli_parse_vex and vendor_jvm_cli: all pass;
  • workspace clippy and fmt: clean.

Generated by Claude Code

…alse-attestation

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-cli/src/commands/vex_sources.rs
#	crates/socket-patch-core/src/vex/discover/mod.rs
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Merged main (7c08fd8, with #2012 Cargo and #2009 pnpm) into this PR as 05906f4. No other changes.

Conflicts. Every one is a both-sides addition at the same spot, and both sides are kept:

#2009's pnpm and origin changes merged without conflicts, and the Maven code compiles against them.

Local runs on 05906f4:

  • cargo check --workspace --all-targets passes;
  • CI's clippy command is clean;
  • core maven / vex:: / patch::redirect::: 2013 passed;
  • redirect_golden, CLI vex_sources, vex, e2e_vex, cli_parse_vex and vendor_jvm_cli: all pass.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants