pre-push(17.3): quick rules as the fast first tier

Before the engine and inside the same deadline, the shipped quick rules run
over the head side of the changed files (push/quick.py) and keep matches on
changed lines or in changed functions. They print as advisory pattern matches,
labeled apart from admitted alerts, are kept in the artifact's quick_tier with
the files and bytes read, and never admit or block. A new branch with no
remote base runs the quick tier over its last commit and says so. The planted
eval in serve-static's new debug.js now surfaces; the planted Python
%-formatted SQL in records does not (a quick-rule coverage gap, no rule
edited). Requirement 9.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-10-02 22:18:14 +02:00
co-authored by Claude Opus 5.5
parent f555dee640
commit 15999a09e1
7 changed files with 350 additions and 63 deletions
+2 -1
View File
@@ -636,10 +636,11 @@ Preserve all completed task evidence below.
- Evidence: the three repositories' terminal output names engine-missing, Go not covered and the new branch. - Evidence: the three repositories' terminal output names engine-missing, Go not covered and the new branch.
- _Done 2026-10-02:_ new `push/explain.py` turns every raw coverage reason into one plain `Skipped:` line per distinct kind (hex paths decoded, sha256 tokens dropped and counted, unknown reasons still printed); the existing one-line `Notice:` keeps its next action and gains an engine-missing variant. `language_not_covered:<lang>:<changed>:<total>` is recorded for a changed language with no engine frontend and no quick rules; `ousast scan` prints `notice: language not covered: go (N files)` and its report says so. `reports.py` renders the eight previously dropped degradations and prints any reason it has no sentence for. The hook passes `OUSAST_COMPARISON_BASE`. The new-branch line names the base it lacks; what was checked instead lands with the quick tier (17.3). Tests: `tests/test_push_explain.py` (6), two in `tests/test_reports.py`; module-audit manifest regenerated for the new module. Gates: 1995 passed, 42 skipped; ruff, format, mypy clean. - _Done 2026-10-02:_ new `push/explain.py` turns every raw coverage reason into one plain `Skipped:` line per distinct kind (hex paths decoded, sha256 tokens dropped and counted, unknown reasons still printed); the existing one-line `Notice:` keeps its next action and gains an engine-missing variant. `language_not_covered:<lang>:<changed>:<total>` is recorded for a changed language with no engine frontend and no quick rules; `ousast scan` prints `notice: language not covered: go (N files)` and its report says so. `reports.py` renders the eight previously dropped degradations and prints any reason it has no sentence for. The hook passes `OUSAST_COMPARISON_BASE`. The new-branch line names the base it lacks; what was checked instead lands with the quick tier (17.3). Tests: `tests/test_push_explain.py` (6), two in `tests/test_reports.py`; module-audit manifest regenerated for the new module. Gates: 1995 passed, 42 skipped; ruff, format, mypy clean.
- _Requirements: 9.2, 5.2, 8.2_ - _Requirements: 9.2, 5.2, 8.2_
- [ ] 17.3 Run the quick rules as the fast first tier - [x] 17.3 Run the quick rules as the fast first tier
- Inside the deadline and before the engine, the shipped quick rules run over the head side of the changed files and keep the findings on changed lines or in changed functions. They print as advisory pattern matches, labeled apart from admitted alerts, recorded in the artifact's `quick_tier`, and never block. - Inside the deadline and before the engine, the shipped quick rules run over the head side of the changed files and keep the findings on changed lines or in changed functions. They print as advisory pattern matches, labeled apart from admitted alerts, recorded in the artifact's `quick_tier`, and never block.
- Files: `src/openultrasast/push/quick.py` (new), `src/openultrasast/push/runner.py`, `src/openultrasast/push/report.py`. Tests: `tests/test_push_quick.py` (new). - Files: `src/openultrasast/push/quick.py` (new), `src/openultrasast/push/runner.py`, `src/openultrasast/push/report.py`. Tests: `tests/test_push_quick.py` (new).
- Evidence: serve-static's planted `eval` in the new `debug.js` is shown; whether records' planted `%`-formatted SQL is shown is recorded as is, with no rule edits for it. - Evidence: serve-static's planted `eval` in the new `debug.js` is shown; whether records' planted `%`-formatted SQL is shown is recorded as is, with no rule edits for it.
- _Done 2026-10-02:_ new `push/quick.py` runs the shipped quick rules (no shadow rules, no model) over the changed head files inside `_prepare` under the shared deadline, before the engine, keeping matches on a changed line or inside a changed function (entry-point ranges); it records `files`/`bytes_read` as proof of input. The terminal shows them under "Quick rules: N pattern match(es) on changed code ... Advisory: not verified by the engine."; the artifact keeps `quick_tier`; admission and push disposition are untouched. A new branch runs the quick tier over its last commit's changes and says so. `_analyze` gained `engine="off"`. On the audit's serve-static planted commit the `eval` in the new `debug.js` is now shown (js-eval, js-xss-response at debug.js:2, ~1.1 s on the host); the planted `%`-formatted SQL in records is NOT matched by any quick rule (coverage gap, no rule edited). Tests: `tests/test_push_quick.py` (3). Gates: 1998 passed, 42 skipped; ruff, format, mypy clean.
- _Requirements: 9.3_ - _Requirements: 9.3_
- [ ] 17.4 Classify a file new in head as new, not unknown - [ ] 17.4 Classify a file new in head as new, not unknown
- `ChangeContext` records the paths Git reports as added. An operation in an added file, when no add/delete pairing is unresolved and its head evidence is complete, is `new` with reason `file_added_in_head`, and admission accepts that reason as change evidence. An engine finding with established novelty whose only vetoes are capability vetoes renders as an advisory engine finding, not an alert. Every other unknown comparison keeps its veto. - `ChangeContext` records the paths Git reports as added. An operation in an added file, when no add/delete pairing is unresolved and its head evidence is complete, is `new` with reason `file_added_in_head`, and admission accepts that reason as change evidence. An engine finding with established novelty whose only vetoes are capability vetoes renders as an advisory engine finding, not an alert. Every other unknown comparison keeps its veto.
@@ -1,7 +1,7 @@
{ {
"modules": 152, "modules": 153,
"counts": { "counts": {
"load_bearing": 147, "load_bearing": 148,
"standalone_capability": 5 "standalone_capability": 5
}, },
"orphaned": [], "orphaned": [],
@@ -229,6 +229,7 @@
"model.report", "model.report",
"pairs", "pairs",
"plane.tasks.alerts", "plane.tasks.alerts",
"push.quick",
"push.runner", "push.runner",
"rank", "rank",
"regress.candidate", "regress.candidate",
@@ -561,6 +562,7 @@
"pairs", "pairs",
"plane.tasks.alerts", "plane.tasks.alerts",
"preprocess", "preprocess",
"push.quick",
"push.runner" "push.runner"
] ]
}, },
@@ -604,6 +606,7 @@
"push.cache", "push.cache",
"push.contracts", "push.contracts",
"push.policy", "push.policy",
"push.quick",
"push.report", "push.report",
"push.reuse", "push.reuse",
"push.runner", "push.runner",
@@ -651,6 +654,7 @@
"push.cache", "push.cache",
"push.contracts", "push.contracts",
"push.policy", "push.policy",
"push.quick",
"push.report", "push.report",
"push.reuse", "push.reuse",
"push.runner", "push.runner",
@@ -1097,6 +1101,7 @@
"improve.evolve", "improve.evolve",
"improve.validator", "improve.validator",
"plane.tasks.loop", "plane.tasks.loop",
"push.quick",
"scoring.project_score", "scoring.project_score",
"semantic.overlay" "semantic.overlay"
] ]
@@ -1136,6 +1141,7 @@
"plane.tasks.alerts", "plane.tasks.alerts",
"plane.tasks.features", "plane.tasks.features",
"plane.tasks.repo_facts", "plane.tasks.repo_facts",
"push.quick",
"push.runner", "push.runner",
"rank", "rank",
"regress.candidate", "regress.candidate",
@@ -1258,6 +1264,14 @@
"push.vetoes" "push.vetoes"
] ]
}, },
{
"module": "push.quick",
"classification": "load_bearing",
"reason": "reachable from gate, map_gate, pair_gate, cli",
"importers": [
"push.runner"
]
},
{ {
"module": "push.report", "module": "push.report",
"classification": "load_bearing", "classification": "load_bearing",
@@ -1328,7 +1342,8 @@
"improve.evolve", "improve.evolve",
"map_gate", "map_gate",
"pairs", "pairs",
"plane.tasks.alerts" "plane.tasks.alerts",
"push.quick"
] ]
}, },
{ {
+1
View File
@@ -57,6 +57,7 @@ GLOSSARY: dict[str, str] = {
"query_too_expensive": "some engine questions were too expensive to answer", "query_too_expensive": "some engine questions were too expensive to answer",
"regions_truncated": "only the highest-ranked regions were examined (--max-regions)", "regions_truncated": "only the highest-ranked regions were examined (--max-regions)",
"files_unparsed": "some files could not be parsed by the engine", "files_unparsed": "some files could not be parsed by the engine",
"engine_off": "engine checks were switched off for this run (--engine off)",
"cross_partition_semantics_unresolved": "flows between languages in this repository are not followed", "cross_partition_semantics_unresolved": "flows between languages in this repository are not followed",
"resolution": "the pushed ref could not be resolved ({detail})", "resolution": "the pushed ref could not be resolved ({detail})",
"push_input_failed": "the push input could not be read ({detail})", "push_input_failed": "the push input could not be read ({detail})",
+130
View File
@@ -0,0 +1,130 @@
"""The quick-rule tier of pre-push: the fast first look (task 17.3, Requirement 9.3).
The shipped pattern rules run over the head side of the files a push changes, before the engine and
inside the same deadline, and keep the matches that fall on a changed line or inside a changed
function. They are pattern matches: shown as advisory, labeled apart from admitted alerts, never
admitted and never blocking. Requirement 3.2 still governs every engine candidate.
"""
from __future__ import annotations
import time
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
from openultrasast.model.contracts import ChangeContext
Basis = Literal["comparison", "last_commit"]
@dataclass(frozen=True)
class QuickTierResult:
status: Literal["completed", "not_run", "failed"]
basis: Basis
files: tuple[str, ...] = ()
bytes_read: int = 0
findings: tuple[Mapping[str, object], ...] = ()
reason: str | None = None
seconds: float = 0.0
basis_commit: str | None = None
def to_payload(self) -> dict[str, object]:
return {
"label": "quick rules: pattern matches on changed lines; advisory, not admitted, never blocking",
"status": self.status,
"basis": self.basis,
"basis_commit": self.basis_commit,
"files": list(self.files),
"bytes_read": self.bytes_read,
"findings": [dict(item) for item in self.findings],
"reason": self.reason,
"seconds": self.seconds,
}
def head_spans(context: ChangeContext) -> dict[str, list[tuple[int, int]]]:
"""Changed head lines per decoded path, for every changed file that still exists in head."""
deleted = set(context.deleted_paths)
spans: dict[str, list[tuple[int, int]]] = {}
for path in context.changed_paths:
if path not in deleted:
spans.setdefault(context.decode_path(path), [])
for span in context.spans:
if span.side == "head":
spans.setdefault(context.decode_path(span.path), []).append((span.start_line, span.end_line))
return spans
def run_quick_tier(
root: Path, spans: Mapping[str, Sequence[tuple[int, int]]], *, basis: Basis = "comparison", basis_commit: str | None = None
) -> QuickTierResult:
"""Pattern rules over the changed head files of a materialized snapshot at `root`.
Pure and bounded by its caller: it reads only the named files, runs no project code and calls no
model. `files`/`bytes_read` prove what it read, so a zero is never an unread input.
"""
from openultrasast.findings import PATTERN_RULES, quick_scan_findings
from openultrasast.mapping import analyze_entry_points, attach_reachability_hints
from openultrasast.policy import load_policy
from openultrasast.preprocess import build_file_target, detect_language
from openultrasast.rank import rank_targets
started = time.monotonic()
resolved = root.resolve()
paths = []
read = 0
for name in sorted(spans):
path = resolved / name
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(resolved):
continue
if detect_language(path) == "unknown":
continue
paths.append(path)
read += path.stat().st_size
targets = [build_file_target(resolved, path) for path in paths]
targets = attach_reachability_hints(targets, analyze_entry_points(resolved, targets))
rules = {rule.rule_id: rule for rule in PATTERN_RULES}
kept: list[Mapping[str, object]] = []
for finding in quick_scan_findings(resolved, targets, rank_targets(targets), PATTERN_RULES, load_policy()):
if finding.status == "shadow" or finding.line is None:
continue
changed = spans.get(finding.path, ())
where = _changed_scope(finding.line, changed, finding.reachability_evidence)
if where is None:
continue
rule_id = finding.finding_id.split(":", 1)[0]
rule = rules.get(rule_id)
kept.append(
{
"rule": rule_id,
"title": finding.title,
"cwe": rule.cwe if rule is not None else None,
"severity": finding.severity,
"path": finding.path,
"line": finding.line,
"function": finding.function_name,
"scope": where,
}
)
return QuickTierResult(
"completed",
basis,
tuple(target.path for target in targets),
read,
tuple(kept),
seconds=time.monotonic() - started,
basis_commit=basis_commit,
)
def _changed_scope(line: int, changed: Sequence[tuple[int, int]], hints: Sequence[Mapping[str, object]]) -> str | None:
if any(start <= line <= end for start, end in changed):
return "changed_line"
for hint in hints:
start, end = hint.get("line"), hint.get("end_line")
inside = isinstance(start, int) and isinstance(end, int) and start <= line <= end
if inside and any(a <= end and start <= b for a, b in changed): # type: ignore[operator]
return "changed_function"
return None
+36
View File
@@ -45,6 +45,8 @@ class PushReport:
# Labeled M1a recorded-veto evaluation. Present only under the explicit replay flag; # Labeled M1a recorded-veto evaluation. Present only under the explicit replay flag;
# it is retained beside the decision and never feeds admission or enforcement. # it is retained beside the decision and never feeds admission or enforcement.
experimental: Mapping[str, object] = field(default_factory=dict) experimental: Mapping[str, object] = field(default_factory=dict)
# The quick-rule tier (task 17.3): one payload per comparison it ran for; advisory only.
quick: tuple[Mapping[str, object], ...] = ()
def __post_init__(self) -> None: def __post_init__(self) -> None:
object.__setattr__(self, "model_assistance", MappingProxyType(dict(self.model_assistance))) object.__setattr__(self, "model_assistance", MappingProxyType(dict(self.model_assistance)))
@@ -134,6 +136,7 @@ def _write_artifact(report: PushReport, target: Path, temporary: Path, connectio
"timing_scope": "Caller stage measurements; publication and rendering duration is returned in ReportDelivery.", "timing_scope": "Caller stage measurements; publication and rendering duration is returned in ReportDelivery.",
"resolution": report.resolution.to_payload() if report.resolution else None, "resolution": report.resolution.to_payload() if report.resolution else None,
"experimental": dict(report.experimental), "experimental": dict(report.experimental),
"quick_tier": [dict(item) for item in report.quick],
} }
json.dump(payload, stream, ensure_ascii=True, allow_nan=False, indent=2) json.dump(payload, stream, ensure_ascii=True, allow_nan=False, indent=2)
stream.write("\n") stream.write("\n")
@@ -216,6 +219,37 @@ def _experimental_line(experimental: Mapping[str, object]) -> str:
) )
# Quick-rule matches shown per comparison; the artifact keeps every one.
_QUICK_LINES = 5
def _quick_lines(payload: Mapping[str, object]) -> list[str]:
"""The quick tier in the terminal: advisory pattern matches, never presented as alerts."""
files = payload.get("files")
count = len(files) if isinstance(files, list) else 0
read = f"{count} file(s), {payload.get('bytes_read', 0)} bytes read"
lines: list[str] = []
if payload.get("basis") == "last_commit":
commit = str(payload.get("basis_commit") or "")[:7]
lines.append(f"New branch: instead, quick rules checked the files its last commit changed (compared with {commit}).")
if payload.get("status") != "completed":
return [*lines, f"Quick rules did not run ({_short(str(payload.get('reason') or 'unknown'))})."]
findings = payload.get("findings")
found = [item for item in findings if isinstance(item, Mapping)] if isinstance(findings, list) else []
if not found:
return [*lines, f"Quick rules: no pattern match on changed code ({read})."]
lines.append(f"Quick rules: {len(found)} pattern match(es) on changed code ({read}). Advisory: not verified by the engine.")
for item in found[:_QUICK_LINES]:
cwe = f"{item.get('cwe')}, " if item.get("cwe") else ""
lines.append(
f"- {_short(str(item.get('rule')))} ({cwe}{_short(str(item.get('severity')))}) at "
f"{_short(str(item.get('path')), 4096)}:{item.get('line')}: {_short(str(item.get('title')))}"
)
if len(found) > _QUICK_LINES:
lines.append(f"{len(found) - _QUICK_LINES} more quick-rule matches retained in the detailed result.")
return lines
def render_report(report: PushReport, *, artifact: Path | None, error: str | None = None) -> str: def render_report(report: PushReport, *, artifact: Path | None, error: str | None = None) -> str:
lines: list[str] = [] lines: list[str] = []
defects = report.admission.defects defects = report.admission.defects
@@ -251,6 +285,8 @@ def render_report(report: PushReport, *, artifact: Path | None, error: str | Non
) )
if len(defects) > 3: if len(defects) > 3:
lines.append(f"{len(defects) - 3} more actionable defects retained in the detailed result.") lines.append(f"{len(defects) - 3} more actionable defects retained in the detailed result.")
for payload in report.quick:
lines.extend(_quick_lines(payload))
if not defects: if not defects:
coverage = report.result.coverage_status coverage = report.result.coverage_status
if coverage == "complete_within_scope": if coverage == "complete_within_scope":
+50
View File
@@ -42,6 +42,7 @@ from openultrasast.push.policy import (
dependency_gaps, dependency_gaps,
semantics_digest, semantics_digest,
) )
from openultrasast.push.quick import Basis, QuickTierResult, head_spans, run_quick_tier
from openultrasast.push.report import PushReport, ReportDelivery, ScanRecord, _completed_scan, deliver_report, render_report from openultrasast.push.report import PushReport, ReportDelivery, ScanRecord, _completed_scan, deliver_report, render_report
from openultrasast.push.reuse import ReusingBackend, declaration_identity, discovery from openultrasast.push.reuse import ReusingBackend, declaration_identity, discovery
from openultrasast.push.snapshot import SnapshotAdapter from openultrasast.push.snapshot import SnapshotAdapter
@@ -267,6 +268,8 @@ def _analyze(
resolved: PushComparison | None = None, resolved: PushComparison | None = None,
record_vetoes: bool = False, record_vetoes: bool = False,
declarations: Path | None = None, declarations: Path | None = None,
engine: Literal["inline", "off"] = "inline",
quick_basis: Basis = "comparison",
) -> PushReport: ) -> PushReport:
"""Run a single explicit comparison; exit policy never stands for coverage. """Run a single explicit comparison; exit policy never stands for coverage.
@@ -295,6 +298,7 @@ def _analyze(
comparison = resolved comparison = resolved
context = None context = None
experimental: dict[str, object] = {"label": LABEL, "flag": FLAG, "status": "not_evaluated"} if record_vetoes else {} experimental: dict[str, object] = {"label": LABEL, "flag": FLAG, "status": "not_evaluated"} if record_vetoes else {}
quick: QuickTierResult | None = None
timings: dict[str, float] = { timings: dict[str, float] = {
"deadline_seconds": config.deadline_seconds, "deadline_seconds": config.deadline_seconds,
"cancellation_allowance_seconds": config.cancellation_allowance_seconds, "cancellation_allowance_seconds": config.cancellation_allowance_seconds,
@@ -375,6 +379,20 @@ def _analyze(
reasons.extend(context.unresolved_boundaries) reasons.extend(context.unresolved_boundaries)
reasons.extend(_uncovered_languages(tip.manifest, context)) reasons.extend(_uncovered_languages(tip.manifest, context))
stage_started = _stage(timings, stage, stage_started) stage_started = _stage(timings, stage, stage_started)
stage = "quick"
# The fast first tier: pattern rules over the changed head lines, before the engine
# and inside the same deadline (Requirement 9.3). Advisory, never admitted.
try:
spans = head_spans(context)
basis_commit = comparison.base_oid if quick_basis == "last_commit" else None
quick = _prepare(lambda: run_quick_tier(tip.root, spans, basis=quick_basis, basis_commit=basis_commit), budget)
except Exception as error: # noqa: BLE001 -- a failed quick tier is a stated gap, never the end of the check
quick = QuickTierResult("failed", quick_basis, reason=type(error).__name__)
reasons.append("quick_rules_failed:" + type(error).__name__)
stage_started = _stage(timings, stage, stage_started)
if engine == "off":
reasons.append("engine_off")
if engine == "inline":
stage = "head" stage = "head"
head_scan = scan_repository( head_scan = scan_repository(
tip.root, tip.root,
@@ -478,6 +496,30 @@ def _analyze(
snapshots=tuple(manifests), snapshots=tuple(manifests),
change_context=context, change_context=context,
experimental=experimental, experimental=experimental,
quick=(quick.to_payload(),) if quick is not None else (),
)
def _last_commit_quick(
repository: Path, comparison: PushComparison, *, artifact: Path, config: PushConfig, budget: ExecutionBudget
) -> PushReport | None:
"""Quick rules over the files the pushed head's last commit changed; None for a root commit."""
try:
parent = SnapshotAdapter(repository, execution_budget=budget).resolve_replay(comparison.head_oid + "^", comparison.head_oid)
except Exception: # noqa: BLE001 -- a root commit has no parent to compare with
return None
resolved = PushComparison(comparison.head_oid, parent.base_oid, "new_branch_last_commit", comparison.refs)
assert resolved.base_oid is not None
return _analyze(
repository,
base=resolved.base_oid,
head=resolved.head_oid,
artifact=artifact,
config=config,
execution_budget=budget,
resolved=resolved,
engine="off",
quick_basis="last_commit",
) )
@@ -549,6 +591,7 @@ def push(
budget = execution_budget or ExecutionBudget(started + config.deadline_seconds, config.cancellation_allowance_seconds) budget = execution_budget or ExecutionBudget(started + config.deadline_seconds, config.cancellation_allowance_seconds)
reports: list[PushReport] = [] reports: list[PushReport] = []
analyses: list[ComparisonAnalysis] = [] analyses: list[ComparisonAnalysis] = []
fallbacks: list[PushReport] = []
reasons: list[str] = [] reasons: list[str] = []
resolution = None resolution = None
try: try:
@@ -562,6 +605,12 @@ def push(
if comparison.base_oid is None or time.monotonic() >= budget.deadline_monotonic: if comparison.base_oid is None or time.monotonic() >= budget.deadline_monotonic:
analyses.append(ComparisonAnalysis(comparison, (), (), "unavailable")) analyses.append(ComparisonAnalysis(comparison, (), (), "unavailable"))
reasons.append("base_comparison_unavailable" if comparison.base_oid is None else "deadline_exhausted") reasons.append("base_comparison_unavailable" if comparison.base_oid is None else "deadline_exhausted")
if comparison.base_oid is None:
# A new branch has no remote base. Say what was checked instead: the quick rules
# over the files its last commit changed (Requirement 9.2, 9.3).
fallback = _last_commit_quick(repository, comparison, artifact=artifact, config=config, budget=budget)
if fallback is not None:
fallbacks.append(fallback)
continue continue
report = _analyze( report = _analyze(
repository, repository,
@@ -624,6 +673,7 @@ def push(
resolution=resolution, resolution=resolution,
snapshots=tuple(s for r in reports for s in r.snapshots), snapshots=tuple(s for r in reports for s in r.snapshots),
change_contexts=tuple(r.change_context for r in reports if r.change_context is not None), change_contexts=tuple(r.change_context for r in reports if r.change_context is not None),
quick=tuple(q for r in (*reports, *fallbacks) for q in r.quick),
) )
from openultrasast.push.assistance import assist from openultrasast.push.assistance import assist
+54
View File
@@ -0,0 +1,54 @@
"""The quick-rule tier of pre-push (task 17.3, Requirement 9.3): fast, advisory, never an alert."""
from __future__ import annotations
import json
from test_push_runner import history
from openultrasast.cpg.backend import NullBackend
from openultrasast.push.quick import run_quick_tier
from openultrasast.push.runner import push, replay
HANDLER = "module.exports = function debug (req, res) {\n res.end(String(eval(req.query.code)))\n}\n"
def test_only_matches_on_changed_lines_are_kept_and_input_is_proven(tmp_path):
(tmp_path / "debug.js").write_text(HANDLER)
(tmp_path / "old.js").write_text("eval(x)\n")
hit = run_quick_tier(tmp_path, {"debug.js": [(2, 2)], "old.js": [(5, 5)]})
assert hit.status == "completed" and hit.files == ("debug.js", "old.js")
assert hit.bytes_read == len(HANDLER) + len("eval(x)\n")
assert {(f["rule"], f["path"], f["line"], f["scope"]) for f in hit.findings} >= {("js-eval", "debug.js", 2, "changed_line")}
assert all(f["path"] == "debug.js" for f in hit.findings)
miss = run_quick_tier(tmp_path, {"debug.js": [(3, 3)]})
assert miss.findings == () and miss.files == ("debug.js",)
def test_replay_shows_quick_matches_as_advisory_and_never_admits_or_blocks(tmp_path):
root, _, base, git = history(tmp_path)
(root / "debug.js").write_text(HANDLER)
git("add", "debug.js")
git("commit", "-m", "debug")
head = git("rev-parse", "HEAD")
artifact = tmp_path / "r.json"
delivery = replay(root, base=base, head=head, artifact=artifact, backend=NullBackend())
assert delivery.exit_code == 0 and delivery.result.actionable_defect_ids == ()
assert "Quick rules: " in delivery.text and "Advisory: not verified by the engine." in delivery.text
assert "- js-eval (CWE-95" in delivery.text and "debug.js:2" in delivery.text
data = json.loads(artifact.read_text())
(tier,) = data["quick_tier"]
assert tier["status"] == "completed" and tier["basis"] == "comparison" and tier["files"] == ["debug.js"]
assert tier["bytes_read"] == len(HANDLER) and "advisory" in tier["label"]
assert data["admission"]["defects"] == []
def test_new_branch_says_there_is_no_base_and_what_was_checked_instead(tmp_path):
root, _, head, git = history(tmp_path)
updates = f"refs/heads/feature {head} refs/heads/feature {'0' * 40}\n"
delivery = push(root, updates=updates, remote_name="origin", remote_url="/r", artifact=tmp_path / "p.json", backend=NullBackend())
assert delivery.exit_code == 0
assert "Skipped: new branch: the remote has no base to compare against" in delivery.text
assert "New branch: instead, quick rules checked the files its last commit changed" in delivery.text
(tier,) = json.loads((tmp_path / "p.json").read_text())["quick_tier"]
assert tier["basis"] == "last_commit" and tier["files"] == ["api.js"]