Skip to content

feat(fix-path): direct upgrades name the command that keeps how they are declared (E8, C2); security placeholders hold for any advisory (L4); H2 vectors - #972

Merged
runyourempire merged 4 commits into
mainfrom
fix/c2-direct-upgrade-commands
Oct 17, 2026
Merged

runyourempire merged 4 commits into
mainfrom
fix/c2-direct-upgrade-commands

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

This finishes audit finding E8, the direct-dependency half. The transitive half was #962.

An Upgrade Plan row for a declared dependency said only "Fixable now via a direct dependency bump". It now names how to move it, read from the declaring project's own manifest (osv::direct_upgrade).

The PR also carries two decisions recorded by recovery-lead and the shared vectors for readiness finding H2.

Every command was run against the tool (2026-10-15/16)

Several naive forms would have rewritten the declaration:

Declared as Naive command What it did What this names
npm exact 1.2.5 npm install x@1.2.8 wrote ^1.2.8 npm install x@1.2.8 --save-exact
npm ~2.1.1 npm install ms@2.1.3 wrote ^2.1.3 npm install "ms@~2.1.3"
yarn 1 ~2.1.1 yarn upgrade ms@2.1.3 wrote exact 2.1.3 yarn upgrade "ms@~2.1.3"
yarn 4 ^1.2.5 yarn up minimist@1.2.8 wrote exact 1.2.8 yarn up "minimist@^1.2.8"
pnpm, any pnpm up x@v kept the style pnpm up with the operator (pnpm add at a workspace root needs -w)
Cargo dev dep cargo add x@v wrote a second [dependencies] entry cargo add x@v --dev (also --build, --target "<cfg>", --rename <key>)
Cargo =/~ pin cargo add x@v rewrote it as a caret cargo add "x@=v" / "x@~v"
Cargo x.workspace = true cargo add x@v replaced the inheritance with a local pin an instruction: bump [workspace.dependencies] in the root
Go direct require n/a n/a go get mod@vX (not for // indirect)
PyPI requirements.txt == pip install changes only the env n/a an edit of the version token only (extras, markers, comments kept)
  • Specs that carry an operator are quoted, because ^ is cmd.exe's escape character.
  • Nothing is named when it is uncertain: a dependency declared twice; a declared spec that does not admit the installed copy; a git, path, alternate-registry, workspace: or alias spec; a >=/|| range; a yarn workspace member; a Go replace; a hashed or pip-compiled requirements file; an unknown tool.

Pinned by fix_command vectors fc-028..fc-053 (kind: "direct", with declared, manifest, go_mod or requirements). The runner builds a project from each vector and calls direct_fix itself, checking command and instruction.

Review fixes (recovery-lead's REQUEST CHANGES, 2026-10-17)

  • S4, the declaration must admit the installed copy. Before any fix is named, the npm spec (requirement_admits), the Cargo requirement (Requirement::cargo), the go.mod require version or the == pin must admit the installed version. Otherwise the row is about a different copy than the one this manifest declares, so nothing is named. A workspace-inherited crate is judged at the root and still gets its instruction. direct_fix now takes installed. Tests: a_declaration_that_does_not_admit_the_installed_copy_names_nothing and the Go/PyPI cases. Vectors: fc-046 (npm ~1.2.5 vs installed 0.0.8) and fc-047 (Cargo ~1.0.5 vs 0.4.8).
  • B1, PyPI. Only the token between == and the first ;, whitespace, \ or # is replaced, so extras, markers and comments stay. A line ending in \, carrying --hash, or followed by a --hash line names nothing, because --require-hashes would fail. pip-compile output also names nothing: edit the .in file instead (N2). Vectors fc-050..fc-052.
  • B2, Cargo source. git, path and registry are tracked in CargoDecl::other_source, and those name nothing. Vector fc-048.
  • S1. Only 0.0.<n>-security counts as a holding placeholder. Vector ho-006. The TS mirror lands with the MCP integration PR.
  • S2. --target "cfg(...)" uses double quotes. I re-ran it through cmd.exe today: with single quotes cargo failed (failed to parse `'cfg(windows)'` as a cfg expression); with double quotes it moved winapi to 0.3.9. fc-040's source records this.
  • S3, yarn berry. Decision: decline when the project is a yarn workspace member, i.e. yarn.lock is not in the project's own directory. yarn up and yarn upgrade resolve across every workspace and would rewrite siblings' declarations. The workspace root still gets its command. Test: a_yarn_workspace_member_names_nothing. Vector fc-053 (member: true).
  • S5. direct_fixes and direct_fix_note moved into osv/direct_upgrade.rs; upgrade_plan.rs is 981 → 920 lines. The inline-table/field reader is now DeclFields, so cargo_declarations only walks lines.
  • N1. Every direct vector now has its own source: the tool and version it was run with plus what that run did, or "Rule, not a tool run" for declines. fc-043 pins the instruction text.
  • N2. A Go module named in a replace (single-line or block) names nothing. Vector fc-049.

L4 (recovery-lead's decision, 2026-10-16)

npm's *-security holding placeholder has no code, so no advisory applies to it, whatever database's id flags the name.

  • Before, only MAL- ids were held. crossenv 0.0.2-security under GHSA-c2m4-w5hm-vqjw ("crossenv is malware", introduced 0, last_affected 1.0.1) read as installed malware.
  • The npm registry lists that release as "security holding package", next to the malicious 1.0.0/1.0.1.
  • fourda_depengine::matching::is_npm_security_holding(ecosystem, version) drops the id condition, so the app, the crate and the wasm build agree. It matches only npm's own placeholder shape 0.0.<n>-security; a publisher's 1.0.0-security prerelease stays affected.
  • New shared vector file holding.jsonl (ho-001..ho-006).

H2 shared vectors (readiness audit of @4da/mcp-server 6.2.5)

The TS engine couldn't order a two-part OSV bound like 6.4. It sent symfony/process 5.1.0 to 8.0.5 and rack 2.0.6 to 3.2.6.

  • The app was already right; these vectors keep both engines right.
  • version_order vo-086..vo-091 (Packagist, RubyGems). Truth: composer/semver and PHP version_compare (composer:2), and Gem::Version (ruby 3.3), run in docker.
  • The Packagist pair 6.4.0/6.4 is deliberately unpinned: composer/semver's normaliser says equal and its comparator says greater.
  • fix_target ft-065 (symfony/process v5.1.0 → v5.4.51) and ft-066 (rack 2.0.6 → 2.2.23 on the pinned records). Truth: OSV querybatch per published release, restricted to the pinned ids.
  • These ids sit after the FX-H5 agent's ft-062..ft-064; IDs were coordinated.

Not here

  • The Brief's own direct rows (FixPath::Bump): the variant is shared with morning_audit/morning_floor. Recovery-lead cleared that for a follow-up PR from me.
  • UpgradeStep (the MCP work-order envelope): unchanged, so no schema bump. The command is in the item text.

Checks

  • Tests: 248 pass under osv:: and evidence::upgrade, including the vector gate and the end-to-end plan test a_direct_row_names_the_command_for_its_declaration on a real project dir (tilde kept). depengine cargo test passes.
  • depengine crate, its own CI steps: rustfmt; cargo clippy --locked -- -D warnings, native and wasm32; cargo test. (--all-targets reports lints in existing test files that this PR doesn't touch: osv.rs, version_order_tests.rs, tests/version_order_vectors.rs.)
  • App: cargo clippy --locked -- -D warnings and rustfmt are clean; the vector gate holds with the new lines.

🤖 Generated with Claude Code

https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH

runyourempire added a commit that referenced this pull request Oct 16, 2026
… the manifest declares, from its own source

- S4: the declared spec (npm, Cargo), go.mod requirement or == pin must
  admit the INSTALLED copy before any fix is named; otherwise the row is
  about another copy than this manifest declares.
- B1 PyPI: only the version token after == changes; extras, environment
  markers and comments stay. Hashed pins (\, --hash on the line or the
  next) and pip-compile output name nothing.
- B2 Cargo: a git, path or alternate-registry dependency names nothing
  (cargo add <crate>@<v> re-points it at crates.io).
- S1: a holding placeholder is only npm's own 0.0.<n>-security shape; a
  publisher's 1.0.0-security prerelease stays affected (ho-006).
- S2: cargo --target is double-quoted. Live through cmd.exe 2026-10-17:
  single quotes failed to parse as a cfg expression.
- S3: a yarn workspace member names nothing (yarn up / upgrade rewrite
  every workspace); the root itself still does.
- S5: direct_fixes / direct_fix_note move into osv/direct_upgrade.rs
  (upgrade_plan.rs 981 -> 920 lines); the inline-table reader is its own
  DeclFields.
- N1: every direct vector carries its own source, saying whether it was a
  tool run (and which version) or a rule; fc-043 pins the instruction text,
  and the runner checks command and instruction through direct_fix on a
  project built from the vector.
- N2: a Go module under a replace directive names nothing.
- New vectors fc-046..fc-053, ho-006.

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/c2-direct-upgrade-commands branch from d556211 to 46e4d3e Compare October 16, 2026 23:19
@runyourempire
runyourempire added this pull request to the merge queue Oct 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 16, 2026
runyourempire added a commit that referenced this pull request Oct 17, 2026
…68 (#972 holds ft-065/066)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 17, 2026
runyourempire added a commit that referenced this pull request Oct 17, 2026
…68 (#972 holds ft-065/066)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
runyourempire added a commit that referenced this pull request Oct 17, 2026
… the manifest declares, from its own source

- S4: the declared spec (npm, Cargo), go.mod requirement or == pin must
  admit the INSTALLED copy before any fix is named; otherwise the row is
  about another copy than this manifest declares.
- B1 PyPI: only the version token after == changes; extras, environment
  markers and comments stay. Hashed pins (\, --hash on the line or the
  next) and pip-compile output name nothing.
- B2 Cargo: a git, path or alternate-registry dependency names nothing
  (cargo add <crate>@<v> re-points it at crates.io).
- S1: a holding placeholder is only npm's own 0.0.<n>-security shape; a
  publisher's 1.0.0-security prerelease stays affected (ho-006).
- S2: cargo --target is double-quoted. Live through cmd.exe 2026-10-17:
  single quotes failed to parse as a cfg expression.
- S3: a yarn workspace member names nothing (yarn up / upgrade rewrite
  every workspace); the root itself still does.
- S5: direct_fixes / direct_fix_note move into osv/direct_upgrade.rs
  (upgrade_plan.rs 981 -> 920 lines); the inline-table reader is its own
  DeclFields.
- N1: every direct vector carries its own source, saying whether it was a
  tool run (and which version) or a rule; fc-043 pins the instruction text,
  and the runner checks command and instruction through direct_fix on a
  project built from the vector.
- N2: a Go module under a replace directive names nothing.
- New vectors fc-046..fc-053, ho-006.

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/c2-direct-upgrade-commands branch from 46e4d3e to cf8c922 Compare October 17, 2026 15:03
runyourempire added a commit that referenced this pull request Oct 17, 2026
…ercased finding path

Findings carry project paths lowercased (normalize_project_path). That
still opens on Windows and macOS, but on a case-sensitive filesystem
`/home/runner/work/4DA/4DA/...` lowercased does not exist, so every
manifest read failed and the direct row named no command. The merge-queue
run of #972 caught it on Linux; locally the end-to-end test never ran (its
`c2-e2e-` tempdir prefix tripped the scaffolding exclusion and it printed
SKIPPED).

- fix_path::on_disk resolves each missing component case-insensitively
  against its parent's entries; two entries that differ only by case
  resolve to nothing.
- direct_fix and Manager::detect (the transitive refresh commands) read
  through it, so Linux users get the same commands Windows users do.
- The end-to-end test's tempdir prefix no longer reads as scaffolding: it
  runs (no SKIPPED line with --nocapture).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
runyourempire added a commit that referenced this pull request Oct 17, 2026
… the manifest declares, from its own source

- S4: the declared spec (npm, Cargo), go.mod requirement or == pin must
  admit the INSTALLED copy before any fix is named; otherwise the row is
  about another copy than this manifest declares.
- B1 PyPI: only the version token after == changes; extras, environment
  markers and comments stay. Hashed pins (\, --hash on the line or the
  next) and pip-compile output name nothing.
- B2 Cargo: a git, path or alternate-registry dependency names nothing
  (cargo add <crate>@<v> re-points it at crates.io).
- S1: a holding placeholder is only npm's own 0.0.<n>-security shape; a
  publisher's 1.0.0-security prerelease stays affected (ho-006).
- S2: cargo --target is double-quoted. Live through cmd.exe 2026-10-17:
  single quotes failed to parse as a cfg expression.
- S3: a yarn workspace member names nothing (yarn up / upgrade rewrite
  every workspace); the root itself still does.
- S5: direct_fixes / direct_fix_note move into osv/direct_upgrade.rs
  (upgrade_plan.rs 981 -> 920 lines); the inline-table reader is its own
  DeclFields.
- N1: every direct vector carries its own source, saying whether it was a
  tool run (and which version) or a rule; fc-043 pins the instruction text,
  and the runner checks command and instruction through direct_fix on a
  project built from the vector.
- N2: a Go module under a replace directive names nothing.
- New vectors fc-046..fc-053, ho-006.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
runyourempire added a commit that referenced this pull request Oct 17, 2026
…ercased finding path

Findings carry project paths lowercased (normalize_project_path). That
still opens on Windows and macOS, but on a case-sensitive filesystem
`/home/runner/work/4DA/4DA/...` lowercased does not exist, so every
manifest read failed and the direct row named no command. The merge-queue
run of #972 caught it on Linux; locally the end-to-end test never ran (its
`c2-e2e-` tempdir prefix tripped the scaffolding exclusion and it printed
SKIPPED).

- fix_path::on_disk resolves each missing component case-insensitively
  against its parent's entries; two entries that differ only by case
  resolve to nothing.
- direct_fix and Manager::detect (the transitive refresh commands) read
  through it, so Linux users get the same commands Windows users do.
- The end-to-end test's tempdir prefix no longer reads as scaffolding: it
  runs (no SKIPPED line with --nocapture).

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/c2-direct-upgrade-commands branch from cf8c922 to ee8cc21 Compare October 17, 2026 15:56
runyourempire and others added 4 commits October 18, 2026 02:42
…are declared (audit E8, C2); npm security placeholders hold for any advisory id (L4)

E8, direct half: an Upgrade Plan row for a declared dependency now names how
to move it, read from the declaring project's own manifest
(osv::direct_upgrade). Before, it said only "Fixable now via a direct
dependency bump".

Each command was run against the tool on 2026-10-15/16, and several naive
forms rewrote the declaration:
- npm: `npm install x@v` wrote ^v over an exact pin and over ~. Now
  --save-exact for an exact pin, and the declared operator otherwise.
- yarn 1 `upgrade` and yarn 4 `up` wrote an exact pin over ^ and ~ unless
  the operator is passed. pnpm `up` keeps the style either way.
- Specs carrying ^ are quoted, because ^ is cmd.exe's escape character.
- cargo `add`: --dev / --build / --target / --rename follow the declaring
  table, and the = / ~ operator is kept. On a `workspace = true` dependency,
  cargo add replaced the inheritance with a local pin, so that case gets an
  instruction to bump [workspace.dependencies] instead.
- go: `go get mod@vX` for a direct requirement.
- PyPI: an exact edit of a requirements.txt `==` line (pip install changes
  only the environment).
- Anything uncertain names nothing: a dependency declared twice; git, path or
  workspace: specs; >= and || ranges.

The rule is pinned in fix_command vectors fc-028..fc-045 (`kind: "direct"`).

L4 (decision recorded by recovery-lead, 2026-10-16): npm's `*-security`
holding placeholder has no code, so no advisory applies to it, whichever
database's id flags the name. The predicate drops its `MAL-` condition in
fourda-depengine, so both engines and the wasm build agree.
- crossenv@0.0.2-security under GHSA-c2m4-w5hm-vqjw read as installed
  malware.
- New shared vectors: holding.jsonl ho-001..ho-005.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
…low two-part bounds (readiness audit H2)

version_order vo-086..vo-091 and fix_target ft-065 (symfony/process v5.1.0 -> v5.4.51) and ft-066 (rack 2.0.6 -> 2.2.23 on the pinned records). The truth comes from the ecosystems' own implementations, run in docker on 2026-10-17: composer/semver and PHP version_compare, and Gem::Version. The fix_target lines use live OSV querybatch per published release, restricted to the pinned ids. The Packagist 6.4.0/6.4 pair is deliberately unpinned, because composer/semver's normaliser and comparator disagree on it. The app engine already passes; the @4da/mcp-server TS engine did not (it sent symfony/process to 8.0.5 and rack to 3.2.6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
… the manifest declares, from its own source

- S4: the declared spec (npm, Cargo), go.mod requirement or == pin must
  admit the INSTALLED copy before any fix is named; otherwise the row is
  about another copy than this manifest declares.
- B1 PyPI: only the version token after == changes; extras, environment
  markers and comments stay. Hashed pins (\, --hash on the line or the
  next) and pip-compile output name nothing.
- B2 Cargo: a git, path or alternate-registry dependency names nothing
  (cargo add <crate>@<v> re-points it at crates.io).
- S1: a holding placeholder is only npm's own 0.0.<n>-security shape; a
  publisher's 1.0.0-security prerelease stays affected (ho-006).
- S2: cargo --target is double-quoted. Live through cmd.exe 2026-10-17:
  single quotes failed to parse as a cfg expression.
- S3: a yarn workspace member names nothing (yarn up / upgrade rewrite
  every workspace); the root itself still does.
- S5: direct_fixes / direct_fix_note move into osv/direct_upgrade.rs
  (upgrade_plan.rs 981 -> 920 lines); the inline-table reader is its own
  DeclFields.
- N1: every direct vector carries its own source, saying whether it was a
  tool run (and which version) or a rule; fc-043 pins the instruction text,
  and the runner checks command and instruction through direct_fix on a
  project built from the vector.
- N2: a Go module under a replace directive names nothing.
- New vectors fc-046..fc-053, ho-006.

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

Findings carry project paths lowercased (normalize_project_path). That
still opens on Windows and macOS, but on a case-sensitive filesystem
`/home/runner/work/4DA/4DA/...` lowercased does not exist, so every
manifest read failed and the direct row named no command. The merge-queue
run of #972 caught it on Linux; locally the end-to-end test never ran (its
`c2-e2e-` tempdir prefix tripped the scaffolding exclusion and it printed
SKIPPED).

- fix_path::on_disk resolves each missing component case-insensitively
  against its parent's entries; two entries that differ only by case
  resolve to nothing.
- direct_fix and Manager::detect (the transitive refresh commands) read
  through it, so Linux users get the same commands Windows users do.
- The end-to-end test's tempdir prefix no longer reads as scaffolding: it
  runs (no SKIPPED line with --nocapture).

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/c2-direct-upgrade-commands branch from ee8cc21 to 6ba7691 Compare October 17, 2026 16:42
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 1a2e905 Oct 17, 2026
25 checks passed
@runyourempire
runyourempire deleted the fix/c2-direct-upgrade-commands branch October 17, 2026 17: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