Repository navigation
Keep vendored artifacts a lock references through escaped paths - #2022
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Conversation
The vendored revert's keep-check (`any_live_file_references`) did a plain text match of `.socket/vendor/<eco>/<uuid>` against the live wiring files. An older composer.lock writes `.socket\/vendor\/composer\/...`, a Windows checkout may write single or doubled backslashes, and either spelling missed the match: the check read "unreferenced" and the revert deleted an artifact the lock still installs from. Discovery's identity sweep already folds every one of those spellings (`\uXXXX` / XML-entity decode, then `\/`, `\\` and `\` to `/`). Move that fold into a pure util, `utils::path_spelling` (`fold_path_spellings`, plus `mentions_path` for "does this text name this path"), have the sweep call it unchanged, and route every vendored deletion gate that matched a vendor path as raw text through it: - `vendor::common::any_live_file_references` (composer / Maven / NuGet drift-keep in `revert::finish`) - `vendor::npm_flavor::lock_text_mentions_uuid` (npm unwired and post-restore guards, yarn classic and berry post-restore guards) - `vendor::pypi::pypi_reference_clause` (pypi unwired / post-restore keep) - `vendor::nuget_feed::config_references` (keeps the lock pin while a config still routes at the feed) - the CLI's `scan_vendor_references`, which the orphan sweep (`vendor --revert`, `scan --prune`, `rollback`, `remove`) uses to decide which un-ledgered dirs are still wired. It folded `\` to `/` only, so `\/` became `//` and the reference was lost. `mentions_path` also matches ASCII case-insensitively. Every change only widens what counts as a reference, so the failure mode is a kept artifact, never a deleted one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Labeled Ready for review (burn-down agent).
Generated by Claude Code |
Final review briefWhat it does. Discovery already normalised path spellings ( Risk: low. The change can only add matches. The needles ( Look here
Verified
Nits (non-blocking, for the author)
Auto-merge is not armed: ci-ok has to go green first. Once it's green it gets armed, and approving then sends it straight to the merge queue. Generated by Claude Code |
Review fixes for #2022: - One matcher, `utils::path_spelling::PathNeedle`: escapes decoded, every run of `/` and `\` collapsed to one `/` (so `\\\\` and `\\\/` match), an exact `contains` fast path, then a single KMP pass over the folded byte stream with the needle folded once. No copy of the scanned file. - Keep-vs-delete checks fold ASCII case (`PathNeedle::keep`). Checks that REFUSE fold case only where the project's file system does (`vendor::common::refusal_needle`, probed via `utils::fs::folds_case`), so a differently-cased path on a case-sensitive disk no longer blocks `vendor --revert`. - pypi: `unwire_superseded`'s residual check and `superseded_entry` read through the refusal needle (a `\/` or backslashed leftover was missed and the old uuid dir then swept as stale); `still_references_artifact` takes the caller's needle (broad for the pypi-lock drift keep, refusal for Hatch); `pypi_reference_clause` is broad for the post-restore keep and refusal-mode for the unwired guard and `vendor --check`; the requirements residual warning and the uv converged/drift checks use the broad needle. - npm: `lock_text_mentions_uuid` uses the refusal needle; its "textual and EXACT" doc is corrected. - CLI scan: `canonical_path_text` collapses separator runs; the orphan sweep reads `scan_vendor_references_any_case` (ASCII case-insensitive, like the revert keep-checks) while the refusing callers fold case only on a case-insensitive disk. - Test doc placement fixed; a different-uuid assertion added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 5165d5c. Configure here.
|
Ready for review re-confirmed at the current head (burn-down agent).
Generated by Claude Code |
Final review brief (updated for 5165d5c)What it does. The revert and orphan-sweep checks now spot a lock or config file that still names a Risk: low-medium. The change mostly widens which files count as still referencing an artifact, so it errs toward keeping it. The one narrowing is on a case-sensitive disk, where a reference in a different case no longer blocks a revert. That is correct, because it names a different directory there. The diff is wide (12 files, many revert paths), which is why this isn't rated low. Look here
Verified
Nits (non-blocking)
Auto-merge is not armed yet: ci-ok must go green first, after #2028 lands and this branch picks it up. Generated by Claude Code |
Second review round. Round 1 made the "refusal" checks fold ASCII case only where the project's disk does. Several of them are also deletion gates: a miss lets the revert delete the artifact. A committed lock or config is installed on case-folding disks (macOS, Windows) too, where a re-cased `.Socket/Vendor/...` path does resolve to the artifact, so a case-sensitive CI run deleted (and committed the deletion of) an artifact teammates still install from. These now use `PathNeedle::keep` (every spelling, any case): - the npm / yarn classic / yarn berry post-restore check (`lock_text_mentions_uuid`); - the unwired pypi revert guard; - the Hatch revert's leftover-reference check (now the same as the pylock revert's); - the superseded pypi entry's residual check before the old uuid dir is swept. `refusal_needle` remains only where a miss is safe (lifting another refusal; the `vendor --check` report). The module docs and the discovery identity-sweep comment say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
When a vendored revert or GC decides whether it can delete
.socket/vendor/<eco>/<uuid>/, it checks whether any live wiring file still names that path. That check was a plaincontains. An older composer.lock writes.socket\/vendor\/composer\/…, and a Windows checkout may use backslashes. Neither spelling matched, so the check returned "unreferenced" and the revert deleted an artifact the lock still installs from. #2017 handles only the composer BOM case through the stranded refusal; this PR fixes the matching itself for every format.Fix
Every "does this project file still name this
.socket/vendor/path?" check now goes through one matcher,utils::path_spelling::PathNeedle. It decodes\uXXXXand XML-entity escapes, collapses every run of/and\to a single/, and can ignore ASCII case. The lookup tries an exactcontainsfirst, then makes one KMP pass over the folded byte stream. The needle is folded once, and the scanned file is never copied.The case rule depends on what the check decides:
PathNeedle::keep/mentions_path). A false "referenced" only keeps an artifact.vendor::common::refusal_needledecides this withutils::fs::folds_case, which looks up.socket/under a flipped-case name. On a case-sensitive disk, a differently-cased path is another directory and must not block a revert.Discovery's identity sweep keeps
fold_path_spellings, which is unchanged and moved into the util along withdecode_escapes. It leaves separator runs alone because its hosted-url detection relies on://. The golden corpus test still passes.Every check moved onto the matcher
vendor::common::any_live_file_referencesvendor::nuget_feed::config_referencesvendor::npm_flavor::lock_text_mentions_uuidvendor::pypi::pypi_reference_clause(post-restore keep, dry-run preview)vendor::pypi::pypi_refusal_clause(unwired guard,vendor --checkviapypi_reference)vendor::pypi::unwire_supersededresidual checkvendor::pypi::superseded_entryvendor::pypi_lock::still_references_artifact, called from pypi-lockvendor::pypi_lock::still_references_artifact, called from Hatchvendor::pypi_requirementsresidual warningvendor_revert_residual_reference)vendor::pypi_uvalready-converged checks, the half-revert drift keep,revert_array_elementsscan_vendor_references_any_case(orphan sweep:vendor --revert,scan --prune,rollback,remove)scan_vendor_references(vendorledger-missing gate, rollback ledger-less gate,vendor --check, repair)Checked and left alone (no path text match, or not a deletion or refusal)
npm_flavor::uuid_proven_unreferencedmatches on the uuid alone, case-insensitively, and parses the JSON.pnpm_lock::pnpm_entry_in_useworks on the parsed lock.composer_lock::stranded_wired_packagesworks on parsed JSON.pypi_uvrespell_unrecorded_path_entriescompares parsed TOML values and only respells.pnpm_lockfile:copy advisory,yarn_classic_berry_migration_riskandpypi_requirementsdirect-reference detection only detect or warn.Review findings
unwire_supersededresidual usescontains: confirmed and fixed. New testsuperseding_uuid_refuses_over_an_escaped_residual_referencefails on the old check: the uv flavor re-vendored, and the old uuid dir would then have been swept. It passes with the refusal needle.still_references_artifactusescontains: confirmed and fixed. New testleftover_artifact_reference_is_detected_in_any_spellingcovers it. It takes the caller's needle: broad for pypi-lock, which keeps; disk-case for Hatch, which fails the revert.scan_vendor_references_any_case. The refusing callers keepscan_vendor_references, which folds case only on a case-insensitive disk, so they don't refuse over a path that resolves elsewhere. Covered by testany_case_scan_recovers_a_recased_reference.\\\\/\\\/became//and missed: confirmed and fixed by collapsing separator runs inPathNeedleandcanonical_path_text, with new cases in the util and CLI tests. Discovery's own fold is not changed, for the://reason above.containschecks: fixed. Every site is in the table above.guard_unwired_textual_revertis corrected. Testlock_text_mention_folds_case_only_where_the_disk_doesexpects whateverfolds_casereports for the temp dir's.socket. That makes it a real failing-before test only on a case-sensitive disk such as Linux CI; on macOS it passes either way.Tests
New since the first push:
superseding_uuid_refuses_over_an_escaped_residual_referenceleftover_artifact_reference_is_detected_in_any_spellinglock_text_mention_folds_case_only_where_the_disk_doesany_case_scan_recovers_a_recased_referencepath_spellingcases for separator runs, exact case, KMP overlap andcanonical_path_textStill in place from the first push: the revert, composer, npm and CLI scan escaped-spelling tests.
Runs:
--libvendor:: vex::discover:: utils::path_spelling: 3326 passed, golden test included.--librepair, orphans and rollback tests: 78 passed.mode_migration_composer: 7 passed.mode_migration_npm: 32 passed.mode_migration_pypi: 79 passed.in_process_rollback_vendored: 23 passed.in_process_rollback_all_ecosystems: 28 passed.repair: 129 passed.cargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --checkare clean.Review findings, round 2 (
/code-review highon5165d5c, cloud routine)Deletion gates matched case-sensitively on case-sensitive disks: confirmed and fixed. Round 1 made the "refusal" checks fold ASCII case only where the disk folds it. Several of them are also deletion gates: when they miss, the revert deletes the artifact. A committed lock is installed on macOS and Windows too, where
.Socket/Vendor/...resolves to the artifact. So a Linux CIvendor --revertdeleted, and committed the deletion of, an artifact those teammates still install from. Every check whose miss deletes now usesPathNeedle::keep(every spelling, any case):lock_text_mentions_uuid);refusal_needleremains only where a miss is safe: lifting another refusal (superseded_entry) and thevendor --checkreport. Tests:lock_text_mention_folds_case_on_every_disk(replaceslock_text_mention_folds_case_only_where_the_disk_does) andunwired_python_entry_revert_refuses_on_a_recased_reference. Both failed on5165d5c..socket/vendor/<eco>/<uuid>/path now blocks the revert instead of letting it delete the artifact.The discovery identity-sweep comment was stale: fixed.
Declined:
folds_caseprobe on non-unix systems with per-directory case sensitivity, and per-directory folding deeper in the path. After this fix those probes gate only safe-on-miss refusals and the CLI's refusal scan, so a wrong answer can no longer delete anything.Tests (cloud run):
cargo test -p socket-patch-core --lib -- vendor:: utils::path_spelling vex::discover::gives 3323 passed. Its 2 failures are chmod write-failure tests that root ignores.-p socket-patch-cli --lib -- repair vendorand--test in_process_vendorpass. clippy and fmt are clean.Note
Medium Risk
Changes deletion and revert refusal logic across many ecosystems; mistakes could delete artifacts still referenced by locks or block reverts incorrectly, though the design biases keep paths toward safety.
Overview
Replaces plain
containschecks for “does this file still name.socket/vendor/...?” with a sharedPathNeedlematcher inutils/path_spellingthat decodes JSON/XML escapes, collapses/and\runs, and optionally ignores ASCII case.Keep-vs-delete paths (drift keep, orphan sweep, residual warnings) use
PathNeedle::keep/scan_vendor_references_any_caseso a false positive only retains an artifact. Refusal paths (vendor --check, unwired revert guards, rollback ledger gates) userefusal_needle, which folds case only whenfolds_caseshows the project’s.socket/tree is on a case-insensitive volume.The CLI vendor-reference scan now runs
canonical_path_textbefore slicing references; discovery reusesfold_path_spellingsinstead of a local escape decoder. Composer, npm, NuGet, PyPI (uv/requirements/Hatch/lock), andany_live_file_referencesare updated accordingly, with tests for escaped slashes, doubled backslashes, and re-cased anchors.Reviewed by Cursor Bugbot for commit 5165d5c. Configure here.
Generated by Claude Code