diff --git a/benchmarks/measurements/2026-09-08-module-audit.json b/benchmarks/measurements/2026-09-08-module-audit.json index 4d1e5a1..0b05155 100644 --- a/benchmarks/measurements/2026-09-08-module-audit.json +++ b/benchmarks/measurements/2026-09-08-module-audit.json @@ -1,24 +1,13 @@ { - "modules": 98, "counts": { - "load_bearing": 91, + "load_bearing": 92, "standalone_capability": 7 }, + "modules": 99, "orphaned": [], - "standalone": { - "fusion": "two-panel adjudication engine; reached from the scan's report stage and used standalone", - "harness_ext": "HarnessX extension surface; loaded by name when the optional harness is present", - "hunter_harness": "harness wiring for the tool hunter; used by the hunter path and by mcp", - "mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration", - "skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path", - "slot_contract": "the slot/contract protocol stage_processors implements", - "stage_processors": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate" - }, "rows": [ { - "module": "benchmark", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "gate", @@ -26,28 +15,28 @@ "map_gate", "mcp", "pairs" - ] + ], + "module": "benchmark", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "calibration", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" - ] + ], + "module": "calibration", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "cli", "classification": "load_bearing", - "reason": "entry point", "importers": [ "mcp" - ] + ], + "module": "cli", + "reason": "entry point" }, { - "module": "complexity", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "complexity.hints", @@ -57,32 +46,32 @@ "regress.candidate", "semantic.prove_filter", "tool_hunter" - ] + ], + "module": "complexity", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "complexity.hints", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "complexity", "complexity.map" - ] + ], + "module": "complexity.hints", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "complexity.ledger", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "complexity", "complexity.map", "regress.candidate" - ] + ], + "module": "complexity.ledger", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "complexity.map", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "complexity", "complexity.hints", @@ -91,22 +80,22 @@ "regress.candidate", "semantic.prove_filter", "tool_hunter" - ] + ], + "module": "complexity.map", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "complexity.signals", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "complexity", "complexity.hints", "complexity.map" - ] + ], + "module": "complexity.signals", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "config", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "harness", @@ -116,12 +105,12 @@ "regress.candidate", "regress.recipes", "run" - ] + ], + "module": "config", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "cpg", "classification": "load_bearing", - "reason": "model-grounded-detection Req 4: the CPG seam package", "importers": [ "cli", "cpg.backend", @@ -131,12 +120,12 @@ "model.pipeline", "model.scan", "model.taint" - ] + ], + "module": "cpg", + "reason": "model-grounded-detection Req 4: the CPG seam package" }, { - "module": "cpg.backend", "classification": "load_bearing", - "reason": "model-grounded-detection Req 4.1/4.5: the single Joern subprocess boundary", "importers": [ "cli", "model.config_value", @@ -145,20 +134,20 @@ "model.pipeline", "model.scan", "model.taint" - ] + ], + "module": "cpg.backend", + "reason": "model-grounded-detection Req 4.1/4.5: the single Joern subprocess boundary" }, { - "module": "cpg.capability", "classification": "load_bearing", - "reason": "model-grounded-detection Req 4.2/4.3: the engine probe that degrades to suspicion", "importers": [ "cpg.backend" - ] + ], + "module": "cpg.capability", + "reason": "model-grounded-detection Req 4.2/4.3: the engine probe that degrades to suspicion" }, { - "module": "findings", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "benchmark", "calibration", @@ -187,124 +176,124 @@ "tool_hunter", "verification", "verify_judge" - ] + ], + "module": "findings", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "fusion", "classification": "standalone_capability", - "reason": "two-panel adjudication engine; reached from the scan's report stage and used standalone", "importers": [ "cli" - ] + ], + "module": "fusion", + "reason": "two-panel adjudication engine; reached from the scan's report stage and used standalone" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "gate", - "classification": "load_bearing", - "reason": "entry point", - "importers": [ - "cli" - ] + "reason": "entry point" }, { - "module": "harness", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "slot_contract" - ] + ], + "module": "harness", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "harness_ext", "classification": "standalone_capability", - "reason": "HarnessX extension surface; loaded by name when the optional harness is present", "importers": [ "cli", "fusion", "hunter_harness", "stage_processors", "verify_judge" - ] + ], + "module": "harness_ext", + "reason": "HarnessX extension surface; loaded by name when the optional harness is present" }, { - "module": "hunter", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "hunter_harness" - ] + ], + "module": "hunter", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "hunter_harness", "classification": "standalone_capability", - "reason": "harness wiring for the tool hunter; used by the hunter path and by mcp", "importers": [ "cli" - ] + ], + "module": "hunter_harness", + "reason": "harness wiring for the tool hunter; used by the hunter path and by mcp" }, { - "module": "hunter_tools", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "tool_hunter" - ] + ], + "module": "hunter_tools", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "improve", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "improve.evolve" - ] + ], + "module": "improve", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "improve.evolve", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "improve" - ] + ], + "module": "improve.evolve", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "improve", + "improve.evolve" + ], "module": "improve.journal", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" + }, + { "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "improve", "improve.evolve" - ] - }, - { + ], "module": "improve.validator", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "improve", - "improve.evolve" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "index", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "skills" - ] + ], + "module": "index", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [], "module": "map_gate", - "classification": "load_bearing", - "reason": "entry point", - "importers": [] + "reason": "entry point" }, { - "module": "mapping", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "gate", @@ -315,20 +304,20 @@ "pairs", "preprocess", "stage_processors" - ] + ], + "module": "mapping", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "mcp", "classification": "standalone_capability", - "reason": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration", "importers": [ "cli" - ] + ], + "module": "mcp", + "reason": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration" }, { - "module": "model", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "model.candidates", @@ -336,85 +325,87 @@ "model.dominance", "model.execution", "model.judge", + "model.layout", "model.pipeline", "model.regions", "model.report", "model.scan", "model.specs", - "model.taint" - ] + "model.taint", + "preprocess" + ], + "module": "model", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [], "module": "model.audit", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 3: this classifier; run by the maintainer, not by the scan", - "importers": [] + "reason": "model-grounded-detection Req 3: this classifier; run by the maintainer, not by the scan" }, { + "classification": "load_bearing", + "importers": [], "module": "model.calibrate", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 9: the corpus as the model's calibration set", - "importers": [] + "reason": "model-grounded-detection Req 9: the corpus as the model's calibration set" }, { - "module": "model.candidates", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "model.scan" - ] + ], + "module": "model.candidates", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "model.pipeline", + "model.scan" + ], "module": "model.config_value", + "reason": "model-grounded-detection Req 6.3: constant abstraction for the configuration families" + }, + { "classification": "load_bearing", - "reason": "model-grounded-detection Req 6.3: constant abstraction for the configuration families", "importers": [ "model.pipeline", "model.scan" - ] - }, - { + ], "module": "model.dominance", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 6.2: guard dominance, the arbiter for bugs that never crash", - "importers": [ - "model.pipeline", - "model.scan" - ] + "reason": "model-grounded-detection Req 6.2: guard dominance, the arbiter for bugs that never crash" }, { - "module": "model.endpoint", "classification": "load_bearing", - "reason": "model-grounded-detection Req 8: the judge client, wired by model/judge.py in group 3", "importers": [ "cli" - ] + ], + "module": "model.endpoint", + "reason": "model-grounded-detection Req 8: the judge client, wired by model/judge.py in group 3" }, { - "module": "model.evidence", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "model.scan" - ] + ], + "module": "model.evidence", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [], "module": "model.execution", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 10: the deferred execution tier seam, adopted not built", - "importers": [] + "reason": "model-grounded-detection Req 10: the deferred execution tier seam, adopted not built" }, { + "classification": "load_bearing", + "importers": [], "module": "model.judge", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 8: one bounded question, checked against the model", - "importers": [] + "reason": "model-grounded-detection Req 8: one bounded question, checked against the model" }, { - "module": "model.ladder", "classification": "load_bearing", - "reason": "model-grounded-detection Req 5: the evidence ladder every finding carries", "importers": [ "model.config_value", "model.dominance", @@ -423,53 +414,63 @@ "model.pipeline", "model.scan", "model.taint" - ] + ], + "module": "model.ladder", + "reason": "model-grounded-detection Req 5: the evidence ladder every finding carries" + }, + { + "classification": "load_bearing", + "importers": [ + "model.scan", + "preprocess" + ], + "module": "model.layout", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "model.pipeline", "classification": "load_bearing", - "reason": "model-grounded-detection Req 8: the enumerator proposes, the LLM answers, the model disposes", "importers": [ "model.scan" - ] + ], + "module": "model.pipeline", + "reason": "model-grounded-detection Req 8: the enumerator proposes, the LLM answers, the model disposes" }, { - "module": "model.regions", "classification": "load_bearing", - "reason": "contributor-scan Req 2: regions from entry points, with the families each admits", "importers": [ "cli", + "model.layout", "model.scan" - ] + ], + "module": "model.regions", + "reason": "contributor-scan Req 2: regions from entry points, with the families each admits" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "model.report", - "classification": "load_bearing", - "reason": "model-grounded-detection Req 9.3/11.1: per-slice reporting with the overfitting gap", - "importers": [ - "cli" - ] + "reason": "model-grounded-detection Req 9.3/11.1: per-slice reporting with the overfitting gap" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "model.scan", - "classification": "load_bearing", - "reason": "contributor-scan Req 3: the repository driver -- one CPG, one budget, ranked spend", - "importers": [ - "cli" - ] + "reason": "contributor-scan Req 3: the repository driver -- one CPG, one budget, ranked spend" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "model.shipped", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "cli" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "model.specs", "classification": "load_bearing", - "reason": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by", "importers": [ "model.config_value", "model.dominance", @@ -479,54 +480,54 @@ "model.report", "model.scan", "model.taint" - ] + ], + "module": "model.specs", + "reason": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by" }, { - "module": "model.taint", "classification": "load_bearing", - "reason": "model-grounded-detection Req 6.1: taint reachability over the CPG, the flow-family arbiter", "importers": [ "model.judge", "model.pipeline", "model.scan" - ] + ], + "module": "model.taint", + "reason": "model-grounded-detection Req 6.1: taint reachability over the CPG, the flow-family arbiter" }, { - "module": "model.taxonomy", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "model.candidates", "model.report", "model.scan", "model.specs" - ] + ], + "module": "model.taxonomy", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "pair_gate", "classification": "load_bearing", - "reason": "entry point", "importers": [ "cli" - ] + ], + "module": "pair_gate", + "reason": "entry point" }, { - "module": "pairs", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "improve", "improve.evolve", "model.candidates", "pair_gate" - ] + ], + "module": "pairs", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "policy", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "findings", @@ -537,20 +538,20 @@ "scoring.project_score", "semantic.overlay", "stage_processors" - ] + ], + "module": "policy", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "policy.verycode", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "policy" - ] + ], + "module": "policy.verycode", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "preprocess", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "complexity.hints", @@ -573,38 +574,38 @@ "semantic.overlay", "skills", "stage_processors" - ] + ], + "module": "preprocess", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "provenance", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" - ] + ], + "module": "provenance", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "model.endpoint", + "tool_hunter" + ], "module": "provider", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" + }, + { "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "model.endpoint", "tool_hunter" - ] - }, - { + ], "module": "provider.openrouter", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "model.endpoint", - "tool_hunter" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "rank", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "calibration", "cli", @@ -617,84 +618,84 @@ "map_gate", "pairs", "stage_processors" - ] + ], + "module": "rank", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "redaction", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "harness", "model.judge", "model.pipeline", "reports", "semantic.obligations.intent" - ] + ], + "module": "redaction", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "regress", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "regress.candidate" - ] + ], + "module": "regress", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "regress.candidate", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "regress" - ] + ], + "module": "regress.candidate", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "regress", + "regress.candidate" + ], "module": "regress.recipes", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" + }, + { "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "regress", "regress.candidate" - ] - }, - { + ], "module": "regress.safety", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" + }, + { "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "regress", "regress.candidate" - ] - }, - { + ], "module": "regress.verdict", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "regress", - "regress.candidate" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "reports", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "cli" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { + "classification": "load_bearing", + "importers": [ + "cli" + ], "module": "repos", - "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", - "importers": [ - "cli" - ] + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "ruleset", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "findings", @@ -704,85 +705,86 @@ "improve.validator", "pairs", "stage_processors" - ] + ], + "module": "ruleset", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "ruleset.store", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "improve.validator", "ruleset" - ] + ], + "module": "ruleset.store", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "run", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "reports" - ] + ], + "module": "run", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "sandbox", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "regress.candidate", "regress.recipes", "regress.safety", "regress.verdict" - ] + ], + "module": "sandbox", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "sandbox.probe", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "sandbox" - ] + ], + "module": "sandbox.probe", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "sandbox.runner", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "regress.candidate", "regress.recipes", "regress.safety", "sandbox" - ] + ], + "module": "sandbox.runner", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "scoring", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "improve.evolve", "stage_processors" - ] + ], + "module": "scoring", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "scoring.project_score", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "scoring" - ] + ], + "module": "scoring.project_score", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "cpg.capability", "hunter_tools", "mapping", "model.candidates", + "model.layout", "model.specs", "pairs", "semantic.cst", @@ -795,45 +797,46 @@ "semantic.overlay", "semantic.prove_filter", "semantic.taint" - ] + ], + "module": "semantic", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.cst", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic.ir" - ] + ], + "module": "semantic.cst", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.engines", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cpg.capability", "semantic", "semantic.overlay" - ] + ], + "module": "semantic.engines", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.extra", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic", "semantic.cst", "semantic.engines" - ] + ], + "module": "semantic.extra", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.facts", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "hunter_tools", "mapping", "model.candidates", + "model.layout", "model.specs", "pairs", "semantic", @@ -842,23 +845,23 @@ "semantic.obligations.shapes", "semantic.overlay", "semantic.taint" - ] + ], + "module": "semantic.facts", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.functions", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "hunter_tools", "pairs", "semantic.obligations.shapes", "semantic.overlay" - ] + ], + "module": "semantic.functions", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.ir", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "hunter_tools", @@ -871,12 +874,12 @@ "semantic.obligations.shapes", "semantic.overlay", "semantic.taint" - ] + ], + "module": "semantic.ir", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "hunter_tools", @@ -890,33 +893,33 @@ "semantic.obligations.policy", "semantic.obligations.shapes", "semantic.obligations.siblings" - ] + ], + "module": "semantic.obligations", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.check", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "semantic.obligations", "semantic.obligations.intent" - ] + ], + "module": "semantic.obligations.check", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.dominance", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "hunter_tools", "pairs", "semantic.obligations.check" - ] + ], + "module": "semantic.obligations.dominance", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.facts", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "model.specs", "pairs", @@ -926,136 +929,147 @@ "semantic.obligations.policy", "semantic.obligations.shapes", "semantic.obligations.siblings" - ] + ], + "module": "semantic.obligations.facts", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.intent", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" - ] + ], + "module": "semantic.obligations.intent", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.operations", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "model.candidates", "semantic.obligations.check", "semantic.obligations.dominance", "semantic.obligations.siblings" - ] + ], + "module": "semantic.obligations.operations", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.policy", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli", "semantic.obligations.check" - ] + ], + "module": "semantic.obligations.policy", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.shapes", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic.obligations", "semantic.obligations.operations" - ] + ], + "module": "semantic.obligations.shapes", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.obligations.siblings", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic.obligations.check" - ] + ], + "module": "semantic.obligations.siblings", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.overlay", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic", "semantic.prove_filter" - ] + ], + "module": "semantic.overlay", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.prove_filter", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic" - ] + ], + "module": "semantic.prove_filter", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "semantic.taint", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "semantic.overlay" - ] + ], + "module": "semantic.taint", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "skills", "classification": "standalone_capability", - "reason": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path", "importers": [ "hunter", "hunter_harness" - ] + ], + "module": "skills", + "reason": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path" }, { - "module": "slot_contract", "classification": "standalone_capability", - "reason": "the slot/contract protocol stage_processors implements", "importers": [ "stage_processors" - ] + ], + "module": "slot_contract", + "reason": "the slot/contract protocol stage_processors implements" }, { - "module": "stage_processors", "classification": "standalone_capability", - "reason": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate", - "importers": [] + "importers": [], + "module": "stage_processors", + "reason": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate" }, { - "module": "stages", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" - ] + ], + "module": "stages", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "tool_hunter", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "model.endpoint" - ] + ], + "module": "tool_hunter", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "verification", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "calibration", "cli", "fusion", "reports", "verify_judge" - ] + ], + "module": "verification", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, { - "module": "verify_judge", "classification": "load_bearing", - "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" - ] + ], + "module": "verify_judge", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" } - ] + ], + "standalone": { + "fusion": "two-panel adjudication engine; reached from the scan's report stage and used standalone", + "harness_ext": "HarnessX extension surface; loaded by name when the optional harness is present", + "hunter_harness": "harness wiring for the tool hunter; used by the hunter path and by mcp", + "mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration", + "skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path", + "slot_contract": "the slot/contract protocol stage_processors implements", + "stage_processors": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate" + } } diff --git a/benchmarks/ranking/position.py b/benchmarks/ranking/position.py index 0006532..a01ed22 100644 --- a/benchmarks/ranking/position.py +++ b/benchmarks/ranking/position.py @@ -38,7 +38,9 @@ def ordered_regions(root: Path) -> list[ScanRegion]: regions = regions_for(analyze_entry_points(root, targets), targets, shipped=declared_sources(root)) # The scan's own total order. If this drifts from scan.py the baseline measures the wrong thing, which # is why the sort key is the one thing here worth a test against the driver. - return sorted(regions, key=lambda r: (not r.shipped, -r.rank, r.path, r.function or "")) + from openultrasast.model.layout import with_layout + + return sorted(with_layout(regions), key=lambda r: (not r.shipped, -r.rank, r.path, r.function or "")) def evidence_ordered_regions(root: Path, regions: list[ScanRegion], language: str) -> tuple[list[ScanRegion], dict[str, object]]: @@ -58,7 +60,9 @@ def evidence_ordered_regions(root: Path, regions: list[ScanRegion], language: st started = time.monotonic() # The RECIPE's language, never the first region's: WP Statistics's first region is JavaScript, and a # build asked for as JavaScript went looking for jssrc2cpg and lost the whole repository's measurement. - cpg = backend.build(root, language=language) + from openultrasast.model.layout import vendored_directories + + cpg = backend.build(root, language=language, exclude=vendored_directories(root)) build_seconds = round(time.monotonic() - started, 1) if cpg is None: raise SystemExit(f"no graph for {root}: {getattr(backend, 'last_failure', '')}") diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index 1c99491..c4c8de2 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -278,7 +278,7 @@ class JoernBackend: return has_cpg() - def build(self, root: Path, *, language: str = "") -> CpgResult | None: + def build(self, root: Path, *, language: str = "", exclude: Sequence[str] = ()) -> CpgResult | None: """Build a CPG for ``root``. ``None`` on any failure — the caller degrades, never guesses. ``language`` lets a failed ``joern-parse`` retry through the frontend directly, which is what Joern @@ -312,7 +312,7 @@ class JoernBackend: # A frontend whose failures are visible and retryable is worth more than one whose are not. if language.lower() in _PREFER_FRONTEND: self.last_failure = "" - shards, unparsed = self._build_sharded(root, scratch, language) + shards, unparsed = self._build_sharded(root, scratch, language, exclude=exclude) if not shards and self.last_failure: # A build that FAILED BY NAME is not one to retry through the other launcher: the graph the # census could not load would be rebuilt the same and never asked again. @@ -450,7 +450,9 @@ class JoernBackend: ) return "" - def _build_sharded(self, root: Path, scratch: Path, language: str) -> tuple[tuple[Path, ...], tuple[str, ...]]: + def _build_sharded( + self, root: Path, scratch: Path, language: str, exclude: Sequence[str] = () + ) -> tuple[tuple[Path, ...], tuple[str, ...]]: """Build the tree, and give the files it refuses a CPG of their OWN rather than losing them. A frontend that drops a file drops it from the only graph there is, and every question about that @@ -465,7 +467,9 @@ class JoernBackend: Returns the CPGs to query and the files that defeated even a shard of their own. """ # Excluded from the start, because one of them costs the entire graph rather than itself. - defective = self._files_with_frontend_defect(root, language) + # `exclude` is the caller's: the vendored trees the `[[layout]]` facts put out of scope. This seam + # imports only the standard library, so it is handed the names rather than reading the table. + defective = (*exclude, *self._files_with_frontend_defect(root, language)) if defective: logger.warning( "excluding %d file(s) under %s that php2cpg miscompiles (a `global` inside a closure): %s", diff --git a/src/openultrasast/model/layout.py b/src/openultrasast/model/layout.py new file mode 100644 index 0000000..9a7b89c --- /dev/null +++ b/src/openultrasast/model/layout.py @@ -0,0 +1,68 @@ +"""Where the `[[layout]]` facts meet a repository: which paths are vendored, which are tests. + +One place, imported by the preprocessor (vendored trees leave the targets), the scan (test regions are +not product, and vendored directories leave the graph) and the position benchmark (so it measures the +order the scan spends). Facts are rows; this is only the matching. +""" + +from __future__ import annotations + +import fnmatch +from collections.abc import Iterable, Sequence +from pathlib import Path + +from ..semantic.facts import LayoutFact, load_facts +from .regions import ScanRegion + + +def layout_facts(language: str | None = None) -> tuple[LayoutFact, ...]: + """The layout rows -- for one language, or for every language when the question is about a tree.""" + facts = load_facts() + return tuple(facts.for_language(language).layouts if language else facts.layouts) + + +def _matches(relative: str, patterns: Iterable[str]) -> bool: + """gitignore-style: `dir/` names a directory at any depth, anything else a path or basename glob.""" + parts = relative.split("/") + for pattern in patterns: + if pattern.endswith("/"): + if pattern.rstrip("/") in parts[:-1] or pattern.rstrip("/") in parts: + return True + elif fnmatch.fnmatch(relative, pattern) or fnmatch.fnmatch(parts[-1], pattern): + return True + return False + + +def is_vendored(relative: str, facts: Sequence[LayoutFact] | None = None) -> bool: + rows = facts if facts is not None else layout_facts() + return _matches(relative, (p for fact in rows for p in fact.vendored)) + + +def is_test_path(relative: str, facts: Sequence[LayoutFact] | None = None) -> bool: + rows = facts if facts is not None else layout_facts() + return _matches(relative, (p for fact in rows for p in fact.tests)) + + +def vendored_directories(root: Path, facts: Sequence[LayoutFact] | None = None) -> tuple[str, ...]: + """The vendored directories actually present under ``root``, repository-relative, shallowest first -- + what a frontend's ``--exclude`` takes.""" + rows = facts if facts is not None else layout_facts() + names = {p.rstrip("/") for fact in rows for p in fact.vendored if p.endswith("/")} + found: list[str] = [] + for path in sorted(root.rglob("*")): + if not path.is_dir() or path.name not in names: + continue + relative = path.relative_to(root).as_posix() + if any(relative.startswith(f"{seen}/") for seen in found): + continue # already covered by a shallower vendored directory + found.append(relative) + return tuple(found) + + +def with_layout(regions: Sequence[ScanRegion], facts: Sequence[LayoutFact] | None = None) -> tuple[ScanRegion, ...]: + """Test-path regions marked not shipped -- ordered after the product's, never removed. A region a + build manifest already declared shipped stays so: the manifest is the project's own word.""" + from dataclasses import replace + + rows = facts if facts is not None else layout_facts() + return tuple(replace(r, shipped=False) if r.shipped and is_test_path(r.path, rows) else r for r in regions) diff --git a/src/openultrasast/model/scan.py b/src/openultrasast/model/scan.py index beb5bf5..24c2305 100644 --- a/src/openultrasast/model/scan.py +++ b/src/openultrasast/model/scan.py @@ -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] diff --git a/src/openultrasast/preprocess.py b/src/openultrasast/preprocess.py index 672575d..04a7f62 100644 --- a/src/openultrasast/preprocess.py +++ b/src/openultrasast/preprocess.py @@ -97,7 +97,7 @@ def enumerate_source_files(root: Path) -> list[Path]: patterns = _load_ignore_patterns(root) paths: list[Path] = [] for path in root.rglob("*"): - if not path.is_file() or _has_ignored_dir(root, path) or _is_ignored(root, path, patterns): + if not path.is_file() or _has_ignored_dir(root, path) or _is_ignored(root, path, patterns) or _is_vendored(root, path): continue if detect_language(path) != "unknown": paths.append(path) @@ -177,6 +177,15 @@ def _has_ignored_dir(root: Path, path: Path) -> bool: return any(part in IGNORED_DIRS for part in relative_parts[:-1]) +def _is_vendored(root: Path, path: Path) -> bool: + """A tree the `[[layout]]` facts call somebody else's code. Out of the targets entirely: a bundled + library is a separate unit, analysed as one or not at all (contributor-scan 5.13; the decision is the + maintainer's and lives in the fact table, not here).""" + from .model.layout import is_vendored + + return is_vendored(path.relative_to(root).as_posix()) + + def _is_ignored(root: Path, path: Path, patterns: list[str]) -> bool: relative = path.relative_to(root).as_posix() return any(fnmatch.fnmatch(relative, pattern) or fnmatch.fnmatch(path.name, pattern) for pattern in patterns) diff --git a/src/openultrasast/ruleset/semantic/php.toml b/src/openultrasast/ruleset/semantic/php.toml index ab191f2..1f4c3bf 100644 --- a/src/openultrasast/ruleset/semantic/php.toml +++ b/src/openultrasast/ruleset/semantic/php.toml @@ -200,3 +200,13 @@ parameterized = true id = "wordpress_hooks" register = ["add_action", "add_filter", "add_shortcode"] apply = ["apply_filters", "apply_filters_ref_array", "do_action", "do_action_ref_array"] + +# What a PHP repository's paths say about scope. `vendored` trees are out of the scan and out of the graph +# (a bundled library is a separate unit); `tests` paths stay in scope but are not product, so their +# regions are ordered after the product's. Measured before this row: 207 of WP Statistics's 357 PHP files +# were vendored and two of them were 60% of its graph; 14 of the 67 regions ahead of MW WP Form's CVE +# were test-mail.php and test-mail-parser.php. +[[layout]] +id = "php_composer_and_wordpress" +vendored = ["vendor/", "node_modules/", "third_party/", "third-party/", "lib/vendor/"] +tests = ["tests/", "test/", "spec/", "test-*.php", "*Test.php", "*.test.php", "*_test.php", "phpunit/"] diff --git a/src/openultrasast/semantic/facts.py b/src/openultrasast/semantic/facts.py index aa2aae1..694ac0e 100644 --- a/src/openultrasast/semantic/facts.py +++ b/src/openultrasast/semantic/facts.py @@ -74,6 +74,27 @@ class DispatchFact: language: str +@dataclass(frozen=True) +class LayoutFact: + """What a repository's PATHS say about scope, per ecosystem: which trees are somebody else's code and + which files are tests. + + Both are policy, which is why they are rows and not constants. Vendored code is a separate unit -- + "divide and rule": a library is analysed as a library, or not at all, never as part of the project + that bundles it -- so a `vendored` tree is out of the targets AND out of the graph. A `tests` path is + in scope but not product: its regions are ordered after the product's, never removed, because a test + that reaches a sink still names the sink. + + Patterns are gitignore-style, matched against the repository-relative path and against each directory + on it: `vendor/` names any directory called vendor at any depth, `test-*.php` any file so named. + """ + + id: str + vendored: tuple[str, ...] + tests: tuple[str, ...] + language: str + + @dataclass(frozen=True) class SemanticFacts: version: str @@ -81,6 +102,7 @@ class SemanticFacts: sinks: tuple[SinkFact, ...] sanitizers: tuple[SanitizerFact, ...] dispatches: tuple[DispatchFact, ...] = () + layouts: tuple[LayoutFact, ...] = () def for_language(self, language: str) -> SemanticFacts: key = _LANGUAGE_ALIASES.get(language, language) @@ -90,6 +112,7 @@ class SemanticFacts: sinks=tuple(item for item in self.sinks if item.language == key), sanitizers=tuple(item for item in self.sanitizers if item.language == key), dispatches=tuple(item for item in self.dispatches if item.language == key), + layouts=tuple(item for item in self.layouts if item.language == key), ) @@ -101,6 +124,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts: sinks: list[SinkFact] = [] sanitizers: list[SanitizerFact] = [] dispatches: list[DispatchFact] = [] + layouts: list[LayoutFact] = [] versions: list[str] = [] found = False for path in sorted(root.glob("*.toml")): @@ -115,6 +139,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts: sinks.extend(_sinks(payload.get("sink"), language, path)) sanitizers.extend(_sanitizers(payload.get("sanitizer"), language, path)) dispatches.extend(_dispatches(payload.get("dispatch"), language, path)) + layouts.extend(_layouts(payload.get("layout"), language, path)) if not found: raise FactLoadError(f"no semantic fact files in {root}") return SemanticFacts( @@ -123,6 +148,7 @@ def load_facts(directory: Path | None = None) -> SemanticFacts: sinks=tuple(sinks), sanitizers=tuple(sanitizers), dispatches=tuple(dispatches), + layouts=tuple(layouts), ) @@ -143,6 +169,23 @@ def _dispatches(value: object, language: str, path: Path) -> list[DispatchFact]: return facts +def _layouts(value: object, language: str, path: Path) -> list[LayoutFact]: + facts: list[LayoutFact] = [] + for item in _items(value): + identifier = str(item.get("id", "")).strip() + if not identifier: + raise FactLoadError(f"layout without an id in {path}") + facts.append( + LayoutFact( + id=identifier, + vendored=_strings(item.get("vendored", []), "vendored", path), + tests=_strings(item.get("tests", []), "tests", path), + language=language, + ) + ) + return facts + + def _items(value: object) -> list[dict[str, object]]: if value is None: return [] diff --git a/tests/test_layout_facts.py b/tests/test_layout_facts.py new file mode 100644 index 0000000..71d4d6c --- /dev/null +++ b/tests/test_layout_facts.py @@ -0,0 +1,87 @@ +"""The `[[layout]]` facts: which paths are somebody else's code, which are tests -- rows, not constants.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +def test_layout_rows_load_and_match() -> None: + from openultrasast.model.layout import is_test_path, is_vendored, layout_facts + + rows = layout_facts("php") + assert [r.id for r in rows] == ["php_composer_and_wordpress"] + assert is_vendored("includes/vendor/whichbrowser/parser/data/profiles.php", rows) + assert is_vendored("vendor/autoload.php", rows) + assert not is_vendored("classes/class.mail.php", rows) + assert is_test_path("test-mail.php", rows) and is_test_path("tests/unit/MailTest.php", rows) + assert is_test_path("classes/FooTest.php", rows) + assert not is_test_path("classes/class.mail.php", rows) and not is_test_path("includes/testimonials.php", rows) + + +def test_a_layout_row_without_an_id_is_refused(tmp_path: Path) -> None: + from openultrasast.semantic.facts import FactLoadError, load_facts + + (tmp_path / "php.toml").write_text('[[layout]]\nvendored = ["vendor/"]\n') + with pytest.raises(FactLoadError): + load_facts(tmp_path) + + +def test_vendored_trees_leave_the_targets(tmp_path: Path) -> None: + """A bundled library is a separate unit -- analysed as one or not at all, never as part of the project + that bundles it. Measured before the row: 207 of WP Statistics's 357 PHP files were vendored.""" + from openultrasast.preprocess import preprocess_repository + + (tmp_path / "classes").mkdir() + (tmp_path / "classes" / "a.php").write_text(" None: + from openultrasast.model.layout import vendored_directories + + (tmp_path / "includes" / "vendor" / "x" / "vendor").mkdir(parents=True) + (tmp_path / "vendor").mkdir() + (tmp_path / "src").mkdir() + assert vendored_directories(tmp_path) == ("includes/vendor", "vendor") + + +def test_test_regions_are_not_product_and_order_last() -> None: + """In scope -- a test that reaches a sink still names the sink -- but after the product's regions.""" + from openultrasast.model.layout import with_layout + from openultrasast.model.regions import ScanRegion + + product = ScanRegion(path="classes/class.mail.php", function="_delete_files", language="php", families=("path",), rank=0.3, source="x") + test = ScanRegion(path="test-mail.php", function="hook_mwform_admin_mail", language="php", families=("path",), rank=0.3, source="x") + declared = ScanRegion(path="tests/fixture.php", function="f", language="php", families=("path",), rank=0.3, source="x", shipped=True) + out = {r.path: r.shipped for r in with_layout([product, test])} + assert out == {"classes/class.mail.php": True, "test-mail.php": False} + assert with_layout([declared])[0].shipped is False, ( + "a test path is a test path; only a manifest that lists it as a source would say otherwise, and it is applied first" + ) + + +def test_the_scan_hands_the_vendored_trees_to_the_build(tmp_path: Path) -> None: + from openultrasast.model.regions import ScanRegion + from openultrasast.model.scan import ScanBudget, scan_repository + + (tmp_path / "vendor").mkdir() + seen: dict[str, object] = {} + + class _Backend: + def available(self) -> bool: + return True + + def build(self, root, *, language="", exclude=()): # type: ignore[no-untyped-def] + from openultrasast.cpg.backend import CpgResult + + seen["exclude"] = tuple(exclude) + return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: [], run_batch=lambda q, r: {rid: [] for rid in r}) + + region = ScanRegion(path="a.php", function="f", language="php", families=("injection",), rank=0.5, source="x") + scan_repository(tmp_path, [region], backend=_Backend(), budget=ScanBudget(max_model_calls=0)) + assert seen["exclude"] == ("vendor",)