Skip to content

fix(fix-path): nested and bundled npm parents, copy-exact attribution, direct-upgrade commands (audit A7/E8) - #962

Merged
runyourempire merged 3 commits into
mainfrom
fix/fx-a7-e8-nested-parents-direct-cmds
Oct 16, 2026
Merged

runyourempire merged 3 commits into
mainfrom
fix/fx-a7-e8-nested-parents-direct-cmds

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fixes two findings from the 2026-10-12 adversarial audit:

1. A7: nested parents (ace/scanner.rs)

parse_package_lock_edges skipped every nested packages key ("first-level edges only"). So node_modules/glob/node_modules/minimatch (minimatch 9.0.5, ^2.0.1) was never stored. The live DB held only minimatch 3.1.2 -> brace-expansion ^1.1.7 for navcal.

Nested keys now produce edges, named after the last node_modules/ segment (scope kept). First-level and root edges are unchanged; the existing tests pass unmodified.

2. E8: bundled copies (ace/scanner.rs, osv/fix_path.rs, osv/parent_hint.rs, evidence/upgrade_parent.rs, brief_facts_copies.rs)

A copy with inBundle: true ships inside its owner's tarball:

  • npm update cannot move it;
  • npm overrides do not reach it;
  • only a new owner release can.

It now gets ONE edge, from the bundle owner (the nearest enclosing entry that is not itself inBundle, skipping levels the lockfile does not list), with spec 2.2.0 (bundled). Bundled packages emit no edges of their own, because babel-traverse's ^2.2.0 inside nyc would read as "a refresh is enough".

  • fix_path::override_hint(manager, eco, bundled) returns no override for a bundled copy. The source decides it, so every caller inherits the fix: parent_hint::annotate, and the Brief's fallback route in brief_facts_copies.rs.
  • The Upgrade Plan says "…which ships it bundled inside its own package, so neither a lockfile refresh nor an override can move it to X — {parent move}". It never prints the marker.

3. E8: which copy a parent holds (osv/fix_path.rs, osv/parent_hint.rs)

A parent was attributed to every copy its range admits. NodeGoat's needle 2.2.4 (^2.1.2) was named a parent of debug 2.2.0 and told npm update debug, but it resolves the hoisted 2.6.9.

fix_path::npm_lock_resolutions now reads the package-lock v2/v3 tree with npm's own lookup: the parent's folder, then each enclosing folder, then the root. parent_hint uses it through a new Readers::resolved_children; one parse per project lockfile is cached for the call. A v1 lockfile, or any other tool, keeps the range-based attribution.

4. The bundled rule is a shared vector (rv-066..rv-068)

A requirement can be bundled: true; a line can carry override (whether an override may be offered). A bundled copy needs its owner to move, and no override is offered; an ordinary exact pin (rv-068) also needs its parent to move, but does take an override. Both premises were checked live with npm 10.9.2 (2026-10-15): nyc 7.1.0's bundled debug stayed at 2.2.0 under "overrides": {"debug": "2.6.9"} and under npm update debug. @4da/mcp-server implements the same vectors (its PR follows this merge, pinned to the merged commit).

5. Review fixes (second round)

  • One edge per bundled copy. An owner declares what it bundles (npm 3.10.10 declares all 82; nyc all 25), and its range became a second, refreshable edge. The owner's bundled children are skipped, so the copy keeps only the owner's pin.
  • Bundled means bundled inside a DEPENDENCY (fix_path::npm_lock_entry_bundled, npm_bundle_owner). A project's or workspace member's own bundleDependencies also set inBundle, but those copies are in the user's tree: ordinary edges, refresh and overrides as usual.
  • npm_lock_resolutions skips bundled copies of the parent, and returns None when no copy of the parent declares the child, so the range fallback applies.
  • npm_lock_tree uses the same bounded ascent as Manager::detect, so a workspace member's root lockfile is read. It parses once per file (mtime + size, at most 16 cached).
  • The Brief's fallback route suppresses the override only for the copy it describes.
  • Manager::direct_command and its vectors moved to C2, so nothing ships that production does not call.

Before / after, real lockfiles

Each line is the link parent_hint builds. "Before" is origin/main's first-level-only parser; "after" is this PR with the lockfile tree.

Project, copy Before After
navcal, brace-expansion 2.0.2 → 2.1.7 no parent read: "semver-compatible, refresh should reach it" (inferred) minimatch 9.0.5, ^2.0.1 admits 2.1.7: refresh, read from the requirement
victauri editors/vscode, brace-expansion 5.0.6 → 5.0.12 no parent read (inferred) minimatch 10.2.5, ^5.0.5: refresh, read
NodeGoat, debug 4.1.1 → 4.3.1 "refresh is enough", npm update debug (false) bundled in fsevents 1.2.9: the parent must move, no command, no override
NodeGoat, debug 2.2.0 → 2.6.9 connect and mocha block; needle, snapdragon, tap-mocha-reporter and undefsafe said "refresh, npm update debug" exactly the five copies that hold 2.2.0, all "parent must move": connect and finalhandler (~2.2.0), mocha (pin), nyc and npm (bundled, no override); needle, snapdragon, tap-mocha-reporter and undefsafe resolve the hoisted 2.6.9 and are no longer attributed
NodeGoat, debug 3.2.6 → 3.2.7 @cypress/xvfb, nodemon and tiny-lr refresh; cypress pins unchanged (correct): these are the 3 copies the audit saw npm update debug move

Every reader of the edge spec, and the marker

Reader How it treats X (bundled)
db/dependencies/queries.rs (store/read) opaque text; part of the unique key, so it is distinct from a range edge
osv/parent_hint.rs read_edges / parent_links_with / resolutions / direct_through requirement_admits and strip_spec drop the text after (, so it reads as an exact pin; is_bundled checks it explicitly
evidence/upgrade_parent.rs (link_sentence, requirement_note) bundled branch, so the marker is never printed (tested)
brief_facts_build.rs direct_parent (Brief) Requirement::npm / requirement_admits give an exact pin; requirements feed refresh_verdict only and are never printed
brief_facts_copies.rs override decided by override_hint(.., bundled) from the plan's links for the SAME copy (installed)
ace/dep_scope.rs reads pnpm edges only; never sees npm edges
@4da/mcp-server does not read dependency_edges (grep of src/: none)
crates/fourda-depengine (#960, merged) matching inputs exclude dependency edges (#960's own doc: "minus … dependency edges")
osv/vectors_tests.rs a bundled: true vector requirement is built as text + BUNDLED_SUFFIX, exactly as the scanner stores it

Tests

Each new test failed first on origin/main.

  • Scanner:
    • package_lock_nested_packages_are_parents
    • package_lock_bundled_copies_hang_off_their_owner, with realistic owners that declare what they bundle, including npm → semver
    • package_lock_root_bundles_keep_their_edges
  • Resolver:
    • npm_lock_resolutions_follow_npms_lookup: scoped parent, workspace member, bundled twin, None
    • npm_lock_entry_bundled_needs_a_dependency_as_bundler
  • parent_hint:
    • a_nested_parent_is_named_for_its_own_copy
    • a_bundled_copy_waits_on_its_owner
    • the_lockfile_tree_decides_which_copy_a_parent_holds
  • End to end:
    • a_bundled_copy_names_its_owner_and_no_override
    • the_plan_reads_the_lockfile_tree_on_disk: a real project dir with package.json + package-lock.json, through Manager::Npm detection, the cached tree and the plan text. It ran (not skipped).
  • Brief:
    • a_bundled_copy_gets_no_override_on_the_brief_route, including another copy's bundle
  • Vectors: rv-066..rv-068.

Rebased on d7db30d27 (#961). 104 targeted tests pass; cargo clippy --locked -- -D warnings and rustfmt are clean; the file-size gate gives warnings only (parent_hint.rs 782, fix_path.rs 755 lines; the error threshold is 1000).

Follow-ups (not here)

  • C2: Manager::direct_command (checked live per tool), its vectors fc-028..fc-037, and the wiring into the direct rows. The reviewer's notes are in scope: --build deps; workspace-inherited, target-specific and renamed Cargo deps; --save-exact for exact pins.
  • W1's lane: the Brief's direct_parent still attributes by range; npm_lock_resolutions is ready for it.
  • MCP repo: the same nested/bundled modelling for the TS engine, and the direct vectors.

🤖 Generated with Claude Code

https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH

runyourempire and others added 3 commits October 16, 2026 09:46
…, direct-upgrade commands (audit A7/E8)

A7: package-lock edges were read from first-level keys only, so a copy held
by a nested parent (navcal's brace-expansion 2.0.2 under
glob/node_modules/minimatch 9.0.5) read "no parent 4DA can read". Nested
keys are parents now; first-level and root edges are unchanged.

E8, transitive:
- Bundled copies (inBundle) hang off their bundle owner as an exact pin
  marked "(bundled)": no refresh and no npm override reaches them.
  override_hint takes the bundled flag, so the Upgrade Plan and the Brief's
  fallback route both inherit it. The plan names the owner and never prints
  the marker.
- A parent is attributed only to the copy the package-lock v2/v3 tree
  resolves for it (npm's lookup). NodeGoat's needle 2.2.4 (^2.1.2) resolves
  the hoisted debug 2.6.9 and is no longer told `npm update debug` for 2.2.0.

E8, direct: Manager::direct_command (npm install / pnpm up / yarn upgrade /
yarn up / cargo add [--dev]), each checked live against its tool, with rule
vectors fc-028..fc-037. The TS engine skips them for now. The surfaces call
it after #961.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
…-068)

A bundled requirement (`bundled: true`) is the owner's exact pin: the parent
must move. The optional `override` field says whether an override may be
offered: never for a bundled copy, which npm `overrides` do not reach. That
was checked with npm 10.9.2 on 2026-10-15: nyc 7.1.0's bundled debug stayed
2.2.0 under `"overrides": {"debug": "2.6.9"}` and under `npm update debug`.
rv-068 is the contrast: an ordinary exact pin does take an override.

The `ts` entries in engines_skip.json are dropped. @4da/mcp-server keeps its
own conformance/vectors-skip.json and implements these vectors with the same
pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
… bundlers only, copy-scoped Brief override

- An owner declares what it bundles (npm 3.10.10 declares all 82 of its bundled
  packages), and its range was stored as a second, refreshable edge to the
  bundled copy. The owner's declared children that it bundles are now skipped,
  so the bundled copy keeps exactly one edge: the owner's pin.
- Bundled means bundled inside a DEPENDENCY. A project's or workspace member's
  own bundleDependencies also set inBundle (Arborist's bundler can be the root),
  but those copies are in the user's tree and keep their ordinary edges
  (fix_path::npm_lock_entry_bundled / npm_bundle_owner, used by the scanner and
  by the lockfile-tree attribution).
- npm_lock_resolutions skips bundled copies of the parent, and answers None
  when no copy of the parent declares the child, so the range attribution
  still applies.
- npm_lock_tree finds the lockfile on the same bounded ascent as
  Manager::detect, so a workspace member's root lockfile is read. It caches the
  parsed tree per file, by mtime and size.
- The Brief's fallback route suppresses the override only for the copy it
  describes; another copy's bundle says nothing about this one.
- Manager::direct_command and the fc-028..fc-037 vectors move to the C2
  change, which wires them; production does not call them yet.
- Tests: realistic bundle owners that declare what they bundle; root and
  workspace bundles; the resolver with a scoped parent, a workspace member and
  a bundled twin; an end-to-end plan over a real package-lock.json on disk.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
@runyourempire
runyourempire force-pushed the fix/fx-a7-e8-nested-parents-direct-cmds branch from 5decd5b to 184a3f9 Compare October 16, 2026 00:17
@runyourempire
runyourempire added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 73e0d84 Oct 16, 2026
26 checks passed
@runyourempire
runyourempire deleted the fix/fx-a7-e8-nested-parents-direct-cmds branch October 16, 2026 02:11
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.

1 participant