Repository: semgrep/semgrep (language: python)

Issue:
[Ruby] Heredoc content are not in the CST
followup to https://github.com/returntocorp/semgrep/issues/1580

```
|   |   |   |   ""
update_ruby/home/pad/semgrep/tests/ruby/parsing $ cat heredoc2.rb
query = <<-SQL
SELECT * FROM #{table}
WHERE #{type} = true
SQL
update_ruby/home/pad/semgrep/tests/ruby/parsing $ yy -dump_tree_sitter_cst heredoc2.rb
+ /home/pad/semgrep/_build/default/cli/Main.exe -dump_tree_sitter_cst heredoc2.rb
|   | Rep1_choice_stmt_term_opt_stmt
|   |   |   Stmt_term
|   |   |   |   Exp
|   |   |   |   | Arg
|   |   |   |   |   Assign
|   |   |   |   |   | Choice_lhs_EQ_choice_arg
|   |   |   |   |   |   | Lhs
|   |   |   |   |   |   |   Var
|   |   |   |   |   |   |   | Id
|   |   |   |   |   |   |   | query
|   |   |   |   |   |   "="
|   |   |   |   |   |   | Arg
|   |   |   |   |   |   |   Prim
|   |   |   |   |   |   |   | Here_begin
|   |   |   |   |   |   |   | "<<-SQL"
|   |   |   |   Line_brk
|   |   |   |   ""
update_ruby/home/pad/semgrep/tests/ruby/parsing $ 
```

Tasks:
- [ ]  ~~https://github.com/tree-sitter/tree-sitter-ruby/issues/176~~
- [x] https://github.com/tree-sitter/tree-sitter-ruby/issues/177


Discussion / hints:
The grammar.js for ruby is weird because it has a heredoc_body rule, but it's not referenced by
anything. Might be referenced by the scanner, which makes it hard for us to make the connection.

This issue is being marked `stale` because there hasn't been any activity in 30 days. Please leave a comment if you think this issue is still relevant and should be prioritized, otherwise it will be automatically closed in 7 days (you can always reopen it later).

Stale-bot has closed this stale item. Please reopen it if this is in error.
This issue is being marked `stale` because there hasn't been any activity in 30 days. Please leave a comment if you think this issue is still relevant and should be prioritized, otherwise it will be automatically closed in 7 days (you can always reopen it later).

Stale-bot has closed this stale item. Please reopen it if this is in error.
Current tree-sitter-ruby: [commit 391269d](https://github.com/tree-sitter/tree-sitter-ruby/tree/391269d74d20154bbd0ac9be20b35eced6920290))

What I see is that indeed, our ocaml parser cannot make sense of heredoc contents like the original tree-sitter parser does.

Input:
```
$ cat heredoc2.rb 
query = <<-SQL
SELECT * FROM #{table}
WHERE #{type} = true
SQL
```
Using the work in progress by @ahlinc, we get this output:
![image](https://user-images.githubusercontent.com/343265/123692773-134db900-d80c-11eb-809f-0239da9c99a1.png)

I see two problems:
1. ~~The tree-sitter CST contains all the nodes, unlike the ocaml parser. However, no token is returned for `heredoc_content`. This would have to be fixed but we're not there yet. This [needs to be fixed](https://github.com/tree-sitter/tree-sitter-ruby/issues/177) because ocaml-tree-sitter will expect a token here, not a blank.~~
2. The `heredoc_body` node is missing from the OCaml CST. This is because [`heredoc_body` is declared as an extra](https://github.com/tree-sitter/tree-sitter-ruby/issues/176), meaning it can occur anywhere like a comment. It looks like it was done as a quick hack, but it would have to be revisited.

OK, so [Max explained why there's no better solution than having the heredoc_body occur as an extra](https://github.com/tree-sitter/tree-sitter-ruby/issues/176). The solution I can think of would involve making a special pass on the original CST so as to locate the heredoc_body and move to the just previous heredoc_beginning. We'd have to generate a `CST.ml` as if the grammar had a `seq($.heredoc_beginning, $.heredoc_body)`, and rely on the custom tree-rewriting mechanism to make the CST conform to that structure. It looks like a bit of work and added complexity.
@mjambon The `heredoc_content` doesn't have visible content in the above screenshot because it is multi line token and contains `\n`. I didn't implement printing multi line tokens properly yet.
@ahlinc ok, great. I closed that issue.
@mjambon I've shared new improvements in https://github.com/ahlinc/tree-sitter/pull/4 and now it also prints multi line tokens, but some times with a bit broken indentation for such tokens sot there are still need to work on improvements. You can try to update your tree-sitter version from that branch.
@ahlinc Awesome. Here's the updated screenshot:
![image](https://user-images.githubusercontent.com/343265/123884172-7d935600-d8ff-11eb-9c7a-20ea8536cde2.png)

This issue is being marked `stale` because there hasn't been any activity in 30 days. Please leave a comment if you think this issue is still relevant and should be prioritized, otherwise it will be automatically closed in 7 days (you can always reopen it later).

This issue is being marked `stale` because there hasn't been any activity in 30 days. Please leave a comment if you think this issue is still relevant and should be prioritized, otherwise it will be automatically closed in 7 days (you can always reopen it later).

Stale-bot has closed this stale item. Please reopen it if this is in error.
We definitely want to get this done. Get that, stalebot?
This might help: https://github.com/tree-sitter/tree-sitter-ruby/pull/225

@mjambon what is the current status about this here documents and extra nodes? I've seen some recent work by you on this topic. Is it close to be done?


Fix patch:
diff --git a/.github/workflows/Makefile b/.github/workflows/Makefile
index 621d20b38a67..df269e3b809d 100644
--- a/.github/workflows/Makefile
+++ b/.github/workflows/Makefile
@@ -14,6 +14,7 @@ OBJS = \
   build-test-core-x86.yml \
   build-test-osx-x86.yml build-test-osx-arm64.yml \
   build-test-manylinux-x86.yml build-test-manylinux-aarch64.yml \
+  build-test-windows-x86.yml \
   tests.yml \
   test-e2e-semgrep-ci.yml nightly.yml \
   check-semgrep-pro-version.yml \
diff --git a/.github/workflows/libs/actions.libsonnet b/.github/workflows/libs/actions.libsonnet
index dc2d1251d36f..6c935c6edbf9 100644
--- a/.github/workflows/libs/actions.libsonnet
+++ b/.github/workflows/libs/actions.libsonnet
@@ -75,6 +75,7 @@
           tar czf artifacts.tgz artifacts
         ||| % path,
   },
+  // TODO: switch to v4 for upload/download below
   upload_artifact_step: function(artifact_name, path='artifacts.tgz') {
        uses: 'actions/upload-artifact@v3',
        with: {
diff --git a/.github/workflows/libs/gha.libsonnet b/.github/workflows/libs/gha.libsonnet
index a42489b8ac93..6d10269521ed 100644
--- a/.github/workflows/libs/gha.libsonnet
+++ b/.github/workflows/libs/gha.libsonnet
@@ -36,6 +36,7 @@
     contents: 'read',
   },
   pull_request_permissions: {
+    'id-token': 'write',
     contents: 'write',
     // needed when we want to modify the pull-request (e.g., for snapshot update)
     'pull-requests': 'write',
diff --git a/.gitmodules b/.gitmodules
index 87ad12262ab6..cbb3ba839b58 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -46,10 +46,6 @@ url = https://github.com/returntocorp/semgrep-hack.git
 path = languages/html/tree-sitter/semgrep-html
 url = https://github.com/returntocorp/semgrep-html.git
 
-[submodule "OSS/languages/tree-sitter-lang/semgrep-vue"]
-path = languages/vue/tree-sitter/semgrep-vue
-url = https://github.com/returntocorp/semgrep-vue.git
-
 [submodule "OSS/languages/dockerfile/tree-sitter/semgrep-dockerfile"]
 path = languages/dockerfile/tree-sitter/semgrep-dockerfile
 url = https://github.com/returntocorp/semgrep-dockerfile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e92be88f34f7..22d673db9128 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,40 @@
 
 <!-- insertion point -->
 
+## [1.93.0](https://github.com/returntocorp/semgrep/releases/tag/v1.93.0) - 2024-10-23
+
+
+### Added
+
+
+- Improved naming for Common JS module imports (`require`) in arbitrary
+  expression contexts. Notably, in-line use of `require` should now be linked to
+  the correct module. For instance, the pattern `foo.bar` should now match
+  against `require('foo').bar` and taint is likewise similarily tracked. (code-7485)
+- Secrets: `semgrep ci` output now includes a list of all secrets rules which
+  generated at least one blocking finding (similar to Code) (code-7663)
+- Added experimental support via `--allow-dynamic-dependency-resolution` for dynamic resolution of Maven and Gradle dependencies for projects that do not have lockfiles (in Semgrep Pro only). (gh-2389)
+- Expanded support for pip requirement lockfiles is now available by default. Semgrep will now
+  find any *requirement*.txt file and lockfiles in a requirements folder (**/requirements/*.txt).
+  The existing experimental flag `--enable-experimental-requirements` is now deprecated and
+  will be removed in a future release. (gh-2441)
+
+
+### Changed
+
+
+- Removed support for Vue. The tree-sitter grammar has not been updated in 3 years,
+  there was no community rules added and semgrep-vue is causing linking conflicts
+  when compiling semgrep under Windows so just simpler to remove support for Vue.
+  In theory, extract mode could be a good substitute to parse Vue files. (vue)
+
+
+### Fixed
+
+
+- semgrep will now print exit codes if a segfault/OOM/other terminating signal happens in semgrep-core, or any of semgrep-core's child processes (saf-1646)
+
+
 ## [1.92.0](https://github.com/returntocorp/semgrep/releases/tag/v1.92.0) - 2024-10-17
 
 
diff --git a/README.md b/README.md
index 6caee600e72e..1a95d500f2c7 100644
--- a/README.md
+++ b/README.md
@@ -42,13 +42,17 @@
 </p>
 </br>
 
-This repository contains the source code for Semgrep OSS (open-source software). Semgrep OSS is a fast, open-source, static analysis tool for searching code, finding bugs, and enforcing code standards at editor, commit, and CI time. Semgrep is a semantic grep for code: where `grep "2"` would only match the exact string _2_, Semgrep would [match `x = 1; y = x + 1` when searching for _2_](https://semgrep.dev/playground/s/5rKgj). And it does this in 30+ languages! Semgrep rules look like the code you already write; no abstract syntax trees, regex wrestling, or painful DSLs: read more below.
+Semgrep is a fast, open-source, static analysis tool that searches code, finds bugs, and enforces secure guardrails and coding standards. Semgrep [supports 30+ languages](#language-support) and can run in an IDE, as a pre-commit check, and as part of CI/CD workflows.
 
-For companies who need SAST, SCA, and Secret scanning, we provide a product suite on top of Semgrep OSS that scans code and package dependencies for known issues, software vulnerabilities, and finds secrets with high accuracy:
+Semgrep is semantic grep for code. While running `grep "2"` would only match the exact string _2_, Semgrep would [match `x = 1; y = x + 1` when searching for _2_](https://semgrep.dev/playground/s/5rKgj). Semgrep rules look like the code you already write; no abstract syntax trees, regex wrestling, or painful DSLs.
 
-- [**Semgrep Code**](https://semgrep.dev/products/semgrep-code/) to find bugs & vulnerabilities using the deeper, interfile-analysis enabled [Pro engine](https://semgrep.dev/products/pro-engine/) and high-accuracy Pro rules in addition to the community rules
-- [**Semgrep Supply Chain**](https://semgrep.dev/products/semgrep-supply-chain/) to find dependencies with known vulnerabilities function-level reachability analysis
-- [**Semgrep Secrets**](https://semgrep.dev/products/semgrep-secrets/) to find hard-coded credentials that shouldn't be checked into source code
+Note that in security contexts, Semgrep OSS will miss many true positives as it can only analyze code within the boundaries of a single function or file. If you want to use Semgrep for security purposes (**SAST**, **SCA**, or **secrets scanning**), the Semgrep AppSec Platform is strongly recommended since it adds the following critical capabilities:
+
+1. Improved core analysis capabilities (cross-file, cross-function, data-flow reachability) that greatly reduce false positives by 25% and increase detected true positives by 250%
+2. Contextual post-processing of findings with Semgrep Assistant (AI) to further reduce noise by [~20%](https://a.storyblok.com/f/151984/x/2d12dc0223/whitepaper_-ai-powered-appsec-engineer-automate.pdf?cv=1728584410408).  In addition, Assistant enriches findings with tailored, step-by-step remediation guidance that humans find actionable >80% of the time.
+3. Customizable policies and seamless integration into developer workflows, giving security teams granular control over where, when, and how different findings are presented to developers (IDE, PR comment, etc.)
+
+The Semgrep AppSec Platform works out-of-the-box with 20000+ proprietary rules across SAST, SCA, and secrets. Pro rules are written and maintained by the Semgrep security research team and are highly accurate, meaning AppSec teams can feel confident bringing findings directly to developers without slowing them down.
 
 Semgrep analyzes code locally on your computer or in your build environment: **by default, code is never uploaded**. [Get started →.](#getting-started-)
 
@@ -112,9 +116,9 @@ $ docker run -e SEMGREP_APP_TOKEN=<TOKEN> --rm -v "${PWD}:/src" semgrep/semgrep
 
 Logging into Semgrep gets you access to:
 
-- [Semgrep Supply Chain](https://semgrep.dev/products/semgrep-supply-chain): A dependency scanner that detects reachable vulnerabilities in third party libraries
-- [Semgrep Code's Pro rules](https://semgrep.dev/products/semgrep-code): 600+ high confidence rules written by Semgrep's security research team
-- [Semgrep Code's Pro engine](https://semgrep.dev/products/pro-engine/): An advanced code analysis engine, designed to detect complex vulnerabilities, and reduce false positives
+- [Semgrep Supply Chain](https://semgrep.dev/products/semgrep-supply-chain?utm_medium=readme&utm_source=github&utm_content=ssc-product): A dependency scanner that detects reachable vulnerabilities in third party libraries
+- [Semgrep Code's Pro rules](https://semgrep.dev/products/semgrep-code?utm_medium=readme&utm_source=github&utm_content=code-pro-rules): 600+ high confidence rules written by Semgrep's security research team
+- [Semgrep Code's Pro engine](https://semgrep.dev/products/pro-engine?utm_medium=readme&utm_source=github&utm_content=pro-engine): An advanced code analysis engine, designed to detect complex vulnerabilities, and reduce false positives
 
 3.  Go to your app's root directory and run `semgrep ci`. This will scan your project to check for vulnerabilities in your source code and its dependencies.
 
@@ -123,15 +127,21 @@ Logging into Semgrep gets you access to:
 
 ### Semgrep Ecosystem
 
-The Semgrep ecosystem includes the following products:
+The Semgrep ecosystem includes the following:
+
+- [Semgrep OSS](https://semgrep.dev/docs/cli-reference/) - The open-source program analysis engine at the heart of everything. Suitable for ad-hoc use cases with a high tolerance for false positives - think consultants, security auditors, or pentesters.
+
+- [Semgrep AppSec Platform](https://semgrep.dev/login?utm_medium=readme&utm_source=github&utm_content=appsec-platform) - Easily orchestrate and scale SAST, SCA, and Secrets scanning across an organization, with no risk of overwhelming developers.  Customize which findings developers see, where they see them, and integrate with CI providers like GitHub, GitLab, CircleCI, and more. Includes both free and paid tiers.
+
+  - [Semgrep Code (SAST)](https://semgrep.dev/products/semgrep-code?utm_medium=readme&utm_source=github&utm_content=semgrep-code) - Make real progress on your vulnerability backlog with SAST that minimizes noise and empowers developers to quickly fix issues on their own, even if they have no security knowledge. Easy to deploy secure guardrails and tailored, step-by-step remediation guidance mean developers actually fix issues since they don't feel slowed down.  
+
+  - [Semgrep Supply Chain (SSC)](https://semgrep.dev/products/semgrep-supply-chain?utm_medium=readme&utm_source=github&utm_content=ssc) - A high-signal dependency scanner that detects reachable vulnerabilities in open source third-party libraries and functions.
+
+  - [Semgrep Secrets (Secrets scanning)](https://semgrep.dev/blog/2023/introducing-semgrep-secrets/) - Secrets detection that uses semantic analysis, improved entropy analysis, and validation to accurately surface sensitive credentials in the developer workflow.
 
-- [Semgrep Code](https://semgrep.dev/products/semgrep-code) - Scan your code with Semgrep's proprietary rules (written by our Security Research team) using our cross-file and cross-function analysis. Designed to find OWASP Top 10 vulnerabilities and protect against critical security risks. Semgrep Code is available on both [free and paid tiers](https://semgrep.dev/pricing).
-- [Semgrep Supply Chain (SSC)](https://semgrep.dev/products/semgrep-supply-chain) - A high-signal dependency scanner that detects reachable vulnerabilities in open source third-party libraries and functions across the software development life cycle (SDLC). Semgrep Supply Chain is available on both [free and paid tiers](https://semgrep.dev/pricing).
-- [Semgrep Secrets [NEW!]](https://semgrep.dev/blog/2023/introducing-semgrep-secrets/) - Secrets detection that uses semantic analysis, improved entropy analysis, and validation together to accurately detect sensitive credentials in developer workflows. [Book a demo](https://get.semgrep.dev/secrets-beta-request.html) to request early access to the product.
-- [Semgrep AppSec Platform](https://semgrep.dev/login) - Deploy, manage, and monitor Semgrep at scale, with [free and paid tiers](https://semgrep.dev/pricing). Integrates with continuous integration (CI) providers such as GitHub, GitLab, CircleCI, and more.
-- [Semgrep OSS Engine](https://semgrep.dev/docs/cli-reference/) - The open-source engine and community-contributed rules at the heart of everything (this project).
+  - [Semgrep Assistant (AI)](https://semgrep.dev/products/semgrep-code/assistant?utm_medium=readme&utm_source=github&utm_content=assistant) - Assistant is an AI-powered AppSec engineer that helps both developers and AppSec teams prioritize, triage, and remediate Semgrep findings at scale. Humans agree with Assistant auto-triage decisions 97% of the time, and rate generated remediation guidance as helpful 80% of the time. For an overview of how Assistant works, [read this overview](https://a.storyblok.com/f/151984/x/2d12dc0223/whitepaper_-ai-powered-appsec-engineer-automate.pdf?cv=1728584410408).
 
-To learn more about Semgrep, visit:
+Additional resources:
 
 - [Semgrep Playground](https://semgrep.dev/editor) - An online interactive tool for writing and sharing rules.
 - [Semgrep Registry](https://semgrep.dev/explore) - 2,000+ community-driven rules covering security, correctness, and dependency vulnerabilities.
diff --git a/cli/bin/semgrep b/cli/bin/semgrep
index c1f254f62f53..15d6cd491aa6 100755
--- a/cli/bin/semgrep
+++ b/cli/bin/semgrep
@@ -30,6 +30,7 @@
 # if you want to test the change under `pipenv shell`.
 
 import os
+import platform
 import sys
 import importlib.resources
 import shutil
@@ -49,6 +50,9 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
 PATH = os.environ.get("PATH", "")
 os.environ["PATH"] = PATH + os.pathsep + os.path.dirname(os.path.abspath(__file__))
 
+IS_WINDOWS = platform.system() == "Windows"
+
+
 class CoreNotFound(Exception):
     def __init__(self, value):
         self.value = value
@@ -81,6 +85,10 @@ def find_semgrep_core_path(pro=False, extra_message=""):
         core = "semgrep-core-proprietary"
     else:
         core = "semgrep-core"
+
+    if IS_WINDOWS:
+        core += ".exe"
+
     # First, try the packaged binary.
     try:
         # the use of .path causes a DeprecationWarning hence the
diff --git a/cli/setup.py b/cli/setup.py
index 6232563aa6e2..4896075ed280 100644
--- a/cli/setup.py
+++ b/cli/setup.py
@@ -139,7 +139,7 @@ def find_executable(env_name, exec_name):
 
 setuptools.setup(
     name="semgrep",
-    version="1.92.0",
+    version="1.93.0",
     author="Semgrep Inc.",
     author_email="support@semgrep.com",
     description="Lightweight static analysis for many languages. Find bug variants with patterns that look like source code.",
diff --git a/cli/src/semdep/lockfile.py b/cli/src/semdep/lockfile.py
deleted file mode 100644
index 6b268e8815c2..000000000000
--- a/cli/src/semdep/lockfile.py
+++ /dev/null
@@ -1,424 +0,0 @@
-from abc import ABC
-from abc import abstractmethod
-from dataclasses import dataclass
-from fnmatch import fnmatch
-from pathlib import Path
-from typing import Dict
-from typing import FrozenSet
-from typing import List
-from typing import Optional
-from typing import Tuple
-
-from semdep.parsers.cargo import parse_cargo
-from semdep.parsers.composer import parse_composer_lock
-from semdep.parsers.gem import parse_gemfile
-from semdep.parsers.go_mod import parse_go_mod
-from semdep.parsers.gradle import parse_gradle
-from semdep.parsers.mix import parse_mix
-from semdep.parsers.package_lock import parse_package_lock
-from semdep.parsers.packages_lock_c_sharp import (
-    parse_packages_lock as parse_packages_lock_c_sharp,
-)
-from semdep.parsers.pipfile import parse_pipfile
-from semdep.parsers.pnpm import parse_pnpm
-from semdep.parsers.poetry import parse_poetry
-from semdep.parsers.pom_tree import parse_pom_tree
-from semdep.parsers.pubspec_lock import parse_pubspec_lock
-from semdep.parsers.requirements import parse_requirements
-from semdep.parsers.swiftpm import parse_package_resolved
-from semdep.parsers.util import DependencyParserError
-from semdep.parsers.util import SemgrepParser
-from semdep.parsers.util import to_parser
-from semdep.parsers.yarn import parse_yarn
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Cargo
-from semgrep.semgrep_interfaces.semgrep_output_v1 import CargoParser
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Composer
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gomod
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Hex
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Maven
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Npm
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Nuget
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pub
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
-from semgrep.semgrep_interfaces.semgrep_output_v1 import ScaParserName
-from semgrep.semgrep_interfaces.semgrep_output_v1 import SwiftPM
-
-
-@dataclass(frozen=True)
-class LockfileMatcher(ABC):
-    """
-    Abstract base class for matching and parsing lockfiles.
-    Attributes:
-        manifest (Optional[str]): Represents the name of the manifest file associated with the lockfile.
-            For example, in the case of Python's Pipfile.lock, the manifest is 'Pipfile'.
-        parser (Parser): A function that parses the lockfile and potentially a manifest file into
-            a list of FoundDependency objects and a list of DependencyParserError.
-        ecosystem (Ecosystem): The ecosystem associated with the lockfile, such as Pypi, Npm, etc.
-    """
-
-    manifest: Optional[str]
-    parser: SemgrepParser
-    ecosystem: Ecosystem
-
-    @abstractmethod
-    def is_match(self, path: Path) -> bool:
-        """
-        Determines whether the given path matches the lockfile criteria for this matcher.
-        Args:
-            path (Path): The path to the lockfile.
-        Returns:
-            bool: True if the path matches, False otherwise.
-        """
-
-    @abstractmethod
-    def get_manifest_path(self, lockfile_path: Path) -> Optional[Path]:
-        """
-        Retrieves the corresponding manifest path based on the lockfile path.
-        Args:
-            lockfile_path (Path): The path to the lockfile.
-        Returns:
-            Optional[Path]: The path to the manifest file if it exists, otherwise None.
-        """
-
-    def get_subproject_root(self, lockfile_path: Path) -> Path:
-        """
-        Retrieves the subproject root directory path based on the lockfile path.
-        Args:
-            lockfile_path (Path): The path to the lockfile.
-        Returns:
-            Path: The parent path of the lockfile.
-        """
-        return lockfile_path.parent
-
-
-@dataclass(frozen=True)
-class ExactLockfileMatcher(LockfileMatcher):
-    """
-    Matcher for lockfiles that have an exact filename.
-    Attributes:
-        lockfile (str): The exact name of the lockfile.
-    Example:
-        For Pipfile.lock, the manifest is Pipfile.
-    """
-
-    lockfile: str
-
-    def is_match(self, path: Path) -> bool:
-        return path.name == self.lockfile
-
-    def get_manifest_path(self, lockfile_path: Path) -> Optional[Path]:
-        if self.manifest:
-            manifest_path = lockfile_path.parent / self.manifest
-            return manifest_path if manifest_path.exists() else None
-        return None
-
-
-@dataclass(frozen=True)
-class PatternLockfileMatcher(LockfileMatcher):
-    """
-    Matcher for lockfiles that follow a specific pattern.
-    Attributes:
-        pattern (str): The pattern that the lockfile name should match.
-    Example:
-        For a pattern of "*requirements*.txt", the manifest could be "*.in".
-    """
-
-    pattern: str
-
-    def is_match(self, path: Path) -> bool:
-        return fnmatch(str(path), self.pattern)
-
-    def get_manifest_path(self, lockfile_path: Path) -> Optional[Path]:
-        if self.manifest:
-            manifest_path = self.get_subproject_root(lockfile_path) / self.manifest
-            if manifest_path.exists():
-                return manifest_path
-
-        return None
-
-
-@dataclass(frozen=True)
-class RequirementsLockfileMatcher(PatternLockfileMatcher):
-    def get_manifest_path(self, lockfile_path: Path) -> Optional[Path]:
-        if self.manifest:
-            # First check the lockfile's parent directory for the manifest
-            manifest_path = self.get_subproject_root(lockfile_path) / self.manifest
-            if manifest_path.exists():
-                return manifest_path
-
-            # If manifest not found, check for a manifest with the same stem as the lockfile
-            lockfile_stem = lockfile_path.stem
-            manifest_name = f"{lockfile_stem}{Path(self.manifest).suffix}"
-            manifest_path = lockfile_path.with_name(manifest_name)
-            return manifest_path if manifest_path.exists() else None
-        return None
-
-    def get_subproject_root(self, lockfile_path: Path) -> Path:
-        """
-        Retrieves the subproject root directory path based on the lockfile path.
-        """
-
-        # Not pretty but we need to handle the case where the lockfile is in a subdirectory
-        # Ex. for requirements/base.txt, the parent path should be the directory containing the
-        # requirements directory. For requirements.txt, the parent path should be the directory
-        # containing the lockfile.
-
-        # Check if the lockfile is in a 'requirements' directory
-        if "requirements" in lockfile_path.parts:
-            # Find the index of 'requirements' in the path
-            req_index = lockfile_path.parts.index("requirements")
-            # Return the parent of the 'requirements' directory
-            return Path(*lockfile_path.parts[:req_index])
-        else:
-            # If not in a 'requirements' directory, return the immediate parent
-            return lockfile_path.parent
-
-
-NEW_REQUIREMENTS_MATCHERS: List[LockfileMatcher] = [
-    ExactLockfileMatcher(
-        lockfile="Pipfile.lock",
-        manifest="Pipfile",
-        parser=parse_pipfile,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="poetry.lock",
-        manifest="pyproject.toml",
-        parser=parse_poetry,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    RequirementsLockfileMatcher(
-        pattern="*requirement*.txt",
-        manifest="requirements.in",
-        parser=parse_requirements,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="requirements.pip",
-        manifest="requirements.in",
-        parser=parse_requirements,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-]
-
-OLD_REQUIREMENTS_MATCHERS: List[LockfileMatcher] = [
-    ExactLockfileMatcher(
-        lockfile="Pipfile.lock",
-        manifest="Pipfile",
-        parser=parse_pipfile,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="poetry.lock",
-        manifest="pyproject.toml",
-        parser=parse_poetry,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="requirements.txt",
-        manifest="requirements.in",
-        parser=parse_requirements,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="requirements3.txt",
-        manifest="requirements.in",
-        parser=parse_requirements,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-    ExactLockfileMatcher(
-        lockfile="requirements.pip",
-        manifest="requirements.in",
-        parser=parse_requirements,
-        ecosystem=Ecosystem(Pypi()),
-    ),
-]
-
-
-ECOSYSTEM_TO_LOCKFILES: Dict[Ecosystem, List[LockfileMatcher]] = {
-    Ecosystem(Npm()): [
-        ExactLockfileMatcher(
-            lockfile="package-lock.json",
-            manifest="package.json",
-            parser=parse_package_lock,
-            ecosystem=Ecosystem(Npm()),
-        ),
-        ExactLockfileMatcher(
-            lockfile="yarn.lock",
-            manifest="package.json",
-            parser=parse_yarn,
-            ecosystem=Ecosystem(Npm()),
-        ),
-        ExactLockfileMatcher(
-            lockfile="pnpm-lock.yaml",
-            manifest=None,
-            parser=parse_pnpm,
-            ecosystem=Ecosystem(Npm()),
-        ),
-    ],
-    Ecosystem(Gem()): [
-        ExactLockfileMatcher(
-            lockfile="Gemfile.lock",
-            manifest=None,
-            parser=parse_gemfile,
-            ecosystem=Ecosystem(Gem()),
-        ),
-    ],
-    Ecosystem(Gomod()): [
-        ExactLockfileMatcher(
-            lockfile="go.mod",
-            manifest=None,
-            parser=parse_go_mod,
-            ecosystem=Ecosystem(Gomod()),
-        ),
-    ],
-    Ecosystem(Cargo()): [
-        ExactLockfileMatcher(
-            lockfile="Cargo.lock",
-            manifest=None,
-            parser=to_parser(parse_cargo, ScaParserName(CargoParser())),
-            ecosystem=Ecosystem(Cargo()),
-        ),
-    ],
-    Ecosystem(Maven()): [
-        ExactLockfileMatcher(
-            lockfile="maven_dep_tree.txt",
-            manifest=None,
-            parser=parse_pom_tree,
-            ecosystem=Ecosystem(Maven()),
-        ),
-        ExactLockfileMatcher(
-            lockfile="gradle.lockfile",
-            manifest="build.gradle",
-            parser=parse_gradle,
-            ecosystem=Ecosystem(Maven()),
-        ),
-    ],
-    Ecosystem(Composer()): [
-        ExactLockfileMatcher(
-            lockfile="composer.lock",
-            manifest="composer.json",
-            parser=parse_composer_lock,
-            ecosystem=Ecosystem(Composer()),
-        ),
-    ],
-    Ecosystem(Nuget()): [
-        ExactLockfileMatcher(
-            lockfile="packages.lock.json",
-            manifest=None,
-            parser=parse_packages_lock_c_sharp,
-            ecosystem=Ecosystem(Nuget()),
-        ),
-    ],
-    Ecosystem(Pub()): [
-        ExactLockfileMatcher(
-            lockfile="pubspec.lock",
-            manifest="pubspec.yaml",
-            parser=parse_pubspec_lock,
-            ecosystem=Ecosystem(Pub()),
-        ),
-    ],
-    Ecosystem(SwiftPM()): [
-        ExactLockfileMatcher(
-            lockfile="Package.resolved",
-            manifest="Package.swift",
-            parser=parse_package_resolved,
-            ecosystem=Ecosystem(SwiftPM()),
-        ),
-    ],
-    Ecosystem(Hex()): [
-        ExactLockfileMatcher(
-            lockfile="mix.lock",
-            manifest="mix.exs",
-            parser=parse_mix,
-            ecosystem=Ecosystem(Hex()),
-        ),
-    ],
-}
-
-
-class EcosystemLockfiles:
-    ecosystem_to_lockfiles: Dict[Ecosystem, List[LockfileMatcher]] = {
-        **ECOSYSTEM_TO_LOCKFILES,
-        # By default, use the old requirements lockfile matchers for Python
-        **{Ecosystem(Pypi()): OLD_REQUIREMENTS_MATCHERS},
-    }
-
-    @classmethod
-    def init(cls, use_new_requirements_matchers: bool = False) -> None:
-        cls.ecosystem_to_lockfiles[Ecosystem(Pypi())] = (
-            NEW_REQUIREMENTS_MATCHERS
-            if use_new_requirements_matchers
-            else OLD_REQUIREMENTS_MATCHERS
-        )
-
-
-def _is_valid_lockfile(ecosystem: Ecosystem, path: Path) -> bool:
-    """
-    Check if a path is a valid lockfile for the given ecosystem.
-    """
-    lockfile_matchers = EcosystemLockfiles.ecosystem_to_lockfiles.get(ecosystem, [])
-    return any(matcher.is_match(path) for matcher in lockfile_matchers)
-
-
-def _create_matcher(path: Path) -> LockfileMatcher:
-    """
-    Method for creating the appropriate LockfileMatcher instance based on
-    the lockfile path.
-    """
-    for _, lockfile_matchers in EcosystemLockfiles.ecosystem_to_lockfiles.items():
-        for lockfile_matcher in lockfile_matchers:
-            if lockfile_matcher.is_match(path):
-                return lockfile_matcher
-
-    raise ValueError(f"Unknown lockfile: {path.name}")
-
-
-def filter_lockfile_paths(
-    ecosystem: Ecosystem, candidates: FrozenSet[Path]
-) -> FrozenSet[Path]:
-    """
-    Filter out paths that are not lockfiles for the given ecosystem
-    """
-    return frozenset(path for path in candidates if _is_valid_lockfile(ecosystem, path))
-
-
-@dataclass(frozen=True)
-class Lockfile:
-    """
-    Class for representing a lockfile and its metadata
-    """
-
-    path: Path
-    root_directory: Path
-    manifest_path: Optional[Path]
-    ecosystem: Ecosystem
-    parser: SemgrepParser
-
-    @staticmethod
-    def from_path(path: Path) -> "Lockfile":
-        """
-        Create a Lockfile instance from a path
-        """
-        matcher = _create_matcher(path)
-        manifest_path = matcher.get_manifest_path(path)
-        root_dir = matcher.get_subproject_root(path)
-
-        return Lockfile(
-            path=path,
-            root_directory=root_dir,
-            manifest_path=manifest_path,
-            ecosystem=matcher.ecosystem,
-            parser=matcher.parser,
-        )
-
-    def parse(self) -> Tuple[List[FoundDependency], List[DependencyParserError]]:
-        """
-        Parse a lockfile and return it as a list of dependency objects
-        Also returns Optional DependencyParseError as second return value if there was a problem
-        parsing the lockfile
-        """
-        return self.parser(self.path, self.manifest_path)
diff --git a/cli/src/semdep/subproject_matchers.py b/cli/src/semdep/subproject_matchers.py
new file mode 100644
index 000000000000..6ca973ef50cc
--- /dev/null
+++ b/cli/src/semdep/subproject_matchers.py
@@ -0,0 +1,594 @@
+from abc import ABC
+from abc import abstractmethod
+from dataclasses import dataclass
+from fnmatch import fnmatch
+from pathlib import Path
+from typing import Dict
+from typing import FrozenSet
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Union
+
+import semgrep.semgrep_interfaces.semgrep_output_v1 as out
+from semgrep.subproject import DependencySource
+from semgrep.subproject import LockfileDependencySource
+from semgrep.subproject import ManifestOnlyDependencySource
+from semgrep.subproject import MultiLockfileDependencySource
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import Subproject
+
+
+@dataclass(frozen=True)
+class SubprojectMatcher(ABC):
+    """
+    This is a base class for "matchers" that create "subprojects" from a list of potential
+    candidates. Subclasses must implement `is_match` and `make_subprojects`.
+    """
+
+    @abstractmethod
+    def is_match(self, path: Path) -> bool:
+        """
+        Whether the given path has a relevant filename for this type of matcher.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def make_subprojects(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[List[Subproject], FrozenSet[Path]]:
+        """
+        Use the files given in `dep_source_files` to make as many subprojects as possible.
+        This may not use all the files in `dep_source_files`.
+
+        Returns:
+        - the list of created subprojects
+        - the set of files that were used to construct the returned subprojects.
+        """
+        raise NotImplementedError
+
+
+@dataclass(frozen=True)
+class LockfileManifestMatcher(SubprojectMatcher):
+    """
+    An abstract class for matchers that look for a single lockfile and a single manifest.
+
+    Subprojects are a match only when the lockfile is found, and are created whether the manifest
+    is found or not.
+
+    Child classes must implement _is_manifest_match, _is_lockfile_match, _lockfile_to_manifest, and _get_subproject_root
+    """
+
+    package_manager_type: PackageManagerType
+
+    @abstractmethod
+    def _is_manifest_match(self, path: Path) -> bool:
+        raise NotImplementedError
+
+    @abstractmethod
+    def _is_lockfile_match(self, path: Path) -> bool:
+        raise NotImplementedError
+
+    @abstractmethod
+    def _lockfile_to_manifest(
+        self, lockfile_path: Path, candidates: FrozenSet[Path]
+    ) -> Optional[Path]:
+        """
+        Given a lockfile path, return the path to the corresponding manifest
+        if it exists in `candidates`.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def _get_subproject_root(
+        self,
+        manifest_lockfile_paths: Union[
+            Tuple[Path, None], Tuple[None, Path], Tuple[Path, Path]
+        ],
+    ) -> Path:
+        """
+        Get the root of the subproject. The arguments are passed in a tuple of (manifest_path, lockfile_path)
+        rather than individually to allow verifying that at least one of the arguments is not None.
+        """
+        raise NotImplementedError
+
+    def is_match(self, path: Path) -> bool:
+        return self._is_manifest_match(path) or self._is_lockfile_match(path)
+
+    def _filter_manifest_lockfiles(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[Set[Path], Set[Path]]:
+        """
+        Classifies the provided source files as lockfiles, manifests, or neither.
+
+        Returns a tuple of (manifest_paths, lockfile_paths)
+        """
+        lockfiles: Set[Path] = set()
+        manifests: Set[Path] = set()
+        for path in dep_source_files:
+            if self._is_lockfile_match(path):
+                lockfiles.add(path)
+            if self._is_manifest_match(path):
+                manifests.add(path)
+        return (manifests, lockfiles)
+
+    def make_subprojects(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[List[Subproject], FrozenSet[Path]]:
+        """
+        Use the files given in `dep_source_files` to make as many subprojects as possible.
+        This may not use all the files in `dep_source_files`.
+        """
+        # grab all lockfiles and all manifests matching the pattern for this matcher.
+        # we will use these to construct subprojects
+        _manifests, lockfiles = self._filter_manifest_lockfiles(dep_source_files)
+
+        # track the manifests that we use in the first lockfile-based step. These manifests
+        # should be skipped in the second manifest-based step.
+        paired_manifests: Set[Path] = set()
+
+        subprojects: List[Subproject] = []
+
+        # first, handle cases where the lockfile exists and manifest may or may not
+        for lockfile_path in lockfiles:
+            matching_manifest_path = self._lockfile_to_manifest(
+                lockfile_path, dep_source_files
+            )
+            if matching_manifest_path is not None:
+                root_dir = self._get_subproject_root(
+                    (matching_manifest_path, lockfile_path)
+                )
+                paired_manifests.add(matching_manifest_path)
+            else:
+                # mypy is not smart enough to allow us to pull root_dir out of the if/else,
+                # so we do it in each branch...
+                root_dir = self._get_subproject_root(
+                    (matching_manifest_path, lockfile_path)
+                )
+            dep_source = LockfileDependencySource(
+                package_manager_type=self.package_manager_type,
+                manifest_path=matching_manifest_path,
+                lockfile_path=lockfile_path,
+            )
+            subprojects.append(
+                Subproject(root_dir=root_dir, dependency_source=dep_source)
+            )
+
+        return subprojects, frozenset(paired_manifests | lockfiles)
+
+
+@dataclass(frozen=True)
+class ExactLockfileManifestMatcher(LockfileManifestMatcher):
+    """
+    Matcher for lockfiles and manifests that have an exact filename.
+    Both manifest and lockfile name must be defined, but a subproject
+    is generated if at least one of the two is present.
+
+    Attributes:
+        lockfile_name (str): The exact name of the lockfile.
+        manifest_name (str): The exact name of the manifest
+    Example:
+        For Pipfile.lock, the manifest is Pipfile.
+    """
+
+    lockfile_name: str
+    manifest_name: str  # might want to make this optional and remove the manifests that weren't there before in the config
+
+    def _is_manifest_match(self, path: Path) -> bool:
+        return path.name == self.manifest_name
+
+    def _is_lockfile_match(self, path: Path) -> bool:
+        return path.name == self.lockfile_name
+
+    def _lockfile_to_manifest(
+        self, lockfile_path: Path, candidates: FrozenSet[Path]
+    ) -> Optional[Path]:
+        if self.manifest_name:
+            manifest_path = lockfile_path.parent / self.manifest_name
+            return manifest_path if manifest_path in candidates else None
+        return None
+
+    def _get_subproject_root(
+        self,
+        manifest_lockfile_paths: Union[
+            Tuple[Path, None], Tuple[None, Path], Tuple[Path, Path]
+        ],
+    ) -> Path:
+        # the subproject root is just the direct parent of the manifest or of the lockfile,
+        # with priority to the manifest if it exists (for no particular reason)
+        return (
+            manifest_lockfile_paths[0].parent
+            if manifest_lockfile_paths[0] is not None
+            else manifest_lockfile_paths[1].parent
+        )
+
+
+@dataclass(frozen=True)
+class PatternLockfileMatcher(LockfileManifestMatcher):
+    """
+    Matcher for lockfiles and manifests that follow a specific pattern
+    Attributes:
+        lockfile_pattern: The pattern that the lockfile name should match.
+            This lockfile name must be at the subproject root.
+        manifest_name: The exact name that the corresponding manifest should match.
+            The manifest is not required, so a match will still
+            be generated if no manifest is found.
+    """
+
+    lockfile_pattern: str
+    manifest_name: str  # we might want to let this be None sometimes, if we have lockfile-only package managers
+
+    def _is_lockfile_match(self, path: Path) -> bool:
+        return fnmatch(str(path), self.lockfile_pattern)
+
+    def _is_manifest_match(self, path: Path) -> bool:
+        return path.name == self.manifest_name
+
+    def _lockfile_to_manifest(
+        self, lockfile_path: Path, candidates: FrozenSet[Path]
+    ) -> Optional[Path]:
+        manifest_path = lockfile_path.parent / self.manifest_name
+        if manifest_path in candidates:
+            return manifest_path
+        return None
+
+    def _get_subproject_root(
+        self,
+        manifest_lockfile_paths: Union[
+            Tuple[Path, None], Tuple[None, Path], Tuple[Path, Path]
+        ],
+    ) -> Path:
+        return (
+            manifest_lockfile_paths[0].parent
+            if manifest_lockfile_paths[0] is not None
+            else manifest_lockfile_paths[1].parent
+        )
+
+
+@dataclass(frozen=True)
+class ManifestOnlyMatcher(SubprojectMatcher):
+    """
+    A matcher for lone manifests. These matchers must be placed after matchers
+    that look for both a manifest and a lockfile.
+
+    Implementors must implement _is_manifest_match and _get_subproject_root
+    """
+
+    manifest_kind: out.ManifestKind
+
+    @abstractmethod
+    def _is_manifest_match(self, path: Path) -> bool:
+        raise NotImplementedError
+
+    @abstractmethod
+    def _get_subproject_root(self, manifest_path: Path) -> Path:
+        raise NotImplementedError
+
+    def is_match(self, path: Path) -> bool:
+        return self._is_manifest_match(path)
+
+    def _filter_matching_manifests(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> FrozenSet[Path]:
+        """
+        Return only the matching manifests from the set of dependency source files.
+        """
+        return frozenset(p for p in dep_source_files if self._is_manifest_match(p))
+
+    def make_subprojects(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[List[Subproject], FrozenSet[Path]]:
+        manifests = self._filter_matching_manifests(dep_source_files)
+
+        subprojects: List[Subproject] = []
+        for manifest_path in manifests:
+            root_dir = self._get_subproject_root(manifest_path)
+            manifest_dep_source = ManifestOnlyDependencySource(
+                manifest_kind=self.manifest_kind,
+                manifest_path=manifest_path,
+            )
+            subprojects.append(
+                Subproject(root_dir=root_dir, dependency_source=manifest_dep_source)
+            )
+
+        return subprojects, manifests
+
+
+@dataclass(frozen=True)
+class ExactManifestOnlyMatcher(ManifestOnlyMatcher):
+    manifest_name: str
+
+    def _get_subproject_root(self, manifest_path: Path) -> Path:
+        return manifest_path.parent
+
+    def _is_manifest_match(self, path: Path) -> bool:
+        return path.name == self.manifest_name
+
+
+@dataclass(frozen=True)
+class PipRequirementsMatcher(SubprojectMatcher):
+    """
+    Matcher for requirements lockfiles specifically, which may be nested inside
+    a folder called "requirements" instead of being directly at the subproject
+    root.
+
+    Also supports multiple lockfiles in a single root directory.
+    """
+
+    # base file pattern is everything except for the last extension. Applies to both
+    # requirements and manifests
+    base_file_pattern: str  # without extension
+
+    requirements_file_extensions: List[str]
+    manifest_file_extension: str
+
+    default_manifest_file_base: str  # without extension
+
+    def _is_requirements_match(self, path: Path) -> bool:
+        for ext in self.requirements_file_extensions:
+            if fnmatch(str(path), f"{self.base_file_pattern}.{ext}"):
+                return True
+        return False
+
+    def _is_manifest_match(self, path: Path) -> bool:
+        return (
+            fnmatch(
+                str(path), f"{self.base_file_pattern}.{self.manifest_file_extension}"
+            )
+            or str(path)
+            == f"{self.default_manifest_file_base}.{self.manifest_file_extension}"
+        )
+
+    def is_match(self, path: Path) -> bool:
+        return self._is_manifest_match(path) or self._is_requirements_match(path)
+
+    def _lockfile_to_manifest(
+        self, requirements_path: Path, candidates: FrozenSet[Path]
+    ) -> Optional[Path]:
+        """
+        Finds the corresponding manifest for the given lockfile_path, if it exists.
+
+        To be recognized, manifests must either names must either:
+        - be exactly the default_manifest_file_name and be located in the root
+            directory of the subproject
+        - match the _stem_ of the requirements path and be located alongside the
+            requirements file
+        """
+        # First check for a manifest with the same stem as the lockfile
+        # e.g. requirements-dev.txt -> requirements-dev.in
+        requirements_stem = requirements_path.stem
+        manifest_name = f"{requirements_stem}.{self.manifest_file_extension}"
+        manifest_path = requirements_path.with_name(manifest_name)
+        if manifest_path in candidates:
+            return manifest_path
+
+        # If that didn't find anything, look in the subproject root directory
+        manifest_path = (
+            self._get_requirements_root(requirements_path)
+            / f"{self.default_manifest_file_base}.{self.manifest_file_extension}"
+        )
+        if manifest_path in candidates:
+            return manifest_path
+        else:
+            return None
+
+    def _get_requirements_root(self, requirements_path: Path) -> Path:
+        # We need to handle the case where the lockfile is in a subdirectory that we recognize.
+        # E.g. for requirements/base.txt, the subproject root directory should be the directory including the
+        # requirements directory. For requirements.txt, the subproject root should be the directory
+        # containing the lockfile.
+
+        # Check if the lockfile is in a 'requirements' directory
+        if "requirements" in requirements_path.parts:
+            # Find the index of 'requirements' in the path
+            req_index = requirements_path.parts.index("requirements")
+            # Return the parent of the 'requirements' directory
+            return Path(*requirements_path.parts[:req_index])
+        else:
+            # If not in a 'requirements' directory, return the immediate parent
+            return requirements_path.parent
+
+    def _filter_manifest_requirements(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[Set[Path], Set[Path]]:
+        """
+        Classifies the provided source files as requirements files, manifests, or neither.
+
+        Returns a tuple of (manifest_paths, requirements_paths)
+        """
+        requirements_files: Set[Path] = set()
+        manifests: Set[Path] = set()
+        for path in dep_source_files:
+            if self._is_requirements_match(path):
+                requirements_files.add(path)
+            if self._is_manifest_match(path):
+                manifests.add(path)
+        return (manifests, requirements_files)
+
+    def make_subprojects(
+        self, dep_source_files: FrozenSet[Path]
+    ) -> Tuple[List[Subproject], FrozenSet[Path]]:
+        # find all manifests and requirements files that we will use to build subprojects
+        manifests, requirements_files = self._filter_manifest_requirements(
+            dep_source_files
+        )
+
+        subprojects: List[Subproject] = []
+
+        # tracks manifests that were accounted for in the first (requirements-based) phase.
+        # These manifests should not be used in the second (manifest-only) phase.
+        paired_manifests: Set[Path] = set()
+
+        # Because multiple lockfiles can exist in a single subproject, we first need to group the
+        # requirements files by root directory
+        requirements_files_by_root_dir: Dict[Path, List[Path]] = {}
+        for requirements_path in requirements_files:
+            root_dir = self._get_requirements_root(requirements_path)
+            if root_dir not in requirements_files_by_root_dir:
+                requirements_files_by_root_dir[root_dir] = []
+            requirements_files_by_root_dir[root_dir].append(requirements_path)
+
+        # Create a subproject from each group of requirements files and the corresponding manifest
+        # for each, if it exists.
+        for (
+            root_dir,
+            local_requirements_paths,
+        ) in requirements_files_by_root_dir.items():
+            lockfile_sources: List[LockfileDependencySource] = []
+            for req_path in sorted(
+                local_requirements_paths
+            ):  # sorting so that there is a deterministic order in tests
+                matching_manifest_path = self._lockfile_to_manifest(
+                    req_path, dep_source_files
+                )
+                if matching_manifest_path is not None:
+                    paired_manifests.add(matching_manifest_path)
+                lockfile_sources.append(
+                    LockfileDependencySource(
+                        package_manager_type=PackageManagerType.PIP,
+                        lockfile_path=req_path,
+                        manifest_path=matching_manifest_path,
+                    )
+                )
+
+            # use the correct dependency source type depending on the number
+            # of lockfiles
+            dep_source: DependencySource
+            if len(lockfile_sources) == 1:
+                dep_source = lockfile_sources[0]
+            else:
+                dep_source = MultiLockfileDependencySource(tuple(lockfile_sources))
+
+            subprojects.append(Subproject(root_dir, dep_source))
+
+        # TODO: (bk) handle lone manifests.
+        # there could be lone manifests remaining (manifests - paired_manifests)
+        # and this code currently does not handle them. For lockfileless and for
+        # ecosystem reporting, we will need to create ManifestOnlyDependencySources
+        # from these.
+
+        return subprojects, frozenset(manifests | requirements_files)
+
+
+# NOTE: the order that these matchers are defined in matters. In find_subprojects, we
+# use each dependency source file for at most one matcher, running the matchers in the
+# order that they are defined here. This means that if a catch-all matcher were placed
+# first, the rest of the matchers would have no chance of matching any subprojects.
+MATCHERS: List[SubprojectMatcher] = [
+    PipRequirementsMatcher(
+        base_file_pattern="*requirement*",
+        requirements_file_extensions=["txt", "pip"],
+        manifest_file_extension="in",
+        default_manifest_file_base="requirements",
+    ),
+    # Npm
+    ExactLockfileManifestMatcher(
+        lockfile_name="package-lock.json",
+        manifest_name="package.json",
+        package_manager_type=PackageManagerType.NPM,
+    ),
+    ExactLockfileManifestMatcher(
+        lockfile_name="yarn.lock",
+        manifest_name="package.json",
+        package_manager_type=PackageManagerType.YARN,
+    ),
+    ExactLockfileManifestMatcher(
+        lockfile_name="pnpm-lock.yaml",
+        manifest_name="package.json",
+        package_manager_type=PackageManagerType.PNPM,
+    ),
+    # Gem
+    ExactLockfileManifestMatcher(
+        lockfile_name="Gemfile.lock",
+        manifest_name="Gemfile",
+        package_manager_type=PackageManagerType.RUBY_GEM,
+    ),
+    # Go modules
+    ExactLockfileManifestMatcher(
+        lockfile_name="go.mod",
+        manifest_name="go.mod",
+        package_manager_type=PackageManagerType.GO_MOD,
+    ),
+    # Cargo
+    ExactLockfileManifestMatcher(
+        lockfile_name="Cargo.lock",
+        manifest_name="Cargo.toml",
+        package_manager_type=PackageManagerType.CARGO,
+    ),
+    # Maven
+    ExactLockfileManifestMatcher(
+        lockfile_name="maven_dep_tree.txt",
+        manifest_name="pom.xml",
+        package_manager_type=PackageManagerType.MAVEN,
+    ),
+    ExactLockfileManifestMatcher(
+        lockfile_name="gradle.lockfile",
+        manifest_name="build.gradle",
+        package_manager_type=PackageManagerType.GRADLE,
+    ),
+    ExactManifestOnlyMatcher(
+        manifest_kind=out.ManifestKind(out.PomXml()),
+        manifest_name="pom.xml",
+    ),
+    ExactManifestOnlyMatcher(
+        manifest_kind=out.ManifestKind(out.BuildGradle()),
+        manifest_name="build.gradle",
+    ),
+    # Composer
+    ExactLockfileManifestMatcher(
+        lockfile_name="composer.lock",
+        manifest_name="composer.json",
+        package_manager_type=PackageManagerType.COMPOSER,
+    ),
+    # Nuget
+    ExactLockfileManifestMatcher(
+        lockfile_name="packages.lock.json",
+        manifest_name="nuget.manifest.json",
+        package_manager_type=PackageManagerType.NUGET,
+    ),
+    # Pub
+    ExactLockfileManifestMatcher(
+        lockfile_name="pubspec.lock",
+        manifest_name="pubspec.yaml",
+        package_manager_type=PackageManagerType.DART_PUB,
+    ),
+    # Swift PM
+    ExactLockfileManifestMatcher(
+        lockfile_name="Package.resolved",
+        manifest_name="Package.swift",
+        package_manager_type=PackageManagerType.SWIFT_PM,
+    ),
+    # Hex
+    ExactLockfileManifestMatcher(
+        lockfile_name="mix.lock",
+        manifest_name="mix.exs",
+        package_manager_type=PackageManagerType.ELIXIR_HEX,
+    ),
+    # Pipenv
+    ExactLockfileManifestMatcher(
+        lockfile_name="Pipfile.lock",
+        manifest_name="Pipfile",
+        package_manager_type=PackageManagerType.PIPENV,
+    ),
+    # Poetry
+    ExactLockfileManifestMatcher(
+        lockfile_name="poetry.lock",
+        manifest_name="pyproject.toml",
+        package_manager_type=PackageManagerType.POETRY,
+    ),
+]
+
+
+def filter_dependency_source_files(candidates: FrozenSet[Path]) -> FrozenSet[Path]:
+    """
+    Returns the paths in `candidates` that are dependency source files.
+    """
+    return frozenset(path for path in candidates if _is_dependency_source_file(path))
+
+
+def _is_dependency_source_file(path: Path) -> bool:
+    """
+    Check if a path is a valid dependency source file (lockfile, manifest, SBOM, etc)
+    """
+    return any(matcher.is_match(path) for matcher in MATCHERS)
diff --git a/cli/src/semgrep/__init__.py b/cli/src/semgrep/__init__.py
index 6ffa8b0c1866..b1014c858f0f 100644
--- a/cli/src/semgrep/__init__.py
+++ b/cli/src/semgrep/__init__.py
@@ -1,1 +1,1 @@
-__VERSION__ = "1.92.0"
+__VERSION__ = "1.93.0"
diff --git a/cli/src/semgrep/app/scans.py b/cli/src/semgrep/app/scans.py
index a45352f77890..141a0b18f19e 100644
--- a/cli/src/semgrep/app/scans.py
+++ b/cli/src/semgrep/app/scans.py
@@ -51,7 +51,16 @@ class ScanCompleteResult:
 
 
 class ScanHandler:
-    def __init__(self, dry_run: bool = False) -> None:
+    def __init__(
+        self, dry_run: bool = False, partial_output: Optional[Path] = None
+    ) -> None:
+        """
+        When dry_run is True, semgrep ci would get the config from the app,
+        run the scans, but would not upload the results.
+
+        When partial_output is not None, the scan results for semgrep ci
+        will also be saved to disk on the path that is specified.
+        """
         state = get_state()
         self.local_id = str(state.local_scan_id)
         self.scan_metadata = out.ScanMetadata(
@@ -64,6 +73,7 @@ def __init__(self, dry_run: bool = False) -> None:
         self.dry_run = dry_run
         self._scan_params: str = ""
         self.ci_scan_results: Optional[out.CiScanResults] = None
+        self.partial_output = partial_output
 
     @property
     def scan_id(self) -> Optional[int]:
@@ -272,6 +282,16 @@ def report_failure(self, exit_code: int) -> None:
         and return what exit code semgrep should exit with.
         """
         state = get_state()
+
+        if self.partial_output:
+            self.partial_output.write_text(
+                out.PartialScanResult(
+                    out.PartialScanError(
+                        out.CiScanFailure(exit_code=exit_code, stderr="")
+                    )
+                ).to_json_string()
+            )
+
         if self.dry_run:
             logger.info(f"Would have reported failure to semgrep.dev: {exit_code}")
             return
@@ -442,6 +462,13 @@ def report_findings(
             ),
         )
 
+        if self.partial_output:
+            self.partial_output.write_text(
+                out.PartialScanResult(
+                    out.PartialScanOk((self.ci_scan_results, complete)),
+                ).to_json_string()
+            )
+
         if self.dry_run:
             logger.info(
                 f"Would have sent findings and ignores blob: {json.dumps(findings_and_ignores, indent=4)}"
@@ -450,10 +477,10 @@ def report_findings(
                 f"Would have sent complete blob: {json.dumps(complete.to_json(), indent=4)}"
             )
             return ScanCompleteResult(True, False, "")
-        else:
-            # old: was also logging {json.dumps(findings_and_ignores, indent=4)}
-            # alt: save it in ~/.semgrep/logs/findings_and_ignores.json?
-            logger.debug(f"Sending findings and ignores blob")
+
+        # old: was also logging {json.dumps(findings_and_ignores, indent=4)}
+        # alt: save it in ~/.semgrep/logs/findings_and_ignores.json?
+        logger.debug(f"Sending findings and ignores blob")
 
         results_task = progress_bar.add_task("Uploading scan results")
         response = state.app_session.post(
diff --git a/cli/src/semgrep/commands/ci.py b/cli/src/semgrep/commands/ci.py
index 445f51bd0bbd..f47f2741d8e0 100644
--- a/cli/src/semgrep/commands/ci.py
+++ b/cli/src/semgrep/commands/ci.py
@@ -187,6 +187,18 @@ def fix_head_if_github_action(metadata: GitMeta) -> None:
     default=0,
     hidden=True,
 )
+@click.option(
+    "--x-partial-config",
+    "partial_config",
+    type=click.Path(allow_dash=True, path_type=Path),
+    hidden=True,
+)
+@click.option(
+    "--x-partial-output",
+    "partial_output",
+    type=click.Path(allow_dash=True, path_type=Path),
+    hidden=True,
+)
 @handle_command_errors
 def ci(
     ctx: click.Context,
@@ -248,7 +260,10 @@ def ci(
     verbose: bool,
     path_sensitive: bool,
     enable_experimental_requirements: bool,
+    allow_dynamic_dependency_resolution: bool,
     dump_n_rule_partitions: Optional[int],
+    partial_config: Optional[Path],
+    partial_output: Optional[Path],
 ) -> None:
     state = get_state()
 
@@ -283,6 +298,20 @@ def ci(
                 "WARNING: `semgrep ci` is meant to be run from the root of a git repo.\nWhen `semgrep ci` is not run from a git repo, it will not be able to perform all operations.\nWhen `semgrep ci` is run from a git repo, but not the root, links in the uploaded findings may be broken.\n\nTo run `semgrep ci` on only a subdirectory of a git repo, see `--subdir`."
             )
 
+        if config and partial_config:
+            logger.info(
+                "The `--config` and `--x-partial-config` flags are mutually exclusive. They serve different purposes."
+            )
+            sys.exit(FATAL_EXIT_CODE)
+
+        if (partial_config and not partial_output) or (
+            not partial_config and partial_output
+        ):
+            logger.info(
+                "Both or none of --x-partial-config and --x-partial-output must be specified."
+            )
+            sys.exit(FATAL_EXIT_CODE)
+
         token = state.app_session.token
         if not token and not config:
             # Not logged in and no explicit config
@@ -304,7 +333,11 @@ def ci(
             # scanning or uploading results to the app.
             if dump_n_rule_partitions:
                 dry_run = True
-            scan_handler = ScanHandler(dry_run=dry_run)
+            # Partial scans also implies a dry run. We'll be saving results
+            # to disk, but not upload them yet.
+            if partial_output:
+                dry_run = True
+            scan_handler = ScanHandler(dry_run=dry_run, partial_output=partial_output)
         else:  # impossible state… until we break the code above
             raise RuntimeError("The token and/or config are misconfigured")
 
@@ -402,7 +435,13 @@ def ci(
                     )
                     sys.exit(MISSING_CONFIG_EXIT_CODE)
 
-                config = (scan_handler.rules,)
+                # Partial config overrides the config we get from the app,
+                # but we still need to communicate with the app to get other
+                # configs such as products, deployment ID, etc.
+                if partial_config:
+                    config = (str(partial_config),)
+                else:
+                    config = (scan_handler.rules,)
 
         except Exception as e:
             import traceback
@@ -558,6 +597,7 @@ def ci(
             "diff_depth": diff_depth,
             "capture_core_stderr": capture_core_stderr,
             "enable_experimental_requirements": enable_experimental_requirements,
+            "allow_dynamic_dependency_resolution": allow_dynamic_dependency_resolution,
             "dump_n_rule_partitions": dump_n_rule_partitions,
         }
 
diff --git a/cli/src/semgrep/commands/scan.py b/cli/src/semgrep/commands/scan.py
index 9590fa7b5dba..7ca564da8a74 100644
--- a/cli/src/semgrep/commands/scan.py
+++ b/cli/src/semgrep/commands/scan.py
@@ -382,6 +382,12 @@ def convert(
         is_flag=True,
         default=False,
     ),
+    optgroup.option(
+        "--allow-dynamic-dependency-resolution",
+        "allow_dynamic_dependency_resolution",
+        is_flag=True,
+        default=False,
+    ),
 ]
 
 
@@ -573,6 +579,7 @@ def scan(
     x_ls: bool,
     path_sensitive: bool,
     enable_experimental_requirements: bool,
+    allow_dynamic_dependency_resolution: bool,
 ) -> Optional[Tuple[RuleMatchMap, List[SemgrepError], List[Rule], Set[Path]]]:
     if version:
         print(__VERSION__)
@@ -848,6 +855,7 @@ def scan(
                         path_sensitive=path_sensitive,
                         capture_core_stderr=capture_core_stderr,
                         enable_experimental_requirements=enable_experimental_requirements,
+                        allow_dynamic_dependency_resolution=allow_dynamic_dependency_resolution,
                     )
                 except SemgrepError as e:
                     output_handler.handle_semgrep_errors([e])
diff --git a/cli/src/semgrep/core_runner.py b/cli/src/semgrep/core_runner.py
index c9c787dbc6c5..59956782d61b 100644
--- a/cli/src/semgrep/core_runner.py
+++ b/cli/src/semgrep/core_runner.py
@@ -2,7 +2,7 @@
 import collections
 import contextlib
 import json
-import resource
+import platform
 import sys
 import tempfile
 from datetime import datetime
@@ -50,7 +50,7 @@
 from semgrep.semgrep_types import Language
 from semgrep.state import DesignTreatment
 from semgrep.state import get_state
-from semgrep.subproject import Subproject
+from semgrep.subproject import ResolvedSubproject
 from semgrep.target_manager import TargetManager
 from semgrep.target_mode import TargetModeConfig
 from semgrep.verbose_logging import getLogger
@@ -71,6 +71,10 @@
 # test/e2e/test_performance.py is one test that exercises this risk.
 LARGE_READ_SIZE: int = 1024 * 1024 * 512
 
+IS_WINDOWS = platform.system() == "Windows"
+if not IS_WINDOWS:
+    import resource
+
 
 def setrlimits_preexec_fn() -> None:
     """
@@ -88,6 +92,9 @@ def setrlimits_preexec_fn() -> None:
     # which have their own output requirements so that CLI can parse its stdout,
     # we use a different logger than the usual "semgrep" one
     core_logger = getLogger("semgrep_core")
+    if IS_WINDOWS:
+        core_logger.info("Skipping setting stack limits on Windows")
+        return
 
     # Get current soft and hard stack limits
     old_soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_STACK)
@@ -401,20 +408,51 @@ async def _stream_exec_subprocess(self) -> int:
         # Set parent span id as close to fork as possible to ensure core
         # spans nest under the correct pysemgrep parent span.
         get_state().traces.inject()
-        process = await asyncio.create_subprocess_exec(
-            *self._cmd,
-            stdout=asyncio.subprocess.PIPE,
-            stderr=stderr_arg,
-            limit=INPUT_BUFFER_LIMIT,
-            preexec_fn=setrlimits_preexec_fn,
-        )
+        if IS_WINDOWS:
+            process = await asyncio.create_subprocess_exec(
+                *self._cmd,
+                stdout=asyncio.subprocess.PIPE,
+                stderr=stderr_arg,
+                limit=INPUT_BUFFER_LIMIT,
+                # preexec_fn is not supported on Windows
+            )
+        else:
+            process = await asyncio.create_subprocess_exec(
+                *self._cmd,
+                stdout=asyncio.subprocess.PIPE,
+                stderr=stderr_arg,
+                limit=INPUT_BUFFER_LIMIT,
+                preexec_fn=setrlimits_preexec_fn,
+            )
 
         # Ensured by passing stdout/err named parameters above.
         assert process.stdout
         if self._capture_stderr:
             assert process.stderr
 
-        await self._handle_process_outputs(process.stdout, process.stderr)
+        try:
+            await self._handle_process_outputs(process.stdout, process.stderr)
+        # Usually happens when the process is killed by the OS
+        except SemgrepError as e:
+            # Since this is error handling code, it's extra important to be
+            # defensive. As such, let's not have this be a straight wait call
+            # which will wait indefinitely for the subprocess to complete. let's
+            # instead just wait for a second so we don't risk getting stuck.
+            # This is fine since this is expected to only happen when the
+            # semgrep-core process was killed
+            try:
+                exit_code = await asyncio.wait_for(process.wait(), timeout=1.0)
+            except TimeoutError:
+                logger.error(
+                    "semgrep timed out waiting for the semgrep-core process to exit after an exception was raised"
+                )
+                raise e
+
+            # let's log this and reraise as if we got a non zero exit code with
+            # a semgrep error then we segfaulted or OOMd and so should
+            # immediately exit instead of assuming we got something usuable
+            logger.error(f"semgrep-core exited with {exit_code}!")
+            raise e
 
         # Return exit code of cmd. process should already be done
         return await process.wait()
@@ -573,7 +611,11 @@ def _parse_core_output(
             if returncode == -11 or returncode == -9:
                 # Killed by signal 11 (segmentation fault), this could be a
                 # stack overflow that was not intercepted by the OCaml runtime.
-                soft_limit, _hard_limit = resource.getrlimit(resource.RLIMIT_STACK)
+                soft_limit, _hard_limit = (
+                    (-1, -1)
+                    if IS_WINDOWS
+                    else resource.getrlimit(resource.RLIMIT_STACK)
+                )
                 tip = f"""
                 Semgrep exceeded system resources. This may be caused by
                     1. Stack overflow. Try increasing the stack limit to
@@ -631,7 +673,7 @@ def _fail(
     def plan_core_run(
         rules: List[Rule],
         target_manager: TargetManager,
-        sca_subprojects: Dict[out.Ecosystem, List[Subproject]],
+        sca_subprojects: Dict[out.Ecosystem, List[ResolvedSubproject]],
         *,
         all_targets: Optional[Set[Path]] = None,
         product: Optional[out.Product] = None,
@@ -704,7 +746,7 @@ def _run_rules_direct_to_semgrep_core_helper(
         run_secrets: bool,
         disable_secrets_validation: bool,
         target_mode_config: TargetModeConfig,
-        sca_subprojects: Dict[out.Ecosystem, List[Subproject]],
+        sca_subprojects: Dict[out.Ecosystem, List[ResolvedSubproject]],
     ) -> Tuple[RuleMatchMap, List[SemgrepError], OutputExtra,]:
         state = get_state()
         logger.debug(f"Passing whole rules directly to semgrep_core")
@@ -728,7 +770,9 @@ def _run_rules_direct_to_semgrep_core_helper(
         rule_file = exit_stack.enter_context(
             (state.env.user_data_folder / "semgrep_rules.json").open("w+")
             if dump_command_for_core
-            else tempfile.NamedTemporaryFile("w+", suffix=".json")
+            else tempfile.NamedTemporaryFile(
+                "w+", suffix=".json", delete=(not IS_WINDOWS)
+            )
         )
         # A historical scan does not create a targeting file since targeting is
         # performed directly by core.
@@ -736,13 +780,13 @@ def _run_rules_direct_to_semgrep_core_helper(
             target_file = exit_stack.enter_context(
                 (state.env.user_data_folder / "semgrep_targets.txt").open("w+")
                 if dump_command_for_core
-                else tempfile.NamedTemporaryFile("w+")
+                else tempfile.NamedTemporaryFile("w+", delete=(not IS_WINDOWS))
             )
         if target_mode_config.is_pro_diff_scan:
             diff_target_file = exit_stack.enter_context(
                 (state.env.user_data_folder / "semgrep_diff_targets.txt").open("w+")
                 if dump_command_for_core
-                else tempfile.NamedTemporaryFile("w+")
+                else tempfile.NamedTemporaryFile("w+", delete=(not IS_WINDOWS))
             )
 
         with exit_stack:
@@ -1010,7 +1054,7 @@ def _run_rules_direct_to_semgrep_core(
         run_secrets: bool,
         disable_secrets_validation: bool,
         target_mode_config: TargetModeConfig,
-        sca_subprojects: Dict[out.Ecosystem, List[Subproject]],
+        sca_subprojects: Dict[out.Ecosystem, List[ResolvedSubproject]],
     ) -> Tuple[RuleMatchMap, List[SemgrepError], OutputExtra,]:
         """
         Sometimes we may run into synchronicity issues with the latest DeepSemgrep binary.
@@ -1071,7 +1115,7 @@ def invoke_semgrep_core(
         run_secrets: bool,
         disable_secrets_validation: bool,
         target_mode_config: TargetModeConfig,
-        sca_subprojects: Dict[out.Ecosystem, List[Subproject]],
+        sca_subprojects: Dict[out.Ecosystem, List[ResolvedSubproject]],
     ) -> Tuple[RuleMatchMap, List[SemgrepError], OutputExtra,]:
         """
         Takes in rules and targets and returns object with findings
@@ -1124,7 +1168,9 @@ def validate_configs(self, configs: Tuple[str, ...]) -> Sequence[SemgrepError]:
 
         parsed_errors = []
 
-        with tempfile.NamedTemporaryFile("w", suffix=".yaml") as rule_file:
+        with tempfile.NamedTemporaryFile(
+            "w", suffix=".yaml", delete=(not IS_WINDOWS)
+        ) as rule_file:
             yaml = YAML()
             yaml.dump(
                 {"rules": [metacheck._raw for metacheck in metachecks]}, rule_file
diff --git a/cli/src/semgrep/core_targets_plan.py b/cli/src/semgrep/core_targets_plan.py
index 07d47ca4d068..d0ca6627dd6d 100644
--- a/cli/src/semgrep/core_targets_plan.py
+++ b/cli/src/semgrep/core_targets_plan.py
@@ -27,7 +27,7 @@
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_types import Language
 from semgrep.state import get_state
-from semgrep.subproject import Subproject
+from semgrep.subproject import ResolvedSubproject
 from semgrep.verbose_logging import getLogger
 
 logger = getLogger(__name__)
@@ -108,7 +108,7 @@ def __init__(
         rules: List[Rule],
         *,
         product: Optional[out.Product] = None,
-        sca_subprojects: Optional[Dict[out.Ecosystem, List[Subproject]]] = None,
+        sca_subprojects: Optional[Dict[out.Ecosystem, List[ResolvedSubproject]]] = None,
         unused_rules: Optional[List[Rule]] = None,
     ):
         self.target_mappings = TargetMappings(mappings)
@@ -243,7 +243,7 @@ def table_by_ecosystem(self) -> Table:
                 lockfile_paths = ", ".join(
                     str(lockfile_path)
                     for proj in self.sca_subprojects.get(ecosystem, [])
-                    for lockfile_path in proj.get_lockfile_paths()
+                    for lockfile_path in proj.dependency_source.get_display_paths()
                 )
             else:
                 lockfile_paths = "N/A"
diff --git a/cli/src/semgrep/dependency_aware_rule.py b/cli/src/semgrep/dependency_aware_rule.py
index e511f8f71551..34f44ed363bd 100644
--- a/cli/src/semgrep/dependency_aware_rule.py
+++ b/cli/src/semgrep/dependency_aware_rule.py
@@ -26,7 +26,7 @@
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitive
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitivity
 from semgrep.subproject import find_closest_subproject
-from semgrep.subproject import Subproject
+from semgrep.subproject import ResolvedSubproject
 from semgrep.verbose_logging import getLogger
 
 logger = getLogger(__name__)
@@ -68,7 +68,7 @@ def parse_depends_on_yaml(entries: List[Dict[str, str]]) -> Iterator[DependencyP
 def generate_unreachable_sca_findings(
     rule: Rule,
     already_reachable: Callable[[Path, FoundDependency], bool],
-    resolved_deps: Dict[Ecosystem, List[Subproject]],
+    resolved_deps: Dict[Ecosystem, List[ResolvedSubproject]],
 ) -> Tuple[List[RuleMatch], List[SemgrepError]]:
     """
     Returns matches to a only a rule's sca-depends-on patterns; ignoring any reachabiliy patterns it has
@@ -82,8 +82,8 @@ def generate_unreachable_sca_findings(
     non_reachable_matches = []
     match_based_keys: Dict[tuple[str, Path, str], int] = defaultdict(int)
     for ecosystem in ecosystems:
-        for sca_project in resolved_deps.get(ecosystem, []):
-            deps = sca_project.found_dependencies
+        for subproject in resolved_deps.get(ecosystem, []):
+            deps = list(subproject.found_dependencies.iter_found_dependencies())
 
             dependency_matches = list(
                 dependencies_range_match_any(depends_on_entries, list(deps))
@@ -162,7 +162,7 @@ def transitive_dep_is_also_direct(
 def generate_reachable_sca_findings(
     matches: List[RuleMatch],
     rule: Rule,
-    resolved_deps: Dict[Ecosystem, List[Subproject]],
+    resolved_deps: Dict[Ecosystem, List[ResolvedSubproject]],
 ) -> Tuple[
     List[RuleMatch], List[SemgrepError], Callable[[Path, FoundDependency], bool]
 ]:
@@ -178,14 +178,14 @@ def generate_reachable_sca_findings(
     for ecosystem in ecosystems:
         for match in matches:
             try:
-                sca_project = find_closest_subproject(
+                subproject = find_closest_subproject(
                     match.path, ecosystem, resolved_deps.get(ecosystem, [])
                 )
 
-                if sca_project is None:
+                if subproject is None:
                     continue
 
-                deps = sca_project.found_dependencies if sca_project is not None else []
+                deps = list(subproject.found_dependencies.iter_found_dependencies())
                 frozen_deps = tuple((dep.package, dep.transitivity) for dep in deps)
 
                 dependency_matches = list(
diff --git a/cli/src/semgrep/formatter/text.py b/cli/src/semgrep/formatter/text.py
index 52e9e5df6944..a995cf0e56df 100644
--- a/cli/src/semgrep/formatter/text.py
+++ b/cli/src/semgrep/formatter/text.py
@@ -841,10 +841,14 @@ def format(
                 (out.Product(out.Secrets()), "generic-secrets"): [],
             }
 
+            secrets_blocking_rules = set()
+
             for match in rule_matches:
                 if isinstance(match.product.value, out.SAST):
                     subgroup = "blocking" if match.is_blocking else "nonblocking"
                 elif isinstance(match.product.value, out.Secrets):
+                    if match.is_blocking:
+                        secrets_blocking_rules.add(match.match.check_id.value)
                     if match.metadata.get("generic_secrets", False):
                         subgroup = "generic-secrets"
                     else:
@@ -865,14 +869,14 @@ def format(
 
                 grouped_matches[match.product, subgroup].append(match)
 
-            first_party_blocking_rules = {
+            code_blocking_rules = {
                 match.match.check_id.value
                 for match in grouped_matches[out.Product(out.SAST()), "blocking"]
             }
 
             # When ephemeral rules are run with the -e or --pattern flag in the command-line, the rule_id is set to -.
             # The rule is ran in the command-line and has no associated rule_id
-            first_party_blocking_rules.discard("-")
+            code_blocking_rules.discard("-")
 
             if not is_ci_invocation:
                 grouped_matches[(out.Product(out.SAST()), "merged")] = [
@@ -911,9 +915,15 @@ def format(
                         extra["dataflow_traces"],
                     )
 
-            if first_party_blocking_rules and is_ci_invocation:
+            if code_blocking_rules and is_ci_invocation:
                 console.print(Title("Blocking Code Rules Fired:", order=2))
-                for rule_id in sorted(first_party_blocking_rules):
+                for rule_id in sorted(code_blocking_rules):
+                    console.print(f"  {rule_id}")
+                console.reset_title(order=1)
+
+            if secrets_blocking_rules and is_ci_invocation:
+                console.print(Title("Blocking Secrets Rules Fired:", order=2))
+                for rule_id in sorted(secrets_blocking_rules):
                     console.print(f"  {rule_id}")
                 console.reset_title(order=1)
 
diff --git a/cli/src/semgrep/join_rule.py b/cli/src/semgrep/join_rule.py
index e0d7d4297c72..085e7b734af7 100644
--- a/cli/src/semgrep/join_rule.py
+++ b/cli/src/semgrep/join_rule.py
@@ -401,7 +401,7 @@ def json_to_rule_match(join_rule: Dict[str, Any], match: Dict[str, Any]) -> Rule
 def run_join_rule(
     join_rule: Dict[str, Any],
     targets: List[Path],
-    enable_experimental_requirements: bool = False,
+    allow_dynamic_dependency_resolution: bool = False,
 ) -> Tuple[List[RuleMatch], List[SemgrepError]]:
     """
     Run a 'join' mode rule.
@@ -492,7 +492,7 @@ def run_join_rule(
             targets=targets,
             no_rewrite_rule_ids=True,
             optimizations="all",
-            enable_experimental_requirements=enable_experimental_requirements,
+            allow_dynamic_dependency_resolution=allow_dynamic_dependency_resolution,
         )
 
     assert isinstance(output, dict)  # placate mypy
diff --git a/cli/src/semgrep/resolve_subprojects.py b/cli/src/semgrep/resolve_subprojects.py
new file mode 100644
index 000000000000..a7ecf59d9cde
--- /dev/null
+++ b/cli/src/semgrep/resolve_subprojects.py
@@ -0,0 +1,224 @@
+from pathlib import Path
+from typing import Dict
+from typing import FrozenSet
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+
+import semgrep.semgrep_interfaces.semgrep_output_v1 as out
+from semdep.parsers.cargo import parse_cargo
+from semdep.parsers.composer import parse_composer_lock
+from semdep.parsers.gem import parse_gemfile
+from semdep.parsers.go_mod import parse_go_mod
+from semdep.parsers.gradle import parse_gradle
+from semdep.parsers.mix import parse_mix
+from semdep.parsers.package_lock import parse_package_lock
+from semdep.parsers.packages_lock_c_sharp import (
+    parse_packages_lock as parse_packages_lock_c_sharp,
+)
+from semdep.parsers.pipfile import parse_pipfile
+from semdep.parsers.pnpm import parse_pnpm
+from semdep.parsers.poetry import parse_poetry
+from semdep.parsers.pom_tree import parse_pom_tree
+from semdep.parsers.pubspec_lock import parse_pubspec_lock
+from semdep.parsers.requirements import parse_requirements
+from semdep.parsers.swiftpm import parse_package_resolved
+from semdep.parsers.util import DependencyParserError
+from semdep.parsers.util import SemgrepParser
+from semdep.parsers.util import to_parser
+from semdep.parsers.yarn import parse_yarn
+from semdep.subproject_matchers import MATCHERS
+from semdep.subproject_matchers import SubprojectMatcher
+from semgrep.rpc_call import resolve_dependencies
+from semgrep.semgrep_interfaces.semgrep_output_v1 import CargoParser
+from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyParserError
+from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
+from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
+from semgrep.semgrep_interfaces.semgrep_output_v1 import ScaParserName
+from semgrep.subproject import DependencySource
+from semgrep.subproject import LockfileDependencySource
+from semgrep.subproject import ManifestOnlyDependencySource
+from semgrep.subproject import MultiLockfileDependencySource
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import ResolutionMethod
+from semgrep.subproject import ResolvedSubproject
+from semgrep.subproject import Subproject
+from semgrep.target_manager import TargetManager
+from semgrep.verbose_logging import getLogger
+
+logger = getLogger(__name__)
+
+# argument order is lockfile path, manifest path
+PARSERS_BY_PACKAGE_MANAGER_TYPE: Dict[PackageManagerType, SemgrepParser] = {
+    PackageManagerType.PIPENV: parse_pipfile,
+    PackageManagerType.PIP: parse_requirements,
+    PackageManagerType.POETRY: parse_poetry,
+    PackageManagerType.NPM: parse_package_lock,
+    PackageManagerType.YARN: parse_yarn,
+    PackageManagerType.PNPM: parse_pnpm,
+    PackageManagerType.RUBY_GEM: parse_gemfile,
+    PackageManagerType.COMPOSER: parse_composer_lock,
+    PackageManagerType.GO_MOD: parse_go_mod,
+    PackageManagerType.CARGO: to_parser(parse_cargo, ScaParserName(CargoParser())),
+    PackageManagerType.MAVEN: parse_pom_tree,
+    PackageManagerType.GRADLE: parse_gradle,
+    PackageManagerType.NUGET: parse_packages_lock_c_sharp,
+    PackageManagerType.DART_PUB: parse_pubspec_lock,
+    PackageManagerType.SWIFT_PM: parse_package_resolved,
+    PackageManagerType.ELIXIR_HEX: parse_mix,
+}
+
+ECOSYSTEM_BY_PACKAGE_MANAGER_TYPE: Dict[PackageManagerType, Ecosystem] = {
+    PackageManagerType.PIPENV: Ecosystem(out.Pypi()),
+    PackageManagerType.PIP: Ecosystem(out.Pypi()),
+    PackageManagerType.POETRY: Ecosystem(out.Pypi()),
+    PackageManagerType.NPM: Ecosystem(out.Npm()),
+    PackageManagerType.YARN: Ecosystem(out.Npm()),
+    PackageManagerType.PNPM: Ecosystem(out.Npm()),
+    PackageManagerType.RUBY_GEM: Ecosystem(out.Gem()),
+    PackageManagerType.COMPOSER: Ecosystem(out.Composer()),
+    PackageManagerType.GO_MOD: Ecosystem(out.Gomod()),
+    PackageManagerType.CARGO: Ecosystem(out.Cargo()),
+    PackageManagerType.MAVEN: Ecosystem(out.Maven()),
+    PackageManagerType.GRADLE: Ecosystem(out.Maven()),
+    PackageManagerType.NUGET: Ecosystem(out.Nuget()),
+    PackageManagerType.DART_PUB: Ecosystem(out.Pub()),
+    PackageManagerType.SWIFT_PM: Ecosystem(out.SwiftPM()),
+    PackageManagerType.ELIXIR_HEX: Ecosystem(out.Mix()),
+}
+
+
+def _resolve_dependency_source(
+    dep_source: DependencySource, enable_dynamic_resolution: bool = True
+) -> Tuple[
+    Optional[Ecosystem], List[FoundDependency], List[DependencyParserError], List[Path]
+]:
+    """
+    Resolve the dependencies in the dependency source. Returns:
+    - The ecosystem the resulting dependencies belong to. If there are no dependencies, this value is None
+    - The list of FoundDependency objects that were resolved
+    - The list of dependency parser errors encountered
+    - The list of paths that should be considered dependency targets
+    """
+    ecosystem: Optional[Ecosystem] = None
+    if isinstance(dep_source, LockfileDependencySource):
+        parser = PARSERS_BY_PACKAGE_MANAGER_TYPE[dep_source.package_manager_type]
+        ecosystem = ECOSYSTEM_BY_PACKAGE_MANAGER_TYPE[dep_source.package_manager_type]
+        resolved_deps, parse_errors = parser(
+            dep_source.lockfile_path, dep_source.manifest_path
+        )
+        return ecosystem, resolved_deps, parse_errors, [dep_source.lockfile_path]
+    elif isinstance(dep_source, MultiLockfileDependencySource):
+        all_resolved_deps: List[FoundDependency] = []
+        all_parse_errors: List[DependencyParserError] = []
+        all_dep_targets: List[Path] = []
+
+        for lockfile_source in dep_source.sources:
+            ecosystem, new_deps, new_errors, new_targets = _resolve_dependency_source(
+                lockfile_source
+            )
+            all_resolved_deps.extend(new_deps)
+            all_parse_errors.extend(new_errors)
+            all_dep_targets.extend(new_targets)
+
+        return ecosystem, all_resolved_deps, all_parse_errors, all_dep_targets
+    elif (
+        isinstance(dep_source, ManifestOnlyDependencySource)
+        and enable_dynamic_resolution
+    ):
+        manifest_arg = out.Manifest(
+            kind=dep_source.manifest_kind, path=out.Fpath(str(dep_source.manifest_path))
+        )
+        response = resolve_dependencies([manifest_arg])
+        if response is None:
+            # we failed to resolve somehow
+            # TODO: handle this and generate an error
+            return (None, [], [], [])
+        if len(response) > 1:
+            logger.warning(
+                f"Too many responses from dynamic dependency resolution RPC. Expected 1, got {len(response)}"
+            )
+        result = response[0][1]
+        if isinstance(result.value, out.ResolutionOk):
+            resolved_deps = result.value.value
+            # right now we only support lockfileless for the maven ecosystem, so hardcode that here
+            # TODO: move this ecosystem identification into the ocaml code when we redo the interface there
+            ecosystem = Ecosystem(out.Maven())
+            return ecosystem, resolved_deps, [], [dep_source.manifest_path]
+        else:
+            # some error occured in resolution
+            # TODO: error handling / bubbling up
+            return (None, [], [], [])
+
+    else:
+        # dependency source type is not supported, do nothing
+        return (None, [], [], [])
+
+
+def find_subprojects(
+    dependency_source_files: FrozenSet[Path], matchers: List[SubprojectMatcher]
+) -> List[Subproject]:
+    """
+    Using the given dependency source files and the given list of matchers, return all the subprojects that could be
+    created. Note that each dependency source file will be used by at most one matcher, and matching will be attempted
+    in the order that the matchers are provided.
+    """
+    unresolved_subprojects: List[Subproject] = []
+    used_files: Set[Path] = set()
+    for matcher in matchers:
+        # for each matcher, pass only those files that have not yet been used by another matcher.
+        new_subprojects, new_used_files = matcher.make_subprojects(
+            dependency_source_files - used_files
+        )
+        used_files |= new_used_files
+        unresolved_subprojects.extend(new_subprojects)
+    return unresolved_subprojects
+
+
+def resolve_subprojects(
+    target_manager: TargetManager,
+    allow_dynamic_resolution: bool = False,
+) -> Tuple[
+    Dict[Ecosystem, List[ResolvedSubproject]], List[DependencyParserError], List[Path]
+]:
+    """
+    Identify lockfiles and manifest files to resolve dependency information from the environment
+
+    If `allow_dynamic_resolution` is True, this function may cause projects that are scanned to be built. This may involve:
+    - Downloading packages from the internet
+    - Executing code that is included in the scanned project or in downloaded packages
+
+    When `allow_dynamic_resolution` is False, dependencies are resolved only by parsing existing files (lockfiles and manifests).
+    """
+    dependency_parser_errors: List[DependencyParserError] = []
+
+    # first, find all the subprojects
+    dependency_source_files = target_manager.get_all_dependency_source_files(
+        ignore_baseline_handler=True
+    )
+    unresolved_subprojects = find_subprojects(dependency_source_files, MATCHERS)
+
+    # targets that were considered in generating the dependency tree
+    dependency_targets: List[Path] = []
+
+    resolved: Dict[Ecosystem, List[ResolvedSubproject]] = {}
+    # Dispatch each subproject to a resolver for resolution
+    for to_resolve in unresolved_subprojects:
+        ecosystem, deps, errors, targets = _resolve_dependency_source(
+            to_resolve.dependency_source, allow_dynamic_resolution
+        )
+        dependency_parser_errors.extend(errors)
+        dependency_targets.extend(targets)
+
+        if ecosystem is not None:
+            # ecosystem is only None when dependency resolution failed in some way
+            resolved_subproject = ResolvedSubproject.from_unresolved(
+                to_resolve, ResolutionMethod.LOCKFILE_PARSING, deps, ecosystem
+            )
+
+            if ecosystem not in resolved:
+                resolved[ecosystem] = []
+            resolved[ecosystem].append(resolved_subproject)
+
+    return resolved, dependency_parser_errors, dependency_targets
diff --git a/cli/src/semgrep/rpc_call.py b/cli/src/semgrep/rpc_call.py
index 527ca4d3dd83..ae9caff87082 100644
--- a/cli/src/semgrep/rpc_call.py
+++ b/cli/src/semgrep/rpc_call.py
@@ -68,7 +68,8 @@ def resolve_dependencies(
     )
     if ret is None:
         logger.warning("failed to resolve dependencies")
-    return None
+        return None
+    return ret.value
 
 
 def dump_rule_partitions(args: out.DumpRulePartitionsParams) -> bool:
diff --git a/cli/src/semgrep/run_scan.py b/cli/src/semgrep/run_scan.py
index 3502c56a7715..e73dda9b7fdc 100644
--- a/cli/src/semgrep/run_scan.py
+++ b/cli/src/semgrep/run_scan.py
@@ -37,7 +37,6 @@
 
 import semgrep.scan_report as scan_report
 import semgrep.semgrep_interfaces.semgrep_output_v1 as out
-from semdep.lockfile import EcosystemLockfiles
 from semdep.parsers.util import DependencyParserError
 from semgrep import __VERSION__
 from semgrep import tracing
@@ -51,6 +50,8 @@
 from semgrep.constants import TOO_MUCH_DATA
 from semgrep.core_runner import CoreRunner
 from semgrep.core_runner import Plan
+from semgrep.dependency_aware_rule import dependencies_range_match_any
+from semgrep.dependency_aware_rule import parse_depends_on_yaml
 from semgrep.engine import EngineType
 from semgrep.error import FilesNotFoundError
 from semgrep.error import MISSING_CONFIG_EXIT_CODE
@@ -68,6 +69,7 @@
 from semgrep.output import OutputSettings
 from semgrep.output_extra import OutputExtra
 from semgrep.profile_manager import ProfileManager
+from semgrep.resolve_subprojects import resolve_subprojects
 from semgrep.rpc_call import dump_rule_partitions
 from semgrep.rule import Rule
 from semgrep.rule_match import RuleMatches
@@ -83,8 +85,7 @@
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Product
 from semgrep.semgrep_types import JOIN_MODE
 from semgrep.state import get_state
-from semgrep.subproject import resolve_subprojects
-from semgrep.subproject import Subproject
+from semgrep.subproject import ResolvedSubproject
 from semgrep.target_manager import FileTargetingLog
 from semgrep.target_manager import SAST_PRODUCT
 from semgrep.target_manager import SCA_PRODUCT
@@ -94,6 +95,7 @@
 from semgrep.util import unit_str
 from semgrep.verbose_logging import getLogger
 
+
 logger = getLogger(__name__)
 
 ##############################################################################
@@ -183,6 +185,47 @@ def remove_matches_in_baseline(
     return kept_matches_by_rule
 
 
+def filter_dependency_aware_rules(
+    dependency_aware_rules: List[Rule],
+    resolved_deps: Dict[Ecosystem, List[ResolvedSubproject]],
+) -> List[Rule]:
+    """Returns the list of filtered rules that have matching dependencies in the project"""
+    rules_to_check = [r for r in dependency_aware_rules if r.should_run_on_semgrep_core]
+    # List to store filtered rules
+    filtered_rules = []
+
+    # Loop through each rule to check for matching dependencies
+    for rule in rules_to_check:
+        depends_on_keys = rule.project_depends_on
+        depends_on_entries = list(parse_depends_on_yaml(depends_on_keys))
+        ecosystems = list(rule.ecosystems)
+
+        # Flag to track if we found a matching dependency
+        has_matching_dependency = False
+        # Check for each ecosystem in the rule
+        for ecosystem in ecosystems:
+            for sca_project in resolved_deps.get(ecosystem, []):
+                deps = list(sca_project.found_dependencies.iter_found_dependencies())
+                # Match the dependencies based on version ranges
+                dependency_matches = list(
+                    dependencies_range_match_any(depends_on_entries, deps)
+                )
+
+                # If any dependency matches, we flag it as a match
+                if dependency_matches:
+                    has_matching_dependency = True
+                    break
+
+            if has_matching_dependency:
+                break
+
+        # If we found a matching dependency, include this rule
+        if has_matching_dependency:
+            filtered_rules.append(rule)
+
+    return filtered_rules
+
+
 # This runs semgrep-core (and also handles SCA and join rules)
 @tracing.trace()
 def run_rules(
@@ -202,7 +245,7 @@ def run_rules(
     *,
     with_code_rules: bool = True,
     with_supply_chain: bool = False,
-    enable_experimental_requirements: bool = False,
+    allow_dynamic_dependency_resolution: bool = False,
 ) -> Tuple[
     RuleMatchMap,
     List[SemgrepError],
@@ -217,35 +260,36 @@ def run_rules(
     join_rules, rest_of_the_rules = partition(
         filtered_rules, lambda rule: rule.mode == JOIN_MODE
     )
+
+    # Get rules that rely on dependencies from the project's lockfile
     dependency_aware_rules = [r for r in rest_of_the_rules if r.project_depends_on]
-    dependency_only_rules, rest_of_the_rules = partition(
-        rest_of_the_rules, lambda rule: not rule.should_run_on_semgrep_core
-    )
 
-    resolved_deps: Dict[Ecosystem, List[Subproject]] = {}
+    # Initialize data structures for dependencies
+    filtered_dependency_aware_rules = []
+    resolved_deps: Dict[Ecosystem, List[ResolvedSubproject]] = {}
     dependency_parser_errors: List[DependencyParserError] = []
     sca_dependency_targets: List[Path] = []
 
-    # Temporary flag to allow us to test the new requirements lockfile matchers
-    # TODO(sal): remove once GA
-    EcosystemLockfiles.init(
-        use_new_requirements_matchers=enable_experimental_requirements
-    )
-
     if len(dependency_aware_rules) > 0:
-        # identify and parse lockfiles before beginning the scan
-        # to produce dependency information that is used throughout.
-        # only do so if there is at least one dependency aware rule to
-        # use the information.
+        # Parse lockfiles to get dependency information, if there are relevant rules
         (
             resolved_deps,
             dependency_parser_errors,
             sca_dependency_targets,
         ) = resolve_subprojects(
             target_manager,
-            enable_experimental_requirements=enable_experimental_requirements,
+            allow_dynamic_resolution=allow_dynamic_dependency_resolution,
         )
 
+        # Filter rules that match the dependencies
+        filtered_dependency_aware_rules = filter_dependency_aware_rules(
+            dependency_aware_rules, resolved_deps
+        )
+
+    rest_of_the_rules = [
+        r for r in rest_of_the_rules if r not in dependency_aware_rules
+    ] + filtered_dependency_aware_rules
+
     cli_ux = get_state().get_cli_ux_flavor()
     plans = scan_report.print_scan_status(
         filtered_rules,
@@ -284,7 +328,7 @@ def run_rules(
             join_rule_matches, join_rule_errors = join_rule.run_join_rule(
                 rule.raw,
                 [target.path for target in target_manager.targets],
-                enable_experimental_requirements=enable_experimental_requirements,
+                allow_dynamic_dependency_resolution=allow_dynamic_dependency_resolution,
             )
             join_rule_matches_set = RuleMatches(rule)
             for m in join_rule_matches:
@@ -340,21 +384,40 @@ def run_rules(
                 output_handler.handle_semgrep_errors(dep_rule_errors)
 
         # The caller expects a map from lockfile path to `FoundDependency` items rather than our Subproject representation
-        found_dependencies: Dict[str, List[FoundDependency]] = {}
+        deps_by_lockfile: Dict[str, List[FoundDependency]] = {}
         for ecosystem in resolved_deps:
             for proj in resolved_deps[ecosystem]:
-                found_dependencies.update(proj.map_lockfile_to_dependencies())
+                (
+                    proj_deps_by_lockfile,
+                    unknown_lockfile_deps,
+                ) = proj.found_dependencies.make_dependencies_by_source_path()
+                deps_by_lockfile.update(proj_deps_by_lockfile)
+
+                # We don't really expect to have any dependencies with an unknown lockfile, but we can't enforce
+                # this with types due to backwards compatibility guarantees on FoundDependency. If we see any
+                # dependencies without lockfile path, we assign them to a fake lockfile at the root of each subproject.
+                for dep in unknown_lockfile_deps:
+                    if (
+                        str(proj.root_dir.joinpath(Path("unknown_lockfile")))
+                        not in deps_by_lockfile
+                    ):
+                        deps_by_lockfile[
+                            str(proj.root_dir.joinpath(Path("unknown_lockfile")))
+                        ] = []
+                    deps_by_lockfile[
+                        str(proj.root_dir.joinpath(Path("unknown_lockfile")))
+                    ].append(dep)
 
         for target in sca_dependency_targets:
             output_extra.all_targets.add(target)
     else:
-        found_dependencies = {}
+        deps_by_lockfile = {}
 
     return (
         rule_matches_by_rule,
         semgrep_errors,
         output_extra,
-        found_dependencies,
+        deps_by_lockfile,
         dependency_parser_errors,
         plans,
     )
@@ -424,6 +487,7 @@ def run_scan(
     path_sensitive: bool = False,
     capture_core_stderr: bool = True,
     enable_experimental_requirements: bool = False,
+    allow_dynamic_dependency_resolution: bool = False,
     dump_n_rule_partitions: Optional[int] = None,
 ) -> Tuple[
     RuleMatchMap,
@@ -693,7 +757,7 @@ def run_scan(
         target_mode_config,
         with_code_rules=with_code_rules,
         with_supply_chain=with_supply_chain,
-        enable_experimental_requirements=enable_experimental_requirements,
+        allow_dynamic_dependency_resolution=allow_dynamic_dependency_resolution,
     )
     profiler.save("core_time", core_start_time)
     semgrep_errors: List[SemgrepError] = config_errors + scan_errors
@@ -797,7 +861,7 @@ def run_scan(
                         run_secrets,
                         disable_secrets_validation,
                         baseline_target_mode_config,
-                        enable_experimental_requirements=enable_experimental_requirements,
+                        allow_dynamic_dependency_resolution=allow_dynamic_dependency_resolution,
                     )
                     rule_matches_by_rule = remove_matches_in_baseline(
                         rule_matches_by_rule,
diff --git a/cli/src/semgrep/scan_report.py b/cli/src/semgrep/scan_report.py
index 46a02ec17199..253eecb90237 100644
--- a/cli/src/semgrep/scan_report.py
+++ b/cli/src/semgrep/scan_report.py
@@ -23,7 +23,7 @@
 from semgrep.rule import Rule
 from semgrep.state import DesignTreatment
 from semgrep.state import get_state
-from semgrep.subproject import Subproject
+from semgrep.subproject import ResolvedSubproject
 from semgrep.target_manager import TargetManager
 from semgrep.target_mode import TargetModeConfig
 from semgrep.util import unit_str
@@ -318,7 +318,7 @@ def print_scan_status(
     rules: Sequence[Rule],
     target_manager: TargetManager,
     target_mode_config: TargetModeConfig,
-    sca_subprojects: Dict[out.Ecosystem, List[Subproject]],
+    sca_subprojects: Dict[out.Ecosystem, List[ResolvedSubproject]],
     dependency_parser_errors: List[out.DependencyParserError],
     *,
     cli_ux: DesignTreatment = DesignTreatment.LEGACY,
diff --git a/cli/src/semgrep/semgrep_interfaces b/cli/src/semgrep/semgrep_interfaces
index 2351c5e528cb..2f2de99cde5d 160000
--- a/cli/src/semgrep/semgrep_interfaces
+++ b/cli/src/semgrep/semgrep_interfaces
@@ -1,1 +1,1 @@
-Subproject commit 2351c5e528cb7430422208dc66707894c066b508
+Subproject commit 2f2de99cde5de1fee51bba3f700f80ba62f4ffa9
diff --git a/cli/src/semgrep/subproject.py b/cli/src/semgrep/subproject.py
index c9c368f080fc..dc7a9f596efb 100644
--- a/cli/src/semgrep/subproject.py
+++ b/cli/src/semgrep/subproject.py
@@ -2,275 +2,264 @@
 from abc import abstractmethod
 from collections import defaultdict
 from dataclasses import dataclass
+from enum import auto
+from enum import Enum
 from pathlib import Path
 from typing import Dict
+from typing import Generator
 from typing import List
 from typing import Optional
+from typing import Set
 from typing import Tuple
 
-from semdep.lockfile import EcosystemLockfiles
-from semdep.lockfile import Lockfile
-from semgrep.semgrep_interfaces.semgrep_output_v1 import DependencyParserError
+import semgrep.semgrep_interfaces.semgrep_output_v1 as out
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
-from semgrep.target_manager import TargetManager
+
+
+class PackageManagerType(Enum):
+    PIP = auto()
+    POETRY = auto()
+    PIPENV = auto()
+    NPM = auto()
+    YARN = auto()
+    PNPM = auto()
+    RUBY_GEM = auto()
+    GO_MOD = auto()
+    CARGO = auto()
+    MAVEN = auto()
+    GRADLE = auto()
+    COMPOSER = auto()
+    NUGET = auto()
+    DART_PUB = auto()
+    SWIFT_PM = auto()
+    ELIXIR_HEX = auto()
+
+
+class ResolutionMethod(Enum):
+    LOCKFILE_PARSING = auto()
 
 
 class DependencySource(ABC):
     @abstractmethod
-    def get_lockfile_paths(self) -> List[Path]:
-        pass
+    def get_display_paths(self) -> List[Path]:
+        return []
 
 
 @dataclass(frozen=True)
-class LockfileDependencySource(DependencySource):
-    manifest_path: Optional[Path]
-    lockfile_path: Path  # required for now, but this will change as we start to support lockfileless scanning
+class ManifestOnlyDependencySource(DependencySource):
+    manifest_path: Path
+    # we don't include package manager type for the manifest-only source,
+    # because it's possible that we don't know (e.g. package.json, pyproject.toml
+    # are both used for multiple package managers.)
+    manifest_kind: out.ManifestKind
 
-    def get_lockfile_paths(self) -> List[Path]:
-        return [self.lockfile_path]
+    def get_display_paths(self) -> List[Path]:
+        return [self.manifest_path]
 
 
 @dataclass(frozen=True)
-class MultiLockfileDependencySource(DependencySource):
-    sources: List[LockfileDependencySource]
-
-    def get_lockfile_paths(self) -> List[Path]:
-        return [source.lockfile_path for source in self.sources]
+class PackageManagerDependencySource(DependencySource):
+    package_manager_type: PackageManagerType
 
 
 @dataclass(frozen=True)
-class Subproject:
-    """
-    A subproject, defined by some kind of manifest file (e.g. pyproject.toml, package.json, ...).
-    This may be at the root of the repo being scanned or may be some other folder.
+class LockfileDependencySource(PackageManagerDependencySource):
+    manifest_path: Optional[Path]
+    lockfile_path: Path
 
-    Used as the unit of analysis for supply chain.
-    """
+    def get_display_paths(self) -> List[Path]:
+        return [self.lockfile_path]
 
-    # the root of the subproject
-    root_dir: Path
 
-    ecosystem: Ecosystem
+@dataclass(frozen=True)
+class MultiLockfileDependencySource(DependencySource):
+    # note: we use a tuple instead of a list here to allow hashing the whole type
+    sources: Tuple[LockfileDependencySource, ...]
+
+    def get_display_paths(self) -> List[Path]:
+        return [source.lockfile_path for source in self.sources]
 
-    # the dependency source is how we resolved the dependencies. This might be a lockfile/manifest pair (the only current one),
-    # but in the future it might also be dynamic resolution based on a manifest, an SBOM, or something else
-    dependency_source: DependencySource
-    found_dependencies: List[FoundDependency]
 
-    def map_lockfile_to_dependencies(self) -> Dict[str, List[FoundDependency]]:
+@dataclass(frozen=True)
+class ResolvedDependencies:
+    # we use this mapping to efficiently find child dependencies from a FoundDependency
+    # We need to store multiple FoundDependencies per package/version pair because a package
+    # might come from multiple places in a lockfile
+    _dependencies_by_package_version_pair: Dict[
+        out.DependencyChild, List[FoundDependency]
+    ]
+
+    @classmethod
+    def from_resolved_interfaces(
+        cls, resolved: out.ResolutionOk
+    ) -> "ResolvedDependencies":
+        return cls.from_found_dependencies(resolved.value)
+
+    @classmethod
+    def from_found_dependencies(
+        cls, found_deps: List[FoundDependency]
+    ) -> "ResolvedDependencies":
+        mapping: Dict[out.DependencyChild, List[FoundDependency]] = {}
+        for dep in found_deps:
+            k = out.DependencyChild(dep.package, dep.version)
+            if k not in mapping:
+                mapping[k] = []
+            mapping[k].append(dep)
+        return cls(mapping)
+
+    def iter_found_dependencies(self) -> Generator[FoundDependency, None, None]:
+        for dep_group in self._dependencies_by_package_version_pair.values():
+            for dep in dep_group:
+                yield dep
+
+    def make_dependencies_by_source_path(
+        self,
+    ) -> Tuple[Dict[str, List[FoundDependency]], List[FoundDependency]]:
         """
-        Returns a mapping of lockfile paths to dependencies found in that lockfile
+        Returns a mapping of lockfile paths to dependencies found in that lockfile.
+
+        Also returns a list of FoundDependencies that did not have a lockfile available.
         """
-        lockfile_to_deps = defaultdict(list)
+        lockfile_to_deps: Dict[str, List[FoundDependency]] = defaultdict(list)
+        unknown_lockfile: List[FoundDependency] = []
 
-        for dep in self.found_dependencies:
+        for dep in self.iter_found_dependencies():
             if dep.lockfile_path is not None:
                 lockfile_to_deps[str(dep.lockfile_path.value)].append(dep)
             else:
-                # if the dependency doesn't have a lockfile path, we just put it in the root directory
-                lockfile_to_deps[
-                    str(self.root_dir.joinpath(Path("unknown_lockfile")))
-                ].append(dep)
+                unknown_lockfile.append(dep)
 
-        return dict(lockfile_to_deps)
+        return dict(lockfile_to_deps), unknown_lockfile
 
-    def get_lockfile_paths(self) -> List[Path]:
+    def pretty_print(self) -> None:
         """
-        Returns a list of lockfile paths for this subproject
+        Print the dependency graph with nice indentation. This is
+        only for debugging purposes.
         """
-        return self.dependency_source.get_lockfile_paths()
 
+        def pretty_print_dependency(
+            dep: FoundDependency,
+            indent: int = 0,
+            already_printed: Optional[Set[out.DependencyChild]] = None,
+        ) -> Set[out.DependencyChild]:
+            """
+            Returns package, version pairs of the dependencies that were printed
+            """
+            if already_printed is None:
+                already_printed = set()
+
+            print(f"{' '*indent*2}- {dep.package}@{dep.version}")
+
+            # if we already printed this dependency once, don't print its children again.
+            # depends on mutation of already_printed in child calls to avoid re-printing
+            if out.DependencyChild(dep.package, dep.version) in already_printed:
+                print(f"{' '*2*(indent + 1)} (already printed)")
+                return already_printed
+            already_printed.add(out.DependencyChild(dep.package, dep.version))
+
+            if dep.children is not None:
+                for child in dep.children:
+                    # always take the first child dependencies - we don't care to disambiguate
+                    # between deps that have the same package and version
+                    child_dep = self._dependencies_by_package_version_pair[child][0]
+                    already_printed |= pretty_print_dependency(
+                        child_dep, indent + 2, already_printed
+                    )
 
-def _get_lockfiles_by_root_dir(
-    target_manager: TargetManager, ecosystem: Ecosystem
-) -> Dict[Path, List[Lockfile]]:
-    """
-    This function returns a mapping of parent paths to lockfiles for the given ecosystem.
-
-    Here we assume if there are multiple lockfiles in the same directory, we should combine the
-    dependencies from all of them. This is a simplification that may not be correct in all cases.
-    """
-    lockfiles = target_manager.get_lockfiles(ecosystem, ignore_baseline_handler=True)
-
-    # group by parent
-    lockfiles_by_parent_path: Dict[Path, List[Lockfile]] = defaultdict(list)
-
-    for lockfile in lockfiles:
-        root_dir = lockfile.root_directory
-        lockfiles_by_parent_path[root_dir].append(lockfile)
+            return already_printed
 
-    return lockfiles_by_parent_path
+        print("Direct dependencies (plus children if available)")
+        directs = [
+            dep
+            for dep in self.iter_found_dependencies()
+            if dep.transitivity == out.Transitivity(out.Direct())
+        ]
+        printed_in_graph: set[out.DependencyChild] = set()
+        for direct in directs:
+            printed_in_graph |= pretty_print_dependency(direct)
+
+        # print any remaining transitives/unknowns that were not already printed
+        transitives = [
+            dep
+            for package_version_pair, dep_group in self._dependencies_by_package_version_pair.items()
+            for dep in dep_group
+            if dep.transitivity == out.Transitivity(out.Transitive())
+            and package_version_pair not in printed_in_graph
+        ]
+        unknown = [
+            dep
+            for package_version_pair, dep_group in self._dependencies_by_package_version_pair.items()
+            for dep in dep_group
+            if dep.transitivity == out.Transitivity(out.Unknown())
+            and package_version_pair not in printed_in_graph
+        ]
+        print("other transitives:")
+        for dependency in transitives:
+            print(f"- {dependency.package}@{dependency.version}")
+        print("other unknown transitivity:")
+        for dependency in unknown:
+            print(f"- {dependency.package}@{dependency.version}")
 
 
-def _parse_lockfiles(
-    lockfiles: List[Lockfile],
-) -> Tuple[List[FoundDependency], List[DependencyParserError], List[Path]]:
+@dataclass(frozen=True)
+class Subproject:
     """
-    Parse a list of lockfiles and aggregate the results.
-
-    Args:
-        lockfiles (List[Lockfile]): A list of Lockfile objects to parse.
+    A subproject, defined by some kind of manifest file (e.g. pyproject.toml, package.json, ...).
+    This may be at the root of the repo being scanned or may be some other folder.
 
-    Returns:
-        Tuple[List[FoundDependency], List[DependencyParserError], List[Path]]:
-        - A list of all found dependencies
-        - A list of all parsing errors encountered
-        - A list of paths to the parsed lockfiles
+    Used as the unit of analysis for supply chain.
     """
-    all_deps: List[FoundDependency] = []
-    all_parse_errors: List[DependencyParserError] = []
-    sca_dependency_targets: List[Path] = []
 
-    for lockfile in lockfiles:
-        deps, parse_errors = lockfile.parse()
-        all_deps.extend(deps)
-        all_parse_errors.extend(parse_errors)
-        sca_dependency_targets.append(lockfile.path)
+    # the root of the subproject
+    root_dir: Path
 
-    return all_deps, all_parse_errors, sca_dependency_targets
+    # the dependency source is how we resolved the dependencies. This might be a lockfile/manifest pair (the only current one),
+    # but in the future it might also be dynamic resolution based on a manifest, an SBOM, or something else
+    dependency_source: DependencySource
 
 
-def _create_dependency_source(lockfiles: List[Lockfile]) -> DependencySource:
+@dataclass(frozen=True)
+class ResolvedSubproject(Subproject):
     """
-    Create a DependencySource based on the number of lockfiles.
-
-    Args:
-        lockfiles (List[Lockfile]): A list of Lockfile objects.
-
-    Returns:
-        DependencySource: Either a LockfileDependencySource or a MultiLockfileDependencySource.
-
-    Raises:
-        ValueError: If no lockfiles are provided.
+    A subproject plus its resolved set of dependencies.
     """
-    if not lockfiles:
-        raise ValueError("No lockfiles found")
-
-    if len(lockfiles) == 1:
-        return LockfileDependencySource(
-            manifest_path=lockfiles[0].manifest_path,
-            lockfile_path=lockfiles[0].path,
-        )
 
-    return MultiLockfileDependencySource(
-        sources=[
-            LockfileDependencySource(
-                manifest_path=lockfile.manifest_path,
-                lockfile_path=lockfile.path,
-            )
-            for lockfile in lockfiles
-        ]
-    )
-
-
-def _process_ecosystem(
-    target_manager: TargetManager, ecosystem: Ecosystem
-) -> Tuple[List[Subproject], List[DependencyParserError], List[Path]]:
-    """
-    Process a specific ecosystem to extract dependencies and create subprojects.
+    ecosystem: Ecosystem
 
-    Args:
-        target_manager: An object managing the targets for dependency resolution.
-        ecosystem (Ecosystem): The ecosystem to process.
-
-    Returns:
-        Tuple[List[Subproject], List[DependencyParserError], List[Path]]:
-        - A list of resolved Subprojects
-        - A list of all parsing errors encountered
-        - A list of all dependency target paths
-    """
-    resolved_deps: List[Subproject] = []
-    all_parse_errors: List[DependencyParserError] = []
-    all_sca_dependency_targets: List[Path] = []
-
-    for parent_path, lockfiles in _get_lockfiles_by_root_dir(
-        target_manager, ecosystem
-    ).items():
-        all_deps, parse_errors, sca_dependency_targets = _parse_lockfiles(lockfiles)
-        dependency_source = _create_dependency_source(lockfiles)
-
-        subproject = Subproject(
-            root_dir=parent_path,
-            dependency_source=dependency_source,
+    # the resolution method is how we determined the dependencies from the dependency source. This might be
+    # lockfile parsing, dependency resolution, SBOM ingest, or something else.
+    resolution_method: ResolutionMethod
+
+    # the dependencies that were found
+    found_dependencies: ResolvedDependencies
+
+    @classmethod
+    def from_unresolved(
+        cls,
+        unresolved: Subproject,
+        resolution_method: ResolutionMethod,
+        found_dependencies: List[FoundDependency],
+        ecosystem: Ecosystem,
+    ) -> "ResolvedSubproject":
+        return cls(
+            root_dir=unresolved.root_dir,
+            dependency_source=unresolved.dependency_source,
             ecosystem=ecosystem,
-            found_dependencies=all_deps,
+            found_dependencies=ResolvedDependencies.from_found_dependencies(
+                found_dependencies
+            ),
+            resolution_method=resolution_method,
         )
 
-        resolved_deps.append(subproject)
-        all_parse_errors.extend(parse_errors)
-        all_sca_dependency_targets.extend(sca_dependency_targets)
-
-    return resolved_deps, all_parse_errors, all_sca_dependency_targets
-
-
-def resolve_subprojects(
-    target_manager: TargetManager,
-    allow_dynamic_resolution: bool = False,
-    enable_experimental_requirements: bool = False,
-) -> Tuple[Dict[Ecosystem, List[Subproject]], List[DependencyParserError], List[Path]]:
-    """
-    Identify lockfiles and manifest files to resolve dependency information from the environment
-
-    If `allow_dynamic_resolution` is True, this function may cause projects that are scanned to be built. This may involve:
-    - Downloading packages from the internet
-    - Executing code that is included in the scanned project or in downloaded packages
-
-    When `allow_dynamic_resolution` is False, dependencies are resolved only by parsing existing files (lockfiles and manifests).
-    """
-    resolved_deps: Dict[Ecosystem, List[Subproject]] = {}
-    dependency_parser_errors: List[DependencyParserError] = []
-
-    # targets that were considered in generating the dependency tree
-    sca_dependency_targets: List[Path] = []
-    if allow_dynamic_resolution:
-        raise NotImplementedError
-    else:
-        # for safety, when `allow_dynamic_resolution` is disabled we follow
-        # a lockfile-first approach to resolve projects that matches previous behavior
-        if enable_experimental_requirements:
-            for ecosystem in EcosystemLockfiles.ecosystem_to_lockfiles.keys():
-                (
-                    ecosystem_subprojects,
-                    parse_errors,
-                    dependency_targets,
-                ) = _process_ecosystem(target_manager, ecosystem)
-
-                resolved_deps.setdefault(ecosystem, []).extend(ecosystem_subprojects)
-                dependency_parser_errors.extend(parse_errors)
-                sca_dependency_targets.extend(dependency_targets)
-        else:
-            for ecosystem in EcosystemLockfiles.ecosystem_to_lockfiles.keys():
-                for lockfile in target_manager.get_lockfiles(
-                    ecosystem, ignore_baseline_handler=True
-                ):
-                    deps, parse_errors = lockfile.parse()
-                    sca_dependency_targets.append(lockfile.path)
-                    subproject = Subproject(
-                        root_dir=lockfile.path.parent,
-                        dependency_source=LockfileDependencySource(
-                            manifest_path=lockfile.manifest_path,
-                            lockfile_path=lockfile.path,
-                        ),
-                        ecosystem=ecosystem,
-                        found_dependencies=deps,
-                    )
-
-                    if ecosystem not in resolved_deps:
-                        resolved_deps[ecosystem] = []
-                    resolved_deps[ecosystem].append(subproject)
-                    dependency_parser_errors.extend(parse_errors)
-
-    return resolved_deps, dependency_parser_errors, sca_dependency_targets
-
 
 def find_closest_subproject(
-    path: Path, ecosystem: Ecosystem, candidates: List[Subproject]
-) -> Optional[Subproject]:
+    path: Path, ecosystem: Ecosystem, candidates: List[ResolvedSubproject]
+) -> Optional[ResolvedSubproject]:
     """
     Find the best SCA project for the given match by looking at the parent path of the match
     and comparing it to the root directories of the provided candidates. The best SCA project is
-    the one with the closest root directory to the match.
+    the one with the closest root directory to the match that has the provided ecosystem
 
     ! All provided candidates must have the same ecosystem.
 
diff --git a/cli/src/semgrep/target_manager.py b/cli/src/semgrep/target_manager.py
index cf825b0aba8a..8c6f133f6b82 100644
--- a/cli/src/semgrep/target_manager.py
+++ b/cli/src/semgrep/target_manager.py
@@ -23,8 +23,7 @@
 from typing import Union
 
 import semgrep.semgrep_interfaces.semgrep_output_v1 as out
-from semdep.lockfile import filter_lockfile_paths
-from semdep.lockfile import Lockfile
+from semdep.subproject_matchers import filter_dependency_source_files
 from semgrep.git import BaselineHandler
 
 # usually this would be a try...except ImportError
@@ -57,18 +56,6 @@
 from semgrep.util import with_color
 from semgrep.verbose_logging import getLogger
 
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Cargo
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gomod
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Hex
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Maven
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Npm
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Composer
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Nuget
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pub
-from semgrep.semgrep_interfaces.semgrep_output_v1 import SwiftPM
 
 logger = getLogger(__name__)
 
@@ -138,9 +125,9 @@ class FileTargetingLog:
 
     # Indicates which files were NOT scanned by each language
     # e.g. for python, should be a list of all non-python-compatible files
-    by_language: Dict[Union[Language, Ecosystem], Set[Path]] = Factory(
-        lambda: defaultdict(set)
-    )
+    by_language: Dict[
+        Union[Language, Literal["dependency_source_files"]], Set[Path]
+    ] = Factory(lambda: defaultdict(set))
     rule_includes: Dict[str, Set[Path]] = Factory(lambda: defaultdict(set))
     rule_excludes: Dict[str, Set[Path]] = Factory(lambda: defaultdict(set))
 
@@ -627,15 +614,17 @@ def globfilter(self, candidates: Iterable[Path], pattern: str) -> List[Path]:
         return cast(List[Path], result)
 
     def filter_by_language(
-        self, language: Union[None, Language, Ecosystem], *, candidates: FrozenSet[Path]
+        self,
+        language: Union[None, Language],
+        *,
+        candidates: FrozenSet[Path],
     ) -> FilteredFiles:
         """
-        Returns only paths that have the correct extension or shebang, or are the correct lockfile format
+        Returns only paths that have the correct extension or shebang
 
         Finds all files in a collection of paths that either:
         - end with one of a set of extension
         - is a script that executes with one of a set of programs
-        - are lockfiles associated with a given ecosystem
         """
         if isinstance(language, Language):
             kept = frozenset(
@@ -644,8 +633,6 @@ def filter_by_language(
                 if any(str(path).endswith(ext) for ext in language.definition.exts)
                 or self.executes_with_shebang(path, language.definition.shebangs)
             )
-        elif isinstance(language, Ecosystem):
-            kept = filter_lockfile_paths(language, candidates)
         else:
             kept = frozenset(candidates)
         return FilteredFiles(kept, frozenset(candidates - kept))
@@ -725,7 +712,7 @@ def get_all_files(self, ignore_baseline_handler: bool = False) -> FrozenSet[Path
     @lru_cache(maxsize=None)
     def get_files_for_language(
         self,
-        lang: Union[None, Language, Ecosystem],
+        lang: Union[None, Language, Literal["dependency_source_files"]],
         product: out.Product,
         ignore_baseline_handler: bool = False,
     ) -> FilteredFiles:
@@ -734,7 +721,12 @@ def get_files_for_language(
         an extension matching LANG or are a lockfile for LANG ecosystem that match any pattern in INCLUDES and do not
         match any pattern in EXCLUDES. Any file in TARGET bypasses excludes and includes.
         If a file in TARGET has a known extension that is not for langugage LANG then
-        it is also filtered out
+        it is also filtered out.
+
+        Lang can be:
+        - A true language, which causes this function to return source code files in that language
+        - `None`, which skips language filtering altogether
+        - `"dependency_source_files"`, which finds files that might contain dependency source information across all project types
 
         Note also filters out any directory and descendants of `.git`
 
@@ -742,9 +734,13 @@ def get_files_for_language(
         """
         all_files = self.get_all_files(ignore_baseline_handler)
 
-        if lang:
+        if isinstance(lang, Language):
             files = self.filter_by_language(lang, candidates=all_files)
             self.ignore_log.by_language[lang].update(files.removed)
+        elif lang == "dependency_source_files":
+            kept = filter_dependency_source_files(candidates=all_files)
+            files = FilteredFiles(kept, all_files - kept)
+            self.ignore_log.by_language[lang].update(files.removed)
         else:
             files = FilteredFiles(frozenset(all_files), frozenset())
 
@@ -760,7 +756,7 @@ def get_files_for_language(
         self.ignore_log.always_skipped.update(files.removed)
 
         # Lockfiles are easy to parse, and regularly surpass 1MB for big repos
-        if not isinstance(lang, Ecosystem):
+        if lang != "dependency_source_files":
             files = self.filter_by_size(self.max_target_bytes, candidates=files.kept)
             self.ignore_log.size_limit.update(files.removed)
 
@@ -776,12 +772,12 @@ def get_files_for_language(
             t.path for t in self.targets if not t.path.is_dir() and t.path.is_file()
         )
         explicit_files_for_lang = self.filter_by_language(
-            lang, candidates=explicit_files
+            lang if isinstance(lang, Language) else None, candidates=explicit_files
         )
         kept_files |= explicit_files_for_lang.kept
-        if self.allow_unknown_extensions and not isinstance(lang, Ecosystem):
+        if self.allow_unknown_extensions and lang != "dependency_source_files":
             # add unknown extensions back in for languages. Don't do so when searching
-            # for lockfiles and manifests (when lang is an Ecosystem)
+            # for dependency source information
             explicit_files_of_unknown_lang = self.filter_known_extensions(
                 candidates=explicit_files
             )
@@ -819,50 +815,14 @@ def get_files_for_rule(
 
         return paths.kept
 
-    def get_all_lockfiles(self) -> Dict[Ecosystem, FrozenSet[Path]]:
-        """
-        Return a dict mapping each ecosystem to the set of lockfiles for that ecosystem
-        """
-        ALL_ECOSYSTEMS: Set[Ecosystem] = {
-            Ecosystem(Npm()),
-            Ecosystem(Pypi()),
-            Ecosystem(Gem()),
-            Ecosystem(Gomod()),
-            Ecosystem(Cargo()),
-            Ecosystem(Maven()),
-            Ecosystem(Composer()),
-            Ecosystem(Nuget()),
-            Ecosystem(Pub()),
-            Ecosystem(SwiftPM()),
-            Ecosystem(Hex()),
-        }
-
-        return {
-            ecosystem: frozenset(
-                lockfile.path for lockfile in self.get_lockfiles(ecosystem)
-            )
-            for ecosystem in ALL_ECOSYSTEMS
-        }
-
-    @lru_cache(maxsize=None)
-    def get_lockfiles(
+    def get_all_dependency_source_files(
         self,
-        ecosystem: Ecosystem,
-        product: out.Product = SCA_PRODUCT,
         ignore_baseline_handler: bool = False,
-    ) -> List[Lockfile]:
+    ) -> FrozenSet[Path]:
         """
-        Return set of paths to lockfiles for a given ecosystem
-
-        Respects semgrepignore/exclude flag
-
-        ignore_baseline_handler: if True, will ignore the baseline handler and scan all files. Used in the context of scanning unchanged lockfiles for their dependencies and doing reachability analysis.
+        Return all files that might be used as a source of dependency information
         """
-        files = self.get_files_for_language(
-            ecosystem, product, ignore_baseline_handler
-        ).kept
-
-        # Sort files so file paths are returned in a deterministic order
-        sorted_files = sorted(files)
-
-        return [Lockfile.from_path(path) for path in sorted_files]
+        all_files = self.get_files_for_language(
+            "dependency_source_files", out.Product(out.SCA()), ignore_baseline_handler
+        )
+        return all_files.kept
diff --git a/dune-project b/dune-project
index ec117a9ceabe..22f35660404f 100644
--- a/dune-project
+++ b/dune-project
@@ -21,7 +21,7 @@
 (generate_opam_files)
 
 ;; set here so the semgrep package below can use it and we can easily bump it
-(version 1.92.0)
+(version 1.93.0)
 
 ;; Default attributes of opam packages
 (source (github semgrep/semgrep))
diff --git a/languages/cpp/ast/ast_cpp.ml b/languages/cpp/ast/ast_cpp.ml
index ec666b4c6a0a..b501b98fbcc4 100644
--- a/languages/cpp/ast/ast_cpp.ml
+++ b/languages/cpp/ast/ast_cpp.ml
@@ -1153,12 +1153,9 @@ let get_original_token_location = function
  * used by parsing hacks
  *)
 let make_expanded ii =
-  let no_virt_loc =
-    (* TODO? use Pos.fake_pos? *)
-    let pos = Pos.make 0 in
-    ({ Tok.str = ""; pos }, -1)
-  in
-  Tok.ExpandedTok (get_original_token_location ii, no_virt_loc)
+  let loc = get_original_token_location ii in
+  let virt_loc = (loc, -1) in
+  Tok.ExpandedTok (loc, virt_loc)
 
 let make_param ?p_name ?(p_specs = []) ?p_val t =
   { p_name; p_type = t; p_specs; p_val }
diff --git a/languages/cpp/menhir/parse_cpp.ml b/languages/cpp/menhir/parse_cpp.ml
index 1914b692d940..1d1dbe933c41 100644
--- a/languages/cpp/menhir/parse_cpp.ml
+++ b/languages/cpp/menhir/parse_cpp.ml
@@ -367,7 +367,7 @@ let parse_with_lang ?(lang = Flag_parsing_cpp.Cplusplus) file :
           if !was_define && !Flag_cpp.filter_define_error then ()
           else if
             (* bugfix: *)
-            checkpoint_file = checkpoint2_file && checkpoint_file = !!file
+            checkpoint_file =*= checkpoint2_file && checkpoint_file =*= file
           then
             Log.err (fun m ->
                 m "%s"
@@ -389,7 +389,7 @@ let parse_with_lang ?(lang = Flag_parsing_cpp.Cplusplus) file :
     let checkpoint2_file = Tok.file_of_tok info in
 
     let diffline =
-      if checkpoint_file = checkpoint2_file && checkpoint_file = !!file then
+      if checkpoint_file =*= checkpoint2_file && checkpoint_file =*= file then
         checkpoint2 - checkpoint
       else 0
       (* TODO? so if error come in middle of something ? where the
diff --git a/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml b/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml
index e4639f3b8d0e..2f0dfaa062d7 100644
--- a/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml
+++ b/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml
@@ -57,11 +57,14 @@ let concat_tokens first_tok other_toks : string wrap =
    In the end, we want something like "a && \\\n\\\n            b".
 *)
 let concat_shell_fragments first_tok other_toks : string wrap =
-  let tok =
+  let tok_opt =
     Tok.combine_sparse_toks ~ignorable_newline:"\\\n" ~ignorable_blank:' '
       first_tok other_toks
   in
-  (Tok.content_of_tok tok, tok)
+  match tok_opt with
+  (* TODO What should I do here *)
+  | None -> failwith "concat of fake token"
+  | Some tok -> (Tok.content_of_tok tok, tok)
 
 (* TODO: This basic stuff should not exist here. Move it to Tok. *)
 let opt_concat_tokens toks : string wrap option =
diff --git a/languages/php/menhir/lexer_php.mll b/languages/php/menhir/lexer_php.mll
index 4218b981d295..bc13635255cc 100644
--- a/languages/php/menhir/lexer_php.mll
+++ b/languages/php/menhir/lexer_php.mll
@@ -59,7 +59,8 @@ let yyback n lexbuf =
 let tok = Lexing.lexeme
 let tokinfo = Tok.tok_of_lexbuf
 
-let tokinfo_str_pos = Tok.tok_of_str_and_bytepos
+let tokinfo_file_str_pos (file : Fpath.t) (str : string) (bytepos : int) : Tok.t
+  = Tok.make ~str ~bytepos ~file
 
 (* all string passed to T_IDENT or T_VARIABLE should go through case_str *)
 let case_str s =
@@ -540,13 +541,14 @@ rule st_in_scripting = parse
 
         let syminfo = Tok.rewrap_str sym info in
 
+        let file = Tok.file_of_tok info in
         let parse_info = Tok.unsafe_loc_of_tok info in
         let pos_after_sym   =
           parse_info.Tok.pos.bytepos + String.length sym in
         let pos_after_white = pos_after_sym + String.length white in
 
-        let whiteinfo = tokinfo_str_pos white pos_after_sym in
-        let lblinfo = tokinfo_str_pos label pos_after_white in
+        let whiteinfo = tokinfo_file_str_pos file white pos_after_sym in
+        let lblinfo = tokinfo_file_str_pos file label pos_after_white in
 
         push_token (T_IDENT (case_str label, lblinfo));
        (* todo: could be newline ... *)
@@ -631,8 +633,9 @@ rule st_in_scripting = parse
         let info = tokinfo lexbuf in
         let dollarinfo = Tok.rewrap_str (String.make 1 dollar) info in
         let parse_info = Tok.unsafe_loc_of_tok info in
+        let file = Tok.file_of_tok info in
         let pos_after_sym = parse_info.Tok.pos.bytepos + 2 in
-        let lblinfo = tokinfo_str_pos s pos_after_sym in
+        let lblinfo = tokinfo_file_str_pos file s pos_after_sym in
 
         push_token (T_VARIABLE(case_str s, lblinfo));
         TDOLLAR dollarinfo
@@ -901,12 +904,12 @@ and st_double_quotes = parse
     | "$" (LABEL as s)     { t_variable_or_metavar s (tokinfo lexbuf) }
     | "$" (LABEL as s) "[" {
           let info = tokinfo lexbuf in
-
+          let file = Tok.file_of_tok info in
           let varinfo = Tok.rewrap_str ("$" ^ s) info in
           let charpos_info = Tok.bytepos_of_tok varinfo in
           let pos_after_label = charpos_info + String.length ("$" ^ s) in
 
-          let bra_info = tokinfo_str_pos "[" pos_after_label in
+          let bra_info = tokinfo_file_str_pos file "[" pos_after_label in
           push_token (TOBRA bra_info);
           push_mode ST_VAR_OFFSET;
           T_VARIABLE(case_str s, varinfo)
@@ -950,11 +953,12 @@ and st_backquote = parse
     | "$" (LABEL as s) "[" {
           let info = tokinfo lexbuf in
 
+          let file = Tok.file_of_tok info in
           let varinfo = Tok.rewrap_str ("$" ^ s) info in
           let charpos_info = Tok.bytepos_of_tok varinfo in
           let pos_after_label = charpos_info + String.length ("$" ^ s) in
 
-          let bra_info = tokinfo_str_pos "[" pos_after_label in
+          let bra_info = tokinfo_file_str_pos file "[" pos_after_label in
           push_token (TOBRA bra_info);
           push_mode ST_VAR_OFFSET;
           T_VARIABLE(case_str s, varinfo)
@@ -997,14 +1001,15 @@ and st_start_heredoc stopdoc = parse
 
       let lbl_info = Tok.rewrap_str s info in
 
+      let file = Tok.file_of_tok info in
       let pos = Tok.bytepos_of_tok info in
       let pos_after_label = pos + String.length s in
       let pos_after_semi = pos_after_label + String.length semi in
 
       let colon_info =
-        tokinfo_str_pos semi pos_after_label in
+        tokinfo_file_str_pos file semi pos_after_label in
       let space_info =
-        tokinfo_str_pos (Common2.string_of_char space) pos_after_semi in
+        tokinfo_file_str_pos file (Common2.string_of_char space) pos_after_semi in
 
       if s = stopdoc
       then begin
@@ -1027,11 +1032,12 @@ and st_start_heredoc stopdoc = parse
     | "$" (LABEL as s) "[" {
           let info = tokinfo lexbuf in
 
+          let file = Tok.file_of_tok info in
           let varinfo = Tok.rewrap_str ("$" ^ s) info in
           let charpos_info = Tok.bytepos_of_tok varinfo in
           let pos_after_label = charpos_info + String.length ("$" ^ s) in
 
-          let bra_info = tokinfo_str_pos "[" pos_after_label in
+          let bra_info = tokinfo_file_str_pos file "[" pos_after_label in
           push_token (TOBRA bra_info);
           push_mode ST_VAR_OFFSET;
           T_VARIABLE(case_str s, varinfo)
@@ -1069,14 +1075,15 @@ and st_start_nowdoc stopdoc = parse
 
       let lbl_info = Tok.rewrap_str s info in
 
+      let file = Tok.file_of_tok info in
       let pos = Tok.bytepos_of_tok info in
       let pos_after_label = pos + String.length s in
       let pos_after_semi = pos_after_label + String.length semi in
 
       let colon_info =
-        tokinfo_str_pos semi pos_after_label in
+        tokinfo_file_str_pos file semi pos_after_label in
       let space_info =
-        tokinfo_str_pos (Common2.string_of_char space) pos_after_semi in
+        tokinfo_file_str_pos file (Common2.string_of_char space) pos_after_semi in
 
       if s = stopdoc
       then begin
diff --git a/languages/scala/generic/scala_to_generic.ml b/languages/scala/generic/scala_to_generic.ml
index 33150b629fff..66f95ca2236f 100644
--- a/languages/scala/generic/scala_to_generic.ml
+++ b/languages/scala/generic/scala_to_generic.ml
@@ -462,6 +462,14 @@ and v_expr e : G.expr =
       let v1 = v_expr v1 and v2 = v_tok v2 and v3 = v_ident v3 in
       let name = H.name_of_id v3 in
       G.DotAccess (v1, v2, G.FN name) |> G.e
+  | Apply (Name (Id ((s, _t) as id), [ ("apply", apply_tok) ]), [ args ])
+    when String_.is_capitalized s ->
+      New
+        ( apply_tok,
+          TyN (G.Id (id, G.empty_id_info ())) |> G.t,
+          G.empty_id_info (),
+          v_arguments args )
+      |> G.e
   | Apply (v1, v2) ->
       let v1 = v_expr v1 and v2 = v_list v_arguments v2 in
       v2 |> List.fold_left (fun acc xs -> G.Call (acc, xs) |> G.e) v1
diff --git a/languages/vue/generic/Parse_vue_tree_sitter.ml b/languages/vue/generic/Parse_vue_tree_sitter.ml
deleted file mode 100644
index 33569cc7d05e..000000000000
--- a/languages/vue/generic/Parse_vue_tree_sitter.ml
+++ /dev/null
@@ -1,429 +0,0 @@
-(* Yoann Padioleau
- *
- * Copyright (c) 2021 R2C
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * version 2.1 as published by the Free Software Foundation, with the
- * special exception on linking described in file LICENSE.
- *
- * This library is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
- * LICENSE for more details.
- *)
-open Common
-open Fpath_.Operators
-module CST = Tree_sitter_vue.CST
-module H = Parse_tree_sitter_helpers
-open AST_generic
-module G = AST_generic
-
-(*****************************************************************************)
-(* Prelude *)
-(*****************************************************************************)
-(* Vue parser using tree-sitter-lang/semgrep-vue and converting
- * to ast_js.ml
- *
- * There are similarities with the code in Parse_html_tree_sitter.ml.
- *)
-
-(*****************************************************************************)
-(* Helpers *)
-(*****************************************************************************)
-type extra = {
-  (* todo: later we should also propagate parsing errors *)
-  parse_js_program : string wrap -> AST_generic.program;
-  parse_js_expr : string wrap -> AST_generic.expr;
-}
-
-type env = extra H.env
-
-let fake = AST_generic.fake
-let token = H.token
-let str = H.str
-let fb = Tok.unsafe_fake_bracket
-
-(* val str_if_wrong_content_temporary_fix :
-   'a env -> Tree_sitter_run.Token.t -> string * Parse_info.t
-*)
-(* This is a temporary fix until
- * https://github.com/returntocorp/ocaml-tree-sitter-core/issues/5
- * is fixed.
- *)
-let str_if_wrong_content_temporary_fix ({ file; conv; _ } : env)
-    (tok : Tree_sitter_run.Token.t) =
-  let loc, _wrong_str = tok in
-
-  let bytepos, line, column =
-    let pos = loc.Tree_sitter_run.Loc.start in
-    (* Parse_info is 1-line based and 0-column based, like Emacs *)
-    let line = pos.Tree_sitter_run.Loc.row + 1 in
-    let column = pos.Tree_sitter_run.Loc.column in
-    try (conv (line, column), line, column) with
-    | Not_found ->
-        failwith
-          (spf "could not find line:%d x col:%d in %s" line column !!file)
-  in
-  let charpos2 =
-    let pos = loc.Tree_sitter_run.Loc.end_ in
-    (* Parse_info is 1-line based and 0-column based, like Emacs *)
-    let line = pos.Tree_sitter_run.Loc.row + 1 in
-    let column = pos.Tree_sitter_run.Loc.column in
-    try conv (line, column) with
-    | Not_found ->
-        failwith
-          (spf "could not find line:%d x col:%d in %s" line column !!file)
-  in
-  (* Range.t is inclusive, so we need -1 to remove the char at the pos *)
-  let charpos2 = charpos2 - 1 in
-  let r = { Range.start = bytepos; end_ = charpos2 } in
-  let str = Range.content_at_range file r in
-  let pos = Pos.make ~file:!!file ~line ~column bytepos in
-  let tok_loc = { Tok.str; pos } in
-  (str, Tok.tok_of_loc tok_loc)
-
-(*****************************************************************************)
-(* Boilerplate converter *)
-(*****************************************************************************)
-(* This was started by copying tree-sitter-lang/semgrep-vue/Boilerplate.ml *)
-
-(**
-   Boilerplate to be used as a template when mapping the vue CST
-   to another type of tree.
-*)
-
-let map_end_tag (env : env) ((v1, v2, v3) : CST.end_tag) : tok =
-  let v1 = token env v1 (* "</" *) in
-  let v2 = token env v2 (* end_tag_name *) in
-  let v3 = token env v3 (* ">" *) in
-  Tok.combine_toks v1 [ v2; v3 ]
-
-let map_text (env : env) (x : CST.text) =
-  match x with
-  | `Text_frag tok -> str env tok (* text_fragment *)
-  (* ?? not an interpolation? *)
-  | `LCURLLCURL tok -> str env tok
-
-(* "{{" *)
-
-let map_quoted_attribute_value (env : env) (x : CST.quoted_attribute_value) :
-    string wrap bracket =
-  match x with
-  | `SQUOT_opt_pat_58fbb2e_SQUOT (v1, v2, v3) ->
-      let l = token env v1 (* "'" *) in
-      let xs =
-        match v2 with
-        | Some tok -> [ str env tok ] (* pattern "[^']+" *)
-        | None -> []
-      in
-      let r = token env v3 (* "'" *) in
-      G.string_ (l, xs, r)
-  | `DQUOT_opt_pat_98d585a_DQUOT (v1, v2, v3) ->
-      let l = token env v1 (* "\"" *) in
-      let xs =
-        match v2 with
-        | Some tok -> [ str env tok ] (* pattern "[^\"]+" *)
-        | None -> []
-      in
-      let r = token env v3 (* "\"" *) in
-      G.string_ (l, xs, r)
-
-let map_directive_modifiers (env : env) (xs : CST.directive_modifiers) =
-  List_.map
-    (fun (v1, v2) ->
-      let v1 = token env v1 (* "." *) in
-      let v2 = str env v2 (* pattern "[^<>\"'/=\\s.]+" *) in
-      (v1, v2))
-    xs
-
-let map_anon_choice_attr_value_5986531 (env : env)
-    (x : CST.anon_choice_attr_value_5986531) : a_xml_attr_value =
-  match x with
-  | `Attr_value tok ->
-      let x = str env tok (* pattern "[^<>\"'=\\s]+" *) in
-      N (Id (x, G.empty_id_info ())) |> G.e
-  | `Quoted_attr_value x ->
-      let x = map_quoted_attribute_value env x in
-      L (String x) |> G.e
-
-let map_anon_choice_dire_arg_b33821e (env : env)
-    (x : CST.anon_choice_dire_arg_b33821e) =
-  match x with
-  | `Dire_arg tok ->
-      let id = str env tok (* pattern "[^<>\"'/=\\s.]+" *) in
-      Either.Left id
-  | `Dire_dyna_arg (v1, v2, v3) ->
-      let v1 = token env v1 (* "[" *) in
-      let v2 =
-        match v2 with
-        | Some tok -> Some (str env tok) (* pattern "[^<>\"'/=\\s\\]]+" *)
-        | None -> None
-      in
-      let v3 = token env v3 (* "]" *) in
-      Either.Right (v1, v2, v3)
-
-let map_anon_choice_attr_a1991da (env : env) (x : CST.anon_choice_attr_a1991da)
-    : xml_attribute =
-  match x with
-  | `Attr (v1, v2) ->
-      let id = str env v1 (* pattern "[^<>\"'=/\\s]+" *) in
-      let teq, v2 =
-        match v2 with
-        | Some (v1, v2) ->
-            let v1 = token env v1 (* "=" *) in
-            let v2 = map_anon_choice_attr_value_5986531 env v2 in
-            (v1, v2)
-        | None ->
-            (* <foo a /> <=> <foo a=true>? That's what we do for JSX, but should
-             * we instead introduce a XmlAttrNoValue in AST_generic?
-             *)
-            let v = L (Bool (true, fake "true")) |> G.e in
-            (fake "=", v)
-      in
-      XmlAttr (id, teq, v2)
-  | `Dire_attr (v1, v2, v3) ->
-      let id =
-        match v1 with
-        | `Dire_name_opt_imm_tok_prec_p1_colon_choice_dire_arg (v1, v2) ->
-            let v1 = str env v1 (* directive_name *) in
-            let _v2TODO =
-              match v2 with
-              | Some (v1, v2) ->
-                  let v1 = token env v1 (* ":" *) in
-                  let v2 = map_anon_choice_dire_arg_b33821e env v2 in
-                  Some (v1, v2)
-              | None -> None
-            in
-            v1
-        | `Dire_shor_choice_dire_arg (v1, v2) ->
-            let v1 = str env v1 (* directive_shorthand *) in
-            let _v2 = map_anon_choice_dire_arg_b33821e env v2 in
-            v1
-      in
-      let _v2TODO =
-        match v2 with
-        | Some x -> map_directive_modifiers env x
-        | None -> []
-      in
-      let teq, v3 =
-        match v3 with
-        | Some (v1, v2) ->
-            let v1 = token env v1 (* "=" *) in
-            let v2 = map_anon_choice_attr_value_5986531 env v2 in
-            (v1, v2)
-        | None ->
-            (* <foo a /> <=> <foo a=true>? That's what we do for JSX, but should
-             * we instead introduce a XmlAttrNoValue in AST_generic?
-             *)
-            let v = L (Bool (true, fake "true")) |> G.e in
-            (fake "=", v)
-      in
-      (* TODO: XmlDynAttr? *)
-      XmlAttr (id, teq, v3)
-
-let map_start_tag (env : env) ((v1, v2, v3, v4) : CST.start_tag) =
-  let v1 = token env v1 (* "<" *) in
-  let v2 = Id (str env v2 (* start_tag_name *), G.empty_id_info ()) in
-  let v3 = List_.map (map_anon_choice_attr_a1991da env) v3 in
-  let v4 = token env v4 (* ">" *) in
-  (v1, v2, v3, v4)
-
-let map_template_start_tag (env : env)
-    ((v1, v2, v3, v4) : CST.template_start_tag) =
-  let v1 = token env v1 (* "<" *) in
-  let v2 = Id (str env v2 (* template_start_tag_name *), G.empty_id_info ()) in
-  let v3 = List_.map (map_anon_choice_attr_a1991da env) v3 in
-  let v4 = token env v4 (* ">" *) in
-  (v1, v2, v3, v4)
-
-let map_style_start_tag (env : env) ((v1, v2, v3, v4) : CST.style_start_tag) =
-  let v1 = token env v1 (* "<" *) in
-  let v2 = Id (str env v2 (* style_start_tag_name *), G.empty_id_info ()) in
-  let v3 = List_.map (map_anon_choice_attr_a1991da env) v3 in
-  let v4 = token env v4 (* ">" *) in
-  (v1, v2, v3, v4)
-
-let map_script_start_tag (env : env) ((v1, v2, v3, v4) : CST.script_start_tag) =
-  let v1 = token env v1 (* "<" *) in
-  let v2 = Id (str env v2 (* script_start_tag_name *), G.empty_id_info ()) in
-  let v3 = List_.map (map_anon_choice_attr_a1991da env) v3 in
-  let v4 = token env v4 (* ">" *) in
-  (v1, v2, v3, v4)
-
-let map_style_element (env : env) ((v1, v2, v3) : CST.style_element) : xml =
-  let l, id, attrs, r = map_style_start_tag env v1 in
-  let v2 =
-    match v2 with
-    | Some tok -> [ XmlText (str env tok) (* raw_text *) ]
-    | None -> []
-  in
-  let v3 = map_end_tag env v3 in
-  { xml_kind = XmlClassic (l, id, r, v3); xml_attrs = attrs; xml_body = v2 }
-
-let map_script_element (env : env) ((v1, v2, v3) : CST.script_element) =
-  let l, id, attrs, r = map_script_start_tag env v1 in
-  let v2 =
-    match v2 with
-    | Some tok ->
-        (* TODO: https://github.com/returntocorp/ocaml-tree-sitter-core/issues/5 *)
-        let v = str_if_wrong_content_temporary_fix env tok in
-        Some v
-        (* raw_text *)
-    | None -> None
-  in
-  let v3 = map_end_tag env v3 in
-  (l, id, r, v3, attrs, v2)
-
-let rec map_element (env : env) (x : CST.element) : xml =
-  match x with
-  | `Start_tag_rep_node_choice_end_tag (v1, v2, v3) ->
-      let l, id, attrs, r = map_start_tag env v1 in
-      let v2 = List.concat_map (map_node env) v2 in
-      let v3 =
-        match v3 with
-        | `End_tag x -> map_end_tag env x
-        | `Impl_end_tag tok -> token env tok
-        (* implicit_end_tag *)
-      in
-      { xml_kind = XmlClassic (l, id, r, v3); xml_attrs = attrs; xml_body = v2 }
-  | `Self_clos_tag (v1, v2, v3, v4) ->
-      let l = token env v1 (* "<" *) in
-      let id = Id (str env v2 (* start_tag_name *), G.empty_id_info ()) in
-      let attrs = List_.map (map_anon_choice_attr_a1991da env) v3 in
-      let r = token env v4 (* "/>" *) in
-      { xml_kind = XmlSingleton (l, id, r); xml_attrs = attrs; xml_body = [] }
-
-and map_node (env : env) (x : CST.node) : xml_body list =
-  match x with
-  | `Comm tok ->
-      let _x = token env tok (* comment *) in
-      []
-  | `Text x ->
-      let x = map_text env x in
-      [ XmlText x ]
-  | `Interp (v1, v2, v3) ->
-      let v1 = token env v1 (* "{{" *) in
-      let v2 =
-        match v2 with
-        | Some tok ->
-            let x = str env tok (* interpolation_text *) in
-            (* TODO: parse as JS *)
-            Some (L (String (fb x)) |> G.e)
-        | None -> None
-      in
-      let v3 = token env v3 (* "}}" *) in
-      [ XmlExpr (v1, v2, v3) ]
-  | `Elem x ->
-      let xml = map_element env x in
-      [ XmlXml xml ]
-  | `Temp_elem x ->
-      let xml = map_template_element env x in
-      [ XmlXml xml ]
-  | `Script_elem x ->
-      let l, id, r, rend, xml_attrs, body_opt = map_script_element env x in
-      let xml_body =
-        match body_opt with
-        (* TODO: parse as JS *)
-        | Some s -> [ XmlText s ]
-        | None -> []
-      in
-      let xml =
-        { xml_kind = XmlClassic (l, id, r, rend); xml_attrs; xml_body }
-      in
-      [ XmlXml xml ]
-  (* less: parse as CSS *)
-  | `Style_elem x ->
-      let xml = map_style_element env x in
-      [ XmlXml xml ]
-  | `Errons_end_tag (v1, v2, v3) ->
-      let l = token env v1 (* "</" *) in
-      let id =
-        Id (str env v2 (* erroneous_end_tag_name *), G.empty_id_info ())
-      in
-      let r = token env v3 (* ">" *) in
-      (* todo? raise an exn instead? *)
-      let xml =
-        { xml_kind = XmlSingleton (l, id, r); xml_attrs = []; xml_body = [] }
-      in
-      [ XmlXml xml ]
-
-and map_template_element (env : env) ((v1, v2, v3) : CST.template_element) : xml
-    =
-  let l, id, attrs, r = map_template_start_tag env v1 in
-  let v2 = List.concat_map (map_node env) v2 in
-  let v3 = map_end_tag env v3 in
-  { xml_kind = XmlClassic (l, id, r, v3); xml_attrs = attrs; xml_body = v2 }
-
-let map_component (env : env) (xs : CST.component) : stmt list =
-  List.concat_map
-    (fun x ->
-      match x with
-      | `Comm tok ->
-          let _x = token env tok (* comment *) in
-          []
-      | `Elem x ->
-          let xml = map_element env x in
-          [ G.exprstmt (Xml xml |> G.e) ]
-      | `Temp_elem x ->
-          let xml = map_template_element env x in
-          [ G.exprstmt (Xml xml |> G.e) ]
-      (* Note that right now the AST will not contain the enclosing
-       * <script>, because XmlExpr contain single expressions, not
-       * full programs, so it's simpler to just lift up the
-       * program and remove the enclosing <script>. If at some point
-       * people want to explicitly restrict their code search to
-       * the <script> part we might revisit that.
-       *)
-      | `Script_elem x -> (
-          let _l, _id, _r, _rend, _xml_attrs, body_opt =
-            map_script_element env x
-          in
-          match body_opt with
-          | Some s -> env.extra.parse_js_program s
-          | None -> [])
-      (* less: parse as CSS *)
-      | `Style_elem x ->
-          let xml = map_style_element env x in
-          [ G.exprstmt (Xml xml |> G.e) ])
-    xs
-
-(*****************************************************************************)
-(* Entry point *)
-(*****************************************************************************)
-
-(* TODO: move in Parse_tree_sitter_helpers.ml and avoid using tmp file *)
-let parse_string_and_adjust_wrt_base contents tbase fparse =
-  (* nosemgrep: forbid-tmp *)
-  UTmp.with_temp_file ~contents ~suffix:".js" (fun file ->
-      let x = fparse file in
-
-      let visitor =
-        object (_self : 'self)
-          inherit [_] AST_generic.map_legacy
-
-          method! visit_tok _env t =
-            let base_loc = Tok.unsafe_loc_of_tok tbase in
-            Tok.adjust_tok_wrt_base base_loc t
-        end
-      in
-      visitor#visit_program () x)
-
-let parse parse_js (file : Fpath.t) =
-  H.wrap_parser
-    (fun () -> Tree_sitter_vue.Parse.file !!file)
-    (fun cst _extras ->
-      let extra =
-        {
-          parse_js_program =
-            (fun (s, t) -> parse_string_and_adjust_wrt_base s t parse_js);
-          parse_js_expr =
-            (fun (s, t) ->
-              let _xs = parse_string_and_adjust_wrt_base s t parse_js in
-              failwith "TODO: extract expr");
-        }
-      in
-      let env = { H.file; conv = H.line_col_to_pos file; extra } in
-
-      let xs = map_component env cst in
-      xs)
diff --git a/languages/vue/generic/Parse_vue_tree_sitter.mli b/languages/vue/generic/Parse_vue_tree_sitter.mli
deleted file mode 100644
index a7671f1d7a3e..000000000000
--- a/languages/vue/generic/Parse_vue_tree_sitter.mli
+++ /dev/null
@@ -1,5 +0,0 @@
-(* the first argument is to parse the JS inside <script> *)
-val parse :
-  (Fpath.t -> AST_generic.program) ->
-  Fpath.t ->
-  (AST_generic.program, unit) Tree_sitter_run.Parsing_result.t
diff --git a/languages/vue/generic/dune b/languages/vue/generic/dune
deleted file mode 100644
index a0d841edd53f..000000000000
--- a/languages/vue/generic/dune
+++ /dev/null
@@ -1,13 +0,0 @@
-(library
- (public_name parser_vue.ast_generic)
- (name parser_vue_ast_generic)
- (wrapped false)
- (libraries
-   commons
-   ast_generic
-   semgrep_core; just for Range
-   lib_parsing_tree_sitter
-   tree-sitter-lang.vue
- )
- (preprocess (pps ppx_deriving.show))
-)
diff --git a/languages/vue/tree-sitter/semgrep-vue b/languages/vue/tree-sitter/semgrep-vue
deleted file mode 160000
index 2672b0cc747e..000000000000
--- a/languages/vue/tree-sitter/semgrep-vue
+++ /dev/null
@@ -1,1 +0,0 @@
-Subproject commit 2672b0cc747efce8773831f1107a72e3d1443c24
diff --git a/languages/yaml/generic/yaml_to_generic.ml b/languages/yaml/generic/yaml_to_generic.ml
index d7fdcfae66d7..01e8a1eb9171 100644
--- a/languages/yaml/generic/yaml_to_generic.ml
+++ b/languages/yaml/generic/yaml_to_generic.ml
@@ -97,7 +97,7 @@ let _pos_str
     s_index (s_line + 1) s_column e_index (e_line + 1) e_column
 
 let tok (index, line, column) str env =
-  let pos = Pos.make ~file:!!(env.file) ~line:(line + 1) ~column index in
+  let pos = Pos.make env.file ~line:(line + 1) ~column index in
   Tok.OriginTok { str; pos }
 
 let mk_tok ?(style : Y.scalar_style = `Plain)
diff --git a/libs/ast_generic/AST_generic_to_v1.ml b/libs/ast_generic/AST_generic_to_v1.ml
index 2b975cda13ea..31a43afa74f5 100644
--- a/libs/ast_generic/AST_generic_to_v1.ml
+++ b/libs/ast_generic/AST_generic_to_v1.ml
@@ -13,6 +13,7 @@
  * LICENSE for more details.
  *)
 open Common
+open Fpath_.Operators
 open OCaml (* map_of_string, map_of_option, ... *)
 open AST_generic
 module G = AST_generic
@@ -70,7 +71,7 @@ let ident_of_name = function
 (*****************************************************************************)
 
 let map_token_location { Tok.str; pos = { bytepos; line; column; file; _ } } =
-  { B.str; charpos = bytepos; line; column; filename = file }
+  { B.str; charpos = bytepos; line; column; filename = !!file }
 
 (* generated by ocamltarzan with: camlp4o -o /tmp/yyy.ml -I pa/ pa_type_conv.cmo pa_map.cmo  pr_o.cmo /tmp/xxx.ml  *)
 
diff --git a/libs/ast_generic/Meta_AST.ml b/libs/ast_generic/Meta_AST.ml
index 1628581cf500..92c6d3c5e8e0 100644
--- a/libs/ast_generic/Meta_AST.ml
+++ b/libs/ast_generic/Meta_AST.ml
@@ -2,6 +2,7 @@
  * Semgrep playground!
  * Use AST_generic.show_any instead.
  *)
+open Fpath_.Operators
 open Tok
 open AST_generic
 
@@ -30,7 +31,7 @@ let vof_token_location
         };
     } =
   let bnds = [] in
-  let arg = vof_filename v_file in
+  let arg = vof_filename !!v_file in
   let bnd = ("file", arg) in
   let bnds = bnd :: bnds in
   let arg = OCaml.vof_int v_column in
diff --git a/libs/commons/Chan.ml b/libs/commons/Chan.ml
index a369359962f5..50f7b7583b43 100644
--- a/libs/commons/Chan.ml
+++ b/libs/commons/Chan.ml
@@ -1,6 +1,6 @@
 (* Yoann Padioleau
  *
- * Copyright (C) 2023 Semgrep Inc.
+ * Copyright (C) 2023-2024 Semgrep Inc.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public License
@@ -29,7 +29,9 @@
 (* Types *)
 (*****************************************************************************)
 
-(* alt: "in", but reserved keyword, and "in_" is ugly *)
+(* alt: "in", but reserved keyword, and "in_" is ugly
+ * alt: use an Origin.t instead of Fpath.t? so can also get Stdin or Network?
+ *)
 
 type i = { ic : in_channel; p : Fpath.t }
 type o = { oc : out_channel; p : Fpath.t }
diff --git a/libs/commons/Filename_.ml b/libs/commons/Filename_.ml
index 2fd8b4cff47b..802b0a5494b8 100644
--- a/libs/commons/Filename_.ml
+++ b/libs/commons/Filename_.ml
@@ -43,6 +43,10 @@ let readable ~root s =
           failwith (spf "file %s shouldn't start with / when root is ." s)
       | s when s =~ "^\\./\\(.*\\)" -> Common.matched1 s
       | _ -> s)
+  (* ugly: to support readable "./foo/bar" "foo/bar/foo.c" *)
+  | _ when (not (s =~ "^\\./")) && root =~ "^\\./\\(.*\\)$" ->
+      let root = Common.matched1 root in
+      filename_without_leading_path root s
   | _ -> filename_without_leading_path root s
 
 (*****************************************************************************)
diff --git a/libs/commons/Fpath_.ml b/libs/commons/Fpath_.ml
index 7ea8fce048ec..4853dfbcaa4d 100644
--- a/libs/commons/Fpath_.ml
+++ b/libs/commons/Fpath_.ml
@@ -79,3 +79,13 @@ let () =
         split_ext ~multi:true (Fpath.v "a/foo.tar.gz")
         =*= (Fpath.v "a/foo", ".tar.gz")));
   ()
+
+(*****************************************************************************)
+(* Alias *)
+(*****************************************************************************)
+type t = Fpath.t [@@deriving show, eq, ord]
+
+let t_of_sexp (sexp : Sexplib.Sexp.t) : t =
+  Fpath.v (Sexplib.Std.string_of_sexp sexp)
+
+let sexp_of_t (t : t) : Sexplib.Sexp.t = Sexplib.Std.sexp_of_string !!t
diff --git a/libs/commons/Fpath_.mli b/libs/commons/Fpath_.mli
index 4ffd38c48f95..5eccb821574b 100644
--- a/libs/commons/Fpath_.mli
+++ b/libs/commons/Fpath_.mli
@@ -27,6 +27,9 @@ val to_strings : Fpath.t list -> string list
 val to_yojson : Fpath.t -> Yojson.Safe.t
 val of_yojson : Yojson.Safe.t -> (Fpath.t, string) result
 
+(* alias but with derived available *)
+type t = Fpath.t [@@deriving show, eq, ord, sexp]
+
 (*
    Take a nonempty list of path segments and turn them in to relative path.
    Only the last segment may by empty, representing a trailing slash.
diff --git a/libs/commons/Sys_.ml b/libs/commons/Sys_.ml
new file mode 100644
index 000000000000..0ec49ae0c2aa
--- /dev/null
+++ b/libs/commons/Sys_.ml
@@ -0,0 +1,167 @@
+(* Austin Theriault
+ *
+ * Copyright (C) Semgrep, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1 as published by the Free Software Foundation, with the
+ * special exception on linking described in file LICENSE.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
+ * LICENSE for more details.
+ *)
+
+(* Commentary *)
+(* Some helper code that should be in Sys.ml *)
+
+(*****************************************************************************)
+(* Types *)
+(*****************************************************************************)
+(* See https://man7.org/linux/man-pages/man7/signal.7.html*)
+type signal =
+  | SIGHUP
+  | SIGINT
+  | SIGQUIT
+  | SIGILL
+  | SIGTRAP
+  | SIGABRT (* alt: SIGIOT *)
+  | SIGBUS
+  | SIGFPE
+  | SIGKILL
+  | SIGUSR1
+  | SIGSEGV
+  | SIGUSR2
+  | SIGPIPE
+  | SIGALRM
+  | SIGTERM
+  | SIGSTKFLT
+  | SIGCHLD
+  | SIGCONT
+  | SIGSTOP
+  | SIGTSTP
+  | SIGTTIN
+  | SIGTTOU
+  | SIGURG
+  | SIGXCPU
+  | SIGXFSZ
+  | SIGVTALRM
+  | SIGPROF
+  | SIGWINCH
+  | SIGPOLL (* alt: SIGIO *)
+  | SIGPWR
+  | SIGSYS (* alt: SIGUNUSED *)
+  | UNKNOWN of int
+
+(*****************************************************************************)
+(* Code *)
+(*****************************************************************************)
+
+let signal_to_string = function
+  | SIGHUP -> "SIGHUP"
+  | SIGINT -> "SIGINT"
+  | SIGQUIT -> "SIGQUIT"
+  | SIGILL -> "SIGILL"
+  | SIGTRAP -> "SIGTRAP"
+  | SIGABRT -> "SIGABRT"
+  | SIGBUS -> "SIGBUS"
+  | SIGFPE -> "SIGFPE"
+  | SIGKILL -> "SIGKILL"
+  | SIGUSR1 -> "SIGUSR1"
+  | SIGSEGV -> "SIGSEGV"
+  | SIGUSR2 -> "SIGUSR2"
+  | SIGPIPE -> "SIGPIPE"
+  | SIGALRM -> "SIGALRM"
+  | SIGTERM -> "SIGTERM"
+  | SIGSTKFLT -> "SIGSTKFLT"
+  | SIGCHLD -> "SIGCHLD"
+  | SIGCONT -> "SIGCONT"
+  | SIGSTOP -> "SIGSTOP"
+  | SIGTSTP -> "SIGTSTP"
+  | SIGTTIN -> "SIGTTIN"
+  | SIGTTOU -> "SIGTTOU"
+  | SIGURG -> "SIGURG"
+  | SIGXCPU -> "SIGXCPU"
+  | SIGXFSZ -> "SIGXFSZ"
+  | SIGVTALRM -> "SIGVTALRM"
+  | SIGPROF -> "SIGPROF"
+  | SIGWINCH -> "SIGWINCH"
+  | SIGPOLL -> "SIGPOLL"
+  | SIGPWR -> "SIGPWR"
+  | SIGSYS -> "SIGSYS"
+  | UNKNOWN i -> Printf.sprintf "UNKNOWN(%d)" i
+
+(* NOTE these are not portable to Alpha/SPARC/MIPS/PARISC *)
+let signal_to_linux_signal = function
+  | SIGHUP -> 1
+  | SIGINT -> 2
+  | SIGQUIT -> 3
+  | SIGILL -> 4
+  | SIGTRAP -> 5
+  | SIGABRT -> 6
+  | SIGBUS -> 7
+  | SIGFPE -> 8
+  | SIGKILL -> 9
+  | SIGUSR1 -> 10
+  | SIGSEGV -> 11
+  | SIGUSR2 -> 12
+  | SIGPIPE -> 13
+  | SIGALRM -> 14
+  | SIGTERM -> 15
+  | SIGSTKFLT -> 16
+  | SIGCHLD -> 17
+  | SIGCONT -> 18
+  | SIGSTOP -> 19
+  | SIGTSTP -> 20
+  | SIGTTIN -> 21
+  | SIGTTOU -> 22
+  | SIGURG -> 23
+  | SIGXCPU -> 24
+  | SIGXFSZ -> 25
+  | SIGVTALRM -> 26
+  | SIGPROF -> 27
+  | SIGWINCH -> 28
+  | SIGPOLL -> 29
+  | SIGPWR -> 30
+  | SIGSYS -> 31
+  | UNKNOWN i -> i
+
+(* This is what a lot of Linux programs do, it's best practice according to the
+   advanced bash scripting guide
+
+   See: https://tldp.org/LDP/abs/html/exitcodes.html *)
+let signal_to_linux_exit_code signal = -128 - signal_to_linux_signal signal
+
+(* OCaml does not number signals normally, i.e. -9 != sigkill so let's convert
+   them to [signal] here. *)
+let ocaml_signal_to_signal = function
+  | s when s = USys.sigabrt -> SIGABRT
+  | s when s = USys.sigalrm -> SIGALRM
+  | s when s = USys.sigfpe -> SIGFPE
+  | s when s = USys.sighup -> SIGHUP
+  | s when s = USys.sigill -> SIGILL
+  | s when s = USys.sigint -> SIGINT
+  | s when s = USys.sigkill -> SIGKILL
+  | s when s = USys.sigpipe -> SIGPIPE
+  | s when s = USys.sigquit -> SIGQUIT
+  | s when s = USys.sigsegv -> SIGSEGV
+  | s when s = USys.sigterm -> SIGTERM
+  | s when s = USys.sigusr1 -> SIGUSR1
+  | s when s = USys.sigusr2 -> SIGUSR2
+  | s when s = USys.sigchld -> SIGCHLD
+  | s when s = USys.sigcont -> SIGCONT
+  | s when s = USys.sigstop -> SIGSTOP
+  | s when s = USys.sigtstp -> SIGTSTP
+  | s when s = USys.sigttin -> SIGTTIN
+  | s when s = USys.sigttou -> SIGTTOU
+  | s when s = USys.sigvtalrm -> SIGVTALRM
+  | s when s = USys.sigprof -> SIGPROF
+  | s when s = USys.sigbus -> SIGBUS
+  | s when s = USys.sigpoll -> SIGPOLL
+  | s when s = USys.sigsys -> SIGSYS
+  | s when s = USys.sigtrap -> SIGTRAP
+  | s when s = USys.sigurg -> SIGURG
+  | s when s = USys.sigxcpu -> SIGXCPU
+  | s when s = USys.sigxfsz -> SIGXFSZ
+  | other -> UNKNOWN other
diff --git a/libs/commons/Sys_.mli b/libs/commons/Sys_.mli
new file mode 100644
index 000000000000..b773de79caa8
--- /dev/null
+++ b/libs/commons/Sys_.mli
@@ -0,0 +1,46 @@
+(** [signal] is a nice data type representing most linux/posix/unix signals *)
+type signal =
+  | SIGHUP
+  | SIGINT
+  | SIGQUIT
+  | SIGILL
+  | SIGTRAP
+  | SIGABRT
+  | SIGBUS
+  | SIGFPE
+  | SIGKILL
+  | SIGUSR1
+  | SIGSEGV
+  | SIGUSR2
+  | SIGPIPE
+  | SIGALRM
+  | SIGTERM
+  | SIGSTKFLT
+  | SIGCHLD
+  | SIGCONT
+  | SIGSTOP
+  | SIGTSTP
+  | SIGTTIN
+  | SIGTTOU
+  | SIGURG
+  | SIGXCPU
+  | SIGXFSZ
+  | SIGVTALRM
+  | SIGPROF
+  | SIGWINCH
+  | SIGPOLL
+  | SIGPWR
+  | SIGSYS
+  | UNKNOWN of int
+
+val signal_to_string : signal -> string
+(** Pretty print a [signal] *)
+
+val signal_to_linux_signal : signal -> int
+(** Convert a [signal] to its linux signal number *)
+
+val signal_to_linux_exit_code : signal -> int
+(** Convert a [signal] to its linux exit code. This is [-128 - (signal_to_linux_signal s)] *)
+
+val ocaml_signal_to_signal : int -> signal
+(** Convert an ocaml signal [Sys.sig*] to a [signal] *)
diff --git a/libs/commons/UCommon.ml b/libs/commons/UCommon.ml
index a35af075b8b2..9445c7609fd7 100644
--- a/libs/commons/UCommon.ml
+++ b/libs/commons/UCommon.ml
@@ -79,8 +79,13 @@ let main_boilerplate f =
         let default_handler signal =
           Sys.Signal_handle
             (fun _ ->
+              let linux_signal = Sys_.ocaml_signal_to_signal signal in
+              let handled_name = Sys_.signal_to_string linux_signal in
               (* Feel free to match on signal here :D *)
-              pr2 "C-c intercepted, will do some cleaning before exiting";
+              pr2
+                (handled_name
+               ^ " signal intercepted, will do some cleaning before exiting");
+              USys.set_signal signal Sys.Signal_default;
               (* But if do some try ... with e -> and if do not reraise the exn,
                * the bubble never goes at top and so I cant really C-c.
                *
@@ -89,10 +94,8 @@ let main_boilerplate f =
                * The current solution is to not do some wild  try ... with e
                * by having in the exn handler a case: UnixExit x -> raise ... | e ->
                *)
-              USys.set_signal signal Sys.Signal_default;
-              (* TODO: Raise UnixExit with the actual signal code, not ocaml's,
-                 since theirs is random *)
-              raise (UnixExit signal))
+              (* Convert to a "standard" unix exit code from a unix signal *)
+              raise (UnixExit Sys_.(signal_to_linux_exit_code linux_signal)))
         in
 
         (* ref: https://faculty.cs.niu.edu/~hutchins/csci480/signals.htm *)
diff --git a/libs/lib_parsing/Parsing_helpers.ml b/libs/lib_parsing/Parsing_helpers.ml
index ef27f539f3fe..2fdad5f86a66 100644
--- a/libs/lib_parsing/Parsing_helpers.ml
+++ b/libs/lib_parsing/Parsing_helpers.ml
@@ -14,6 +14,7 @@
  * license.txt for more details.
  *)
 open Common
+open Fpath_.Operators
 module Log = Log_lib_parsing.Log
 
 (*****************************************************************************)
@@ -223,10 +224,10 @@ let error_message_token_location (info : Tok.location) =
   let filename = info.pos.file in
   let lexeme = info.str in
   let lexstart = info.pos.bytepos in
-  try error_messagebis filename (lexeme, lexstart) 0 with
+  try error_messagebis !!filename (lexeme, lexstart) 0 with
   | End_of_file ->
-      "PB in Common.error_message, position " ^ i_to_s lexstart
-      ^ " given out of file:" ^ filename
+      spf "PB in Common.error_message, position %d given out of file:%s"
+        lexstart !!filename
 
 let error_message_info info =
   let loc = Tok.unsafe_loc_of_tok info in
diff --git a/libs/lib_parsing/Pos.ml b/libs/lib_parsing/Pos.ml
index 0d4ede9b4336..5aaf085c6de1 100644
--- a/libs/lib_parsing/Pos.ml
+++ b/libs/lib_parsing/Pos.ml
@@ -15,6 +15,7 @@
  *)
 open Common
 open Sexplib.Std
+open Fpath_.Operators
 
 (*****************************************************************************)
 (* Prelude *)
@@ -61,8 +62,8 @@ type t = {
   (* Those two fields can be derived from bytepos (See complete_position() *)
   line : int; (* 1-based *)
   column : int; (* 0-based *)
-  (* TODO: use Fpath.t *)
-  file : string;
+  (* TODO: use an Src.t/Origin.t instead? (see spacegrep Src_file.source *)
+  file : Fpath_.t;
 }
 [@@deriving show, eq, ord, sexp]
 
@@ -72,18 +73,17 @@ type linecol = { l : int; c : int } [@@deriving show, eq]
 (* alt: could use @@deriving make.
  * TODO? should we use 0 instead? -1 clearly mark the field has not been set
  *)
-let make ?(line = -1) ?(column = -1) ?(file = "NO FILE INFO YET") bytepos =
+let make ?(line = -1) ?(column = -1) file bytepos =
   { bytepos; line; column; file }
 
 (*****************************************************************************)
 (* Helpers *)
 (*****************************************************************************)
 
-let fake_pos = make (-1)
-let first_pos_of_file file = make ~line:1 ~column:0 ~file 0
+let first_pos_of_file file = make ~line:1 ~column:0 (Fpath.v file) 0
 
 (* for error reporting *)
-let string_of_pos { file; line; column; _ } = spf "%s:%d:%d" file line column
+let string_of_pos { file; line; column; _ } = spf "%s:%d:%d" !!file line column
 let to_linecol { line; column; _ } = { l = line; c = column }
 
 (*****************************************************************************)
@@ -110,10 +110,10 @@ type bytepos_linecol_converters = {
  *   - in each lexer you need to take care of newlines and update manually
  *     the field.
  *)
-let complete_position filename converters (x : t) =
+let complete_position (filename : string) converters (x : t) =
   {
     x with
-    file = filename;
+    file = Fpath.v filename;
     line = fst (converters.bytepos_to_linecol_fun x.bytepos);
     column = snd (converters.bytepos_to_linecol_fun x.bytepos);
   }
diff --git a/libs/lib_parsing/Pos.mli b/libs/lib_parsing/Pos.mli
index a6e9dd2ba59f..52e55c3735bd 100644
--- a/libs/lib_parsing/Pos.mli
+++ b/libs/lib_parsing/Pos.mli
@@ -11,11 +11,11 @@ type t = {
   bytepos : int; (* 0-based *)
   line : int; (* 1-based *)
   column : int; (* 0-based *)
-  file : string;
+  file : Fpath.t;
 }
 [@@deriving show, eq, ord, sexp]
 
-val make : ?line:int -> ?column:int -> ?file:string -> int -> t
+val make : ?line:int -> ?column:int -> Fpath.t -> int -> t
 
 (* basic file position (used to be Common2.filepos) (used in codemap) *)
 type linecol = { l : int; c : int } [@@deriving show, eq]
@@ -24,7 +24,6 @@ type linecol = { l : int; c : int } [@@deriving show, eq]
 (* Helpers *)
 (*****************************************************************************)
 
-val fake_pos : t
 val first_pos_of_file : string (* filename *) -> t
 
 (* for error reporting *)
diff --git a/libs/lib_parsing/Tok.ml b/libs/lib_parsing/Tok.ml
index 27246dedc883..20586bdc7d86 100644
--- a/libs/lib_parsing/Tok.ml
+++ b/libs/lib_parsing/Tok.ml
@@ -164,8 +164,6 @@ let is_origintok ii =
   | OriginTok _ -> true
   | _ -> false
 
-let fake_location = { str = ""; pos = Pos.fake_pos }
-
 (* Synthesize a fake token *)
 let unsafe_fake_tok str : t = FakeTok (str, None)
 
@@ -273,18 +271,26 @@ let end_pos_of_loc loc =
 
 let tok_of_loc loc = OriginTok loc
 
-let tok_of_str_and_bytepos str pos =
+let make ~str ~file ~bytepos =
   let loc =
     {
       str;
       (* the pos will be filled in post-lexing phase, see complete_location *)
-      pos = Pos.make pos;
+      pos = Pos.make file bytepos;
     }
   in
   tok_of_loc loc
 
+(* TODO: we can't rely on Lexing.lexbuf.pos_fname to have
+ * been set correctly by the caller (or need an "origin" when lexbuf is stdin)
+ * and actually in many case where we do a Lexbuf.of_string, the
+ * pos_fname is the empty string
+ *)
 let tok_of_lexbuf lexbuf =
-  tok_of_str_and_bytepos (Lexing.lexeme lexbuf) (Lexing.lexeme_start lexbuf)
+  let fname = lexbuf.Lexing.lex_curr_p.pos_fname in
+  (* see Lexing.zero_pos *)
+  let file = if fname = "" then Fpath_.fake_file else Fpath.v fname in
+  make ~str:(Lexing.lexeme lexbuf) ~file ~bytepos:(Lexing.lexeme_start lexbuf)
 
 let first_loc_of_file file = { str = ""; pos = Pos.first_pos_of_file file }
 let first_tok_of_file file = fake_tok_loc (first_loc_of_file file) ""
@@ -372,19 +378,17 @@ let combine_sparse_toks ?(ignorable_newline = "\n") ?(ignorable_blank = ' ')
         String.length ignorable_newline - (newline_pos + 1)
     | None -> assert false
   in
-  let orig_pos =
-    match loc_of_tok first_tok with
-    | Error _ -> Pos.fake_pos
-    | Ok loc -> loc.pos
-  in
-  let current_line = ref orig_pos.line in
-  let current_column = ref orig_pos.column in
-  let buf = Buffer.create 100 in
-  let add_tok tok =
-    match loc_of_tok tok with
-    | Error _ -> ()
-    | Ok { str; pos } ->
-        (*
+  match loc_of_tok first_tok with
+  | Error _ -> None
+  | Ok { pos = orig_pos; _ } ->
+      let current_line = ref orig_pos.line in
+      let current_column = ref orig_pos.column in
+      let buf = Buffer.create 100 in
+      let add_tok tok =
+        match loc_of_tok tok with
+        | Error _ -> ()
+        | Ok { str; pos } ->
+            (*
            Insert padding before the token string to match the original
            line number, column, and byte offset.
 
@@ -409,49 +413,49 @@ let combine_sparse_toks ?(ignorable_newline = "\n") ?(ignorable_blank = ' ')
            found in the source file when converting a (line, col) position
            into a bytepos by consulting the original file.
         *)
-        let missing_newlines = max 0 (pos.line - !current_line) in
-        let missing_newline_bytes =
-          missing_newlines * String.length ignorable_newline
-        in
-        let column_after_adding_missing_newlines =
-          if missing_newlines > 0 then column_after_an_ignorable_newline
-          else !current_column
-        in
-        let missing_indent =
-          max 0 (pos.column - column_after_adding_missing_newlines)
-        in
-        let missing_bytes =
-          max 0
-            (pos.bytepos - orig_pos.bytepos - missing_newline_bytes
-           - missing_indent)
-        in
-        (* It's safe to insert missing bytes only if they're followed
-           by a newline that resets the indentation. *)
-        if missing_newlines > 0 then
-          for (* Adjust bytepos *)
-              _ = 1 to missing_bytes do
-            Buffer.add_char buf ignorable_blank;
-            incr current_column
-          done;
-        (* Adjust line number *)
-        for _ = 1 to missing_newlines do
-          Buffer.add_string buf ignorable_newline;
-          update_column current_column ignorable_newline
-        done;
-        (* Adjust column number *)
-        for _ = 1 to missing_indent do
-          Buffer.add_char buf ignorable_blank;
-          incr current_column
-        done;
-        (* Add the token string *)
-        Buffer.add_string buf str;
-        update_column current_column str;
-        let newlines_in_tok = count_char '\n' str in
-        current_line := !current_line + missing_newlines + newlines_in_tok
-  in
-  List.iter add_tok (first_tok :: toks);
-  let str = Buffer.contents buf in
-  OriginTok { str; pos = orig_pos }
+            let missing_newlines = max 0 (pos.line - !current_line) in
+            let missing_newline_bytes =
+              missing_newlines * String.length ignorable_newline
+            in
+            let column_after_adding_missing_newlines =
+              if missing_newlines > 0 then column_after_an_ignorable_newline
+              else !current_column
+            in
+            let missing_indent =
+              max 0 (pos.column - column_after_adding_missing_newlines)
+            in
+            let missing_bytes =
+              max 0
+                (pos.bytepos - orig_pos.bytepos - missing_newline_bytes
+               - missing_indent)
+            in
+            (* It's safe to insert missing bytes only if they're followed
+               by a newline that resets the indentation. *)
+            if missing_newlines > 0 then
+              for (* Adjust bytepos *)
+                  _ = 1 to missing_bytes do
+                Buffer.add_char buf ignorable_blank;
+                incr current_column
+              done;
+            (* Adjust line number *)
+            for _ = 1 to missing_newlines do
+              Buffer.add_string buf ignorable_newline;
+              update_column current_column ignorable_newline
+            done;
+            (* Adjust column number *)
+            for _ = 1 to missing_indent do
+              Buffer.add_char buf ignorable_blank;
+              incr current_column
+            done;
+            (* Add the token string *)
+            Buffer.add_string buf str;
+            update_column current_column str;
+            let newlines_in_tok = count_char '\n' str in
+            current_line := !current_line + missing_newlines + newlines_in_tok
+      in
+      List.iter add_tok (first_tok :: toks);
+      let str = Buffer.contents buf in
+      Some (OriginTok { str; pos = orig_pos })
 
 let empty_tok_after tok : t =
   match loc_of_tok tok with
diff --git a/libs/lib_parsing/Tok.mli b/libs/lib_parsing/Tok.mli
index c459919923df..049d067706ec 100644
--- a/libs/lib_parsing/Tok.mli
+++ b/libs/lib_parsing/Tok.mli
@@ -88,7 +88,7 @@ val tok_of_lexbuf : Lexing.lexbuf -> t
 val tok_of_loc : location -> t
 
 (* deprecated: TODO used only in Lexer_php.mll *)
-val tok_of_str_and_bytepos : string -> int -> t
+val make : str:string -> file:Fpath.t -> bytepos:int -> t
 
 (* the token will be empty, but its pos will be the beginning of the file *)
 val first_tok_of_file : string (* filename *) -> t
@@ -139,7 +139,11 @@ val combine_toks : t -> t list -> t
    - ignorable_blank: ' ' (space)
 *)
 val combine_sparse_toks :
-  ?ignorable_newline:string -> ?ignorable_blank:char -> t -> t list -> t
+  ?ignorable_newline:string ->
+  ?ignorable_blank:char ->
+  t ->
+  t list ->
+  t Option.t
 
 (* Create the empty token corresponding to the position right after a
    given token. This is intended for representing empty strings and such. *)
@@ -169,7 +173,6 @@ val is_origintok : t -> bool
 
 exception NoTokenLocation of string
 
-val fake_location : location
 val fake_tok : t -> string -> t
 val unsafe_fake_tok : string -> t
 
@@ -203,7 +206,7 @@ val content_of_tok_opt : t -> string option
 val line_of_tok : t -> int
 val col_of_tok : t -> int
 val bytepos_of_tok : t -> int
-val file_of_tok : t -> string (* filename *)
+val file_of_tok : t -> Fpath.t
 
 (* Token positions in loc.pos denote the beginning of a token.
    Suppose we are interested in having instead the line, column, and charpos
diff --git a/libs/lib_parsing/Unit_tok.ml b/libs/lib_parsing/Unit_tok.ml
index 4718502696fb..0280b2a2f9d2 100644
--- a/libs/lib_parsing/Unit_tok.ml
+++ b/libs/lib_parsing/Unit_tok.ml
@@ -18,7 +18,7 @@ let test_end_pos_of_loc () =
     let line, column, bytepos = start in
     let expected_line, expected_col, expected_charpos = expected in
     let loc : Tok.location =
-      { str; pos = { bytepos; line; column; file = "test.txt" } }
+      { str; pos = { bytepos; line; column; file = Fpath.v "test.txt" } }
     in
     let line, col, charpos = Tok.end_pos_of_loc loc in
     assert (line = expected_line);
@@ -33,7 +33,7 @@ let test_end_pos_of_loc () =
 
 let test_combine_sparse_toks () =
   let tok str line column bytepos : Tok.t =
-    let pos : Pos.t = { bytepos; line; column; file = "<none>" } in
+    let pos : Pos.t = { bytepos; line; column; file = Fpath_.fake_file } in
     OriginTok { str; pos }
   in
   let test tokens expected_combined_token =
@@ -46,7 +46,9 @@ let test_combine_sparse_toks () =
       Tok.combine_sparse_toks ~ignorable_newline:"\\\n" ~ignorable_blank:'-'
         first_tok other_toks
     in
-    check_token "equal" expected_combined_token res
+    match res with
+    | None -> assert false
+    | Some res -> check_token "equal" expected_combined_token res
   in
   (*
      Here, we test only realistic cases of token sequences that
diff --git a/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml b/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml
index 11af4f8372ff..7428d609172e 100644
--- a/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml
+++ b/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml
@@ -91,7 +91,7 @@ let token env (tok : Tree_sitter_run.Token.t) =
                  file %s. Invalid location for token %S."
                 line column !!file str))
   in
-  let pos = Pos.make ~line ~column ~file:!!file bytepos in
+  let pos = Pos.make ~line ~column file bytepos in
   let tok_loc = { Tok.str; pos } in
   Tok.tok_of_loc tok_loc
 
diff --git a/libs/ojsonnet/Parse_jsonnet.ml b/libs/ojsonnet/Parse_jsonnet.ml
index ad83107565df..fea587507afc 100644
--- a/libs/ojsonnet/Parse_jsonnet.ml
+++ b/libs/ojsonnet/Parse_jsonnet.ml
@@ -38,7 +38,7 @@ let loc_of_tree_sitter_error (err : Tree_sitter_run.Tree_sitter_error.t) =
   {
     Tok.str = err.substring;
     pos =
-      Pos.make ~file:err.file.name ~line:(start.row + 1) (* fake *)
+      Pos.make (Fpath.v err.file.name) ~line:(start.row + 1) (* fake *)
         ~column:start.column 0;
   }
 
diff --git a/libs/parallelism/Parmap_.ml b/libs/parallelism/Parmap_.ml
index 624ea32e1161..a46c6ac3f46e 100644
--- a/libs/parallelism/Parmap_.ml
+++ b/libs/parallelism/Parmap_.ml
@@ -1,3 +1,47 @@
+exception Parmap_unhandled_children of (string * int) list
+(** List of children spawned by [Parmap] that were not awaited correctly by
+    [Parmap]. The list is tuples of [status, code] where [code] is a standard
+    Unix exit code, and [status] is one of:
+    - [WEXITED] -> exited on its own
+    - [WSIGNALED] -> Received a signal like sigkill or sigsegv
+    - [WSTOPPED] -> process was paused for some reason, this is unlikely
+
+    This exception is usually raised when a child (process running [f] passed to
+    [parmap]) is killed from OOM, or segfaults, because Parmap does not handle
+    it. Sadly due to the way that Parmap is written, this may only be raised if
+    all children are unhandled, a lot of times Parmap in the parent process
+    (caller of [parmap]) will simply segfault if only some of the children
+    segfault/exit abnormally. This is a bug in Parmap, but because of OCaml 5 it
+    is unlikely to be fixed except by us. *)
+
+exception Parmap_marshalling_failure
+(** Parmap failed to unmarshal data from a child. This could be due to an
+    uncaught [Parmap_unhandled_children] exception, or more likely the type of
+    the value returned by the [f] passed to [parmap] was not what was expected,
+    and was shorter than the type expected (i.e. returned an [exn] when type
+    [some_large_struct_or_buffer] was expected). This happens due to how the
+    marshaling library works, as it just essentially reads a certain length of
+    data from some buffer somewhere, and when it expects to be able to keep
+    reading this data when unmarshaling, but reaches the end of the buffer, it
+    raises [End_of_file] which we then convert to this exception. This should
+    not happen as we try and catch all exceptions. Most likely this means an
+    exception is being raised in the [exception_handler] passed to [parmap] *)
+
+let () =
+  Printexc.register_printer (fun e ->
+      match e with
+      | Parmap_unhandled_children status_and_codes ->
+          let tuples =
+            status_and_codes
+            |> List_.map (fun (status_str, code) ->
+                   Printf.sprintf "(%s,%d)" status_str code)
+            |> String.concat ","
+          in
+          Some (Printf.sprintf "Parmap_unhandled_children([%s])" tuples)
+      | _ -> None)
+
+let debugging = Parmap.debugging
+
 let default_exception_handler (_x : 'a) (e : Exception.t) =
   Exception.to_string e
 
@@ -29,7 +73,85 @@ let parmap _caps ?init ?finalize ~ncores ~chunksize ~exception_handler f xs =
            datatype etc. *)
         Error (exception_handler x e)
   in
-  Parmap.parmap ?init ?finalize ~ncores ~chunksize f' (Parmap.L xs)
+  let finally () =
+    (* Parmap doesn't handle its child processes that well, and so if they exit
+       abnormally (e.g. segfault) then we have to clean up its mess.
+
+       See: Parmap_unhandled_children for more info *)
+    (* wait () will wait for any children that exited up until this point, and
+       return their reason for exiting and the associated code *)
+    let wait () =
+      (* status_and_codes is a list of (string * int) option list where the
+         string is the kind of exit a child process had, and the corresponding
+         code, or the item is None if the exit was normal. the same data is
+         returned, as on every call we call waitpid and try reaping another
+         child. See [Parmap_unhandled_children] for more info *)
+      let rec wait_aux status_and_codes =
+        try
+          (* WNOHANG so we don't block, and -1 to reap all exited children *)
+          let pid, status = Unix.waitpid [ Unix.WNOHANG ] (-1) in
+          (* If there are unreaped children that have yet to exit, let's not
+             wait since we only really care about the parmap children who
+             signaled/exit/stopped. This means some parmap children may become
+             zombies/keep running but usually this is a fatal error anyways so
+             we should be fine. *)
+          if pid = 0 then status_and_codes
+          else
+            let status_and_code_opt =
+              match status with
+              | WEXITED 0 ->
+                  (* This is a really weird edgecase, in which there is /some/
+                     process that exited normally, but was not reaped. The
+                     expectation here is that this is a parmap child process,
+                     and what most likely has happened is that End_of_file was
+                     raised before parmap could reap its own children. See the
+                     comment for Parmap_marshalling_failure for more info on
+                     this. the other option is that sometime before this parmap
+                     call, a child process was spawned and never reaped.*)
+                  (* nosemgrep here as this is almost always a fatal error so
+                     let's always print it *)
+                  (* nosemgrep: no-logs-in-library *)
+                  Logs.warn (fun m ->
+                      m
+                        "Child process %d exited normally but was not reaped \
+                         before a parmap call finished"
+                        pid);
+                  None
+              | WEXITED c -> Some ("WEXITED", c)
+              | WSTOPPED c -> Some ("WSTOPPED", c)
+              | WSIGNALED c -> Some ("WSIGNALED", c)
+            in
+            match status_and_code_opt with
+            | Some (status_str, code) ->
+                (* nosemgrep here as this is almost always a fatal error so
+                   let's always print it *)
+                (* nosemgrep: no-logs-in-library *)
+                Logs.err (fun m ->
+                    m "Parmap child %d was not reaped and exited with: %s: %d"
+                      pid status_str code);
+                wait_aux ((status_str, code) :: status_and_codes)
+            | None -> wait_aux status_and_codes
+        with
+        | Unix.Unix_error (Unix.ECHILD, _, _) -> status_and_codes
+      in
+      wait_aux []
+    in
+    match wait () with
+    | [] -> ()
+    | status_and_codes -> raise (Parmap_unhandled_children status_and_codes)
+  in
+  try
+    Common.protect ~finally (fun () ->
+        Parmap.parmap ?init ?finalize ~ncores ~chunksize f' (Parmap.L xs))
+  with
+  | End_of_file as exn ->
+      let e = Exception.catch exn in
+      (* nosemgrep here as this is almost always a fatal error so
+         let's always print it *)
+      (* nosemgrep: no-logs-in-library *)
+      Logs.err (fun m ->
+          m "Parmap marshalling failure: %s" Exception.(to_string e));
+      raise Parmap_marshalling_failure
 
 (* this is just because we forget every call to Parmap.$F in
  * TCB/forbid_process.jsonnet so we need that
diff --git a/libs/parallelism/Parmap_.mli b/libs/parallelism/Parmap_.mli
index c056294e1529..6423586e6da2 100644
--- a/libs/parallelism/Parmap_.mli
+++ b/libs/parallelism/Parmap_.mli
@@ -1,6 +1,21 @@
 (* Capability aware wrapper over Parmap (and a few helpers) *)
 
+exception Parmap_unhandled_children of (string * int) list
+(** List of children spawned by [Parmap] that were not awaited correctly by
+    [Parmap]. This usually happens when a child is sigkilled, or segfaults *)
+
+exception Parmap_marshalling_failure
+(** Parmap failed to unmarshal data from a child. This usually only happens when
+    an exception is raised in the [exception_handler] passed to [parmap] *)
+
+val debugging : bool -> unit
+(** [debugging true] will enable debugging in [Parmap]. This will print which
+    tasks are sent to what workers, how long they took, and some other info.
+    Useful for debugging issues unique to Parmap *)
+
 val default_exception_handler : 'a -> Exception.t -> string
+(** the default exception handler for [parmap], it will just convert the
+    exception to a string *)
 
 val parmap :
   < Cap.fork > ->
@@ -21,6 +36,12 @@ val parmap :
     [?init] takes in the job number and will run before any calls to [f].
     Similarly [?finalize] will run after all calls to [f].
 
+    Can raise [Parmap_unhandled_children] if [f] segfaults or OOMS, but only
+    sometimes. See [Parmap_unhandled_children] for more info.
+
+    Can raise [Parmap_marshalling_failure], usually if an exception is raised in
+    [exception_handler]
+
     Example:
     {[
       let f x = print_endline (string_of_int x); x+1 in
@@ -48,7 +69,7 @@ val parmap :
       Result: 4
       Result: 5
     ]}
- *)
+*)
 
 val disable_core_pinning : unit -> unit
 
diff --git a/release_changes.md b/release_changes.md
index e3effca076b9..cc9c0069fb7a 100644
--- a/release_changes.md
+++ b/release_changes.md
@@ -1,40 +1,32 @@
-## [1.92.0](https://github.com/returntocorp/semgrep/releases/tag/v1.92.0) - 2024-10-17
+## [1.93.0](https://github.com/returntocorp/semgrep/releases/tag/v1.93.0) - 2024-10-23
 
 
 ### Added
 
 
-- Pro: taint-mode: Semgrep has now basic support to track taint through callbacks,
-  when they lead to a sink, e.g.:
+- Improved naming for Common JS module imports (`require`) in arbitrary
+  expression contexts. Notably, in-line use of `require` should now be linked to
+  the correct module. For instance, the pattern `foo.bar` should now match
+  against `require('foo').bar` and taint is likewise similarily tracked. (code-7485)
+- Secrets: `semgrep ci` output now includes a list of all secrets rules which
+  generated at least one blocking finding (similar to Code) (code-7663)
+- Added experimental support via `--allow-dynamic-dependency-resolution` for dynamic resolution of Maven and Gradle dependencies for projects that do not have lockfiles (in Semgrep Pro only). (gh-2389)
+- Expanded support for pip requirement lockfiles is now available by default. Semgrep will now
+  find any *requirement*.txt file and lockfiles in a requirements folder (**/requirements/*.txt).
+  The existing experimental flag `--enable-experimental-requirements` is now deprecated and
+  will be removed in a future release. (gh-2441)
 
-      function unsafe_callback(x) {
-        sink(x); // finding here now !
-      }
 
-      function withCallback(val, callback) {
-        callback(val);
-      }
+### Changed
 
-      withCallback(taint, unsafe_callback); (code-7476)
-- New subcommand `dump-cst` for tree-sitter languages available via `semgrep
-  show`. This shows the concrete syntax tree for a given file. (code-7653)
-- Pro only: Updated C# parser supporting all versions of the language up to 13.0 (.NET 9) (saf-1610)
-- Added support for the Move-on-sui language! (sui)
-- Pro-only: semgrep test now supports the --pro flag to not only use pro languages
-  but also run the tests with the --pro-intrafile engine flag. If a finding
-  is detected only by the pro engine, please use `proruleid:` instead of `ruleid:`
-  and if an OSS finding is actually a false positive for the pro engine, please
-  add the `prook:` to your test annotation. (test_pro)
 
+- Removed support for Vue. The tree-sitter grammar has not been updated in 3 years,
+  there was no community rules added and semgrep-vue is causing linking conflicts
+  when compiling semgrep under Windows so just simpler to remove support for Vue.
+  In theory, extract mode could be a good substitute to parse Vue files. (vue)
 
-### Fixed
 
+### Fixed
 
-- pro: dataflow: Fixed a bug that could cause a class constructor to not be analyzed
-  in the correct dependency order, potentially leading to FNs. (code-7649)
-- Display an ✘ instead of a ✔ in the scan status print out when scanning with Semgrep OSS code
-  is not enabled. (grow-422)
-- semgrep will no longer randomly segfault when --trace is on with -j > 2 (saf-1590)
-- Previously, semgrep fails when --trace-endpoint is specified, but --trace is not.
 
-  Now, we relax this requirement a bit. In this case, we disable tracing, print out a warning, and continue to scan. (sms-550)
+- semgrep will now print exit codes if a segfault/OOM/other terminating signal happens in semgrep-core, or any of semgrep-core's child processes (saf-1646)
diff --git a/scripts/build-wheels.sh b/scripts/build-wheels.sh
index 2cdbd4278cb1..311ffd781d76 100755
--- a/scripts/build-wheels.sh
+++ b/scripts/build-wheels.sh
@@ -22,4 +22,9 @@ cd cli && python3 setup.py sdist bdist_wheel "$@"
 twine check dist/*.whl
 
 # Zipping for a stable name to upload as an artifact
-zip -r dist.zip dist
+if [[ "$OSTYPE" == "msys" ]]; then
+	# zip not available on windows CI runners
+	tar czvf dist.tgz dist
+else
+	zip -r dist.zip dist
+fi
diff --git a/semgrep.opam b/semgrep.opam
index ade53169f93c..6c6c0ece2916 100644
--- a/semgrep.opam
+++ b/semgrep.opam
@@ -1,6 +1,6 @@
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
-version: "1.91.0"
+version: "1.92.0"
 synopsis:
   "Like grep but for code: fast and syntax-aware semantic code pattern for many languages"
 description: """
diff --git a/setup.py b/setup.py
index b886eaf7b870..875cbfc35ba7 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
 
 setup(
     name="semgrep_pre_commit_package",
-    version="1.92.0",
-    install_requires=["semgrep==1.92.0"],
+    version="1.93.0",
+    install_requires=["semgrep==1.93.0"],
     packages=[],
 )
diff --git a/src/core/Core_error.ml b/src/core/Core_error.ml
index 7f26102299a9..08399358e24b 100644
--- a/src/core/Core_error.ml
+++ b/src/core/Core_error.ml
@@ -41,7 +41,12 @@ module Log = Log_semgrep.Log
  *)
 type t = {
   typ : Out.error_type;
-  loc : Tok.location;
+  (* TODO: get rid of option.
+   * Is None when we call error_of_rule_error() or exn_to_error() with an
+   * Fpath_.fake_file
+   * TODO: get rid of Fpath_.fake_file call sites
+   *)
+  loc : Tok.location option;
   msg : string;
   details : string option;
   rule_id : Rule_ID.t option;
@@ -108,7 +113,7 @@ let mk_error ?rule_id ?(msg = "") (loc : Tok.location) (err : Out.error_type) :
     | MissingPlugin ->
         msg
   in
-  { loc; typ = err; msg; details = None; rule_id }
+  { loc = Some loc; typ = err; msg; details = None; rule_id }
 
 let mk_error_tok opt_rule_id (file : Fpath.t) (tok : Tok.t) (msg : string)
     (err : Out.error_type) : t =
@@ -145,7 +150,8 @@ let error_of_rule_error (file : Fpath.t) (err : Rule_error.t) : t =
       {
         rule_id = Some rule_id;
         typ = Out.PatternParseError yaml_path;
-        loc = Tok.unsafe_loc_of_tok pos;
+        (* TODO: Switch to using option and report better info for figuring out why the location is missing *)
+        loc = Some (Tok.unsafe_loc_of_tok pos);
         msg =
           spf
             "Invalid pattern for %s:\n\
@@ -160,13 +166,15 @@ let error_of_rule_error (file : Fpath.t) (err : Rule_error.t) : t =
   | InvalidYaml (msg, pos) -> mk_error_tok rule_id file pos msg Out.InvalidYaml
   | DuplicateYamlKey (s, pos) -> mk_error_tok rule_id file pos s Out.InvalidYaml
   (* TODO?? *)
-  | UnparsableYamlException s ->
+  | UnparsableYamlException msg ->
       (* Based on what previously happened based on exn_to_error logic before
          converting Rule parsing errors to not be exceptions. *)
-      mk_error ?rule_id ~msg:s
-        (if not (Fpath_.is_fake_file file) then Tok.first_loc_of_file !!file
-         else Tok.fake_location)
-        Out.OtherParseError
+      if not (Fpath_.is_fake_file file) then
+        mk_error ?rule_id ~msg
+          (Tok.first_loc_of_file !!file)
+          Out.OtherParseError
+      else
+        { loc = None; typ = Out.OtherParseError; msg; details = None; rule_id }
 
 (*
    This function converts known exceptions to Semgrep errors.
@@ -237,8 +245,9 @@ let exn_to_error (rule_id : Rule_ID.t option) (file : Fpath.t) (e : Exception.t)
           let loc =
             (* TODO: we shouldn't build Tok.t w/out a filename, but
                lets do it here so we don't crash until we do *)
-            if not (Fpath_.is_fake_file file) then Tok.first_loc_of_file !!file
-            else Tok.fake_location
+            if not (Fpath_.is_fake_file file) then
+              Some (Tok.first_loc_of_file !!file)
+            else None
           in
           {
             rule_id;
@@ -271,11 +280,13 @@ let string_of_error err =
     | None -> ""
     | Some s -> spf "\n%s" s
   in
-  spf "%s:%d:%d: %s: %s%s"
-    (source_of_string pos.Tok.pos.file)
-    pos.Tok.pos.line pos.Tok.pos.column
-    (Out.string_of_error_type err.typ)
-    err.msg details
+  let loc =
+    match pos with
+    | None -> "<unknown location>"
+    | Some { pos = { file; line; column; _ }; _ } ->
+        spf "%s:%d:%d" (source_of_string !!file) line column
+  in
+  spf "%s: %s: %s%s" loc (Out.string_of_error_type err.typ) err.msg details
 
 let severity_of_error (typ : Out.error_type) : Out.error_severity =
   match typ with
diff --git a/src/core/Core_error.mli b/src/core/Core_error.mli
index 76e7799164d2..e2da606d1fe4 100644
--- a/src/core/Core_error.mli
+++ b/src/core/Core_error.mli
@@ -8,7 +8,7 @@
 
 type t = {
   typ : Semgrep_output_v1_t.error_type;
-  loc : Tok.location;
+  loc : Tok.location option;
   msg : string;
   (* ?? diff with msg? *)
   details : string option;
diff --git a/src/core/Metavariable.ml b/src/core/Metavariable.ml
index ac57de9ebfde..84acc2f035ca 100644
--- a/src/core/Metavariable.ml
+++ b/src/core/Metavariable.ml
@@ -217,7 +217,7 @@ let program_of_mvalue : mvalue -> G.program option =
           m "program_of_mvalue: not handled '%s'" (show_mvalue mval));
       None
 
-let range_of_mvalue mval =
+let range_of_mvalue (mval : mvalue) : (Fpath.t * Range.t) option =
   let* tok_start, tok_end =
     AST_generic_helpers.range_of_any_opt (mvalue_to_any mval)
   in
diff --git a/src/core/Metavariable.mli b/src/core/Metavariable.mli
index f12ad987a081..89783682acd6 100644
--- a/src/core/Metavariable.mli
+++ b/src/core/Metavariable.mli
@@ -55,7 +55,7 @@ type bindings = (mvar * mvalue) list [@@deriving show, eq]
 val location_aware_equal_mvalue : mvalue -> mvalue -> bool
 val ii_of_mval : mvalue -> Tok.t list
 val str_of_mval : mvalue -> string
-val range_of_mvalue : mvalue -> (string (* filename *) * Range.t) option
+val range_of_mvalue : mvalue -> (Fpath.t * Range.t) option
 
 (* we sometimes need to convert to an any to be able to use
  * Lib_AST.ii_of_any, or Lib_AST.abstract_position_info_any
diff --git a/src/core/Semgrep_output_utils.ml b/src/core/Semgrep_output_utils.ml
index da916de65482..b895966fa8bf 100644
--- a/src/core/Semgrep_output_utils.ml
+++ b/src/core/Semgrep_output_utils.ml
@@ -85,7 +85,7 @@ let position_range min_loc max_loc =
 
 let location_of_token_location loc =
   let start, end_ = position_range loc loc in
-  { path = Fpath.v loc.Tok.pos.file; start; end_ }
+  { path = loc.Tok.pos.file; start; end_ }
 
 (* None if pi has no location information. Fake tokens should have been
  * filtered out earlier, but in case one slipped through we handle this case.
diff --git a/src/core/Version.ml b/src/core/Version.ml
index efa8eba7736c..cb4299eb316d 100644
--- a/src/core/Version.ml
+++ b/src/core/Version.ml
@@ -3,4 +3,4 @@
 
   Automatically modified by scripts/release/bump.
 *)
-let version = "1.92.0"
+let version = "1.93.0"
diff --git a/src/core_scan/Report_pro_findings.ml b/src/core_scan/Report_pro_findings.ml
index eb6d2828f172..6c54a06791b5 100644
--- a/src/core_scan/Report_pro_findings.ml
+++ b/src/core_scan/Report_pro_findings.ml
@@ -1,3 +1,5 @@
+open Fpath_.Operators
+
 (*****************************************************************************)
 (* Purpose *)
 (*****************************************************************************)
@@ -39,7 +41,7 @@ let files_of_toks (ts : Pattern_match.pattern_match_tokens) : FileSet.t =
   |> List.fold_left
        (fun acc_files t ->
          match Tok.loc_of_tok t with
-         | Ok { pos; _ } -> FileSet.add pos.file acc_files
+         | Ok { pos; _ } -> FileSet.add !!(pos.file) acc_files
          | Error _ -> acc_files)
        FileSet.empty
 
diff --git a/src/engine/Eval_generic.ml b/src/engine/Eval_generic.ml
index ebb0815a58f5..5cfabd02ae39 100644
--- a/src/engine/Eval_generic.ml
+++ b/src/engine/Eval_generic.ml
@@ -504,7 +504,7 @@ let text_of_binding mvar mval =
       | Some (min, max) ->
           let file = min.Tok.pos.file in
           let range = Range.range_of_token_locations min max in
-          Some (Range.content_at_range (Fpath.v file) range))
+          Some (Range.content_at_range file range))
 
 let string_of_binding mvar mval =
   let* x = text_of_binding mvar mval in
diff --git a/src/engine/Metavariable_pattern.ml b/src/engine/Metavariable_pattern.ml
index d6be0e32af2b..06e99ba76dbd 100644
--- a/src/engine/Metavariable_pattern.ml
+++ b/src/engine/Metavariable_pattern.ml
@@ -13,7 +13,6 @@
  * LICENSE for more details.
  *)
 open Common
-open Fpath_.Operators
 open Match_env
 module MV = Metavariable
 module RM = Range_with_metavars
@@ -150,7 +149,7 @@ let get_nested_metavar_pattern_bindings get_nested_formula_matches env r mvar
               else pos.column
             in
             let pos =
-              Pos.make ~file:!!file ~column
+              Pos.make file ~column
                 ~line:(pos.line - mast_start_pos.line + 1)
                 (pos.bytepos - mast_start_pos.bytepos)
             in
@@ -168,7 +167,7 @@ let get_nested_metavar_pattern_bindings get_nested_formula_matches env r mvar
               else pos.column
             in
             let pos =
-              Pos.make ~file:mval_file ~column
+              Pos.make mval_file ~column
                 ~line:(pos.line + mast_start_pos.line - 1)
                 (pos.bytepos + mast_start_pos.bytepos)
             in
@@ -189,9 +188,7 @@ let get_nested_metavar_pattern_bindings get_nested_formula_matches env r mvar
                   (* Note that due to symbolic propagation, `mast` may be
                    * outside of the current file/AST, so we must get
                    * `mval_range` from `mval_file` and not from `env.file`! *)
-                  let contents =
-                    Range.content_at_range (Fpath.v mval_file) mval_range
-                  in
+                  let contents = Range.content_at_range mval_file mval_range in
                   (* TODO: find a way to not use tmp files! parse strings *)
                   (* nosemgrep: forbid-tmp *)
                   UTmp.with_temp_file ~contents ~suffix:".mvar-pattern"
@@ -240,7 +237,7 @@ let get_nested_metavar_pattern_bindings get_nested_formula_matches env r mvar
                 | _, MV.E { e = G.L (G.String (_, (content, _tok), _)); _ } ->
                     Some content
                 | (LSpacegrep | LAliengrep), _ ->
-                    Some (Range.content_at_range (Fpath.v mval_file) mval_range)
+                    Some (Range.content_at_range mval_file mval_range)
                 | _else_ -> None
               in
               match content with
@@ -316,7 +313,7 @@ let get_nested_metavar_pattern_bindings get_nested_formula_matches env r mvar
                             {
                               path =
                                 {
-                                  origin = File (Fpath.v mval_file);
+                                  origin = File mval_file;
                                   internal_path_to_content = file;
                                 };
                               xlang;
diff --git a/src/engine/Xpattern_match_aliengrep.ml b/src/engine/Xpattern_match_aliengrep.ml
index e485a1cdc162..29466a4574dd 100644
--- a/src/engine/Xpattern_match_aliengrep.ml
+++ b/src/engine/Xpattern_match_aliengrep.ml
@@ -1,13 +1,12 @@
 (*
    Wrapper around the aliengrep matcher (a generic mode variant)
 *)
-open Fpath_.Operators
 
 let convert_pos ~file (loc : Aliengrep.Match.loc) =
   (* single "token" spanning the whole match *)
   let bytepos = loc.start in
   let line, column = Xpattern_matcher.line_col_of_charpos file bytepos in
-  let pos = Pos.make ~file:!!file ~line ~column bytepos in
+  let pos = Pos.make file ~line ~column bytepos in
   { Tok.str = loc.substring; pos }
 
 let convert_loc ~file (loc : Aliengrep.Match.loc) =
@@ -18,7 +17,7 @@ let convert_loc ~file (loc : Aliengrep.Match.loc) =
   let end_pos =
     let bytepos = loc.start + loc.length in
     let line, column = Xpattern_matcher.line_col_of_charpos file bytepos in
-    let pos = Pos.make ~file:!!file ~line ~column bytepos in
+    let pos = Pos.make file ~line ~column bytepos in
     { Tok.str = ""; pos }
   in
   (start_pos, end_pos)
diff --git a/src/engine/Xpattern_match_regexp.ml b/src/engine/Xpattern_match_regexp.ml
index 24700d184123..d33c23f940c7 100644
--- a/src/engine/Xpattern_match_regexp.ml
+++ b/src/engine/Xpattern_match_regexp.ml
@@ -13,7 +13,6 @@
  * LICENSE for more details.
  *)
 open Common
-open Fpath_.Operators
 open Xpattern_matcher
 module MV = Metavariable
 module Log = Log_engine.Log
@@ -77,13 +76,13 @@ let regexp_matcher ?(base_offset = 0) regex_functions big_str (file : Fpath.t)
          let bytepos = bytepos + base_offset in
          let str = matched_str in
          let line, column = line_col_of_charpos file bytepos in
-         let pos = Pos.make ~file:!!file ~line ~column bytepos in
+         let pos = Pos.make file ~line ~column bytepos in
          let loc1 = { Tok.str; pos } in
 
          let bytepos = bytepos + String.length str in
          let str = "" in
          let line, column = line_col_of_charpos file bytepos in
-         let pos = Pos.make ~file:!!file ~line ~column bytepos in
+         let pos = Pos.make file ~line ~column bytepos in
          let loc2 = { Tok.str; pos } in
 
          (* the names of all capture groups within the regexp *)
@@ -107,7 +106,7 @@ let regexp_matcher ?(base_offset = 0) regex_functions big_str (file : Fpath.t)
                         in
                         let str = regex_functions.get_substring sub n in
                         let line, column = line_col_of_charpos file bytepos in
-                        let pos = Pos.make ~file:!!file ~line ~column bytepos in
+                        let pos = Pos.make file ~line ~column bytepos in
                         let loc = { Tok.str; pos } in
                         let t = Tok.tok_of_loc loc in
                         Some (spf "$%d" n, MV.Text (str, t, t))
@@ -137,7 +136,7 @@ let regexp_matcher ?(base_offset = 0) regex_functions big_str (file : Fpath.t)
                         name sub
                     in
                     let line, column = line_col_of_charpos file bytepos in
-                    let pos = Pos.make ~file:!!file ~line ~column bytepos in
+                    let pos = Pos.make file ~line ~column bytepos in
                     let loc = { Tok.str; pos } in
                     let t = Tok.tok_of_loc loc in
                     Some (spf "$%s" name, MV.Text (str, t, t))
diff --git a/src/engine/Xpattern_match_spacegrep.ml b/src/engine/Xpattern_match_spacegrep.ml
index 833c1a464958..ac7263d6165d 100644
--- a/src/engine/Xpattern_match_spacegrep.ml
+++ b/src/engine/Xpattern_match_spacegrep.ml
@@ -24,7 +24,7 @@ let lexing_pos_to_loc (file : Fpath.t) (x : Lexing.position) str =
    * JSON_report.json_range does the adjust_column + 1.
    *)
   let column = x.Lexing.pos_cnum - x.Lexing.pos_bol in
-  let pos = Pos.make ~line ~column ~file:!!file bytepos in
+  let pos = Pos.make ~line ~column file bytepos in
   { Tok.str; pos }
 
 let spacegrep_matcher (xconfig : Match_env.xconfig) (doc, src) (file : Fpath.t)
diff --git a/src/main/flags.sh b/src/main/flags.sh
index c00712d92764..7999a68d0822 100755
--- a/src/main/flags.sh
+++ b/src/main/flags.sh
@@ -157,8 +157,7 @@ else
             "solidity"
             "swift"
             "tsx"
-            "typescript"
-            "vue")
+            "typescript")
         FLAGS=("-noautolink")
         CCOPT=()
         # TODO: ideally much of this could be generated from dune/opam.
diff --git a/src/matching/Match_patterns.ml b/src/matching/Match_patterns.ml
index d6d1fd120223..74c607c6952c 100644
--- a/src/matching/Match_patterns.ml
+++ b/src/matching/Match_patterns.ml
@@ -461,7 +461,7 @@ let check ~hook ?(has_as_metavariable = false) ?mvar_context
                            "While matching pattern %s in file %s, we skipped \
                             expression at %d:%d-%d:%d (outside any range of \
                             interest)"
-                           rule.pattern_string start_loc.pos.file
+                           rule.pattern_string !!(start_loc.pos.file)
                            start_loc.pos.line start_loc.pos.column
                            end_loc.pos.line end_loc.pos.column);
                      ());
diff --git a/src/metachecking/Translate_rule.ml b/src/metachecking/Translate_rule.ml
index b9d2c82c4e4d..86909001a2b4 100644
--- a/src/metachecking/Translate_rule.ml
+++ b/src/metachecking/Translate_rule.ml
@@ -36,7 +36,7 @@ open Rule
 let range_to_string (range : (Tok.location * Tok.location) option) =
   match range with
   | Some (start, end_) ->
-      UFile.Legacy.with_open_infile start.pos.file (fun chan ->
+      UFile.with_open_in start.pos.file (fun chan ->
           let extract_size = end_.pos.bytepos - start.pos.bytepos in
           seek_in chan start.pos.bytepos;
           really_input_string chan extract_size)
diff --git a/src/osemgrep/cli/CLI.ml b/src/osemgrep/cli/CLI.ml
index 1a3385f850d0..717ab5e054dc 100644
--- a/src/osemgrep/cli/CLI.ml
+++ b/src/osemgrep/cli/CLI.ml
@@ -241,7 +241,7 @@ let dispatch_subcommand (caps : caps) (argv : string array) =
               subcmd_argv
         | "test" ->
             Test_subcommand.main
-              (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm >)
+              (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm ; Cap.tmp >)
               subcmd_argv
         | "validate" ->
             Validate_subcommand.main
diff --git a/src/osemgrep/cli_ci/Ci_CLI.ml b/src/osemgrep/cli_ci/Ci_CLI.ml
index c8bc22ec3c8f..7fda75e5d218 100644
--- a/src/osemgrep/cli_ci/Ci_CLI.ml
+++ b/src/osemgrep/cli_ci/Ci_CLI.ml
@@ -142,6 +142,14 @@ let o_x_dump_n_rule_partitions : int Term.t =
   let info = Arg.info [ "x-dump-rule-partitions" ] ~doc:{|Internal flag.|} in
   Arg.value (Arg.opt Arg.int 0 info)
 
+let o_x_partial_config : string Term.t =
+  let info = Arg.info [ "x-partial-config" ] ~doc:{|Internal flag.|} in
+  Arg.value (Arg.opt Arg.string "" info)
+
+let o_x_partial_output : string Term.t =
+  let info = Arg.info [ "x-partial-output" ] ~doc:{|Internal flag.|} in
+  Arg.value (Arg.opt Arg.string "" info)
+
 (*************************************************************************)
 (* 'scan' subset supported by 'ci' *)
 (*************************************************************************)
@@ -152,19 +160,20 @@ let o_x_dump_n_rule_partitions : int Term.t =
 let scan_subset_cmdline_term : Scan_CLI.conf Term.t =
   (* !The parameters must be in alphabetic orders to match the order
    * of the corresponding '$ o_xx $' further below! *)
-  let combine allow_untrusted_validators autofix baseline_commit common config
-      dataflow_traces diff_depth dryrun _dump_command_for_core emacs
-      emacs_outputs exclude_ exclude_minified_files exclude_rule_ids
-      experimental_requirements_lockfiles files_with_matches force_color
-      gitlab_sast gitlab_sast_outputs gitlab_secrets gitlab_secrets_outputs
-      _historical_secrets include_ incremental_output json json_outputs
-      junit_xml junit_xml_outputs matching_explanations max_chars_per_line
-      max_lines_per_finding max_log_list_entries max_memory_mb max_target_bytes
-      metrics num_jobs no_secrets_validation nosem optimizations oss output pro
-      pro_intrafile pro_lang pro_path_sensitive respect_gitignore
-      rewrite_rule_ids sarif sarif_outputs scan_unknown_extensions secrets text
-      text_outputs timeout _timeout_interfileTODO timeout_threshold trace
-      trace_endpoint version_check vim vim_outputs =
+  let combine allow_dynamic_dependency_resolution allow_untrusted_validators
+      autofix baseline_commit common config dataflow_traces diff_depth dryrun
+      _dump_command_for_core emacs emacs_outputs exclude_ exclude_minified_files
+      exclude_rule_ids experimental_requirements_lockfiles files_with_matches
+      force_color gitlab_sast gitlab_sast_outputs gitlab_secrets
+      gitlab_secrets_outputs _historical_secrets include_ incremental_output
+      json json_outputs junit_xml junit_xml_outputs matching_explanations
+      max_chars_per_line max_lines_per_finding max_log_list_entries
+      max_memory_mb max_target_bytes metrics num_jobs no_secrets_validation
+      nosem optimizations oss output pro pro_intrafile pro_lang
+      pro_path_sensitive respect_gitignore rewrite_rule_ids sarif sarif_outputs
+      scan_unknown_extensions secrets text text_outputs timeout
+      _timeout_interfileTODO timeout_threshold trace trace_endpoint
+      version_check vim vim_outputs =
     let output_format : Output_format.t =
       Scan_CLI.output_format_conf ~text ~files_with_matches ~json ~emacs ~vim
         ~sarif ~gitlab_sast ~gitlab_secrets ~junit_xml
@@ -279,16 +288,17 @@ let scan_subset_cmdline_term : Scan_CLI.conf Term.t =
         test = None;
         ls = false;
         experimental_requirements_lockfiles;
+        allow_dynamic_dependency_resolution;
       }
   in
   (* Term defines 'const' but also the '$' operator *)
   Term.(
     (* !the o_xxx must be in alphabetic orders to match the parameters of
      * combine above! *)
-    const combine $ SC.o_allow_untrusted_validators $ SC.o_autofix
-    $ SC.o_baseline_commit $ CLI_common.o_common $ o_config
-    $ SC.o_dataflow_traces $ SC.o_diff_depth $ SC.o_dryrun
-    $ SC.o_dump_command_for_core $ SC.o_emacs $ SC.o_emacs_outputs
+    const combine $ SC.o_allow_dynamic_dependency_resolution
+    $ SC.o_allow_untrusted_validators $ SC.o_autofix $ SC.o_baseline_commit
+    $ CLI_common.o_common $ o_config $ SC.o_dataflow_traces $ SC.o_diff_depth
+    $ SC.o_dryrun $ SC.o_dump_command_for_core $ SC.o_emacs $ SC.o_emacs_outputs
     $ SC.o_exclude $ SC.o_exclude_minified_files $ SC.o_exclude_rule_ids
     $ SC.o_experimental_requirements_lockfiles $ SC.o_files_with_matches
     $ SC.o_force_color $ SC.o_gitlab_sast $ SC.o_gitlab_sast_outputs
@@ -317,8 +327,8 @@ let cmdline_term : conf Term.t =
    * variables (Romain's idea).
    *)
   let combine scan_conf audit_on code secrets dry_run _internal_ci_scan_results
-      _x_dump_n_rule_partitions subdir supply_chain suppress_errors _git_meta
-      _github_meta =
+      _x_dump_n_rule_partitions _x_partial_config _x_partial_output subdir
+      supply_chain suppress_errors _git_meta _github_meta =
     let products =
       (if secrets then [ `Secrets ] else [])
       @ (if code then [ `SAST ] else [])
@@ -329,8 +339,9 @@ let cmdline_term : conf Term.t =
   Term.(
     const combine $ scan_subset_cmdline_term $ o_audit_on $ o_code
     $ SC.o_secrets $ o_dry_run $ o_internal_ci_scan_results
-    $ o_x_dump_n_rule_partitions $ o_subdir $ o_supply_chain $ o_suppress_errors
-    $ Git_metadata.env $ Github_metadata.env)
+    $ o_x_dump_n_rule_partitions $ o_x_partial_config $ o_x_partial_output
+    $ o_subdir $ o_supply_chain $ o_suppress_errors $ Git_metadata.env
+    $ Github_metadata.env)
 
 let doc = "the recommended way to run semgrep in CI"
 
diff --git a/src/osemgrep/cli_scan/Scan_CLI.ml b/src/osemgrep/cli_scan/Scan_CLI.ml
index 5a50560acc82..9a36d2196237 100644
--- a/src/osemgrep/cli_scan/Scan_CLI.ml
+++ b/src/osemgrep/cli_scan/Scan_CLI.ml
@@ -64,6 +64,7 @@ type conf = {
   test : Test_CLI.conf option;
   ls : bool;
   experimental_requirements_lockfiles : bool;
+  allow_dynamic_dependency_resolution : bool;
 }
 [@@deriving show]
 
@@ -110,6 +111,7 @@ let default : conf =
     test = None;
     ls = false;
     experimental_requirements_lockfiles = false;
+    allow_dynamic_dependency_resolution = false;
   }
 
 (*************************************************************************)
@@ -866,6 +868,21 @@ let o_experimental_requirements_lockfiles : bool Term.t =
   in
   Arg.value (Arg.flag info)
 
+(* This is just intended to be around temporarily while we roll out and test the feature. Once we
+   are confident that the lockfileless
+   approach will not cause failures for customers, we should remove this flag and replace it with
+   a flag to _disable_ dynamic dependency resolution.
+   TODO: (bk) delete this flag
+*)
+let o_allow_dynamic_dependency_resolution : bool Term.t =
+  let info =
+    Arg.info
+      [ "allow-dynamic-dependency-resolution" ]
+      ~doc:
+        {|Experimental: allow resolving dependencies dynamically by communicating with package managers during the scan.|}
+  in
+  Arg.value (Arg.flag info)
+
 (* ------------------------------------------------------------------ *)
 (* Test and debug options *)
 (* ------------------------------------------------------------------ *)
@@ -1245,20 +1262,21 @@ let test_CLI_conf ~test ~target_roots ~config ~json ~optimizations
 let cmdline_term caps ~allow_empty_config : conf Term.t =
   (* !The parameters must be in alphabetic orders to match the order
    * of the corresponding '$ o_xx $' further below! *)
-  let combine allow_untrusted_validators autofix baseline_commit common config
-      dataflow_traces diff_depth dryrun dump_ast dump_command_for_core
-      dump_engine_path emacs emacs_outputs error exclude_ exclude_minified_files
-      exclude_rule_ids experimental_requirements_lockfiles files_with_matches
-      force_color gitlab_sast gitlab_sast_outputs gitlab_secrets
-      gitlab_secrets_outputs _historical_secrets include_ incremental_output
-      json json_outputs junit_xml junit_xml_outputs lang ls
-      matching_explanations max_chars_per_line max_lines_per_finding
-      max_log_list_entries max_memory_mb max_target_bytes metrics num_jobs
-      no_secrets_validation nosem optimizations oss output pattern pro
-      project_root pro_intrafile pro_lang pro_path_sensitive remote replacement
-      respect_gitignore rewrite_rule_ids sarif sarif_outputs
-      scan_unknown_extensions secrets severity show_supported_languages strict
-      target_roots test test_ignore_todo text text_outputs time_flag timeout
+  let combine allow_dynamic_dependency_resolution allow_untrusted_validators
+      autofix baseline_commit common config dataflow_traces diff_depth dryrun
+      dump_ast dump_command_for_core dump_engine_path emacs emacs_outputs error
+      exclude_ exclude_minified_files exclude_rule_ids
+      experimental_requirements_lockfiles files_with_matches force_color
+      gitlab_sast gitlab_sast_outputs gitlab_secrets gitlab_secrets_outputs
+      _historical_secrets include_ incremental_output json json_outputs
+      junit_xml junit_xml_outputs lang ls matching_explanations
+      max_chars_per_line max_lines_per_finding max_log_list_entries
+      max_memory_mb max_target_bytes metrics num_jobs no_secrets_validation
+      nosem optimizations oss output pattern pro project_root pro_intrafile
+      pro_lang pro_path_sensitive remote replacement respect_gitignore
+      rewrite_rule_ids sarif sarif_outputs scan_unknown_extensions secrets
+      severity show_supported_languages strict target_roots test
+      test_ignore_todo text text_outputs time_flag timeout
       _timeout_interfileTODO timeout_threshold trace trace_endpoint
       _use_osemgrep_sarif validate version version_check vim vim_outputs =
     let target_roots, imply_always_select_explicit_targets =
@@ -1439,13 +1457,15 @@ let cmdline_term caps ~allow_empty_config : conf Term.t =
       trace_endpoint;
       ls;
       experimental_requirements_lockfiles;
+      allow_dynamic_dependency_resolution;
     }
   in
   (* Term defines 'const' but also the '$' operator *)
   Term.(
     (* !the o_xxx must be in alphabetic orders to match the parameters of
      * combine above! *)
-    const combine $ o_allow_untrusted_validators $ o_autofix $ o_baseline_commit
+    const combine $ o_allow_dynamic_dependency_resolution
+    $ o_allow_untrusted_validators $ o_autofix $ o_baseline_commit
     $ CLI_common.o_common $ o_config $ o_dataflow_traces $ o_diff_depth
     $ o_dryrun $ o_dump_ast $ o_dump_command_for_core $ o_dump_engine_path
     $ o_emacs $ o_emacs_outputs $ o_error $ o_exclude $ o_exclude_minified_files
diff --git a/src/osemgrep/cli_scan/Scan_CLI.mli b/src/osemgrep/cli_scan/Scan_CLI.mli
index f62652b75792..22550ba28c04 100644
--- a/src/osemgrep/cli_scan/Scan_CLI.mli
+++ b/src/osemgrep/cli_scan/Scan_CLI.mli
@@ -41,6 +41,7 @@ type conf = {
   test : Test_CLI.conf option;
   ls : bool;
   experimental_requirements_lockfiles : bool;
+  allow_dynamic_dependency_resolution : bool;
 }
 [@@deriving show]
 
@@ -69,6 +70,7 @@ val o_exclude : string list Cmdliner.Term.t
 val o_secrets : bool Cmdliner.Term.t
 
 (* exported because used by Ci_CLI.ml *)
+val o_allow_dynamic_dependency_resolution : bool Cmdliner.Term.t
 val o_allow_untrusted_validators : bool Cmdliner.Term.t
 val o_autofix : bool Cmdliner.Term.t
 val o_baseline_commit : string option Cmdliner.Term.t
diff --git a/src/osemgrep/cli_scan/Scan_subcommand.ml b/src/osemgrep/cli_scan/Scan_subcommand.ml
index 9c99ff43b169..b3d1e81a257a 100644
--- a/src/osemgrep/cli_scan/Scan_subcommand.ml
+++ b/src/osemgrep/cli_scan/Scan_subcommand.ml
@@ -885,7 +885,7 @@ let run_conf (caps : caps) (conf : Scan_CLI.conf) : Exit_code.t =
       Exit_code.ok ~__LOC__
   | _ when conf.test <> None ->
       Test_subcommand.run_conf
-        (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm >)
+        (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm ; Cap.tmp >)
         (Common2.some conf.test)
   | _ when conf.validate <> None ->
       Validate_subcommand.run_conf
diff --git a/src/parsing/Parse_target.ml b/src/parsing/Parse_target.ml
index 55b710e5d496..e11c3ea8f63f 100644
--- a/src/parsing/Parse_target.ml
+++ b/src/parsing/Parse_target.ml
@@ -41,7 +41,7 @@ let errors_from_skipped_tokens xs =
   | [] -> Core_error.ErrorSet.empty
   | x :: _ ->
       let e = exn_of_loc x in
-      let err = E.exn_to_error None (Fpath.v x.Tok.pos.file) e in
+      let err = E.exn_to_error None x.Tok.pos.file e in
       let locs =
         xs |> List_.map Semgrep_output_utils.location_of_token_location
       in
diff --git a/src/parsing/Parsing_result2.ml b/src/parsing/Parsing_result2.ml
index cbc46e0593d1..3623e6d211a2 100644
--- a/src/parsing/Parsing_result2.ml
+++ b/src/parsing/Parsing_result2.ml
@@ -30,7 +30,7 @@ let loc_of_tree_sitter_error (err : Err.t) =
   {
     Tok.str = err.substring;
     pos =
-      Pos.make ~file:err.file.name ~line:(start.row + 1) (* fake *)
+      Pos.make (Fpath.v err.file.name) ~line:(start.row + 1) (* fake *)
         ~column:start.column 0;
   }
 
diff --git a/src/parsing/Unit_parsing.ml b/src/parsing/Unit_parsing.ml
index f247bc86d51b..bea4c29fc6e6 100644
--- a/src/parsing/Unit_parsing.ml
+++ b/src/parsing/Unit_parsing.ml
@@ -228,7 +228,6 @@ let langs_with_error_tolerance =
     (Lang.Hack, Strict);
     (Lang.Html, Strict);
     (Lang.Xml, Strict);
-    (Lang.Vue, Strict);
     (Lang.R, Strict);
     (Lang.Solidity, Strict);
     (Lang.Julia, Strict);
diff --git a/src/parsing_languages/Parse_target2.ml b/src/parsing_languages/Parse_target2.ml
index d6fd447ff212..988232f2f618 100644
--- a/src/parsing_languages/Parse_target2.ml
+++ b/src/parsing_languages/Parse_target2.ml
@@ -164,20 +164,7 @@ let just_parse_with_lang lang file : Parsing_result2.t =
       run file
         [ TreeSitter (Parse_typescript_tree_sitter.parse ?dialect:None) ]
         Js_to_generic.program
-  | Lang.Vue ->
-      let parse_embedded_js file =
-        let { Parsing_result2.ast; errors; _ } =
-          Parse_target.just_parse_with_lang Lang.Js file
-        in
-        (* TODO: pass the errors down to Parse_vue_tree_sitter.parse
-         * and accumulate with other vue parse errors
-         *)
-        if errors <> [] then failwith "parse error in embedded JS";
-        ast
-      in
-      run file
-        [ TreeSitter (Parse_vue_tree_sitter.parse parse_embedded_js) ]
-        (fun x -> x)
+  | Lang.Vue -> failwith "Vue support has been removed in 1.93.0"
   (* there is no pfff parsers for C#/Kotlin/... so let's just use
    * tree-sitter, and there's no ast_xxx.ml either so we directly generate
    * a generic AST (no calls to an xxx_to_generic() below)
diff --git a/src/parsing_languages/dune b/src/parsing_languages/dune
index 8c02cdd17fff..05767301281e 100644
--- a/src/parsing_languages/dune
+++ b/src/parsing_languages/dune
@@ -53,6 +53,5 @@
    parser_move_on_sui.ast_generic
    parser_move_on_aptos.ast_generic
    parser_circom.ast_generic
-   parser_vue.ast_generic
  )
 )
diff --git a/src/reporting/Core_json_output.ml b/src/reporting/Core_json_output.ml
index 43b651e78c4a..7de556dcbd79 100644
--- a/src/reporting/Core_json_output.ml
+++ b/src/reporting/Core_json_output.ml
@@ -333,9 +333,9 @@ let unsafe_match_to_match
     *)
     | File path ->
         if
-          (!!path <> min_loc.pos.file || !!path <> max_loc.pos.file)
-          && min_loc.pos.file <> "FAKE TOKEN LOCATION"
-        then (Fpath.v min_loc.pos.file, None)
+          (path <> min_loc.pos.file || path <> max_loc.pos.file)
+          && not (Fpath_.is_fake_file min_loc.pos.file)
+        then (min_loc.pos.file, None)
         else (path, None)
     (* TODO(cooper): if we can have a uri or something more general than a
      * file path here then we can stop doing this hack. *)
@@ -415,21 +415,23 @@ let match_to_match (x : Core_result.processed_match) :
  * so we would not need those conversions
  *)
 let error_to_error (err : Core_error.t) : Out.core_error =
-  let file = err.loc.pos.file in
-  let startp, endp = OutUtils.position_range err.loc err.loc in
+  let location =
+    match err.loc with
+    | Some loc ->
+        let file = loc.pos.file in
+        let startp, endp = OutUtils.position_range loc loc in
+        { Out.path = file; start = startp; end_ = endp }
+    (* TODO get rid of option in err.loc *)
+    | None ->
+        let pos = { Out.line = 1; col = 1; offset = 0 } in
+        { Out.path = Fpath_.fake_file; start = pos; end_ = pos }
+  in
   let rule_id = err.rule_id in
   let error_type = err.typ in
   let severity = E.severity_of_error err.typ in
   let message = err.msg in
   let details = err.details in
-  {
-    error_type;
-    rule_id;
-    severity;
-    location = { path = Fpath.v file; start = startp; end_ = endp };
-    message;
-    details;
-  }
+  { error_type; rule_id; severity; location; message; details }
 
 let extra_to_extra (extra : Matching_explanation.extra) :
     Out.matching_explanation_extra =
diff --git a/src/reporting/Metavar_replacement.ml b/src/reporting/Metavar_replacement.ml
index 0eefe1f5cea7..1ad949ab10b9 100644
--- a/src/reporting/Metavar_replacement.ml
+++ b/src/reporting/Metavar_replacement.ml
@@ -62,9 +62,7 @@ let of_bindings bindings =
          match MV.range_of_mvalue mval with
          | None -> None
          | Some (file, mval_range) ->
-             let mval_content =
-               lazy (Range.content_at_range (Fpath.v file) mval_range)
-             in
+             let mval_content = lazy (Range.content_at_range file mval_range) in
              let propagated_content = propagated_value_string_of_mval mval in
              Some (mvar, { mval_content; propagated_content }))
 
diff --git a/src/reporting/Nosemgrep.ml b/src/reporting/Nosemgrep.ml
index d60df8afad7b..49a02bf4cfc3 100644
--- a/src/reporting/Nosemgrep.ml
+++ b/src/reporting/Nosemgrep.ml
@@ -193,7 +193,7 @@ let rule_match_nosem (pm : Pattern_match.t) : bool * Core_error.t list =
                       bytepos = linecol_to_bytepos_fun (line_num, col);
                       line = line_num;
                       column = col;
-                      file = !!path;
+                      file = path;
                     };
               }
           in
@@ -234,7 +234,7 @@ let rule_match_nosem (pm : Pattern_match.t) : bool * Core_error.t list =
                   Core_error.rule_id = None;
                   typ = SemgrepWarning;
                   msg;
-                  loc;
+                  loc = Some loc;
                   details = None;
                 }
               in


Test patch:
diff --git a/.github/workflows/build-test-windows-x86.jsonnet b/.github/workflows/build-test-windows-x86.jsonnet
new file mode 100644
index 000000000000..7aa8b0f2b512
--- /dev/null
+++ b/.github/workflows/build-test-windows-x86.jsonnet
@@ -0,0 +1,238 @@
+local actions = import 'libs/actions.libsonnet';
+local gha = import 'libs/gha.libsonnet';
+local semgrep = import 'libs/semgrep.libsonnet';
+
+// actually not exported for now to other workflows, but we might,
+// and at least can be downloaded from the GHA job page.
+local artifact_name = 'semgrep-core-and-dependent-libs-w64-artifact-${{ github.sha }}';
+
+local wheel_name = 'windows-x86-wheel';
+local runs_on = 'windows-latest';
+local defaults = {
+  run: {
+    // Windows GHA runners default to pwsh (PowerShell). We want to use bash
+    // to be consistent with our other workflows.
+    shell: 'bash',
+  },
+};
+
+// ----------------------------------------------------------------------------
+// The job
+// ----------------------------------------------------------------------------
+local build_core_job = {
+  'runs-on': runs_on,
+  defaults: defaults,
+  steps: [
+    actions.checkout_with_submodules(),
+    {
+      uses: 'ocaml/setup-ocaml@v2',
+      with: {
+        'ocaml-compiler': '4.14',
+        // we switch from fdopen's opam mingw repo to the official one
+        // otherwise we can't install recent packages like ocamlformat 0.26.2
+        // the opam-repository-mingw has the "sunset" branch because it should
+        // soon be unecessary once opam 2.2 is released.
+        'opam-repositories': |||
+           opam-repository-mingw: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset
+           default: https://github.com/ocaml/opam-repository.git
+        |||,
+        // bogus filename to prevent the action from attempting to install
+        // anything (we want deps only)
+        'opam-local-packages': 'dont_install_local_packages.opam',
+      },
+    },
+    // Why this cache when ocaml/setup-ocaml is already caching things?
+    // - setup-ocaml caches the cygwin and downloaded opam packages, but not the
+    //   installed opam packages
+    // - without the _opam cache we would spend 8-9 minutes every build
+    //   running `opam install`
+    // Note: we must cache after setup-ocaml, not before, because
+    // setup-ocaml would reset the cached _opam
+    semgrep.cache_opam.step(
+      key=semgrep.opam_switch + "-${{ hashFiles('semgrep.opam') }}",
+      // ocaml/setup-ocaml creates the opam switch local to the repository
+      // (vs. ~/.opam in our other workflows)
+      path='_opam',
+    ),
+    { name: 'Debug stuff',
+      run: |||
+        ls
+        # to see the bin symlink for example
+        ls -l
+        set
+        # tree-sitter fails to compile without an ar, you can use
+        # CC=x86_64-w64-mingw32-gcc but there is no AR=x86_64-w64-mingw32-ar
+        which ar
+        ar --version
+        # GHA installs cygwin in a special place
+        export PATH="${CYGWIN_ROOT_BIN}:${PATH}"
+        which ar
+        ar --version
+        which opam
+        # this should be fdopen's opan, so 2.0.10
+        opam --version
+        opam repo
+        # we should be on 4.14.0~mingw
+        opam switch
+     |||,
+    },
+    {
+      name: 'Build tree-sitter',
+      env: {
+        CC: 'x86_64-w64-mingw32-gcc',
+      },
+      // TODO: ideally we should reuse 'make install-deps-for-semgrep-core'
+      // but we do a few things differently here for windows (same issue
+      // with our HomeBrew formula which has some special tree-sitter
+      // installation)
+      // TODO: move this in a script, but I got issue executing this script
+      // from windows, weird
+      run: |||
+        cd libs/ocaml-tree-sitter-core
+        ./configure
+        ./scripts/download-tree-sitter --lazy
+        prefix="$(pwd)/tree-sitter"
+        cd downloads/tree-sitter
+        make PREFIX="$prefix" CFLAGS="-O3 -Wall -Wextra"
+        make PREFIX="$prefix" install
+      |||,
+    },
+    // this should be mostly a noop thx to cache_opam above
+    // TODO: we should also reuse 'make install-deps-for-semgrep-core'
+    {
+      name: 'Install OPAM deps',
+      run: |||
+        export PATH="${CYGWIN_ROOT_BIN}:${PATH}"
+        make install-deps-WINDOWS-for-semgrep-core
+        make install-opam-deps
+      |||,
+    },
+    {
+      name: 'Build semgrep-core',
+      run: |||
+        export PATH=\"${CYGWIN_ROOT_BIN}:${PATH}\"
+        export TREESITTER_INCDIR=$(pwd)/libs/ocaml-tree-sitter-core/tree-sitter/include
+        export TREESITTER_LIBDIR=$(pwd)/libs/ocaml-tree-sitter-core/tree-sitter/lib
+        # We have to strip rpath from the tree-sitter projects because there's no
+        # equivalent in Windows
+        # TODO: investigate removing rpath from the tree-sitter projects
+        for filename in $(find ./languages/ ./libs/ocaml-tree-sitter-core/ -name dune); do
+          grep -v rpath $filename > $filename.new
+          mv $filename.new $filename
+        done
+        opam exec -- dune build _build/install/default/bin/semgrep-core.exe
+      |||,
+    },
+    {
+      name: 'Test semgrep-core',
+      //TODO: semgrep-core displays also parse errors in the JSON output
+      // weird. CRLF windows issue?
+      run: |||
+        _build/install/default/bin/semgrep-core.exe -l python -rules tests/windows/rules.yml -json tests/windows/test.py
+      |||,
+    },
+    {
+      name: 'Package semgrep-core',
+      run: |||
+        mkdir artifacts
+        cp _build/install/default/bin/semgrep-core.exe artifacts/
+
+        # TODO: somehow upgrade to the latest flexdll, which should allow us
+        # to statically link these libraries
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libstdc++-6.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libgcc_s_seh-1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libwinpthread-1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libpcre-1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libgmp-10.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libcurl-4.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libpcre2-8-0.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libeay32.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libidn2-0.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libnghttp2-14.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libssh2-1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/ssleay32.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libzstd-1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/zlib1.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/iconv.dll artifacts/
+        cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libintl-8.dll artifacts/
+
+        tar czvf artifacts.tgz artifacts
+     |||,
+    },
+    actions.upload_artifact_step(artifact_name),
+  ],
+};
+
+local build_wheels_job = {
+  'runs-on': runs_on,
+  defaults: defaults,
+  needs: [
+    'build-core',
+  ],
+  steps: [
+    actions.checkout_with_submodules(),
+    actions.download_artifact_step(artifact_name),
+    {
+      env: {
+        "SEMGREP_FORCE_INSTALL": 1
+      },
+      run: |||
+        tar xvfz artifacts.tgz
+        cp artifacts/semgrep-core cli/src/semgrep/bin
+        ./scripts/build-wheels.sh --plat-name win_amd64
+      |||,
+    },
+    {
+      uses: 'actions/upload-artifact@v3',
+      with: {
+        path: 'cli/dist.tgz',
+        name: wheel_name,
+      },
+    },
+  ]
+};
+
+local test_wheels_job = {
+  'runs-on': runs_on,
+  defaults: defaults,
+  needs: [
+    'build-wheels',
+  ],
+  steps: [
+    actions.download_artifact_step(wheel_name),
+    {
+      run: 'tar xzvf dist.tgz',
+    },
+    // *.whl is fine here because we're building one wheel with the "any"
+    // platform compatibility tag
+    {
+      name: 'install package',
+      run: 'pip3 install dist/*.whl',
+    },
+    {
+      name: 'test package',
+      run: 'semgrep --version',
+    },
+    {
+      name: 'e2e semgrep-core test',
+      run: 'echo \'1 == 1\' | semgrep -l python -e \'$X == $X\' -'
+    },
+  ],
+};
+
+// ----------------------------------------------------------------------------
+// The workflow
+// ----------------------------------------------------------------------------
+
+{
+  name: 'build-test-windows-x86',
+  on: {
+    workflow_dispatch: semgrep.cache_opam.inputs(required=true),
+    workflow_call: semgrep.cache_opam.inputs(required=false),
+  },
+  jobs: {
+    'build-core': build_core_job,
+    'build-wheels': build_wheels_job,
+    'test-wheels': test_wheels_job,
+  },
+}
diff --git a/.github/workflows/build-test-windows-x86.yml b/.github/workflows/build-test-windows-x86.yml
new file mode 100644
index 000000000000..88d9e6502a13
--- /dev/null
+++ b/.github/workflows/build-test-windows-x86.yml
@@ -0,0 +1,164 @@
+# AUTOGENERATED FROM build-test-windows-x86.jsonnet DO NOT MODIFY
+jobs:
+  build-core:
+    defaults:
+      run:
+        shell: bash
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+      - uses: ocaml/setup-ocaml@v2
+        with:
+          ocaml-compiler: "4.14"
+          opam-local-packages: dont_install_local_packages.opam
+          opam-repositories: |
+            opam-repository-mingw: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset
+            default: https://github.com/ocaml/opam-repository.git
+      - env:
+          SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2
+        name: Set GHA cache for OPAM in _opam
+        uses: actions/cache@v3
+        with:
+          key: ${{ runner.os }}-${{ runner.arch }}-v1-opam-4.14.0-${{ hashFiles('semgrep.opam') }}
+          path: _opam
+      - name: Debug stuff
+        run: |
+          ls
+          # to see the bin symlink for example
+          ls -l
+          set
+          # tree-sitter fails to compile without an ar, you can use
+          # CC=x86_64-w64-mingw32-gcc but there is no AR=x86_64-w64-mingw32-ar
+          which ar
+          ar --version
+          # GHA installs cygwin in a special place
+          export PATH="${CYGWIN_ROOT_BIN}:${PATH}"
+          which ar
+          ar --version
+          which opam
+          # this should be fdopen's opan, so 2.0.10
+          opam --version
+          opam repo
+          # we should be on 4.14.0~mingw
+          opam switch
+      - env:
+          CC: x86_64-w64-mingw32-gcc
+        name: Build tree-sitter
+        run: |
+          cd libs/ocaml-tree-sitter-core
+          ./configure
+          ./scripts/download-tree-sitter --lazy
+          prefix="$(pwd)/tree-sitter"
+          cd downloads/tree-sitter
+          make PREFIX="$prefix" CFLAGS="-O3 -Wall -Wextra"
+          make PREFIX="$prefix" install
+      - name: Install OPAM deps
+        run: |
+          export PATH="${CYGWIN_ROOT_BIN}:${PATH}"
+          make install-deps-WINDOWS-for-semgrep-core
+          make install-opam-deps
+      - name: Build semgrep-core
+        run: |
+          export PATH=\"${CYGWIN_ROOT_BIN}:${PATH}\"
+          export TREESITTER_INCDIR=$(pwd)/libs/ocaml-tree-sitter-core/tree-sitter/include
+          export TREESITTER_LIBDIR=$(pwd)/libs/ocaml-tree-sitter-core/tree-sitter/lib
+          # We have to strip rpath from the tree-sitter projects because there's no
+          # equivalent in Windows
+          # TODO: investigate removing rpath from the tree-sitter projects
+          for filename in $(find ./languages/ ./libs/ocaml-tree-sitter-core/ -name dune); do
+            grep -v rpath $filename > $filename.new
+            mv $filename.new $filename
+          done
+          opam exec -- dune build _build/install/default/bin/semgrep-core.exe
+      - name: Test semgrep-core
+        run: |
+          _build/install/default/bin/semgrep-core.exe -l python -rules tests/windows/rules.yml -json tests/windows/test.py
+      - name: Package semgrep-core
+        run: |
+          mkdir artifacts
+          cp _build/install/default/bin/semgrep-core.exe artifacts/
+
+          # TODO: somehow upgrade to the latest flexdll, which should allow us
+          # to statically link these libraries
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libstdc++-6.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libgcc_s_seh-1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libwinpthread-1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libpcre-1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libgmp-10.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libcurl-4.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libpcre2-8-0.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libeay32.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libidn2-0.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libnghttp2-14.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libssh2-1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/ssleay32.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libzstd-1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/zlib1.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/iconv.dll artifacts/
+          cp d:/cygwin/usr/x86_64-w64-mingw32/sys-root/mingw/bin/libintl-8.dll artifacts/
+
+          tar czvf artifacts.tgz artifacts
+      - uses: actions/upload-artifact@v3
+        with:
+          name: semgrep-core-and-dependent-libs-w64-artifact-${{ github.sha }}
+          path: artifacts.tgz
+  build-wheels:
+    defaults:
+      run:
+        shell: bash
+    needs:
+      - build-core
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+      - uses: actions/download-artifact@v3
+        with:
+          name: semgrep-core-and-dependent-libs-w64-artifact-${{ github.sha }}
+      - env:
+          SEMGREP_FORCE_INSTALL: 1
+        run: |
+          tar xvfz artifacts.tgz
+          cp artifacts/semgrep-core cli/src/semgrep/bin
+          ./scripts/build-wheels.sh --plat-name win_amd64
+      - uses: actions/upload-artifact@v3
+        with:
+          name: windows-x86-wheel
+          path: cli/dist.tgz
+  test-wheels:
+    defaults:
+      run:
+        shell: bash
+    needs:
+      - build-wheels
+    runs-on: windows-latest
+    steps:
+      - uses: actions/download-artifact@v3
+        with:
+          name: windows-x86-wheel
+      - run: tar xzvf dist.tgz
+      - name: install package
+        run: pip3 install dist/*.whl
+      - name: test package
+        run: semgrep --version
+      - name: e2e semgrep-core test
+        run: echo '1 == 1' | semgrep -l python -e '$X == $X' -
+name: build-test-windows-x86
+on:
+  workflow_call:
+    inputs:
+      use-cache:
+        default: true
+        description: Use Opam Cache - uncheck the box to disable use of the opam cache, meaning a long-running but completely from-scratch build.
+        required: false
+        type: boolean
+  workflow_dispatch:
+    inputs:
+      use-cache:
+        default: true
+        description: Use Opam Cache - uncheck the box to disable use of the opam cache, meaning a long-running but completely from-scratch build.
+        required: true
+        type: boolean
diff --git a/.github/workflows/tests.jsonnet b/.github/workflows/tests.jsonnet
index 7e676dcf36b0..155c2d6aa85c 100644
--- a/.github/workflows/tests.jsonnet
+++ b/.github/workflows/tests.jsonnet
@@ -583,6 +583,10 @@ local ignore_md = {
       uses: './.github/workflows/build-test-osx-arm64.yml',
       secrets: 'inherit',
     },
+    'build-test-windows-x86': {
+      uses: './.github/workflows/build-test-windows-x86.yml',
+      secrets: 'inherit',
+    },
   },
   export:: {
     // Used in semgrep-proprietary.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f690f0123640..74f39c384020 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -98,6 +98,9 @@ jobs:
   build-test-osx-x86:
     secrets: inherit
     uses: ./.github/workflows/build-test-osx-x86.yml
+  build-test-windows-x86:
+    secrets: inherit
+    uses: ./.github/workflows/build-test-windows-x86.yml
   push-docker-returntocorp:
     if: github.ref == 'refs/heads/develop' || (github.actor != 'dependabot[bot]' && !(github.event.pull_request.head.repo.full_name != github.repository))
     needs:
@@ -114,6 +117,7 @@ jobs:
       - build-test-core-x86
     permissions:
       contents: write
+      id-token: write
       pull-requests: write
     runs-on: ubuntu-22.04
     steps:
diff --git a/cli/tests/default/e2e-other/snapshots/test_ci/test_generic_secrets_output/generic_secrets_and_real_rule/output.txt b/cli/tests/default/e2e-other/snapshots/test_ci/test_generic_secrets_output/generic_secrets_and_real_rule/output.txt
index 824ed38be0f9..9f8b4c09028f 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ci/test_generic_secrets_output/generic_secrets_and_real_rule/output.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ci/test_generic_secrets_output/generic_secrets_and_real_rule/output.txt
@@ -111,6 +111,9 @@ SEMGREP_APP_TOKEN="fake_key" SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS
   BLOCKING CODE RULES FIRED:
     real-rule-example
 
+  BLOCKING SECRETS RULES FIRED:
+    generic-secrets-rule-example
+
 
 === end of stdout - plain
 
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_no_lockfiles/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_no_lockfiles/results.txt
index eed9e33de008..f73241a58811 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_no_lockfiles/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_no_lockfiles/results.txt
@@ -24,7 +24,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
 │ Scan Summary │
 └──────────────┘
 
-Ran 1 rule on 1 file: 0 findings.
+Ran 1 rule on 0 files: 0 findings.
 
 === end of stderr - plain
 
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelodash-4.17.19.yaml-dependency_awarelodash/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelodash-4.17.19.yaml-dependency_awarelodash/results.txt
index 261abcaa2a14..2f2f06e8f258 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelodash-4.17.19.yaml-dependency_awarelodash/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awarelodash-4.17.19.yaml-dependency_awarelodash/results.txt
@@ -12,7 +12,6 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
   "interfile_languages_used": [],
   "paths": {
     "scanned": [
-      "targets/dependency_aware/lodash/useslodash.js",
       "targets/dependency_aware/lodash/yarn.lock"
     ]
   },
@@ -42,7 +41,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
 │ Scan Summary │
 └──────────────┘
 
-Ran 1 rule on 2 files: 0 findings.
+Ran 1 rule on 1 file: 0 findings.
 
 === end of stderr - plain
 
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpm_missing_version/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpm_missing_version/results.txt
index 859f2410bea7..38afa88abd82 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpm_missing_version/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc/rulesdependency_awareswift-sca.yaml-dependency_awareswiftpm_missing_version/results.txt
@@ -12,8 +12,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
   "interfile_languages_used": [],
   "paths": {
     "scanned": [
-      "targets/dependency_aware/swiftpm_missing_version/Package.resolved",
-      "targets/dependency_aware/swiftpm_missing_version/Package.swift"
+      "targets/dependency_aware/swiftpm_missing_version/Package.resolved"
     ]
   },
   "results": [],
@@ -44,7 +43,7 @@ SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERS
 │ Scan Summary │
 └──────────────┘
 
-Ran 1 rule on 2 files: 0 findings.
+Ran 1 rule on 1 file: 0 findings.
 
 === end of stderr - plain
 
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
index d6f7272e12ac..304eee7a3b35 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirement
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirement
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt
new file mode 100644
index 000000000000..431ca793bfe3
--- /dev/null
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirement_pip/results.txt
@@ -0,0 +1,103 @@
+=== command
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirement_pip
+=== end of command
+
+=== exit code
+0
+=== end of exit code
+
+=== stdout - plain
+{
+  "errors": [],
+  "interfile_languages_used": [],
+  "paths": {
+    "scanned": [
+      "targets/dependency_aware/requirement_pip/foo.py",
+      "targets/dependency_aware/requirement_pip/requirement.pip"
+    ]
+  },
+  "results": [
+    {
+      "check_id": "rules.dependency_aware.python-requirements-sca",
+      "end": {
+        "col": 6,
+        "line": 1,
+        "offset": 5
+      },
+      "extra": {
+        "engine_kind": "OSS",
+        "fingerprint": "0x42",
+        "is_ignored": false,
+        "lines": "bad()",
+        "message": "oh no",
+        "metadata": {},
+        "metavars": {},
+        "sca_info": {
+          "dependency_match": {
+            "dependency_pattern": {
+              "ecosystem": "pypi",
+              "package": "pandas",
+              "semver_range": "<= 1.4.2"
+            },
+            "found_dependency": {
+              "allowed_hashes": {},
+              "ecosystem": "pypi",
+              "line_number": 37,
+              "lockfile_path": "targets/dependency_aware/requirement_pip/requirement.pip",
+              "package": "pandas",
+              "transitivity": "direct",
+              "version": "1.4.2"
+            },
+            "lockfile": "targets/dependency_aware/requirement_pip/requirement.pip"
+          },
+          "reachability_rule": true,
+          "reachable": true,
+          "sca_finding_schema": 20220913
+        },
+        "severity": "WARNING",
+        "validation_state": "NO_VALIDATOR"
+      },
+      "path": "targets/dependency_aware/requirement_pip/foo.py",
+      "start": {
+        "col": 1,
+        "line": 1,
+        "offset": 0
+      }
+    }
+  ],
+  "skipped_rules": [],
+  "version": "0.42"
+}
+=== end of stdout - plain
+
+=== stderr - plain
+
+
+┌─────────────┐
+│ Scan Status │
+└─────────────┘
+  Scanning 3 files tracked by git with 0 Code rules, 1 Supply Chain rule:
+
+
+  CODE RULES
+  Nothing to scan.
+
+  SUPPLY CHAIN RULES
+  Scanning 1 file.
+
+
+┌──────────────┐
+│ Scan Summary │
+└──────────────┘
+
+Ran 1 rule on 2 files: 1 finding.
+
+=== end of stderr - plain
+
+=== stdout - color
+<same as above: stdout - plain>
+=== end of stdout - color
+
+=== stderr - color
+<same as above: stderr - plain>
+=== end of stderr - color
\ No newline at end of file
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
index 2d46aec397d5..b9b913a90a8a 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
index 5dd3224187f5..49bb672a39b0 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_dep_dupes/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_dep_dupes
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_dep_dupes
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
index 633f17fa1f60..8b2116fad019 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_no_src/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_no_src
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_no_src
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
index 15f53ef7d788..0dde9c0ae31e 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_folder_similar_deps/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_similar_deps
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_folder_similar_deps
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
index 2e0cf19182f0..ee1574c5d7dd 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
index 01111c1bf105..3234f25d229d 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
index 1ffb2e920e31..375ca76df722 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_dep_dupes_no_src/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_dep_dupes_no_src
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
index 77b7ea6673a1..3f2a356db277 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_no_src/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_no_src
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_no_src
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
index c9b683a4a853..7037bd8fce18 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_multiple_lockfiles_similar_deps/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_similar_deps
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_multiple_lockfiles_similar_deps
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested/results.txt
index 41c2523df466..8e2ea30bed2e 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_dep_dupes/results.txt
index ae022d967fe3..adecf608a676 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_dep_dupes/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_dep_dupes
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_dep_dupes
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder/results.txt
index f1420488379f..faafdc3f9b65 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_dep_dupes/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_dep_dupes/results.txt
index ad40ea8b6703..e30e6883755d 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_dep_dupes/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_dep_dupes/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder_dep_dupes
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder_dep_dupes
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_no_src/results.txt
index be4906158362..813a22f0dbda 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_folder_no_src/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder_no_src
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_folder_no_src
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_no_src/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_no_src/results.txt
index 1080f21f302c..3577634a3a7f 100644
--- a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_no_src/results.txt
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_nested_no_src/results.txt
@@ -1,5 +1,5 @@
 === command
-SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --enable-experimental-requirements --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_no_src
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_nested_no_src
 === end of command
 
 === exit code
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt
new file mode 100644
index 000000000000..4106cb16d76b
--- /dev/null
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip/results.txt
@@ -0,0 +1,103 @@
+=== command
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_pip
+=== end of command
+
+=== exit code
+0
+=== end of exit code
+
+=== stdout - plain
+{
+  "errors": [],
+  "interfile_languages_used": [],
+  "paths": {
+    "scanned": [
+      "targets/dependency_aware/requirements_pip/foo.py",
+      "targets/dependency_aware/requirements_pip/requirements.pip"
+    ]
+  },
+  "results": [
+    {
+      "check_id": "rules.dependency_aware.python-requirements-sca",
+      "end": {
+        "col": 6,
+        "line": 1,
+        "offset": 5
+      },
+      "extra": {
+        "engine_kind": "OSS",
+        "fingerprint": "0x42",
+        "is_ignored": false,
+        "lines": "bad()",
+        "message": "oh no",
+        "metadata": {},
+        "metavars": {},
+        "sca_info": {
+          "dependency_match": {
+            "dependency_pattern": {
+              "ecosystem": "pypi",
+              "package": "pandas",
+              "semver_range": "<= 1.4.2"
+            },
+            "found_dependency": {
+              "allowed_hashes": {},
+              "ecosystem": "pypi",
+              "line_number": 37,
+              "lockfile_path": "targets/dependency_aware/requirements_pip/requirements.pip",
+              "package": "pandas",
+              "transitivity": "direct",
+              "version": "1.4.2"
+            },
+            "lockfile": "targets/dependency_aware/requirements_pip/requirements.pip"
+          },
+          "reachability_rule": true,
+          "reachable": true,
+          "sca_finding_schema": 20220913
+        },
+        "severity": "WARNING",
+        "validation_state": "NO_VALIDATOR"
+      },
+      "path": "targets/dependency_aware/requirements_pip/foo.py",
+      "start": {
+        "col": 1,
+        "line": 1,
+        "offset": 0
+      }
+    }
+  ],
+  "skipped_rules": [],
+  "version": "0.42"
+}
+=== end of stdout - plain
+
+=== stderr - plain
+
+
+┌─────────────┐
+│ Scan Status │
+└─────────────┘
+  Scanning 3 files tracked by git with 0 Code rules, 1 Supply Chain rule:
+
+
+  CODE RULES
+  Nothing to scan.
+
+  SUPPLY CHAIN RULES
+  Scanning 1 file.
+
+
+┌──────────────┐
+│ Scan Summary │
+└──────────────┘
+
+Ran 1 rule on 2 files: 1 finding.
+
+=== end of stderr - plain
+
+=== stdout - color
+<same as above: stdout - plain>
+=== end of stdout - color
+
+=== stderr - color
+<same as above: stderr - plain>
+=== end of stderr - color
\ No newline at end of file
diff --git a/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt
new file mode 100644
index 000000000000..c6b2056abda7
--- /dev/null
+++ b/cli/tests/default/e2e-other/snapshots/test_ssc/test_ssc__requirements_lockfiles/rulesdependency_awarepython-requirements-sca.yaml-dependency_awarerequirements_pip_folder/results.txt
@@ -0,0 +1,104 @@
+=== command
+SEMGREP_USER_AGENT_APPEND="pytest" SEMGREP_SETTINGS_FILE="<MASKED>" SEMGREP_VERSION_CACHE_PATH="<MASKED>" SEMGREP_ENABLE_VERSION_CHECK="0" SEMGREP_SEND_METRICS="off" semgrep --strict --config rules/dependency_aware/python-requirements-sca.yaml --json targets/dependency_aware/requirements_pip_folder
+=== end of command
+
+=== exit code
+0
+=== end of exit code
+
+=== stdout - plain
+{
+  "errors": [],
+  "interfile_languages_used": [],
+  "paths": {
+    "scanned": [
+      "targets/dependency_aware/requirements_pip_folder/foo.py",
+      "targets/dependency_aware/requirements_pip_folder/requirements/prod.pip",
+      "targets/dependency_aware/requirements_pip_folder/requirements/stage.pip"
+    ]
+  },
+  "results": [
+    {
+      "check_id": "rules.dependency_aware.python-requirements-sca",
+      "end": {
+        "col": 6,
+        "line": 1,
+        "offset": 5
+      },
+      "extra": {
+        "engine_kind": "OSS",
+        "fingerprint": "0x42",
+        "is_ignored": false,
+        "lines": "bad()",
+        "message": "oh no",
+        "metadata": {},
+        "metavars": {},
+        "sca_info": {
+          "dependency_match": {
+            "dependency_pattern": {
+              "ecosystem": "pypi",
+              "package": "pandas",
+              "semver_range": "<= 1.4.2"
+            },
+            "found_dependency": {
+              "allowed_hashes": {},
+              "ecosystem": "pypi",
+              "line_number": 1,
+              "lockfile_path": "targets/dependency_aware/requirements_pip_folder/requirements/prod.pip",
+              "package": "pandas",
+              "transitivity": "direct",
+              "version": "1.4.2"
+            },
+            "lockfile": "targets/dependency_aware/requirements_pip_folder/requirements/prod.pip"
+          },
+          "reachability_rule": true,
+          "reachable": true,
+          "sca_finding_schema": 20220913
+        },
+        "severity": "WARNING",
+        "validation_state": "NO_VALIDATOR"
+      },
+      "path": "targets/dependency_aware/requirements_pip_folder/foo.py",
+      "start": {
+        "col": 1,
+        "line": 1,
+        "offset": 0
+      }
+    }
+  ],
+  "skipped_rules": [],
+  "version": "0.42"
+}
+=== end of stdout - plain
+
+=== stderr - plain
+
+
+┌─────────────┐
+│ Scan Status │
+└─────────────┘
+  Scanning 4 files tracked by git with 0 Code rules, 1 Supply Chain rule:
+
+
+  CODE RULES
+  Nothing to scan.
+
+  SUPPLY CHAIN RULES
+  Scanning 1 file.
+
+
+┌──────────────┐
+│ Scan Summary │
+└──────────────┘
+
+Ran 1 rule on 3 files: 1 finding.
+
+=== end of stderr - plain
+
+=== stdout - color
+<same as above: stdout - plain>
+=== end of stdout - color
+
+=== stderr - color
+<same as above: stderr - plain>
+=== end of stderr - color
\ No newline at end of file
diff --git a/cli/tests/default/e2e-other/test_ssc.py b/cli/tests/default/e2e-other/test_ssc.py
index 70b5e6a3d723..b0f60e506aab 100644
--- a/cli/tests/default/e2e-other/test_ssc.py
+++ b/cli/tests/default/e2e-other/test_ssc.py
@@ -1,14 +1,21 @@
 import json
 import logging
 from pathlib import Path
+from typing import Dict
 
 import pytest
 from tests.conftest import RULES_PATH
 from tests.conftest import TARGETS_PATH
 from tests.fixtures import RunSemgrep
 
-from semdep.lockfile import Lockfile
 from semdep.package_restrictions import is_in_range
+from semdep.parsers.package_lock import parse_package_lock
+from semdep.parsers.pipfile import parse_pipfile
+from semdep.parsers.pnpm import parse_pnpm
+from semdep.parsers.poetry import parse_poetry
+from semdep.parsers.requirements import parse_requirements
+from semdep.parsers.util import SemgrepParser
+from semdep.parsers.yarn import parse_yarn
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Maven
 
@@ -124,10 +131,6 @@
             "rules/dependency_aware/python-requirements-sca.yaml",
             "dependency_aware/requirements",
         ),
-        (
-            "rules/dependency_aware/python-requirements-sca.yaml",
-            "dependency_aware/requirements_pip",
-        ),
         (
             "rules/dependency_aware/python-requirements-sca.yaml",
             "dependency_aware/requirements_with_uppercase_package_name",
@@ -254,10 +257,22 @@ def test_ssc(run_semgrep_on_copied_files: RunSemgrep, snapshot, rule, target):
             "rules/dependency_aware/python-requirements-sca.yaml",
             "dependency_aware/requirement",
         ),
+        (
+            "rules/dependency_aware/python-requirements-sca.yaml",
+            "dependency_aware/requirements_pip",
+        ),
+        (
+            "rules/dependency_aware/python-requirements-sca.yaml",
+            "dependency_aware/requirement_pip",
+        ),
         (
             "rules/dependency_aware/python-requirements-sca.yaml",
             "dependency_aware/requirements_folder",
         ),
+        (
+            "rules/dependency_aware/python-requirements-sca.yaml",
+            "dependency_aware/requirements_pip_folder",
+        ),
         (
             "rules/dependency_aware/python-requirements-sca.yaml",
             "dependency_aware/requirements_folder_dep_dupes",
@@ -321,11 +336,9 @@ def test_ssc__requirements_lockfiles(
     run_semgrep_on_copied_files: RunSemgrep, snapshot, rule, target
 ):
     """
-    Seperated out from test_ssc to avoid polluting with extra requirements lockfile tests
+    Separated out from test_ssc to avoid polluting with extra requirements lockfile tests
     """
-    result = run_semgrep_on_copied_files(
-        rule, target_name=target, options=["--enable-experimental-requirements"]
-    )
+    result = run_semgrep_on_copied_files(rule, target_name=target)
 
     snapshot.assert_match(
         result.as_snapshot(),
@@ -357,6 +370,16 @@ def test_maven_version_comparison(version, specifier, outcome):
     assert is_in_range(Ecosystem(Maven()), specifier, version) == outcome
 
 
+LOCKFILE_NAME_TO_PARSER: Dict[str, SemgrepParser] = {
+    "requirements.txt": parse_requirements,
+    "yarn.lock": parse_yarn,
+    "package-lock.json": parse_package_lock,
+    "Pipfile.lock": parse_pipfile,
+    "poetry.lock": parse_poetry,
+    "pnpm-lock.yaml": parse_pnpm,
+}
+
+
 @pytest.mark.parametrize(
     "target",
     [
@@ -441,13 +464,14 @@ def test_maven_version_comparison(version, specifier, outcome):
 # They also include random lockfiles we want to make sure we parse predictably
 @pytest.mark.no_semgrep_cli
 @pytest.mark.osemfail
-def test_parsing(caplog, target, snapshot, lockfile_path_in_tmp):
+def test_parsing(caplog, target: str, snapshot, lockfile_path_in_tmp):
     # Setup
     caplog.set_level(logging.ERROR)
 
+    target_path = Path(target)
     # Parse
-    lockfile = Lockfile.from_path(Path(target))
-    dependencies, error = lockfile.parse()
+    parser: SemgrepParser = LOCKFILE_NAME_TO_PARSER[target_path.name]
+    dependencies, error = parser(Path(target), None)  # no manifest for any of these
 
     # Assert
 
@@ -472,7 +496,9 @@ def test_parsing(caplog, target, snapshot, lockfile_path_in_tmp):
 # a target that begins with "targets", as that dir contains every kind of lockfile
 # So we add the keyword arg to run_semgrep and manually do some cd-ing
 @pytest.mark.osemfail
-def test_no_lockfiles(run_semgrep: RunSemgrep, monkeypatch, tmp_path, snapshot):
+def test_no_lockfiles(
+    run_semgrep: RunSemgrep, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, snapshot
+):
     (tmp_path / "targets").symlink_to(TARGETS_PATH.resolve())
     (tmp_path / "rules").symlink_to(RULES_PATH.resolve())
     monkeypatch.chdir(tmp_path / "targets" / "basic")
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/foo.py b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/foo.py
new file mode 100644
index 000000000000..11b682df9e6e
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/foo.py
@@ -0,0 +1,1 @@
+bad()
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirement.pip b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirement.pip
new file mode 100644
index 000000000000..965c28ae73e5
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirement.pip
@@ -0,0 +1,123 @@
+#
+# This file is autogenerated by pip-compile with python 3.8
+# To update, run:
+#
+#    pip-compile
+#
+astroid==2.11.5
+    # via pylint
+attrs==21.4.0
+    # via pytest
+bracex==2.2.1
+    # via wcmatch
+bunch==1.0.1
+    # via -r requirements.in
+coverage==6.3.3
+    # via -r requirements.in
+dill==0.3.4
+    # via pylint
+dominate==2.6.0
+    # via -r requirements.in
+execnet==1.9.0
+    # via pytest-xdist
+iniconfig==1.1.1
+    # via pytest
+isort==5.10.1
+    # via pylint
+lazy-object-proxy==1.7.1
+    # via astroid
+mccabe==0.6.1
+    # via pylint
+numpy==1.23.2
+    # via
+    #   -r requirements.in
+    #   pandas
+packaging==21.3
+    # via pytest
+pandas==1.4.2
+    # via -r requirements.in
+platformdirs==2.4.0
+    # via pylint
+plotly==5.8.0
+    # via -r requirements.in
+pluggy==1.0.0
+    # via pytest
+psutil==5.9.0
+    # via -r requirements.in
+py==1.11.0
+    # via
+    #   pytest
+    #   pytest-forked
+pylint==2.13.9
+    # via -r requirements.in
+pyparsing==3.0.7
+    # via packaging
+pytest==7.1.2
+    # via
+    #   -r requirements.in
+    #   pytest-forked
+    #   pytest-xdist
+pytest-forked==1.4.0
+    # via
+    #   -r requirements.in
+    #   pytest-xdist
+pytest-xdist==2.5.0
+    # via -r requirements.in
+python-dateutil==2.8.2
+    # via pandas
+pytz==2021.3
+    # via pandas
+six==1.16.0
+    # via python-dateutil
+tenacity==8.0.1
+    # via plotly
+tomli==2.0.0 # Comment
+    # via
+    #   pylint
+    #   pytest
+tqdm==4.64.0
+    # via -r requirements.in
+wcmatch==8.3
+    # via -r requirements.in
+wrapt==1.13.3
+    # via astroid
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
+
+pytest
+pytest-cov
+beautifulsoup4
+
+# 2) reqs file features from https://pip.pypa.io/en/stable/reference/requirements-file-format/#example
+
+space-eqeq == 0.6.1
+eqeq-star [security] == 2.8.*, <= 2.8.1 ; python_version < "2.7"
+extras-only [security] == 2.8.4 ; python_version < "2.7"
+condition-only ==2.8.5 ; python_version < "2.7"
+extras-and-two-conditions[security] == 2.8.3 ; python_version < "2.7" or sys_platform == 'darwin'
+
+at-url @ https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip
+at-url-with-hash @ https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip#look-this-is-not-a-comment
+
+# It is possible to refer to other requirement files or constraints files.
+-r other-requirements.txt
+-c constraints.txt
+
+# It is possible to refer to specific local distribution paths.
+./downloads/numpy-1.9.2-cp34-none-win32.whl
+
+# It is possible to refer to URLs.
+http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl
+
+# 3) This is from pip-tools https://github.com/jazzband/pip-tools#using-hashes
+
+first-with-hash==3.2.3 \
+    --hash=sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0 \
+    --hash=sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5
+    # via second-with-hash
+second-with-hash==3.0.3 \
+    --hash=sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283 \
+    --hash=sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a
+    # via -r requirements.in
+third-with-hash==2019.3 --hash=sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d --hash=sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirements.in b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirements.in
new file mode 100644
index 000000000000..7577083075a3
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirement_pip/requirements.in
@@ -0,0 +1,13 @@
+bunch==1.0.1
+coverage==6.3.3
+dominate==2.6.0
+numpy==1.23.2
+pandas #Comment
+plotly
+psutil==5.9.0
+pylint<=2.13.9
+pytest>=7.1.2
+pytest-forked==1.4.0 #Comment
+pytest-xdist==2.5.0
+tqdm==4.64.0
+wcmatch==8.3
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/foo.py b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/foo.py
new file mode 100644
index 000000000000..11b682df9e6e
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/foo.py
@@ -0,0 +1,1 @@
+bad()
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements.in b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements.in
new file mode 100644
index 000000000000..7577083075a3
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements.in
@@ -0,0 +1,13 @@
+bunch==1.0.1
+coverage==6.3.3
+dominate==2.6.0
+numpy==1.23.2
+pandas #Comment
+plotly
+psutil==5.9.0
+pylint<=2.13.9
+pytest>=7.1.2
+pytest-forked==1.4.0 #Comment
+pytest-xdist==2.5.0
+tqdm==4.64.0
+wcmatch==8.3
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/prod.pip b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/prod.pip
new file mode 100644
index 000000000000..0f7b0176c259
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/prod.pip
@@ -0,0 +1,1 @@
+pandas==1.4.2
\ No newline at end of file
diff --git a/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/stage.pip b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/stage.pip
new file mode 100644
index 000000000000..6eaff8301ffc
--- /dev/null
+++ b/cli/tests/default/e2e/targets/dependency_aware/requirements_pip_folder/requirements/stage.pip
@@ -0,0 +1,7 @@
+#
+# This file is autogenerated by pip-compile with python 3.8
+# To update, run:
+#
+#    pip-compile
+#
+astroid==2.11.5
\ No newline at end of file
diff --git a/cli/tests/default/e2e/test_misc.py b/cli/tests/default/e2e/test_misc.py
index cd79eb2029ba..8f6d2149df8a 100644
--- a/cli/tests/default/e2e/test_misc.py
+++ b/cli/tests/default/e2e/test_misc.py
@@ -209,7 +209,6 @@ def test_terminal_output_quiet(run_semgrep_in_tmp: RunSemgrep, snapshot):
 
 # Feed 'a' to semgrep's stdin and search for the pattern 'a', expecting
 # one finding.
-@pytest.mark.osemfail
 @pytest.mark.kinda_slow
 def test_stdin_input(snapshot):
     unique_settings_file = tempfile.NamedTemporaryFile().name
@@ -234,7 +233,6 @@ def test_stdin_input(snapshot):
     snapshot.assert_match(mask_variable_text(stdout), "results.json")
 
 
-@pytest.mark.osemfail
 @pytest.mark.kinda_slow
 def test_subshell_input(snapshot):
     unique_settings_file = tempfile.NamedTemporaryFile().name
diff --git a/cli/tests/default/unit/test_filter_dependency_aware_rules.py b/cli/tests/default/unit/test_filter_dependency_aware_rules.py
new file mode 100644
index 000000000000..08099a50f7d2
--- /dev/null
+++ b/cli/tests/default/unit/test_filter_dependency_aware_rules.py
@@ -0,0 +1,151 @@
+from collections import defaultdict
+from pathlib import Path
+
+import pytest
+
+from semgrep.rule import Rule
+from semgrep.run_scan import filter_dependency_aware_rules
+from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
+from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
+from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
+from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitivity
+from semgrep.subproject import LockfileDependencySource
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import ResolutionMethod
+from semgrep.subproject import ResolvedSubproject
+
+
+@pytest.fixture
+def sample_rules():
+    rules = [
+        Rule(
+            {
+                "id": "rules.ssc-58d96261-a0dd-47e9-bad6-110669fa8c14",
+                "r2c-internal-project-depends-on": {
+                    "namespace": "pypi",
+                    "package": "protobuf",
+                    "version": ">=3.0.0",
+                },
+                "languages": ["python"],
+                "patterns": ["pattern"],
+            }
+        ),
+        Rule(
+            {
+                "id": "rules.ssc-78d96261-a0dd-47e9-bad6-110669fa8c14",
+                "r2c-internal-project-depends-on": {
+                    "namespace": "pypi",
+                    "package": "protobuf",
+                    "version": "<=3.0.0",
+                },
+                "languages": ["python"],
+                "patterns": ["pattern"],
+            }
+        ),
+        Rule(
+            {
+                "id": "rules.ssc-2258d96261-a0dd-47e9-bad6-110669fa8c14",
+                "r2c-internal-project-depends-on": {
+                    "namespace": "npm",
+                    "package": "other-package",
+                    "version": ">=1.0.0",
+                },
+                "languages": ["javascript"],
+                "patterns": ["pattern"],
+            }
+        ),
+        Rule(
+            {
+                "id": "rules.ssc-5108d96261-a0dd-47e9-bad6-110669fa8c14",
+                "r2c-internal-project-depends-on": {
+                    "namespace": "pypi",
+                    "package": "test",
+                    "version": ">=1.0.0",
+                },
+                "languages": ["python"],
+                "patterns": ["pattern"],
+            }
+        ),
+    ]
+
+    # Ensure `should_run_on_semgrep_core` is True by adding a key like 'patterns' to `_raw` for each rule
+    for rule in rules:
+        rule._raw["patterns"] = [
+            "pattern"
+        ]  # Any valid key that should trigger `should_run_on_semgrep_core`
+
+    return rules
+
+
+@pytest.fixture
+def sample_resolved_deps():
+    # Accurate found_dependencies for protobuf and test packages, including empty allowed_hashes
+    found_dependencies = [
+        FoundDependency(
+            package="protobuf",
+            version="3.14.0",
+            ecosystem=Ecosystem(value=Pypi()),
+            allowed_hashes=defaultdict(list),  # Empty allowed_hashes
+            transitivity=Transitivity("Direct"),
+            resolved_url=None,
+            children=None,
+            git_ref=None,
+        ),
+        FoundDependency(
+            package="test",
+            version="1.16.0",
+            ecosystem=Ecosystem(value=Pypi()),
+            allowed_hashes=defaultdict(list),  # Empty allowed_hashes
+            transitivity=Transitivity("Direct"),
+            resolved_url=None,
+            children=None,
+            git_ref=None,
+        ),
+    ]
+
+    # Create LockfileDependencySource
+    dependency_source = LockfileDependencySource(
+        package_manager_type=PackageManagerType.PIP,
+        manifest_path=Path("Pipfile"),
+        lockfile_path=Path("Pipfile.lock"),
+    )
+
+    resolution_method = ResolutionMethod.LOCKFILE_PARSING
+
+    # Create ResolvedSubproject with accurate found_dependencies and resolution_method
+    subprojects = [
+        ResolvedSubproject.from_unresolved(
+            unresolved=ResolvedSubproject(
+                root_dir=Path("."),
+                ecosystem=Ecosystem(value=Pypi()),
+                dependency_source=dependency_source,
+                found_dependencies=found_dependencies,
+                resolution_method=resolution_method,
+            ),
+            resolution_method=resolution_method,
+            found_dependencies=found_dependencies,
+            ecosystem=Ecosystem(value=Pypi()),
+        )
+    ]
+
+    return {
+        Ecosystem(
+            value=Pypi()
+        ): subprojects  # Ensure the ecosystem is lowercase to match rules
+    }
+
+
+# The actual test function
+@pytest.mark.quick
+def test_filter_dependency_aware_rules(sample_rules, sample_resolved_deps):
+    result = filter_dependency_aware_rules(sample_rules, sample_resolved_deps)
+    # Expected result
+    expected_result = [
+        sample_rules[0],  # Rule with id=rules.ssc-58d96261-a0dd-47e9-bad6-110669fa8c14
+        sample_rules[
+            3
+        ],  # Rule with id=rules.ssc-5108d96261-a0dd-47e9-bad6-110669fa8c14
+    ]
+    print(result)
+    # Assert the result matches the expected output
+    assert result == expected_result
diff --git a/cli/tests/default/unit/test_lockfile.py b/cli/tests/default/unit/test_lockfile.py
deleted file mode 100644
index 61194b3e9f3b..000000000000
--- a/cli/tests/default/unit/test_lockfile.py
+++ /dev/null
@@ -1,449 +0,0 @@
-from pathlib import Path
-
-import pytest
-
-from semdep.lockfile import _create_matcher
-from semdep.lockfile import _is_valid_lockfile
-from semdep.lockfile import EcosystemLockfiles
-from semdep.lockfile import ExactLockfileMatcher
-from semdep.lockfile import filter_lockfile_paths
-from semdep.lockfile import Lockfile
-from semdep.lockfile import NEW_REQUIREMENTS_MATCHERS
-from semdep.lockfile import OLD_REQUIREMENTS_MATCHERS
-from semdep.lockfile import PatternLockfileMatcher
-from semdep.lockfile import RequirementsLockfileMatcher
-from semdep.parsers.pipfile import parse_pipfile
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Cargo
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Composer
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gem
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Gomod
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Hex
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Maven
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Npm
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Nuget
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pub
-from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
-from semgrep.semgrep_interfaces.semgrep_output_v1 import SwiftPM
-
-
-class TestExactLockfileMatcher:
-    @pytest.mark.quick
-    def test_is_match(self):
-        matcher = ExactLockfileMatcher(
-            lockfile="Pipfile.lock",
-            manifest="Pipfile",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        assert matcher.is_match(Path("Pipfile.lock")) is True
-        assert matcher.is_match(Path("a/b/c/Pipfile.lock")) is True
-        assert matcher.is_match(Path("a/b/c/Pipfile")) is False
-        assert matcher.is_match(Path("requirements.txt")) is False
-
-    @pytest.mark.quick
-    @pytest.mark.parametrize("create_manifest", [True, False])
-    def test_get_manifest_path(self, tmp_path, monkeypatch, create_manifest):
-        if create_manifest:
-            (tmp_path / "Pipfile").touch()
-
-        monkeypatch.chdir(tmp_path)
-
-        matcher = ExactLockfileMatcher(
-            lockfile="Pipfile.lock",
-            manifest="Pipfile",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-        manifest_path = matcher.get_manifest_path(Path("Pipfile.lock"))
-        assert manifest_path == (Path("Pipfile") if create_manifest else None)
-
-    @pytest.mark.quick
-    def test_get_parent_path(self):
-        matcher = ExactLockfileMatcher(
-            lockfile="Pipfile.lock",
-            manifest="Pipfile",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        assert matcher.get_subproject_root(Path("Pipfile.lock")) == Path(".")
-        assert matcher.get_subproject_root(Path("a/b/c/Pipfile.lock")) == Path("a/b/c")
-
-
-class TestPatternLockfileMatcher:
-    @pytest.mark.quick
-    def test_is_match(self):
-        matcher = PatternLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        # Basic cases
-        assert matcher.is_match(Path("requirements.txt")) is True
-        assert matcher.is_match(Path("requirements3.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements3.txt")) is True
-
-        # Non-standard requirements cases
-        assert matcher.is_match(Path("requirements.dev.txt")) is True
-        assert matcher.is_match(Path("requirements.dev3.txt")) is True
-        assert matcher.is_match(Path("requirements-prod.txt")) is True
-        assert matcher.is_match(Path("requirements-dev.txt")) is True
-        assert matcher.is_match(Path("dev-requirements.txt")) is True
-        assert matcher.is_match(Path("dev-requirements3.txt")) is True
-        assert matcher.is_match(Path("prod-requirements.txt")) is True
-        assert matcher.is_match(Path("requirements_lock.txt")) is True
-
-        # Requirement folder cases
-        assert matcher.is_match(Path("requirements/prod.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements/base.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements/hello/dev.txt")) is True
-
-        # Non-requirements cases
-        assert matcher.is_match(Path("a/b/c/requirements.in")) is False
-        assert matcher.is_match(Path("unknown.lock")) is False
-
-    @pytest.mark.quick
-    @pytest.mark.parametrize("create_manifest", [True, False])
-    @pytest.mark.parametrize(
-        ("lockfile_path", "manifest_path"),
-        [
-            ("requirements.txt", "requirements.in"),
-            ("requirements3.txt", "requirements.in"),
-            ("a/b/c/requirements.txt", "a/b/c/requirements.in"),
-            ("a/b/c/requirements3.txt", "a/b/c/requirements.in"),
-        ],
-    )
-    def test_get_manifest_path(
-        self, tmp_path, monkeypatch, create_manifest, lockfile_path, manifest_path
-    ):
-        lockfile_full_path = tmp_path / Path(lockfile_path)
-        lockfile_full_path.parent.mkdir(parents=True, exist_ok=True)
-        lockfile_full_path.touch()
-        if create_manifest:
-            manifest_full_path = tmp_path / Path(manifest_path)
-            manifest_full_path.touch()
-
-        monkeypatch.chdir(tmp_path)
-
-        matcher = PatternLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-        matcher_manifest_path = matcher.get_manifest_path(Path(lockfile_path))
-        assert matcher_manifest_path == (
-            Path(manifest_path) if create_manifest else None
-        )
-
-    @pytest.mark.quick
-    def test_get_parent_path(self):
-        matcher = PatternLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        assert matcher.get_subproject_root(Path("requirements.txt")) == Path(".")
-        assert matcher.get_subproject_root(Path("a/b/c/requirements.txt")) == Path(
-            "a/b/c"
-        )
-
-
-class TestRequirementsLockfileMatcher:
-    @pytest.mark.quick
-    def test_is_match(self) -> None:
-        matcher = RequirementsLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        # Basic cases
-        assert matcher.is_match(Path("requirements.txt")) is True
-        assert matcher.is_match(Path("requirements3.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements3.txt")) is True
-
-        # Non-standard requirements cases
-        assert matcher.is_match(Path("requirements.dev.txt")) is True
-        assert matcher.is_match(Path("requirements.dev3.txt")) is True
-        assert matcher.is_match(Path("requirements-prod.txt")) is True
-        assert matcher.is_match(Path("requirements-dev.txt")) is True
-        assert matcher.is_match(Path("dev-requirements.txt")) is True
-        assert matcher.is_match(Path("dev-requirements3.txt")) is True
-        assert matcher.is_match(Path("prod-requirements.txt")) is True
-        assert matcher.is_match(Path("requirements_lock.txt")) is True
-
-        # Requirement folder cases
-        assert matcher.is_match(Path("requirements/prod.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements/base.txt")) is True
-        assert matcher.is_match(Path("a/b/c/requirements/hello/dev.txt")) is True
-
-        # Non-requirements cases
-        assert matcher.is_match(Path("a/b/c/requirements.in")) is False
-        assert matcher.is_match(Path("unknown.lock")) is False
-
-    @pytest.mark.quick
-    @pytest.mark.parametrize("create_manifest", [True, False])
-    @pytest.mark.parametrize(
-        ("lockfile_path", "manifest_path"),
-        [
-            ("requirements.txt", "requirements.in"),
-            ("requirements3.txt", "requirements.in"),
-            ("a/b/c/requirements.txt", "a/b/c/requirements.in"),
-            ("a/b/c/requirements3.txt", "a/b/c/requirements.in"),
-            ("requirements-dev.txt", "requirements.in"),
-            ("requirements-prod.txt", "requirements.in"),
-            ("dev-requirements.txt", "requirements.in"),
-            ("prod-requirements.txt", "requirements.in"),
-            ("requirements_lock.txt", "requirements.in"),
-            ("requirements/prod.txt", "requirements.in"),
-            # TODO(sal): Is this even a case that appears?
-            ("requirements/prod.txt", "requirements/prod.in"),
-        ],
-    )
-    def test_get_manifest_path(
-        self, tmp_path, monkeypatch, create_manifest, lockfile_path, manifest_path
-    ):
-        lockfile_full_path = tmp_path / Path(lockfile_path)
-        lockfile_full_path.parent.mkdir(parents=True, exist_ok=True)
-        lockfile_full_path.touch()
-        if create_manifest:
-            manifest_full_path = tmp_path / Path(manifest_path)
-            manifest_full_path.touch()
-
-        monkeypatch.chdir(tmp_path)
-
-        matcher = RequirementsLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-        matcher_manifest_path = matcher.get_manifest_path(Path(lockfile_path))
-        assert matcher_manifest_path == (
-            Path(manifest_path) if create_manifest else None
-        )
-
-    @pytest.mark.quick
-    def test_get_subproject_root(self):
-        matcher = RequirementsLockfileMatcher(
-            pattern="*requirements*.txt",
-            manifest="requirements.in",
-            parser=parse_pipfile,
-            ecosystem=Ecosystem(Pypi()),
-        )
-
-        assert matcher.get_subproject_root(Path("requirements.txt")) == Path(".")
-        assert matcher.get_subproject_root(Path("a/b/c/requirements.txt")) == Path(
-            "a/b/c"
-        )
-        assert matcher.get_subproject_root(Path("requirements-dev.txt")) == Path(".")
-        assert matcher.get_subproject_root(Path("requirements/prod.txt")) == Path(".")
-        assert matcher.get_subproject_root(Path("a/b/c/requirements/base.txt")) == Path(
-            "a/b/c"
-        )
-
-        # TODO(sal): Is this even a case we want to support? Unsure if we want to support
-        # non standard requirements files in subdirectories under `requirements/`.
-        assert matcher.get_subproject_root(
-            Path("a/b/c/requirements/base/dev.txt")
-        ) == Path("a/b/c")
-
-
-class TestCreateMatcher:
-    @pytest.mark.quick
-    @pytest.mark.parametrize(
-        "lockfile",
-        [
-            "Pipfile.lock",
-            "poetry.lock",
-            "package-lock.json",
-            "yarn.lock",
-            "pnpm-lock.yaml",
-            "Gemfile.lock",
-            "go.mod",
-            "Cargo.lock",
-            "maven_dep_tree.txt",
-            "gradle.lockfile",
-            "composer.lock",
-            "packages.lock.json",
-            "pubspec.lock",
-            "Package.resolved",
-            "mix.lock",
-        ],
-    )
-    def test_valid_match(self, lockfile):
-        path = Path(lockfile)
-        matcher = _create_matcher(path)
-        assert isinstance(matcher, ExactLockfileMatcher)
-        assert matcher.lockfile == lockfile
-
-    @pytest.mark.quick
-    @pytest.mark.parametrize(
-        "lockfile",
-        [
-            "requirements.txt",
-            "requirements.dev.txt",
-            "requirements/prod.txt",
-            "a/b/c/requirements/base.txt",
-        ],
-    )
-    def test_valid_pattern_match(self, lockfile):
-        EcosystemLockfiles.init(use_new_requirements_matchers=True)
-
-        matcher = _create_matcher(Path(lockfile))
-
-        assert isinstance(matcher, PatternLockfileMatcher)
-        assert matcher.pattern == "*requirement*.txt"
-
-    @pytest.mark.quick
-    def test_unknown_match(self):
-        path = Path("unknown.lock")
-        with pytest.raises(ValueError, match="Unknown lockfile"):
-            _create_matcher(path)
-
-
-class TestLockfile:
-    @pytest.mark.kinda_slow
-    @pytest.mark.parametrize(
-        "lockfile_path, ecosystem, manifest",
-        [
-            ("Pipfile.lock", Ecosystem(Pypi()), "Pipfile"),
-            ("poetry.lock", Ecosystem(Pypi()), "pyproject.toml"),
-            ("requirements.txt", Ecosystem(Pypi()), "requirements.in"),
-            ("package-lock.json", Ecosystem(Npm()), "package.json"),
-            ("yarn.lock", Ecosystem(Npm()), "package.json"),
-            ("pnpm-lock.yaml", Ecosystem(Npm()), None),
-            ("Gemfile.lock", Ecosystem(Gem()), None),
-            ("go.mod", Ecosystem(Gomod()), None),
-            ("Cargo.lock", Ecosystem(Cargo()), None),
-            ("maven_dep_tree.txt", Ecosystem(Maven()), None),
-            ("gradle.lockfile", Ecosystem(Maven()), "build.gradle"),
-            ("composer.lock", Ecosystem(Composer()), "composer.json"),
-            ("packages.lock.json", Ecosystem(Nuget()), None),
-            ("pubspec.lock", Ecosystem(Pub()), "pubspec.yaml"),
-            ("Package.resolved", Ecosystem(SwiftPM()), "Package.swift"),
-            ("mix.lock", Ecosystem(Hex()), "mix.exs"),
-            ("a/Pipfile.lock", Ecosystem(Pypi()), "Pipfile"),
-            ("a/b/poetry.lock", Ecosystem(Pypi()), "pyproject.toml"),
-            ("a/b/c/requirements.txt", Ecosystem(Pypi()), "requirements.in"),
-            ("./package-lock.json", Ecosystem(Npm()), "package.json"),
-        ],
-    )
-    def test_lockfile_from_path(
-        self, lockfile_path, ecosystem, manifest, tmp_path, monkeypatch
-    ):
-        # Create the lockfile and manifest in the temporary directory
-        lockfile_full_path = tmp_path / Path(lockfile_path)
-        lockfile_full_path.parent.mkdir(parents=True, exist_ok=True)
-        lockfile_full_path.touch()
-        if manifest:
-            manifest_full_path = tmp_path / Path(lockfile_path).parent / Path(manifest)
-            manifest_full_path.touch()
-        monkeypatch.chdir(tmp_path)
-
-        # Create the Lockfile instance
-        lockfile = Lockfile.from_path(Path(lockfile_path))
-
-        # Assert the Lockfile instance is correct
-        assert lockfile.path == Path(lockfile_path)
-        assert lockfile.ecosystem == ecosystem
-        assert lockfile.manifest_path == (
-            Path(lockfile_path).parent / Path(manifest) if manifest else None
-        )
-
-    @pytest.mark.kinda_slow
-    @pytest.mark.parametrize(
-        "lockfile_path, ecosystem, manifest_path",
-        [
-            ("requirements.txt", Ecosystem(Pypi()), "requirements.in"),
-            ("a/b/c/requirements.txt", Ecosystem(Pypi()), "a/b/c/requirements.in"),
-            (
-                "a/b/c/requirements/base.txt",
-                Ecosystem(Pypi()),
-                "a/b/c/requirements/base.in",
-            ),
-        ],
-    )
-    def test_pattern_matchers(
-        self, lockfile_path, ecosystem, manifest_path, tmp_path, monkeypatch
-    ):
-        EcosystemLockfiles.init(use_new_requirements_matchers=True)
-
-        # Create the lockfile and manifest in the temporary directory
-        lockfile_full_path = tmp_path / Path(lockfile_path)
-        lockfile_full_path.parent.mkdir(parents=True, exist_ok=True)
-        lockfile_full_path.touch()
-        manifest_full_path = tmp_path / Path(manifest_path)
-        manifest_full_path.touch()
-        monkeypatch.chdir(tmp_path)
-
-        # Create the Lockfile instance
-        lockfile = Lockfile.from_path(Path(lockfile_path))
-
-        # Assert the Lockfile instance is correct
-        assert lockfile.path == Path(lockfile_path)
-        assert lockfile.ecosystem == ecosystem
-        assert lockfile.manifest_path == Path(manifest_path)
-
-
-@pytest.mark.quick
-def test_filter_lockfile_paths():
-    ecosystem = Ecosystem(Pypi())
-    valid_paths = {Path("Pipfile.lock"), Path("requirements.txt")}
-    invalid_paths = {Path("unknown.lock"), Path("setup.py")}
-    candidates = valid_paths | invalid_paths
-
-    filtered_paths = filter_lockfile_paths(ecosystem, frozenset(candidates))
-
-    assert filtered_paths == valid_paths
-
-
-@pytest.mark.quick
-@pytest.mark.parametrize(
-    ("lockfile", "expected"),
-    [
-        (Path("Pipfile.lock"), True),
-        (Path("requirements.txt"), True),
-        (Path("unknown.lock"), False),
-        (Path("setup.py"), False),
-    ],
-)
-def test_is_valid_lockfile(lockfile, expected):
-    ecosystem = Ecosystem(Pypi())
-    assert _is_valid_lockfile(ecosystem, lockfile) is expected
-
-
-class TestEcosystemLockfiles:
-    @pytest.mark.quick
-    def test_base_case(self):
-        EcosystemLockfiles.init(use_new_requirements_matchers=False)
-
-        # Use the old requirements lockfile matchers for Python
-        assert (
-            EcosystemLockfiles.ecosystem_to_lockfiles[Ecosystem(Pypi())]
-            == OLD_REQUIREMENTS_MATCHERS
-        )
-
-        # Use the new requirements lockfile matchers for Python
-        EcosystemLockfiles.init(use_new_requirements_matchers=True)
-        assert (
-            EcosystemLockfiles.ecosystem_to_lockfiles[Ecosystem(Pypi())]
-            == NEW_REQUIREMENTS_MATCHERS
-        )
-
-        # Use the old requirements lockfile matchers for Python
-        EcosystemLockfiles.init(use_new_requirements_matchers=False)
-        assert (
-            EcosystemLockfiles.ecosystem_to_lockfiles[Ecosystem(Pypi())]
-            == OLD_REQUIREMENTS_MATCHERS
-        )
diff --git a/cli/tests/default/unit/test_paths_to_transitivity.py b/cli/tests/default/unit/test_paths_to_transitivity.py
index 6a42821ec137..8f5379e86883 100644
--- a/cli/tests/default/unit/test_paths_to_transitivity.py
+++ b/cli/tests/default/unit/test_paths_to_transitivity.py
@@ -2,7 +2,8 @@
 
 import pytest
 
-from semdep.lockfile import Lockfile
+from semdep.parsers.pom_tree import parse_pom_tree
+from semdep.parsers.util import SemgrepParser
 
 pytestmark = pytest.mark.kinda_slow
 
@@ -63,14 +64,17 @@
 # These are file targets which are ecosystems that we can calculate the path to
 # transitivity for
 @pytest.mark.parametrize(
-    "target_supports_path_to_transitivity",
+    ("target_supports_path_to_transitivity", "parser"),
     [
-        "targets/dependency_aware/log4j/maven_dep_tree.txt",
+        ("targets/dependency_aware/log4j/maven_dep_tree.txt", parse_pom_tree),
     ],
 )
-def test_child_construction(lockfile_path_in_tmp, target_supports_path_to_transitivity):
-    lockfile = Lockfile.from_path(Path(target_supports_path_to_transitivity))
-    dependencies, error = lockfile.parse()
+def test_child_construction(
+    lockfile_path_in_tmp,
+    target_supports_path_to_transitivity: str,
+    parser: SemgrepParser,
+):
+    dependencies, error = parser(Path(target_supports_path_to_transitivity), None)
 
     """
     This might be weird but I wanted to create a simple test that people can reuse as we
diff --git a/cli/tests/default/unit/test_resolve_subprojects.py b/cli/tests/default/unit/test_resolve_subprojects.py
new file mode 100644
index 000000000000..02e85fe1ea0b
--- /dev/null
+++ b/cli/tests/default/unit/test_resolve_subprojects.py
@@ -0,0 +1,94 @@
+from pathlib import Path
+from typing import List
+
+import pytest
+
+from semdep.subproject_matchers import ExactLockfileManifestMatcher
+from semdep.subproject_matchers import SubprojectMatcher
+from semgrep.resolve_subprojects import find_subprojects
+from semgrep.subproject import LockfileDependencySource
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import Subproject
+
+
+@pytest.mark.quick
+@pytest.mark.parametrize(
+    ["file_paths", "matchers", "expected_subprojects"],
+    [
+        (
+            # verify that when two matchers both look for the same files, only the first subproject includes the overlapping file.
+            [
+                Path("requirements.in"),
+                Path("requirements.txt"),
+                Path("requirements3.txt"),
+            ],
+            [
+                ExactLockfileManifestMatcher(
+                    lockfile_name="requirements.txt",
+                    manifest_name="requirements.in",
+                    package_manager_type=PackageManagerType.PIP,
+                ),
+                ExactLockfileManifestMatcher(
+                    lockfile_name="requirements3.txt",
+                    manifest_name="requirements.in",
+                    package_manager_type=PackageManagerType.PIP,
+                ),
+            ],
+            [
+                Subproject(
+                    root_dir=Path(),
+                    dependency_source=LockfileDependencySource(
+                        package_manager_type=PackageManagerType.PIP,
+                        manifest_path=Path("requirements.in"),
+                        lockfile_path=Path("requirements.txt"),
+                    ),
+                ),
+                Subproject(
+                    root_dir=Path(),
+                    dependency_source=LockfileDependencySource(
+                        package_manager_type=PackageManagerType.PIP,
+                        manifest_path=None,
+                        lockfile_path=Path("requirements3.txt"),
+                    ),
+                ),
+            ],
+        ),
+        (
+            # verify that we correctly use the second matcher when the first is a match for the manifest
+            # but not for the lockfile.
+            [
+                Path("requirements.in"),
+                Path("requirements3.txt"),
+            ],
+            [
+                ExactLockfileManifestMatcher(
+                    lockfile_name="requirements.txt",
+                    manifest_name="requirements.in",
+                    package_manager_type=PackageManagerType.PIP,
+                ),
+                ExactLockfileManifestMatcher(
+                    lockfile_name="requirements3.txt",
+                    manifest_name="requirements.in",
+                    package_manager_type=PackageManagerType.PIP,
+                ),
+            ],
+            [
+                Subproject(
+                    root_dir=Path(),
+                    dependency_source=LockfileDependencySource(
+                        package_manager_type=PackageManagerType.PIP,
+                        manifest_path=Path("requirements.in"),
+                        lockfile_path=Path("requirements3.txt"),
+                    ),
+                ),
+            ],
+        ),
+    ],
+)
+def test_find_subprojects(
+    file_paths: List[Path],
+    matchers: List[SubprojectMatcher],
+    expected_subprojects: List[Subproject],
+) -> None:
+    result = find_subprojects(frozenset(file_paths), matchers)
+    assert result == expected_subprojects
diff --git a/cli/tests/default/unit/test_subproject.py b/cli/tests/default/unit/test_subproject.py
index fe7d737cfce6..e6083c0316c1 100644
--- a/cli/tests/default/unit/test_subproject.py
+++ b/cli/tests/default/unit/test_subproject.py
@@ -2,8 +2,6 @@
 
 import pytest
 
-from semdep.lockfile import EcosystemLockfiles
-from semdep.lockfile import Lockfile
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Ecosystem
 from semgrep.semgrep_interfaces.semgrep_output_v1 import FoundDependency
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Fpath
@@ -11,11 +9,13 @@
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Pypi
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Transitivity
 from semgrep.semgrep_interfaces.semgrep_output_v1 import Unknown
-from semgrep.subproject import _create_dependency_source
 from semgrep.subproject import find_closest_subproject
 from semgrep.subproject import LockfileDependencySource
 from semgrep.subproject import MultiLockfileDependencySource
-from semgrep.subproject import Subproject
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import ResolutionMethod
+from semgrep.subproject import ResolvedDependencies
+from semgrep.subproject import ResolvedSubproject
 
 
 def create_tmp_file(path: Path):
@@ -33,24 +33,28 @@ def test_finds_subproject_in_same_directory(self, tmp_path, monkeypatch):
 
         monkeypatch.chdir(tmp_path)
 
-        expected = Subproject(
+        expected = ResolvedSubproject(
             root_dir=Path("a/b/c"),
             dependency_source=LockfileDependencySource(
                 lockfile_path=lockfile_path,
                 manifest_path=Path("a/b/c/requirements.in"),
+                package_manager_type=PackageManagerType.PIP,
             ),
+            found_dependencies=ResolvedDependencies.from_found_dependencies([]),
             ecosystem=Ecosystem(Pypi()),
-            found_dependencies=[],
+            resolution_method=ResolutionMethod.LOCKFILE_PARSING,
         )
         extra = [
-            Subproject(
+            ResolvedSubproject(
                 root_dir=Path("a/b"),
                 dependency_source=LockfileDependencySource(
                     lockfile_path=extra_lockfile_path,
                     manifest_path=Path("a/b/requirements.in"),
+                    package_manager_type=PackageManagerType.PIP,
                 ),
+                found_dependencies=ResolvedDependencies.from_found_dependencies([]),
                 ecosystem=Ecosystem(Pypi()),
-                found_dependencies=[],
+                resolution_method=ResolutionMethod.LOCKFILE_PARSING,
             )
         ]
 
@@ -70,24 +74,28 @@ def test_finds_subproject_for_requested_ecosystem(self, tmp_path, monkeypatch):
 
         monkeypatch.chdir(tmp_path)
 
-        expected = Subproject(
+        expected = ResolvedSubproject(
             root_dir=Path("a/b"),
             dependency_source=LockfileDependencySource(
                 lockfile_path=lockfile_path,
                 manifest_path=Path("a/b/build.gradle"),
+                package_manager_type=PackageManagerType.GRADLE,
             ),
+            found_dependencies=ResolvedDependencies.from_found_dependencies([]),
             ecosystem=Ecosystem(Maven()),
-            found_dependencies=[],
+            resolution_method=ResolutionMethod.LOCKFILE_PARSING,
         )
         extra = [
-            Subproject(
+            ResolvedSubproject(
                 root_dir=Path("a/b/c"),
                 dependency_source=LockfileDependencySource(
                     lockfile_path=extra_lockfile_path,
                     manifest_path=Path("a/b/c/requirements.in"),
+                    package_manager_type=PackageManagerType.PIP,
                 ),
+                found_dependencies=ResolvedDependencies.from_found_dependencies([]),
                 ecosystem=Ecosystem(Pypi()),
-                found_dependencies=[],
+                resolution_method=ResolutionMethod.LOCKFILE_PARSING,
             )
         ]
 
@@ -102,7 +110,7 @@ class TestSubproject:
     @pytest.mark.parametrize(
         "lockfile_path", [Path("a/b/c/requirements.txt"), Path("requirements.txt")]
     )
-    def test_base_case(self, lockfile_path):
+    def test_base_case(self, lockfile_path: Path):
         found_dependencies = [
             FoundDependency(
                 package="requests",
@@ -114,21 +122,29 @@ def test_base_case(self, lockfile_path):
             )
         ]
 
-        subproject = Subproject(
+        subproject = ResolvedSubproject(
             root_dir=Path("a/b/c"),
             dependency_source=LockfileDependencySource(
                 lockfile_path=lockfile_path,
                 manifest_path=Path("a/b/c/requirements.in"),
+                package_manager_type=PackageManagerType.PIP,
             ),
+            resolution_method=ResolutionMethod.LOCKFILE_PARSING,
             ecosystem=Ecosystem(Pypi()),
-            found_dependencies=found_dependencies,
+            found_dependencies=ResolvedDependencies.from_found_dependencies(
+                found_dependencies
+            ),
         )
-
-        assert subproject.map_lockfile_to_dependencies() == {
+        (
+            lockfile_dep_map,
+            unknown_lockfile_deps,
+        ) = subproject.found_dependencies.make_dependencies_by_source_path()
+        assert len(unknown_lockfile_deps) == 0
+        assert lockfile_dep_map == {
             str(lockfile_path): found_dependencies
         }, "Should return mapping of lockfile path to dependencies"
 
-        assert subproject.get_lockfile_paths() == [
+        assert subproject.dependency_source.get_display_paths() == [
             lockfile_path
         ], "Should return lockfile path"
 
@@ -156,28 +172,39 @@ def test_multiple_lockfiles(self):
         ]
 
         multi_lockfile_source = MultiLockfileDependencySource(
-            sources=[
+            sources=(
                 LockfileDependencySource(
-                    lockfile_path=lockfile_path, manifest_path=None
+                    lockfile_path=lockfile_path,
+                    manifest_path=None,
+                    package_manager_type=PackageManagerType.PIP,
                 ),
                 LockfileDependencySource(
-                    lockfile_path=extra_lockfile_path, manifest_path=None
+                    lockfile_path=extra_lockfile_path,
+                    manifest_path=None,
+                    package_manager_type=PackageManagerType.PIP,
                 ),
-            ]
+            )
         )
 
-        subproject = Subproject(
+        subproject = ResolvedSubproject(
             root_dir=Path("a/b/c"),
             dependency_source=multi_lockfile_source,
             ecosystem=Ecosystem(Pypi()),
-            found_dependencies=found_dependencies,
+            resolution_method=ResolutionMethod.LOCKFILE_PARSING,
+            found_dependencies=ResolvedDependencies.from_found_dependencies(
+                found_dependencies
+            ),
         )
 
-        lockfile_deps_map = subproject.map_lockfile_to_dependencies()
+        (
+            lockfile_deps_map,
+            unknown_lockfile_deps,
+        ) = subproject.found_dependencies.make_dependencies_by_source_path()
+        assert len(unknown_lockfile_deps) == 0
         assert lockfile_deps_map[str(lockfile_path)][0] == found_dependencies[0]
         assert lockfile_deps_map[str(extra_lockfile_path)][0] == found_dependencies[1]
 
-        assert subproject.get_lockfile_paths() == [
+        assert subproject.dependency_source.get_display_paths() == [
             lockfile_path,
             extra_lockfile_path,
         ], "Should return lockfile paths"
@@ -195,23 +222,28 @@ def test_dep_missing_lockfile_path(self):
             )
         ]
 
-        subproject = Subproject(
+        subproject = ResolvedSubproject(
             root_dir=Path("a/b/c"),
             dependency_source=LockfileDependencySource(
                 lockfile_path=lockfile_path,
                 manifest_path=Path("a/b/c/requirements.in"),
+                package_manager_type=PackageManagerType.PIP,
             ),
+            resolution_method=ResolutionMethod.LOCKFILE_PARSING,
             ecosystem=Ecosystem(Pypi()),
-            found_dependencies=found_dependencies,
+            found_dependencies=ResolvedDependencies.from_found_dependencies(
+                found_dependencies
+            ),
         )
 
-        assert subproject.map_lockfile_to_dependencies() == {
-            str(
-                subproject.root_dir.joinpath(Path("unknown_lockfile"))
-            ): found_dependencies
-        }, "Should return mapping of lockfile path to dependencies"
+        (
+            lockfile_deps_map,
+            unknown_lockfile_deps,
+        ) = subproject.found_dependencies.make_dependencies_by_source_path()
+        assert len(unknown_lockfile_deps) == 1
+        assert len(lockfile_deps_map) == 0
 
-        assert subproject.get_lockfile_paths() == [
+        assert subproject.dependency_source.get_display_paths() == [
             lockfile_path
         ], "Should return lockfile path"
 
@@ -222,10 +254,12 @@ def test_base_case(self):
         lockfile_path = Path("a/b/c/requirements.txt")
 
         source = LockfileDependencySource(
-            lockfile_path=lockfile_path, manifest_path=None
+            lockfile_path=lockfile_path,
+            manifest_path=None,
+            package_manager_type=PackageManagerType.PIP,
         )
 
-        assert source.get_lockfile_paths() == [
+        assert source.get_display_paths() == [
             lockfile_path
         ], "Should return lockfile path"
 
@@ -237,54 +271,21 @@ def test_base_case(self):
         extra_lockfile_path = Path("a/b/requirements/dev.txt")
 
         source = MultiLockfileDependencySource(
-            sources=[
+            sources=(
                 LockfileDependencySource(
-                    lockfile_path=lockfile_path, manifest_path=None
+                    lockfile_path=lockfile_path,
+                    manifest_path=None,
+                    package_manager_type=PackageManagerType.PIP,
                 ),
                 LockfileDependencySource(
-                    lockfile_path=extra_lockfile_path, manifest_path=None
+                    lockfile_path=extra_lockfile_path,
+                    manifest_path=None,
+                    package_manager_type=PackageManagerType.PIP,
                 ),
-            ]
+            )
         )
 
-        assert source.get_lockfile_paths() == [
+        assert source.get_display_paths() == [
             lockfile_path,
             extra_lockfile_path,
         ], "Should return lockfile paths"
-
-
-class TestCreateDependencySource:
-    @pytest.mark.quick
-    def test_single_lockfile(self):
-        lockfile_path = Path("requirements.txt")
-        lockfiles = [Lockfile.from_path(lockfile_path)]
-
-        source = _create_dependency_source(lockfiles)
-
-        assert source == LockfileDependencySource(
-            lockfile_path=lockfile_path, manifest_path=None
-        ), "Should return LockfileDependencySource"
-
-    @pytest.mark.quick
-    def test_multiple_lockfiles(self):
-        EcosystemLockfiles.init(use_new_requirements_matchers=True)
-
-        lockfile_path = Path("requirements.txt")
-        extra_lockfile_path = Path("requirements/dev.txt")
-        lockfiles = [
-            Lockfile.from_path(lockfile_path),
-            Lockfile.from_path(extra_lockfile_path),
-        ]
-
-        source = _create_dependency_source(lockfiles)
-
-        assert source == MultiLockfileDependencySource(
-            sources=[
-                LockfileDependencySource(
-                    lockfile_path=lockfile_path, manifest_path=None
-                ),
-                LockfileDependencySource(
-                    lockfile_path=extra_lockfile_path, manifest_path=None
-                ),
-            ]
-        ), "Should return MultiLockfileDependencySource"
diff --git a/cli/tests/default/unit/test_subproject_matchers.py b/cli/tests/default/unit/test_subproject_matchers.py
new file mode 100644
index 000000000000..1476104d5c41
--- /dev/null
+++ b/cli/tests/default/unit/test_subproject_matchers.py
@@ -0,0 +1,440 @@
+from pathlib import Path
+from typing import List
+
+import pytest
+
+from semdep.subproject_matchers import ExactLockfileManifestMatcher
+from semdep.subproject_matchers import filter_dependency_source_files
+from semdep.subproject_matchers import PatternLockfileMatcher
+from semdep.subproject_matchers import PipRequirementsMatcher
+from semgrep.subproject import LockfileDependencySource
+from semgrep.subproject import MultiLockfileDependencySource
+from semgrep.subproject import PackageManagerType
+from semgrep.subproject import Subproject
+
+
+class TestExactLockfileMatcher:
+    @pytest.mark.quick
+    def test_lockfile_match(self):
+        matcher = ExactLockfileManifestMatcher(
+            package_manager_type=PackageManagerType.PIPENV,
+            lockfile_name="Pipfile.lock",
+            manifest_name="Pipfile",
+        )
+
+        assert matcher.is_match(Path("Pipfile.lock")) is True
+        assert matcher.is_match(Path("a/b/c/Pipfile.lock")) is True
+        assert matcher.is_match(Path("requirements.txt")) is False
+
+    @pytest.mark.quick
+    def test_manifest_match(self):
+        matcher = ExactLockfileManifestMatcher(
+            package_manager_type=PackageManagerType.PIPENV,
+            lockfile_name="Pipfile.lock",
+            manifest_name="Pipfile",
+        )
+
+        assert matcher.is_match(Path("Pipfile")) is True
+        assert matcher.is_match(Path("a/b/c/Pipfile")) is True
+        assert matcher.is_match(Path("requirements.txt")) is False
+
+    @pytest.mark.quick
+    @pytest.mark.parametrize("create_manifest", [True, False])
+    def test_make_subprojects(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, create_manifest: bool
+    ):
+        manifest_path = tmp_path / "Pipfile"
+        lockfile_path = tmp_path / "Pipfile.lock"
+
+        monkeypatch.chdir(tmp_path)
+
+        matcher = ExactLockfileManifestMatcher(
+            lockfile_name="Pipfile.lock",
+            manifest_name="Pipfile",
+            package_manager_type=PackageManagerType.PIPENV,
+        )
+
+        assert matcher.is_match(manifest_path)
+        assert matcher.is_match(lockfile_path)
+        files = frozenset(
+            [lockfile_path] + ([manifest_path] if create_manifest else [])
+        )
+        subprojects, used_files = matcher.make_subprojects(files)
+        assert used_files == files
+        assert len(subprojects) == 1
+        subproject = subprojects[0]
+        assert subproject.root_dir == tmp_path
+        assert isinstance(subproject.dependency_source, LockfileDependencySource)
+        assert subproject.dependency_source.lockfile_path == lockfile_path
+        if create_manifest:
+            assert subproject.dependency_source.manifest_path == manifest_path
+        else:
+            assert subproject.dependency_source.manifest_path is None
+
+
+class TestPatternLockfileMatcher:
+    @pytest.mark.quick
+    def test_is_match(self):
+        matcher = PatternLockfileMatcher(
+            lockfile_pattern="*requirements*.txt",
+            manifest_name="requirements.in",
+            package_manager_type=PackageManagerType.PIP,
+        )
+
+        # Basic cases
+        assert matcher.is_match(Path("requirements.txt")) is True
+        assert matcher.is_match(Path("requirements3.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements3.txt")) is True
+
+        # Non-standard requirements cases
+        assert matcher.is_match(Path("requirements.dev.txt")) is True
+        assert matcher.is_match(Path("requirements.dev3.txt")) is True
+        assert matcher.is_match(Path("requirements-prod.txt")) is True
+        assert matcher.is_match(Path("requirements-dev.txt")) is True
+        assert matcher.is_match(Path("dev-requirements.txt")) is True
+        assert matcher.is_match(Path("dev-requirements3.txt")) is True
+        assert matcher.is_match(Path("prod-requirements.txt")) is True
+        assert matcher.is_match(Path("requirements_lock.txt")) is True
+
+        # Requirement folder cases
+        assert matcher.is_match(Path("requirements/prod.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements/base.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements/hello/dev.txt")) is True
+
+        # Non-requirements cases
+        assert matcher.is_match(Path("a/b/c/requirements.in")) is True
+        assert matcher.is_match(Path("unknown.lock")) is False
+        assert matcher.is_match(Path("a/b/c/requirements/hello/unknown.lock")) is False
+
+    @pytest.mark.quick
+    @pytest.mark.parametrize("with_manifest", [True, False])
+    def test_make_subprojects(
+        self,
+        with_manifest: bool,
+    ):
+        # with four lockfiles and four corresponding manifests
+        # map lockfile to root directory and manifest path
+        test_data = {
+            Path("requirements.txt"): (Path(""), Path("requirements.in")),
+            Path("requirements3.txt"): (Path(""), Path("requirements.in")),
+            Path("a/b/c/requirements.txt"): (
+                Path("a/b/c"),
+                Path("a/b/c/requirements.in"),
+            ),
+            Path("a/b/c/requirements3.txt"): (
+                Path("a/b/c"),
+                Path("a/b/c/requirements.in"),
+            ),
+        }
+
+        # and a pattern matcher
+        matcher = PatternLockfileMatcher(
+            lockfile_pattern="*requirements*.txt",
+            manifest_name="requirements.in",
+            package_manager_type=PackageManagerType.PIP,
+        )
+
+        # expect the matcher to create four subprojects, with or without manifests
+        manifests = (
+            [manifest for _lockfile, (_root_dir, manifest) in test_data.items()]
+            if with_manifest
+            else []
+        )
+        lockfiles = [lockfile for lockfile in test_data]
+        files = frozenset(manifests + lockfiles)
+        subprojects, used_files = matcher.make_subprojects(files)
+        assert used_files == files
+        assert len(subprojects) == 4
+        for subproject in subprojects:
+            assert isinstance(subproject.dependency_source, LockfileDependencySource)
+            expected_root, expected_manifest = test_data[
+                subproject.dependency_source.lockfile_path
+            ]
+            assert subproject.root_dir == expected_root
+            if with_manifest:
+                assert subproject.dependency_source.manifest_path == expected_manifest
+            else:
+                assert subproject.dependency_source.manifest_path is None
+
+
+class TestRequirementsLockfileMatcher:
+    @pytest.mark.quick
+    def test_is_match(self) -> None:
+        matcher = PipRequirementsMatcher(
+            base_file_pattern="*requirement*",
+            requirements_file_extensions=["txt", "pip"],
+            manifest_file_extension="in",
+            default_manifest_file_base="requirements",
+        )
+
+        # Basic cases
+        assert matcher.is_match(Path("requirements.txt")) is True
+        assert matcher.is_match(Path("requirements3.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements3.txt")) is True
+        assert matcher.is_match(Path("requirements.pip")) is True
+        assert matcher.is_match(Path("requirement.pip")) is True
+        assert matcher.is_match(Path("a/b/c/requirements.pip")) is True
+
+        # Non-standard requirements cases
+        assert matcher.is_match(Path("requirements.dev.txt")) is True
+        assert matcher.is_match(Path("requirements.dev3.txt")) is True
+        assert matcher.is_match(Path("requirements-prod.txt")) is True
+        assert matcher.is_match(Path("requirements-dev.txt")) is True
+        assert matcher.is_match(Path("dev-requirements.txt")) is True
+        assert matcher.is_match(Path("dev-requirements3.txt")) is True
+        assert matcher.is_match(Path("prod-requirements.txt")) is True
+        assert matcher.is_match(Path("requirements_lock.txt")) is True
+
+        # Requirement folder cases
+        assert matcher.is_match(Path("requirements/prod.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements/base.txt")) is True
+        assert matcher.is_match(Path("a/b/c/requirements/hello/dev.txt")) is True
+
+        # Manifest cases
+        assert matcher.is_match(Path("a/b/c/requirements.in")) is True
+        assert matcher.is_match(Path("a/b/c/requirements-prod.in")) is True
+        assert matcher.is_match(Path("a/b/requirements/prod.in")) is True
+
+        # Non-requirements cases
+        assert matcher.is_match(Path("unknown.lock")) is False
+        assert matcher.is_match(Path("a/b/c/requirements/hello/unknown.lock")) is False
+
+    @pytest.mark.quick
+    @pytest.mark.parametrize(
+        ["source_files", "expected_subprojects"],
+        [
+            (
+                [
+                    Path("requirements.txt"),
+                    Path("requirements.in"),
+                    Path("a/b/c/requirements.txt"),
+                    Path("a/b/c/requirements.in"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=LockfileDependencySource(
+                            package_manager_type=PackageManagerType.PIP,
+                            manifest_path=Path("requirements.in"),
+                            lockfile_path=Path("requirements.txt"),
+                        ),
+                    ),
+                    Subproject(
+                        root_dir=Path("a/b/c"),
+                        dependency_source=LockfileDependencySource(
+                            package_manager_type=PackageManagerType.PIP,
+                            manifest_path=Path("a/b/c/requirements.in"),
+                            lockfile_path=Path("a/b/c/requirements.txt"),
+                        ),
+                    ),
+                ],
+            ),
+            (
+                [
+                    Path("requirements3.txt"),
+                    Path("requirements3.in"),
+                    Path("a/b/c/requirements3.txt"),
+                    Path("a/b/c/requirements.in"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=LockfileDependencySource(
+                            package_manager_type=PackageManagerType.PIP,
+                            manifest_path=Path("requirements3.in"),
+                            lockfile_path=Path("requirements3.txt"),
+                        ),
+                    ),
+                    Subproject(
+                        root_dir=Path("a/b/c"),
+                        dependency_source=LockfileDependencySource(
+                            package_manager_type=PackageManagerType.PIP,
+                            manifest_path=Path("a/b/c/requirements.in"),
+                            lockfile_path=Path("a/b/c/requirements3.txt"),
+                        ),
+                    ),
+                ],
+            ),
+            (
+                [
+                    Path("requirements-dev.txt"),
+                    Path("requirements.in"),
+                    Path("requirements-prod.txt"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=MultiLockfileDependencySource(
+                            sources=(
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("requirements-dev.txt"),
+                                ),
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("requirements-prod.txt"),
+                                ),
+                            )
+                        ),
+                    )
+                ],
+            ),
+            (
+                [
+                    Path("dev-requirements.txt"),
+                    Path("requirements.in"),
+                    Path("prod-requirements.txt"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=MultiLockfileDependencySource(
+                            sources=(
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("dev-requirements.txt"),
+                                ),
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("prod-requirements.txt"),
+                                ),
+                            )
+                        ),
+                    )
+                ],
+            ),
+            (
+                [Path("requirements_lock.txt"), Path("requirements.in")],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=LockfileDependencySource(
+                            package_manager_type=PackageManagerType.PIP,
+                            manifest_path=Path("requirements.in"),
+                            lockfile_path=Path("requirements_lock.txt"),
+                        ),
+                    )
+                ],
+            ),
+            (
+                [
+                    Path("requirements/dev.txt"),
+                    Path("requirements.in"),
+                    Path("requirements/prod.txt"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=MultiLockfileDependencySource(
+                            sources=(
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("requirements/dev.txt"),
+                                ),
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("requirements/prod.txt"),
+                                ),
+                            )
+                        ),
+                    )
+                ],
+            ),
+            (
+                [
+                    Path("requirements/dev.txt"),
+                    Path("requirements.in"),
+                    Path("requirements/prod.txt"),
+                    Path("requirements/prod.in"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=MultiLockfileDependencySource(
+                            sources=(
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements.in"),
+                                    lockfile_path=Path("requirements/dev.txt"),
+                                ),
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=Path("requirements/prod.in"),
+                                    lockfile_path=Path("requirements/prod.txt"),
+                                ),
+                            )
+                        ),
+                    )
+                ],
+            ),
+            (
+                [
+                    Path("requirements/dev.txt"),
+                    Path("requirements/prod.txt"),
+                ],
+                [
+                    Subproject(
+                        root_dir=Path(),
+                        dependency_source=MultiLockfileDependencySource(
+                            sources=(
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=None,
+                                    lockfile_path=Path("requirements/dev.txt"),
+                                ),
+                                LockfileDependencySource(
+                                    package_manager_type=PackageManagerType.PIP,
+                                    manifest_path=None,
+                                    lockfile_path=Path("requirements/prod.txt"),
+                                ),
+                            )
+                        ),
+                    )
+                ],
+            ),
+        ],
+    )
+    @pytest.mark.quick
+    def test_make_subprojects(
+        self, source_files: List[Path], expected_subprojects: List[Subproject]
+    ):
+        # with a basic requirements matcher
+        matcher = PipRequirementsMatcher(
+            base_file_pattern="*requirements*",
+            requirements_file_extensions=["txt", "pip"],
+            manifest_file_extension="in",
+            default_manifest_file_base="requirements",
+        )
+
+        source_files_set = frozenset(source_files)
+
+        # when we make subprojects from the provided source files
+        subprojects, used_files = matcher.make_subprojects(source_files_set)
+
+        # expect all files to be used
+        assert used_files == source_files_set
+
+        # and expect the returned subprojects to match, ignoring order
+        expected = set(expected_subprojects)
+        assert len(subprojects) == len(expected_subprojects)
+        for subproject in subprojects:
+            assert subproject in expected
+
+
+@pytest.mark.quick
+def test_filter_dependency_source_files():
+    valid_paths = {Path("Pipfile.lock"), Path("requirements.txt")}
+    invalid_paths = {Path("unknown.lock"), Path("setup.py")}
+    candidates = valid_paths | invalid_paths
+
+    filtered_paths = filter_dependency_source_files(frozenset(candidates))
+
+    assert filtered_paths == valid_paths
diff --git a/cli/tests/performance/test_semdep.py b/cli/tests/performance/test_semdep.py
index e01b3505eec2..12ea84130d64 100644
--- a/cli/tests/performance/test_semdep.py
+++ b/cli/tests/performance/test_semdep.py
@@ -3,39 +3,52 @@
 
 import pytest
 
-from semdep.lockfile import Lockfile
+from semdep.parsers.gem import parse_gemfile
+from semdep.parsers.go_mod import parse_go_mod
+from semdep.parsers.gradle import parse_gradle
+from semdep.parsers.package_lock import parse_package_lock
+from semdep.parsers.pipfile import parse_pipfile
+from semdep.parsers.poetry import parse_poetry
+from semdep.parsers.pom_tree import parse_pom_tree
+from semdep.parsers.requirements import parse_requirements
+from semdep.parsers.util import SemgrepParser
+from semdep.parsers.yarn import parse_yarn
 
 
 @pytest.mark.kinda_slow
 @pytest.mark.parametrize(
-    "file_size,target,max_time",
+    "file_size,target,max_time,parser",
     [
-        (file_size, target, max_time)
+        (file_size, target, max_time, parser)
         # These times are set relative to Github Actions, they should be lower when
         # running locally.
         # Local time expectation is more like 1, 5, 10
         # update: 10k is actually taking more time locally, 3.5 on pad's Linux machine
         for file_size, max_time in [("10k", 4), ("50k", 15), ("100k", 30)]
-        for target in [
-            "Gemfile.lock",
-            "go.mod",
-            "gradle.lockfile",
-            "maven_dep_tree.txt",
-            "package-lock.json",
-            "poetry.lock",
-            "requirements.txt",
-            "yarn.lock",
-            "Pipfile.lock",
+        for target, parser in [
+            ("Gemfile.lock", parse_gemfile),
+            ("go.mod", parse_go_mod),
+            ("gradle.lockfile", parse_gradle),
+            ("maven_dep_tree.txt", parse_pom_tree),
+            ("package-lock.json", parse_package_lock),
+            ("poetry.lock", parse_poetry),
+            ("requirements.txt", parse_requirements),
+            ("yarn.lock", parse_yarn),
+            ("Pipfile.lock", parse_pipfile),
         ]
     ],
 )
 def test_dependency_aware_timing(
-    lockfile_path_in_tmp_for_perf, file_size, target, max_time
+    lockfile_path_in_tmp_for_perf,
+    file_size: str,
+    target: str,
+    max_time: int,
+    parser: SemgrepParser,
 ):
     start = time()
 
     lockfile_path = Path(f"targets_perf_sca/{file_size}/{target}")
-    _, error = Lockfile.from_path(lockfile_path).parse()
+    _, error = parser(lockfile_path, None)
 
     end = time()
 
diff --git a/cli/tests/qa/test_semgrep_rules_repo.py b/cli/tests/qa/test_semgrep_rules_repo.py
deleted file mode 100644
index 0a5f761dc815..000000000000
--- a/cli/tests/qa/test_semgrep_rules_repo.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import os
-import subprocess
-from pathlib import Path
-
-import pytest
-
-
-# We use this to locate the semgrep-rules submodule
-def get_git_project_root() -> str:
-    res = subprocess.Popen(
-        ["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE
-    )
-    return res.communicate()[0].rstrip().decode("utf-8")
-
-
-@pytest.fixture(scope="session", autouse=True)
-def in_semgrep_rules_repo(tmpdir_factory):
-    project_root = Path(get_git_project_root())
-    monkeypatch = pytest.MonkeyPatch()
-    # semgrep-rules is available as a git submodule:
-
-    if project_root.name == "semgrep":
-        repo_dir = project_root / "tests/semgrep-rules"
-    elif project_root.name == "semgrep-proprietary":
-        repo_dir = project_root / "OSS/tests/semgrep-rules"
-    else:
-        raise Exception("Unknown git repository. Couldn't locate semgrep-rules.")
-    monkeypatch.chdir(repo_dir)
-    if not os.listdir("."):
-        raise Exception(
-            "The semgrep-rules folder is empty. Was it properly checked out as a git submodule?"
-        )
-    yield
-    monkeypatch.undo()
-
-
-@pytest.mark.slow
-def test_semgrep_rules_repo__validate(in_semgrep_rules_repo):
-    """Validate the rule files found in the semgrep-rules repo.
-
-    This runs the same tests as semgrep-rules' own CI does to avoid
-    diverging expectations.
-    """
-    p = subprocess.Popen(["make", "validate"])
-    p.communicate()
-    exit_code = p.wait()
-    assert exit_code == 0
-
-
-@pytest.mark.slow
-def test_semgrep_rules_repo__test(in_semgrep_rules_repo):
-    """Test the pairs rule/target found in the semgrep-rules repo.
-
-    This runs the same tests as semgrep-rules' own CI does to avoid
-    diverging expectations.
-    """
-    p = subprocess.Popen(["make", "test-only"])
-    p.communicate()
-    exit_code = p.wait()
-    assert exit_code == 0
diff --git a/src/engine/tests/Test_compare_matches.ml b/src/engine/tests/Test_compare_matches.ml
index 58c0f613e77c..f17bc90f39a6 100644
--- a/src/engine/tests/Test_compare_matches.ml
+++ b/src/engine/tests/Test_compare_matches.ml
@@ -22,11 +22,13 @@ let default_error_regexp = ".*\\(ERROR\\|MATCH\\):"
 
 let location_of_pm { Pattern_match.range_loc; _ } =
   let { Tok.pos = { line; file; _ }; _ }, _ = range_loc in
-  (Fpath.v file, line)
+  (file, line)
 
 let location_of_core_error (err : Core_error.t) =
-  let loc = err.loc in
-  (Fpath.v loc.Tok.pos.file, loc.Tok.pos.line)
+  match err.loc with
+  | Some loc -> (loc.Tok.pos.file, loc.Tok.pos.line)
+  (* TODO(andre) Is there something easier to debug, failwith? *)
+  | None -> (Fpath_.fake_file, -1)
 
 let (expected_error_lines_of_files :
       ?regexp:string ->
diff --git a/src/engine/tests/Unit_engine.ml b/src/engine/tests/Unit_engine.ml
index ec23d7d0d9e6..458ae6dab51e 100644
--- a/src/engine/tests/Unit_engine.ml
+++ b/src/engine/tests/Unit_engine.ml
@@ -66,7 +66,6 @@ let full_lang_info =
     (Lang.Scala, "scala", ".scala");
     (Lang.Swift, "swift", ".swift");
     (Lang.Html, "html", ".html");
-    (Lang.Vue, "vue", ".vue");
     (Lang.Terraform, "terraform", ".tf");
     (Lang.Kotlin, "kotlin", ".kt");
     (Lang.Solidity, "solidity", ".sol");
@@ -306,7 +305,7 @@ let maturity_tests () =
       make_maturity_tests Lang.Jsonnet "jsonnet" ".jsonnet" Experimental;
       make_maturity_tests Lang.Clojure "clojure" ".clj" Experimental
       (* Not even experimental yet *)
-      (* HTML, XML, Vue, Dart *);
+      (* HTML, XML, Dart *);
     ]
 
 (*****************************************************************************)
@@ -658,7 +657,7 @@ let tainting_test (lang : Lang.t) (rules_file : Fpath.t) (file : Fpath.t) =
              {
                rule_id = Some m.rule_id.id;
                typ = OutJ.SemgrepMatchFound;
-               loc = fst m.range_loc;
+               loc = Some (fst m.range_loc);
                msg = m.rule_id.message;
                details = None;
              })
@@ -683,6 +682,9 @@ let tainting_tests_for_lang files lang =
              in
              tainting_test lang rules_file file))
 
+(* DEPRECATED: this is redundant because we now have 'make rules-test'
+ * which calls 'osemgrep-pro test --pro tests/tainting_rules'
+ *)
 let lang_tainting_tests () =
   let taint_tests_path = tests_path / "tainting_rules" in
   Testo.categorize_suites "lang tainting rules"
@@ -745,6 +747,9 @@ let lang_tainting_tests () =
 (* Full rule tests *)
 (*****************************************************************************)
 
+(* DEPRECATED: this is redundant because we now have 'make rules-test'
+ * which calls 'osemgrep-pro test --pro tests/rules tests/rules_v2'
+ *)
 let full_rule_regression_tests () =
   let path = tests_path / "rules" in
   let tests1 = Test_engine.make_tests ~prepend_lang:true [ path ] in
@@ -774,6 +779,8 @@ let full_rule_regression_tests () =
  * TODO: We could have a taint_maturity/POLYGLOT/ directory to put reusable
  * rules that can work for multiple languages (like we have
  * for tests/patterns/POLYGLOT/
+ * DEPRECATED: this is redundant because we now have 'make rules-test'
+ * which calls 'osemgrep-pro test --pro tests/taint_maturity'
  *)
 let full_rule_taint_maturity_tests () =
   let path = tests_path / "taint_maturity" in
@@ -802,6 +809,8 @@ let mark_todo_js (test : Testo.t) =
  * TODO: get rid of this and rely on `osemgrep test` code instead of
  * Test_engine.make_tests as they differ sligltly and we're using
  * osemgrep test in the semgrep-rules repo CI, not -test_rules.
+ * DEPRECATED: this is redundant because we now have 'make rules-test'
+ * which calls 'osemgrep-pro test --pro tests/semgrep-rules'
  *)
 let semgrep_rules_repo_tests () : Testo.t list =
   let path = tests_path / "semgrep-rules" in
diff --git a/src/osemgrep/cli_test/Test_annotation.ml b/src/osemgrep/cli_test/Test_annotation.ml
index 0db8248c39ad..b2bbb8546ceb 100644
--- a/src/osemgrep/cli_test/Test_annotation.ml
+++ b/src/osemgrep/cli_test/Test_annotation.ml
@@ -69,9 +69,8 @@ type engine = OSS | Pro | Deep [@@deriving show]
  * Note that 'ruleid:' implies 'proruleid:' and 'deepruleid:' so you don't need
  * to repeat those annotations. You usually need multiple kind/engine
  * prefix when one engine TP would be another engine TN (e.g., 'ruleid: prook:')
- * Here are a few implicit rules:
- *  - ruleid: > proruleid: > deepruleid:
- *  - prook: > deepok: ?
+ * See Test_subcommand.filter_annots_for_engine comment for more about those
+ * implications.
  *)
 type t = {
   kind : kind;
diff --git a/src/osemgrep/cli_test/Test_subcommand.ml b/src/osemgrep/cli_test/Test_subcommand.ml
index 0d26f3906668..b14dccbfbf80 100644
--- a/src/osemgrep/cli_test/Test_subcommand.ml
+++ b/src/osemgrep/cli_test/Test_subcommand.ml
@@ -51,10 +51,10 @@ module A = Test_annotation
 (*****************************************************************************)
 (* Types and constants *)
 (*****************************************************************************)
-(* = Cap.stdout + Core_scan.caps
+(* = Cap.stdout + Core_scan.caps + Cap.tmp (for Deep_scan.caps)
  * (no need for Cap.network; the tested rules should be local)
  *)
-type caps = < Cap.stdout ; Cap.fork ; Cap.alarm >
+type caps = < Cap.stdout ; Cap.fork ; Cap.alarm ; Cap.tmp >
 
 (* Rules and targets to test together.
  * Usually the target list contains just one file, but in some cases
@@ -120,6 +120,20 @@ let hook_pro_scan : (Core_scan.caps -> Core_scan.func) ref =
   ref (fun _caps _config ->
       failwith "semgrep test --pro not available (need --install-semgrep-pro)")
 
+(* note that we run DeepScan with
+ *  - force_interfile=true (no need for the interfile: true metadata in the rule)
+ *  - experimental_languages=true (analyze with DeepScan also Ruby and a few
+ *    other languages)
+ *)
+let hook_deep_scan :
+    (< Cap.tmp ; Cap.fork ; Cap.alarm > ->
+    Core_scan_config.t ->
+    Fpath.t ->
+    Core_result.result_or_exn)
+    ref =
+  ref (fun _caps _config _root ->
+      failwith "semgrep test --pro not available (need --install-semgrep-pro)")
+
 (*****************************************************************************)
 (* File targeting (the set of tests) *)
 (*****************************************************************************)
@@ -351,7 +365,6 @@ let report_tests_result (caps : < Cap.stdout >) ~matching_diagnosis ~json
           (spf "%d/%d: %d unit tests did not pass:" !passed !total
              (!total - !passed));
         print break_line;
-        (* TODO *)
         print "TODO: print(check_output_lines)");
     (* fix tests *)
     (match () with
@@ -365,7 +378,6 @@ let report_tests_result (caps : < Cap.stdout >) ~matching_diagnosis ~json
              !fixtest_total
              (!fixtest_total - !fixtest_passed));
         print break_line;
-        (* TODO *)
         print "TODO: print(fixtest_file_diffs)");
     if matching_diagnosis then report_diagnosis print res;
     (* TODO: if config_with_errors_output: ... *)
@@ -431,35 +443,59 @@ let core_scan_config (conf : Test_CLI.conf) (rules : Rule.t list)
     respect_rule_paths = false;
   }
 
-let run_rules_against_targets_for_engine caps (conf : Test_CLI.conf)
-    (rules : Rule.t list) (targets : Target.t list) (engine : A.engine) :
-    Core_result.t =
+let run_rules_against_targets_for_engine caps (env : env) (rules : Rule.t list)
+    (targets : Target.t list) : Core_result.t =
   (* old:
    * let xtarget = Test_engine.xtarget_of_file xlang target in
    * let xconf = { Match_env.default_xconfig with matching_explanations = true}in
    * Match_rules.check ~match_hook:(fun _ ->()) ~timeout:None xconf rules xtarget
    *)
-  let config = core_scan_config conf rules targets in
+  let config = core_scan_config env.conf rules targets in
   let res_or_exn : Core_result.result_or_exn =
-    match engine with
-    | A.OSS -> Core_scan.scan caps config
-    | A.Pro -> !hook_pro_scan caps config
-    | A.Deep -> failwith "--pro for DeepScan not implemented yet"
+    match env.engine with
+    | A.OSS -> Core_scan.scan (caps :> Core_scan.caps) config
+    | A.Pro -> !hook_pro_scan (caps :> Core_scan.caps) config
+    | A.Deep ->
+        (* LATER: support also interfile tests where many targets are in
+         * a subdir (using the same name than the rule file)
+         *)
+        let root, _base = Fpath.split_base env.rule_file in
+        !hook_deep_scan (caps :> < Cap.fork ; Cap.alarm ; Cap.tmp >) config root
   in
   match res_or_exn with
   | Error exn -> Exception.reraise exn
-  (* TODO: fail early or add a kind of error in the json output?
+  (* TODO? fail early or add a kind of error in the json output?
      | Ok { errors = _x::_; _} -> failwith "TODO"
   *)
   | Ok res -> res
 
+(* Annotations have also a few implicit rules to avoid having to annotate
+ * too much:
+ *  - ruleid => proruleid => deepruleid,
+ *    so if you have a ruleid:, no need to annotate too with proruleid:
+ *    or deepruleid:, it is implicit. However if some ruleid: are actually
+ *    not found by ProScan or DeepScan you'll need to add further annotations
+ *    such as prook: or deeptodoruleid:
+ *  - prook => deepok, because if the intrafile engine is able to detect
+ *    some FPs compared to CoreScan, then the interfile engine should too.
+ *    If it's not the case it's probably a bug in DeepScan (probably a
+ *    naming bug as DeepScan use Naming_SAST.ml instead of Naming_AST.ml)
+ *    in which case it's good to annotate it with a deeptodook
+ *)
 let filter_annots_for_engine (running_engine : A.engine)
     (annots : A.annotations) : A.annotations =
   annots
   |> List.filter (fun (A.{ kind; engine; others; _ }, _) ->
          match (running_engine, kind, engine) with
          | Pro, _, _ when List.mem (A.Ok, A.Pro) others -> false
+         | Pro, _, _ when List.mem (A.Todoruleid, A.Pro) others -> false
          | Deep, _, _ when List.mem (A.Ok, A.Deep) others -> false
+         | Deep, _, _ when List.mem (A.Todoruleid, A.Deep) others -> false
+         (* prook => deepok (TODO? protodoruleid => deeptodoruleid? *)
+         | Deep, _, _ when List.mem (A.Ok, A.Pro) others -> false
+         (* ruleid => proruleid => deepruleid
+          * (and todok => protodok => deeptodook)
+          *)
          | OSS, (Ruleid | Todook), OSS -> true
          | Pro, (Ruleid | Todook), (OSS | Pro) -> true
          | Deep, (Ruleid | Todook), (OSS | Pro | Deep) -> true
@@ -469,6 +505,20 @@ let filter_annots_for_engine (running_engine : A.engine)
 (* Comparing *)
 (*****************************************************************************)
 
+let diff_findings (actual : int list) (expected : int list) : string =
+  let _common, only_in_expected, only_in_actual =
+    Common2.diff_set_eff expected actual
+  in
+  (if List_.null only_in_expected then ""
+   else
+     spf "missing findings lines %s."
+       (only_in_expected |> List_.map Int.to_string |> String.concat ", "))
+  ^
+  if List_.null only_in_actual then ""
+  else
+    spf "unexpected findings lines %s."
+      (only_in_actual |> List_.map Int.to_string |> String.concat ", ")
+
 (* alt: use Test_compare_matches.compare_actual_to_expected but
  * it does not handle the actual rule id in the annotations and is
  * not compatible with what 'pysemgrep test' was doing when comparing.
@@ -494,7 +544,16 @@ let compare_actual_to_expected (env : env) (matches : Pattern_match.t list)
            ( rule_id,
              pms
              |> Assoc.group_by (fun (pm : Pattern_match.t) ->
-                    pm.path.internal_path_to_content) ))
+                    (* We need Fpath.normalize because for unclear reasons DeepScan
+                     * returns matches with paths that may differ from
+                     * the one below in the annotations so simpler to normalize
+                     * both so path like ./foo/bar.c and foo/bar.c are considered
+                     * the same
+                     * TODO: we don't need that for CoreScan and ProScan so we
+                     * should probably fix DeepScan instead to not mess up
+                     * with the Targets paths.
+                     *)
+                    Fpath.normalize pm.path.internal_path_to_content) ))
   in
   (* expected matches *)
   let expected_by_ruleid_and_file :
@@ -502,6 +561,7 @@ let compare_actual_to_expected (env : env) (matches : Pattern_match.t list)
     let h = Hashtbl.create 101 in
     annots
     |> List.iter (fun (file, annotations) ->
+           let file = Fpath.normalize file in
            let expected_by_rule_id : (Rule_ID.t, A.linenb list) Assoc.t =
              A.group_by_rule_id annotations
            in
@@ -547,8 +607,9 @@ let compare_actual_to_expected (env : env) (matches : Pattern_match.t list)
                     let passed = reported_lines =*= expected_lines in
                     if not passed then
                       Logs.err (fun m ->
-                          m "test failed for rule id %s on target %s%s"
-                            (Rule_ID.to_string id) !!target xtra);
+                          m "test failed for rule id %s on target %s%s (%s)"
+                            (Rule_ID.to_string id) !!target xtra
+                            (diff_findings reported_lines expected_lines));
                     (* TODO: not sure why pysemgrep does not report the real
                      * reported_lines (and expected_lines) and filter those
                      * 'todook:'. It's actually very confusing because sometimes
@@ -639,12 +700,12 @@ let compare_for_autofix (env : env) (rules : Rule.t list)
 (*****************************************************************************)
 
 (* alt: call it run_env? *)
-let run_engine (caps : Core_scan.caps) (env : env) (rules : Rule.t list)
-    (targets : Target.t list)
+let run_engine (caps : < Cap.fork ; Cap.alarm ; Cap.tmp >) (env : env)
+    (rules : Rule.t list) (targets : Target.t list)
     (files_and_annots : (Fpath.t * A.annotations) list) :
     test_result list * fixtest_result list =
   let res : Core_result.t =
-    run_rules_against_targets_for_engine caps env.conf rules targets env.engine
+    run_rules_against_targets_for_engine caps env rules targets
   in
   let expected : (Fpath.t * A.annotations) list =
     files_and_annots
@@ -676,7 +737,7 @@ let run_engine (caps : Core_scan.caps) (env : env) (rules : Rule.t list)
   (checks, fixtest)
 
 (* run one test using the different engines if --pro *)
-let run_test (caps : Core_scan.caps) (conf : Test_CLI.conf)
+let run_test (caps : < Cap.fork ; Cap.alarm ; Cap.tmp >) (conf : Test_CLI.conf)
     (rule_file : Fpath.t) (rules : Rule.t list) (target_files : Fpath.t list)
     (errors : error list ref) : test_result list * fixtest_result list =
   (* note that even one target file can result in different targets
@@ -696,7 +757,7 @@ let run_test (caps : Core_scan.caps) (conf : Test_CLI.conf)
   (* When conf.pro, we should run the engine 3 times:
    *  - with Core_scan and check just the ruleid:
    *  - with Pro_scan and check also the proruleid:
-   *  - TODO with Deep_scan and check also the deepruleid:.
+   *  - with Deep_scan and check also the deepruleid:.
    * The json will give information about those 3 different runs by using
    * different rule IDs suffix (e.g., "myrule--PRO")
    *)
@@ -721,11 +782,23 @@ let run_test (caps : Core_scan.caps) (conf : Test_CLI.conf)
              let id = Rule_ID.of_string_exn (s ^ "--PRO") in
              (id, rule_result))
     in
-    (checks_oss @ checks_pro, fixtest_oss)
+    (* same for deep *)
+    let checks_deep, _ =
+      let env = { env with engine = A.Deep } in
+      run_engine caps env rules targets files_and_annots
+    in
+    let checks_deep =
+      checks_deep
+      |> List_.map (fun (id, rule_result) ->
+             let s = Rule_ID.to_string id in
+             let id = Rule_ID.of_string_exn (s ^ "--DEEP") in
+             (id, rule_result))
+    in
+    (checks_oss @ checks_pro @ checks_deep, fixtest_oss)
   else (checks_oss, fixtest_oss)
 
-let run_tests (caps : Core_scan.caps) (conf : Test_CLI.conf) (tests : tests)
-    (errors : error list ref) :
+let run_tests (caps : < Cap.fork ; Cap.alarm ; Cap.tmp >) (conf : Test_CLI.conf)
+    (tests : tests) (errors : error list ref) :
     (Fpath.t (* rule file *) * test_result list * fixtest_result list) list =
   (* LATER: in theory we could use Parmap here *)
   tests
@@ -787,7 +860,7 @@ let run_conf (caps : caps) (conf : Test_CLI.conf) : Exit_code.t =
 
   (* step2: run the tests *)
   let result : tests_result =
-    run_tests (caps :> Core_scan.caps) conf tests errors
+    run_tests (caps :> < Cap.fork ; Cap.alarm ; Cap.tmp >) conf tests errors
   in
 
   (* step3: report the test results *)
diff --git a/src/osemgrep/cli_test/Test_subcommand.mli b/src/osemgrep/cli_test/Test_subcommand.mli
index 75776caf62e3..604d50c0cc9e 100644
--- a/src/osemgrep/cli_test/Test_subcommand.mli
+++ b/src/osemgrep/cli_test/Test_subcommand.mli
@@ -1,5 +1,5 @@
-(* = Cap.stdout + Core_scan.caps *)
-type caps = < Cap.stdout ; Cap.fork ; Cap.alarm >
+(* = Cap.stdout + Core_scan.caps | Deep_scan.caps *)
+type caps = < Cap.stdout ; Cap.fork ; Cap.alarm ; Cap.tmp >
 
 (*
    Parse a semgrep-test command, execute it and exit.
@@ -18,3 +18,10 @@ val run_conf : caps -> Test_CLI.conf -> Exit_code.t
 (* pro hooks *)
 val hook_pro_init : (unit -> unit) ref
 val hook_pro_scan : (Core_scan.caps -> Core_scan.func) ref
+
+val hook_deep_scan :
+  (< Cap.tmp ; Cap.fork ; Cap.alarm > ->
+  Core_scan_config.t ->
+  Fpath.t (* root *) ->
+  Core_result.result_or_exn)
+  ref
diff --git a/src/parsing/tests/Test_parsing.ml b/src/parsing/tests/Test_parsing.ml
index 7969078ee37b..9fb026d4a91e 100644
--- a/src/parsing/tests/Test_parsing.ml
+++ b/src/parsing/tests/Test_parsing.ml
@@ -209,10 +209,6 @@ let dump_tree_sitter_cst lang file =
       Tree_sitter_html.Parse.file file
       |> dump_and_print_errors Tree_sitter_html.Boilerplate.dump_tree
            Tree_sitter_html.Boilerplate.dump_extras
-  | Lang.Vue ->
-      Tree_sitter_vue.Parse.file file
-      |> dump_and_print_errors Tree_sitter_vue.Boilerplate.dump_tree
-           Tree_sitter_vue.Boilerplate.dump_extras
   | Lang.Php ->
       Tree_sitter_php.Parse.file file
       |> dump_and_print_errors Tree_sitter_php.Boilerplate.dump_tree
@@ -290,8 +286,6 @@ let test_parse_tree_sitter lang root_paths =
                  Tree_sitter_cpp.Parse.file file |> fail_on_error |> ignore
              | Lang.Html ->
                  Tree_sitter_html.Parse.file file |> fail_on_error |> ignore
-             | Lang.Vue ->
-                 Tree_sitter_vue.Parse.file file |> fail_on_error |> ignore
              | Lang.Php ->
                  Tree_sitter_php.Parse.file file |> fail_on_error |> ignore
              | Lang.Terraform ->
diff --git a/src/tests/Test.ml b/src/tests/Test.ml
index 712a47989f3d..2c3a87c20397 100644
--- a/src/tests/Test.ml
+++ b/src/tests/Test.ml
@@ -95,7 +95,8 @@ let tests (caps : Cap.all_caps) =
              ; Cap.chdir
              ; Cap.fork
              ; Cap.alarm >);
-      Unit_test_subcommand.tests (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm >);
+      Unit_test_subcommand.tests
+        (caps :> < Cap.stdout ; Cap.fork ; Cap.alarm ; Cap.tmp >);
       Test_show_subcommand.tests
         (caps :> < Cap.stdout ; Cap.network ; Cap.tmp >);
       Test_publish_subcommand.tests
diff --git a/tests/parsing/vue/basic.vue b/tests/parsing/vue/basic.vue
deleted file mode 100644
index 0aa1e10abbaf..000000000000
--- a/tests/parsing/vue/basic.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-  <p>
-    Hello, <a :[key]="url">{{ name }}</a>!
-  </p>
-</template>
-
-<script>
-//src: https://ikatyang.github.io/tree-sitter-vue/
-module.exports = {
-  data: function () {
-    return {
-      name: 'World',
-      key: 'href',
-      url: 'https://example.com/'
-    }
-  }
-}
-</script>
-
-<style scoped>
-p {
-  font-size: 2em;
-  text-align: center;
-}
-</style>
\ No newline at end of file
diff --git a/tests/parsing/vue/single_line_script.vue b/tests/parsing/vue/single_line_script.vue
deleted file mode 100644
index 615975ab34a3..000000000000
--- a/tests/parsing/vue/single_line_script.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<script>
-foo(1, 2)
-</script>
diff --git a/tests/patterns/scala/apply_as_new.scala b/tests/patterns/scala/apply_as_new.scala
new file mode 100644
index 000000000000..e4d567dda176
--- /dev/null
+++ b/tests/patterns/scala/apply_as_new.scala
@@ -0,0 +1,10 @@
+
+// MATCH:
+val good1 = new Bar()
+// MATCH:
+val good2 = Foo.apply()
+
+// OK:
+val bad1 = foo.apply()
+// OK:
+val bad2 = Foo.a.apply()
\ No newline at end of file
diff --git a/tests/patterns/scala/apply_as_new.sgrep b/tests/patterns/scala/apply_as_new.sgrep
new file mode 100644
index 000000000000..27e2fdc08ce3
--- /dev/null
+++ b/tests/patterns/scala/apply_as_new.sgrep
@@ -0,0 +1,1 @@
+new $NAME()
\ No newline at end of file
diff --git a/tests/patterns/vue/anonymous_metavar.vue b/tests/patterns/vue/anonymous_metavar.vue
deleted file mode 100644
index 9dde8a429711..000000000000
--- a/tests/patterns/vue/anonymous_metavar.vue
+++ /dev/null
@@ -1,4 +0,0 @@
-<script>
-// ERROR:
-2;
-</script>
\ No newline at end of file
diff --git a/tests/patterns/vue/concrete_syntax.vue b/tests/patterns/vue/concrete_syntax.vue
deleted file mode 100644
index 4c201352d898..000000000000
--- a/tests/patterns/vue/concrete_syntax.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-function foo() {
- //ERROR:
-    foo(1, 2);
- //ERROR:
-    foo(1,2);
- //ERROR:
-    foo (1, 2);
- //ERROR:
- foo(1,
-     2);
- //ERROR:
- foo(1, // comment
-     2);
-
- foo(2,1)
-}
-
-
-</script>
diff --git a/tests/patterns/vue/deep_expr_operator.vue b/tests/patterns/vue/deep_expr_operator.vue
deleted file mode 100644
index 09d9094a5659..000000000000
--- a/tests/patterns/vue/deep_expr_operator.vue
+++ /dev/null
@@ -1,10 +0,0 @@
-<script>
-function foo() {
- //ERROR:
- foo(bar(1 + 42);
-
- foo(1);
-}
-
-
-</script>
diff --git a/tests/rules/metavariable_name_resolution.java b/tests/rules/metavariable_name_resolution.java
index 79253a602a2c..a6dfb316bf01 100644
--- a/tests/rules/metavariable_name_resolution.java
+++ b/tests/rules/metavariable_name_resolution.java
@@ -3,7 +3,7 @@
 public class Test {
     public static void main(String[] args) {
         Foo foo = new Foo();
-        // ruleid: metavariable-resolution-test
+        // ruleid: deeptodoruleid: metavariable-resolution-test
         foo.bar();
         foo.baz();
     }
diff --git a/tests/rules/typed_metavar_metavar_regex.go b/tests/rules/typed_metavar_metavar_regex.go
index 0b677052bba9..e53edb426ceb 100644
--- a/tests/rules/typed_metavar_metavar_regex.go
+++ b/tests/rules/typed_metavar_metavar_regex.go
@@ -1,4 +1,4 @@
 func f(c *http.Request) {
-  // ruleid: typed-metavar-metavar-regex
+  // ruleid: deeptodoruleid: typed-metavar-metavar-regex
   c.Foo()
 }
diff --git a/tests/semgrep-rules b/tests/semgrep-rules
index 153588f28b96..0bba56c058e5 160000
--- a/tests/semgrep-rules
+++ b/tests/semgrep-rules
@@ -1,1 +1,1 @@
-Subproject commit 153588f28b9662a005747a81733201c6e39b3e10
+Subproject commit 0bba56c058e51adfb683954acafc8afb02e41467
diff --git a/tests/snapshots/semgrep-core/3e5448ffb606/stdxxx b/tests/snapshots/semgrep-core/3e5448ffb606/stdxxx
index 408ab898e7b9..1d26982d0c67 100644
--- a/tests/snapshots/semgrep-core/3e5448ffb606/stdxxx
+++ b/tests/snapshots/semgrep-core/3e5448ffb606/stdxxx
@@ -1,5 +1,5 @@
 Snapshot for matching diagnosis unexpected match
-[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target ./test.py
+[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target test.py (unexpected findings lines 5.)
 --- begin input files ---
 test.py
 test.yaml
@@ -10,7 +10,7 @@ TODO: print(check_output_lines)
 No tests for fixes found.
 --------------------------------------------------------------------------------
 Matching diagnosis:
-./test.py: Unexpected match at line 5:
+test.py: Unexpected match at line 5:
     foo(good)
 This match originated at this pattern at line 6 in ./test.yaml:
         - pattern: foo(...)
diff --git a/tests/snapshots/semgrep-core/4823773fc66b/stdxxx b/tests/snapshots/semgrep-core/4823773fc66b/stdxxx
index 9e147264118f..57366be98129 100644
--- a/tests/snapshots/semgrep-core/4823773fc66b/stdxxx
+++ b/tests/snapshots/semgrep-core/4823773fc66b/stdxxx
@@ -1,5 +1,5 @@
 Snapshot for matching diagnosis unexpected match (multiple)
-[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-bar-unless-good on target ./test.py
+[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-bar-unless-good on target test.py (unexpected findings lines 5, 9.)
 --- begin input files ---
 test.py
 test.yaml
@@ -10,7 +10,7 @@ TODO: print(check_output_lines)
 No tests for fixes found.
 --------------------------------------------------------------------------------
 Matching diagnosis:
-./test.py: Unexpected match at line 5:
+test.py: Unexpected match at line 5:
     foo(good)
 This match originated at this pattern at line 7 in ./test.yaml:
             - pattern: foo(...)
@@ -19,7 +19,7 @@ Maybe the match was supposed to be filtered out by one of these patterns?
               - not: foo(goood)
   line 6:
             - all:
-./test.py: Unexpected match at line 9:
+test.py: Unexpected match at line 9:
     bar(good)
 This match originated at this pattern at line 10 in ./test.yaml:
             - pattern: bar(...)
diff --git a/tests/snapshots/semgrep-core/958abd4d536d/stdxxx b/tests/snapshots/semgrep-core/958abd4d536d/stdxxx
index e7aef385ec94..d1c6c402b557 100644
--- a/tests/snapshots/semgrep-core/958abd4d536d/stdxxx
+++ b/tests/snapshots/semgrep-core/958abd4d536d/stdxxx
@@ -1,6 +1,6 @@
 Snapshot for matching diagnosis unexpected no match
 [<MASKED TIMESTAMP>][[33mWARNING[0m]: nothing matched for ./test.yaml
-[0m[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target ./test.py
+[0m[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target test.py (missing findings lines 3, 5.)
 --- begin input files ---
 test.py
 test.yaml
@@ -11,10 +11,10 @@ TODO: print(check_output_lines)
 No tests for fixes found.
 --------------------------------------------------------------------------------
 Matching diagnosis:
-./test.py: Unexpected lack of match at line 3
+test.py: Unexpected lack of match at line 3
 A potential match at line 3 was removed by this pattern at line 7 in ./test.yaml:
           - not: foo($X)
-./test.py: Unexpected lack of match at line 5
+test.py: Unexpected lack of match at line 5
 A potential match at line 5 was removed by this pattern at line 7 in ./test.yaml:
           - not: foo($X)
 [0m[33mASSERT[0m exit code
diff --git a/tests/snapshots/semgrep-core/c49a2132d28d/stdxxx b/tests/snapshots/semgrep-core/c49a2132d28d/stdxxx
index b3d5af98ac9e..69c1d07162e0 100644
--- a/tests/snapshots/semgrep-core/c49a2132d28d/stdxxx
+++ b/tests/snapshots/semgrep-core/c49a2132d28d/stdxxx
@@ -1,6 +1,6 @@
 Snapshot for matching diagnosis unexpected no match (redundant not)
 [<MASKED TIMESTAMP>][[33mWARNING[0m]: nothing matched for ./test.yaml
-[0m[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target ./test.py
+[0m[<MASKED TIMESTAMP>][[31mERROR[0m]: test failed for rule id no-foo-unless-good on target test.py (missing findings lines 3, 5.)
 --- begin input files ---
 test.py
 test.yaml
@@ -11,10 +11,10 @@ TODO: print(check_output_lines)
 No tests for fixes found.
 --------------------------------------------------------------------------------
 Matching diagnosis:
-./test.py: Unexpected lack of match at line 3
+test.py: Unexpected lack of match at line 3
 A potential match at line 3 was removed by this pattern at line 7 in ./test.yaml:
           - not: foo($X)
-./test.py: Unexpected lack of match at line 5
+test.py: Unexpected lack of match at line 5
 A potential match at line 5 was removed by this pattern at line 7 in ./test.yaml:
           - not: foo($X)
 [0m[33mASSERT[0m exit code