Repository navigation
Lock-only yarn classic VEX wrongly omits a correctly pinned patch with dependency_not_installed when the patched package.json has a "" range or a dependency yarn de-duplicates (regression from #1548) #1558
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
#1548 (the #1538 fix) added a lock-only check for hosted yarn classic pins whose patch changes package.json. The check requires the pinned block's dependencies: / optionalDependencies: sub-maps to equal the patched package.json's maps (dep_maps_declare). The comparison reads both sides more literally than yarn does, so it fails on locks that install exactly what the patched manifest asks for:
""range. yarn writesms ""in the sub-map, butblock_dep_mapsskips any entry whose range is empty, whilemanifest_dep_mapskeeps("ms", ""). Lock-onlyvexomits the patch on the lock socket-patch's own hosted scan just wrote, with no other step in between.- The same name in
optionalDependenciesanddependencies. yarn 1's manifest normaliser (cleanDependencies) keeps such a name only underoptionalDependencies. The hosted writer (with_manifest_dep_maps) writes it under both. After any yarn command that re-serialises the lock (e.g.yarn add ms), the block has yarn's shape and lock-onlyvexomits the patch. - Same root cause (seen once on 1.22.22):
"*"independenciesplus a range for the same name indevDependencies. yarn writes the non-trivial range into the block, which no longer matches"*".
Every one of these installs patched with --frozen-lockfile, and installed vex attests. Only the lock-only path refuses.
Impact
A false omission, not a false attestation: CI that runs vex on a lock-only checkout (the documented lockfile basis) drops a correctly applied patch from the VEX document, or exits 1 no_applicable_patches when it was the only one. In shape 1 this happens right after a clean scan --mode hosted. In shape 2 it happens after any routine yarn add / yarn upgrade.
Repro (shape 1, yarn 1.22.22, local mock patch API)
The mock serves a patch for is-odd@3.0.1 that prepends a marker to index.js and sets the package.json dependencies to {"is-number": ""}. The record lists both files.
# project
echo '{"name":"p","version":"1.0.0","dependencies":{"is-odd":"3.0.1","is-number":""}}' > package.json
yarn install # locks is-number@: -> 7.0.0
socket-patch scan --mode hosted --json --yes # exit 0, redirect_yarn_classic_dep_manifest_rewritten
# is-odd block now: resolved <mock tarball>, dependencies: is-number ""
# fresh checkout with node_modules:
yarn install --frozen-lockfile # exit 0, is-odd patched
socket-patch vex --output v.json --json # exit 0, not_affected (correct)
# lock-only checkout (package.json + yarn.lock):
socket-patch vex --output v.json --json
# exit 1: skipped pkg:npm/is-odd@3.0.1 errorCode dependency_not_installed
# "the patched package.json declares a dependency that is not installed beside the package"
# error.code no_applicable_patchesShape 2: the patch sets dependencies: {"is-number": "^6.0.0"}, optionalDependencies: {"is-number": "^6.0.0"}. Run the hosted scan (the writer adds is-number to both sub-maps, and lock-only vex attests at this point), then yarn add ms@2.1.3. yarn rewrites the pinned block to optionalDependencies: is-number "^6.0.0" only, and lock-only vex then exits 1 dependency_not_installed.
Controls (1.22.22): shape 2 without the yarn add, and a plain range change (is-number ^7.0.0, locked) with and without yarn add, all attest from the lock.
Expected vs actual
CLI_CONTRACT dependency_not_installed (lock-only): omitted when the pinned block's sub-maps "are not exactly the ones the patched package.json declares, each locked". Here the block declares exactly what yarn installs for that manifest, and every descriptor is locked (is-number@: / is-number@^6.0.0). The patch should be attested, as it is from the installed tree.
Actual: lock-only vex exits 1 / omits the patch with dependency_not_installed.
Matrix (Linux, main 15db5c4, each cell run twice)
| yarn | shape 1 ("") lock-only |
shape 2 (opt+dep after yarn add) lock-only |
frozen install | installed vex |
|---|---|---|---|---|
| 1.7.0 | omitted (bug) | omitted (bug) | patched | attests |
| 1.10.1 | omitted (bug) | omitted (bug) | patched | attests |
| 1.22.22 | omitted (bug) | omitted (bug) | patched | attests |
macOS / Windows not probed: the logic is pure lock-text comparison with no platform-specific code.
First bad commit
e719a9b (#1548). Before it, lock-only vex had no sub-map check.
Suspect code
crates/socket-patch-core/src/formats/yarn/classic_deps.rs:189:block_dep_mapsdrops entries whose range is empty, sofoo ""never matches the manifest's("foo", "").block_dep_entries(line 127) has the same skip.crates/socket-patch-core/src/formats/yarn/classic_deps.rs:210:manifest_dep_mapsdoesn't apply yarn'scleanDependencies(a name kept only in the first of optionalDependencies → dependencies → devDependencies, and a""/"*"range replaced by a non-trivial one from another field).with_manifest_dep_maps(line 28) writes the un-normalised shape, so yarn's next write changes the block.crates/socket-patch-core/src/vex/discover/mod.rs:319(installs_dependencies_of) →crates/socket-patch-cli/src/commands/vex.rs:553(yarn_classic_lock_dependency_gaps).- yarn reference:
cleanDependenciesin yarn 1.22.22lib/cli.js(normalize-manifestfix), withdependencyKeys = ['optionalDependencies', 'dependencies', 'devDependencies'].
Activity
mikolalysenko commentedon Oct 15, 2026
[agent] Triage: priority:p1 (yarn classic). Confirmed a regression from #1548:
block_dep_maps/block_dep_entriesinformats/yarn/classic_deps.rsdrop""ranges, andmanifest_dep_maps/with_manifest_dep_mapsdo not apply yarn 1'scleanDependenciesnormalisation. Not a duplicate of #1538, which was the unlocked-range case #1548 fixed. No open PR covers it yet.Generated by Claude Code
mikolalysenko commentedon Oct 16, 2026
[agent] Claiming this issue (with #1578; shared root cause:
formats/yarn/classic_deps.rsreads the patched package.json's dependency maps literally instead of the way yarn 1 records them in a lock block: it drops""ranges, skipscleanDependenciesnormalisation, and ignores the descriptors the block's sub-map already carries). Branch: agent/fix-yarn-classic-dep-manifest-model. Claim-ID: 2026-10-16T00:20:55Z-09f30cGenerated by Claude Code
mikolalysenko commentedon Oct 16, 2026
[agent] Draft PR: #1596
Generated by Claude Code
Read yarn classic patched manifests as yarn does
mikolalysenko commentedon Oct 16, 2026
[agent] Yarn classic bug-hunt run 43: tested PR #1596 (head
4d3698c1) against main657fce2on Linux with a local mock patch API (is-odd@3.0.1). Main still reproduces every shape below.vexvexis-number "", right after the hosted scandependency_not_installednot_affectedyarn add ms@2.1.3dependency_not_installednot_affected"*"in dependencies with^6.0.0in devDependenciesredirect_yarn_classic_dep_manifest_unlocked)vexmodes attestIn every case the frozen install is patched and installed
vexattests. I also checked backward compatibility: a shape-1 pin written by main attests under the PR's lock-onlyvex, and the PR's rescan leaves that lock byte-identical.Generated by Claude Code