[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.
Kind: tracking. Source: review 4.4, 4.7 C; register E22.
Problem (verified on 9c43dfc)
Seven npm-family vendor drivers repeat one skeleton, about 1,940 lines in total:
Each one runs the same sequence: guard_coordinates → read the lock → flavor preflight → stage_patch_pack (or stage_patch_dir for vlt) → let Some(staged) = staged else { Done { entry: None } } → splice → "nothing changed" → already_patched_result → atomic write, with done_failure_unstage on error → VendorMarker::new("npm", …) + write_marker_or_warn → a 20-field literal VendorEntry whose only differences are flavor, artifact and one optional meta (pnpm, yarn_berry10c0, file_inventory).
Only the middle part (the lock grammar and the splice) is per flavor. The copies have already drifted (read, not executed):
- The "patch rewrites
package.json" warning has two codes and two triggers. npm (#L298-L306) and yarn classic (#L212-L222) recompute the dependency fields and warn vendor_dep_manifest_rewritten, classic only when a block changed. pnpm (#L233-L245), bun (#L439-L451), bun.lockb, berry and vlt preserve them and warn vendor_dep_manifest_stale with five differently worded texts. pnpm and bun warn before the in-sync check, so they warn again on an AlreadyPatched re-run.
bun_binary ignores NpmStagedPack::uuid_dir_preexisted (npm_common.rs#L163-L168) and computes its own with exists() before staging (bun_binary.rs#L77). Today the two agree, but the unstage guard has two sources of truth.
- vlt alone records an entry with empty wiring when it rebuilds a stale artifact, and the tarball flavors return
entry: None in the same state.
Symptoms / impact
No open bug is attributed to this yet. Every cross-flavor rule (unstage on failure, marker, the in-sync contract, the package.json warning) is maintained seven times; that is how the warning drift above happened. Behavior change risk is low if done mechanically, because the per-flavor splice code doesn't move.
Target design
trait NpmLockBackend {
const FLAVOR: &'static str;
type Plan;
async fn preflight(&self, root: &Path, coords: &NpmCoords, w: &mut Vec<VendorWarning>) -> Result<Self::Plan, Box<VendorOutcome>>;
fn splice(&self, plan: Self::Plan, staged: &NpmStagedPack) -> Result<Option<Commit>, String>; // None = in sync
fn entry_meta(&self, entry: &mut VendorEntry);
}
async fn vendor_npm_family<B: NpmLockBackend>(b: &B, req: VendorRequest<'_>) -> VendorOutcome;
The generic driver owns coordinates, staging, the in-sync return, the write and unstage, the marker, the package.json warning and the entry. The flavor files keep only grammar and splicing.
Ordered checklist
Size and scope
About 0.8–1.0K production lines removed overall (review 4.7 C), at 150–400 per child. Out of scope: lock grammars, hosted mode, the dead force/sources parameters (#923) and PackageSource (#800).
Acceptance criteria
Dependencies
Coordinate with #800 (it changes every backend signature) and #835 (the shared wiring kinds and lines↔JSON codec). Blocks the VendorBackend tracking (E21).
[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.
Kind: tracking. Source: review 4.4, 4.7 C; register E22.
Problem (verified on
9c43dfc)Seven npm-family vendor drivers repeat one skeleton, about 1,940 lines in total:
VendorEntrynpm_lock::vendor_npm#L392-L415pnpm_lock::vendor_pnpm_dialect(v9 and legacy since #583)#L393-L421yarn_berry_lock::vendor_yarn_berry#L465-L491yarn_classic_lock::vendor_yarn_classic#L252-L275bun_lock::vendor_bun#L618-L641bun_binary::vendor#L238-L261vlt_lock::vendor_vlt(directory artifact)#L1356-L1379Each one runs the same sequence:
guard_coordinates→ read the lock → flavor preflight →stage_patch_pack(orstage_patch_dirfor vlt) →let Some(staged) = staged else { Done { entry: None } }→ splice → "nothing changed" →already_patched_result→ atomic write, withdone_failure_unstageon error →VendorMarker::new("npm", …)+write_marker_or_warn→ a 20-field literalVendorEntrywhose only differences areflavor,artifactand one optional meta (pnpm,yarn_berry10c0,file_inventory).Only the middle part (the lock grammar and the splice) is per flavor. The copies have already drifted (read, not executed):
package.json" warning has two codes and two triggers. npm (#L298-L306) and yarn classic (#L212-L222) recompute the dependency fields and warnvendor_dep_manifest_rewritten, classic only when a block changed. pnpm (#L233-L245), bun (#L439-L451), bun.lockb, berry and vlt preserve them and warnvendor_dep_manifest_stalewith five differently worded texts. pnpm and bun warn before the in-sync check, so they warn again on anAlreadyPatchedre-run.bun_binaryignoresNpmStagedPack::uuid_dir_preexisted(npm_common.rs#L163-L168) and computes its own withexists()before staging (bun_binary.rs#L77). Today the two agree, but the unstage guard has two sources of truth.entry: Nonein the same state.Symptoms / impact
No open bug is attributed to this yet. Every cross-flavor rule (unstage on failure, marker, the in-sync contract, the
package.jsonwarning) is maintained seven times; that is how the warning drift above happened. Behavior change risk is low if done mechanically, because the per-flavor splice code doesn't move.Target design
The generic driver owns coordinates, staging, the in-sync return, the write and unstage, the marker, the
package.jsonwarning and the entry. The flavor files keep only grammar and splicing.Ordered checklist
VendorEntryconstructor for the npm family plus a shared finish step (marker + entry +Done), deleting the seven literal entries. Build npm-family vendor ledger entries through one constructor instead of seven literal VendorEntry blocks #922NpmLockBackend+vendor_npm_family, migrating npm and yarn classic (the two that recompute dependency fields). Unify thepackage.jsonwarning trigger: after the in-sync check, once per run.bun.lockb.bun_binaryusesstaged.uuid_dir_preexisted.stage_patch_dirvariant), or record why it stays separate.revert_*_optsin the same files gets the same treatment (separate tracking under E24, nine revert mechanisms).Size and scope
About 0.8–1.0K production lines removed overall (review 4.7 C), at 150–400 per child. Out of scope: lock grammars, hosted mode, the dead
force/sourcesparameters (#923) andPackageSource(#800).Acceptance criteria
legacy-ledgersfixtures and thee2e_vendor_*suites stay green unchanged.VendorEntry {literal remains in the npm-family files outside tests.package.json-rewriting patch emits no manifest warning in any flavor.Dependencies
Coordinate with #800 (it changes every backend signature) and #835 (the shared wiring kinds and lines↔JSON codec). Blocks the
VendorBackendtracking (E21).