Repository navigation
Tracking: derive scan candidates, lock entries and hosted refs from one project inventory instead of four discovery systems #1112
Description
[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.
Kind: tracking. Source: §2.1, Part 6.2 and 6.7 of the October 2026 review; register E36.
Problem
On main @ ea09714, "which packages does this project have" is still answered by four discovery systems. Each has its own record type, and they are joined in the CLI by converting everything to CrawledPackage:
| System | Entry point | Returns |
|---|---|---|
| A. Crawlers | crawl_every_ecosystem |
CrawledPackage (installed copies, plus whole machine caches: #595) |
| B. Lock inventory | inventory_project*,`` with two modes Instances::Collapsed / `Instances::Every` |
LockfileEntry (ecosystem: &'static str) |
| C. Wiring discovery | discover_with_ctx,`` with 15 extractors and three contest passes |
Discovery { refs: Vec<PatchedRef>, elsewhere, … } |
| D. Vendor-ledger supplement | vendored_ledger_supplement |
fabricated CrawledPackages |
How they are joined:
- B and D are turned into fake installed packages.
crawled_from_purlsetspath: cwd.join("node_modules").join(name_part)for every ecosystem, including cargo, pypi, gem and golang. - Consumers have to know which paths are fake.
scan/mod.rs#L1807-L1835builds a side set,supplement_purls, only so that the PATH-scope filter (#L1926-L1962) can skip them. The "not installed" partition is a third set,lockfile_only.purls, consulted throughlockfile_only_containsin four places (scan/mod.rs801, 2821, 3121;discovery.rs299).apply.rscomputes its own set throughlockfile_resolved(apply.rs2018) and threads it through 1022-1417. - Liveness reads B a third time from disk.
vex/discover/mod.rs#L1896-L1898callsinventory_project_every_lock(root)rather than the run'sProjectView. vex::discoveris really the hosted-state store. It is imported by 42 files outsidevex/, includingformats/{composer,pnpm,gem,cargo,registry,mod}.rs(aformats→vexedge),patch/redirect/*,vendor/*,rollout.rs,ledgers.rs, and the scan, get, apply, vendor and rollback commands.
Target design
There is one core inventory module that owns a single instance model. Every other view is derived from it:
struct Instance {
purl: CanonicalPurl,
declared_in: Option<Rel>, // which lock/manifest
resolution: Registry { url, integrity, source_kind }
| Hosted { uuid, url, integrity, required }
| Vendored { uuid, artifact_rel, integrity }
| Other,
installed_at: Vec<PathBuf>, // filled by locators (ex-crawlers); empty = not installed
}LockfileEntrybecomes the Registry instances after precedence and dedup.PatchedRef/HostedPinbecome the Hosted and Vendored instances.ResolvedElsewherebecomes Registry plus Other.- Scan candidates are instances. "Not installed" is
installed_at.is_empty(), so no fabricated path and no side sets are needed. - Crawlers become locators for project mode. Enumeration remains for agent mode,
--globaland lockless projects; the project-scoping half is Tracking: scope project-mode cache crawls to what the project resolves #595.
Ordered checklist
- 1. Carry scan's lockfile-only and vendored-ledger candidates without a fabricated node_modules path #1113: carry scan's lockfile-only and vendored-ledger candidates as typed candidates with no installed path. Delete
crawled_from_purlandsupplement_purls(CLI only). - 2. Have discovery liveness read the run's
ProjectView/DiskSnapshotinstead of re-reading every lock from disk withinventory_project_every_lock(root). This waits on Decide vendored-entry liveness through one discovery verdict #1050, which rewrites liveness. - 3. Collapse
Instances::{Collapsed, Every}into one every-instance walk plus acollapse()pass, so each format has one walk. - 4. Move
LockfileEntryand the neutral lock types toformats/inventory(Move LockfileEntry, LockIntegrity, SourceKind and http_url from vendor::lock_inventory into formats::entry #834, child of Tracking: move the pure lock codecs and neutral lock types into formats/ so formats imports nothing from vendor or redirect #833). - 5. Rename
vex::discoverto the hosted-state store (hosted::stateorinventory::wiring). This is a mechanical move with re-exports, done after the redirect split (Tracking: split patch/redirect/mod.rs into per-ecosystem modules and sibling test files #1010) and the open campaign PRs that touch it land. - 6. Introduce
Instanceand deriveLockfileEntry,PatchedRefandResolvedElsewherefrom one per-formatentries()walk, one format family per PR (npm family first, after Walk package-lock entries once for inventory, vendored, hosted and restore #663). - 7. Crawlers as locators in project mode (Tracking: scope project-mode cache crawls to what the project resolves #595).
Symptoms
- Maven hosted scan pins, and VEX attests, artifacts the project doesn't depend on (the crawler lists all of
~/.m2) #265, Project-mode NuGet agent scan patches, and VEX attests, packages the project doesn't depend on (the crawler lists the whole ~/.nuget/packages) #427: whole-cache crawls (Tracking: scope project-mode cache crawls to what the project resolves #595). - The PATH-scope warning
path_scope_excluded_supplementsexists only because the supplement's paths are fake.
Impact
This is the root of the discovery overlap described in Part 6.2. Each new consumer must remember the side sets, and each new ecosystem must be taught to all four systems. Total size is large, so it lands as the children above.
Acceptance criteria
- Each checklist item lands as its own PR that deletes the code it replaces.
-
cargo test -p socket-patch-core --liband the CLIscan,vexandvendore2e suites stay green at every step.
Dependencies
- Blocked in part by Decide vendored-entry liveness through one discovery verdict #1050 (liveness), Walk package-lock entries once for inventory, vendored, hosted and restore #663 (package-lock walk), Tracking: move the pure lock codecs and neutral lock types into formats/ so formats imports nothing from vendor or redirect #833/Move LockfileEntry, LockIntegrity, SourceKind and http_url from vendor::lock_inventory into formats::entry #834 (neutral types) and Tracking: split patch/redirect/mod.rs into per-ecosystem modules and sibling test files #1010 (redirect split).
- Blocks the E32 single hosted pipeline, which needs one instance source.
Consolidated work — backlog review, 2026-10-08
The following standalone issues are now tracked here. Their closure consolidates scheduling; it does not mean their implementation is complete. Original reports and discussion remain linked below.
#1113: Carry scan's lockfile-only and vendored-ledger candidates without a fabricated node_modules path
- Track the remaining scope from Carry scan's lockfile-only and vendored-ledger candidates without a fabricated node_modules path #1113 in this issue. Explicit candidate-type child of the unified inventory tracker; one parent issue is enough to schedule it.
Preserved scope and acceptance criteria from #1113
Proposed change
- Add a scan-local candidate type, for example:
Alternatively, keep
enum Candidate { Installed(CrawledPackage), LockOnly { purl, eco }, Vendored { purl, eco } }
CrawledPackagefor crawler output only and carry the supplement aspurl-only records beside it. - The PATH-scope filter matches
Installedonly. "Not installed" ismatches!(c, Candidate::LockOnly { .. }). - Delete:
crawled_from_purl,supplement_purlsand theLockfileSupplement::packages/LedgerSupplement::packagesvectors of fabricated packages. - Keep
lockfile_only.purlsonly where API-spelled purls are matched (lockfile_only_containsbridges the percent-encoding and composer padding). Where the candidate itself is at hand, derive the answer from the candidate. - No behavior change: same JSON (
lockfileOnlyPackages,notInstalled, the[NOT INSTALLED]marker,path_scope_excluded_supplements), same exit codes.
Size and scope
crates/socket-patch-cli/src/commands/scan/{mod.rs,discovery.rs}, plus any scan helper typed on&[CrawledPackage]that receives supplements. Estimate about 150–300 changed production lines.- Out of scope: the core inventory model (later children of the tracking issue),
apply.rs's ownlockfile_resolvedset, and crawler changes.
Acceptance criteria
-
crawled_from_purlandsupplement_purlsare deleted, and noCrawledPackageis constructed incommands/scan/outside tests. - The existing
scan/discovery.rsunit tests (ledger_supplement_*,corrupt_ledger_*) are ported and green. - The CLI scan e2e suites stay green, including
scan_paths_e2e(path scope and thepath_scope_excluded_supplementswarning), the lockfile-only/notInstalledtests, and the vendored-ledger fresh-clone tests. - A regression test: a path-scoped scan over a project with a lockfile-only cargo or pypi package excludes it with the counted warning, and never treats
<cwd>/node_modules/<name>as its location, even when such a directory exists.
Activity
audit-ecosystems: E36 tracking #1112/#1113, E91 #1114; E71/E72/E75 pa…