feat(facts): the [[layout]] fact -- vendored trees are out, test paths are not product

Two decisions the maintainer made, as rows rather than constants. A vendored tree (vendor/, node_modules/,
third_party/) is somebody else's unit: out of the targets in preprocess and out of the graph through the
frontend's --exclude, which the scan hands to the backend because the CPG seam imports only the standard
library. A test path (tests/, test-*.php, *Test.php) stays in scope -- a test that reaches a sink still
names the sink -- but its regions are not shipped and order after the product's. Measured before the row:
207 of WP Statistics's 357 PHP files were vendored, and 14 of the 67 regions ahead of MW WP Form's CVE
were test files. The position benchmark applies both, so it measures the order the scan spends.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-11 14:27:50 +02:00
co-authored by Claude Fable 5.1
parent 9142cb1fb4
commit b821ac6e4e
9 changed files with 593 additions and 347 deletions
+11 -4
View File
@@ -31,6 +31,7 @@ from .config_value import request_params as config_params
from .dominance import request_params as dominance_params
from .evidence import TIER_EXCLUDE, Evidence, evidence_from_rows
from .ladder import Rung
from .layout import with_layout
from .pipeline import ModelFinding, scan_region
from .regions import MODULE_SCOPE, ScanRegion
from .specs import ConfigSpec, DominanceSpec, TaintSpec, config_specs, dominance_specs, taint_specs
@@ -168,7 +169,7 @@ def scan_repository(
# Sort here rather than trusting the caller. The budget decides what goes unexamined, so the order it is
# spent in belongs to whoever holds the budget.
ordered_regions = sorted(regions, key=lambda r: (not r.shipped, -r.rank, r.path, r.function or ""))
ordered_regions = sorted(with_layout(regions), key=lambda r: (not r.shipped, -r.rank, r.path, r.function or ""))
# The hook link table, built once for the whole scan. It is a property of the repository rather than of
# any region, and it is read from the source text because the graph does not carry it: php2cpg drops a
@@ -441,11 +442,17 @@ def _dispose(cpg: object) -> None:
def _build(backend: Any, root: Path, language: str) -> Any:
"""Build through the backend, passing the language when the backend can use it."""
"""Build through the backend, passing the language and the vendored trees when the backend can use them."""
from .layout import vendored_directories
excluded = vendored_directories(root) if root.is_dir() else ()
try:
return backend.build(root, language=language)
return backend.build(root, language=language, exclude=excluded)
except TypeError: # a backend from before the retry existed, including every test double
return backend.build(root)
try:
return backend.build(root, language=language)
except TypeError:
return backend.build(root)
def _prefetched(rows: list[object]): # type: ignore[no-untyped-def]