Skip to content

Commit 3127b6d

Browse files
fix(friction): reduce regex scan noise with denylist and aggregation (#473) (#513)
Add configurable source denylist defaults, prose damping for markdown outside run artifacts, and collapse identical regex snippets into one candidate with occurrence counts and source paths. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 513af27 commit 3127b6d

4 files changed

Lines changed: 222 additions & 48 deletions

File tree

src/brigade/friction_cmd.py

Lines changed: 124 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import fnmatch
56
import hashlib
67
import json
78
import re
@@ -103,22 +104,30 @@
103104
r"(?i)(?:test result:\s*ok\.?[^.\n]*\b0\s+failed\b|"
104105
r"\d+\s+passed\b[^.\n]*\b0\s+failed\b|(?:all\s+tests?|tests?)\s+passed\b[^.\n]*\b0\s+failed\b)"
105106
)
106-
DOCUMENTATION_NAMES = frozenset(
107+
DEFAULT_DENIED_NAMES = frozenset(
107108
{
108-
"agents.md",
109-
"changelog.md",
110-
"claude.md",
111-
"contributing.md",
112-
"install_for_agents.md",
113-
"quickstart.md",
114-
"readme.md",
115-
"roadmap.md",
116-
"skill.md",
117-
"third_party_notices.md",
109+
"ambient-suggestions.json",
110+
"model-cache.json",
111+
"models-cache.json",
112+
"recommendations.json",
113+
"suggestions.json",
118114
}
119115
)
120-
GENERATED_SUGGESTION_STEMS = frozenset({"recommendations", "suggestions"})
116+
DEFAULT_DENIED_STEMS = frozenset({"ambient-suggestions", "recommendations", "suggestions"})
117+
DEFAULT_DENIED_GLOBS = (
118+
"**/.codex/skills/**",
119+
"**/.claude/skills/**",
120+
"**/templates/skills/**",
121+
)
122+
RUN_ARTIFACT_MARKERS = ("/.brigade/work/", "/.brigade/runs/")
121123
SOURCE_FAMILIES = ("verification", "run", "evaluation", "miseledger", "regex")
124+
SEVERITY_DAMP_MAP = {"high": "low", "medium": "low", "low": "low"}
125+
126+
127+
@dataclass(frozen=True)
128+
class FrictionScanConfig:
129+
deny_names: frozenset[str] = DEFAULT_DENIED_NAMES
130+
deny_globs: tuple[str, ...] = DEFAULT_DENIED_GLOBS
122131

123132

124133
@dataclass(frozen=True)
@@ -158,6 +167,78 @@ def _candidate_id(source: str, friction_type: str, text: str, *, stable_source:
158167
return f"friction-{digest}"
159168

160169

170+
def _load_scan_config(target: Path) -> FrictionScanConfig:
171+
defaults = FrictionScanConfig()
172+
config_path = target / ".brigade" / "friction" / "config.json"
173+
if not config_path.is_file():
174+
return defaults
175+
payload = _read_object(config_path)
176+
if payload is None:
177+
return defaults
178+
deny_names = defaults.deny_names
179+
raw_names = payload.get("deny_names")
180+
if isinstance(raw_names, list):
181+
deny_names = frozenset({str(name).lower() for name in raw_names if str(name).strip()})
182+
deny_globs = defaults.deny_globs
183+
raw_globs = payload.get("deny_globs")
184+
if isinstance(raw_globs, list):
185+
deny_globs = tuple(str(item) for item in raw_globs if str(item).strip())
186+
return FrictionScanConfig(deny_names=deny_names, deny_globs=deny_globs)
187+
188+
189+
def _path_matches_deny_glob(path: Path, pattern: str) -> bool:
190+
normalized = path.as_posix()
191+
if fnmatch.fnmatch(normalized, pattern.lstrip("/")):
192+
return True
193+
if fnmatch.fnmatch(normalized, pattern):
194+
return True
195+
if "**" not in pattern:
196+
return False
197+
anchor = pattern.replace("**", "").strip("/")
198+
if not anchor:
199+
return False
200+
return (
201+
f"/{anchor}/" in f"/{normalized}/" or normalized.endswith(f"/{anchor}") or normalized.startswith(f"{anchor}/")
202+
)
203+
204+
205+
def _is_under_skills_tree(path: Path) -> bool:
206+
parts = tuple(part.lower() for part in path.parts)
207+
for index, part in enumerate(parts):
208+
if part in {".codex", ".claude"} and index + 1 < len(parts) and parts[index + 1] == "skills":
209+
return True
210+
if part == "skills" and index > 0 and parts[index - 1] in {".codex", ".claude", "templates"}:
211+
return True
212+
return False
213+
214+
215+
def _is_denylisted_source(path: Path, config: FrictionScanConfig) -> bool:
216+
lowered_parts = tuple(part.lower() for part in path.parts)
217+
if "memory-handoffs" in lowered_parts and "processed" in lowered_parts:
218+
return True
219+
if path.name.lower() in config.deny_names or path.stem.lower() in config.deny_names:
220+
return True
221+
if path.stem.lower() in DEFAULT_DENIED_STEMS:
222+
return True
223+
if _is_under_skills_tree(path):
224+
return True
225+
for pattern in config.deny_globs:
226+
if _path_matches_deny_glob(path, pattern):
227+
return True
228+
return False
229+
230+
231+
def _should_damp_prose(path: Path) -> bool:
232+
if path.suffix.lower() != ".md":
233+
return False
234+
posix = path.as_posix()
235+
return not any(marker in posix for marker in RUN_ARTIFACT_MARKERS)
236+
237+
238+
def _damp_severity(severity: str) -> str:
239+
return SEVERITY_DAMP_MAP.get(severity, severity)
240+
241+
161242
def _iter_source_roots(target: Path, *, include_agent_logs: bool, agent_logs_only: bool = False) -> list[Path]:
162243
if agent_logs_only:
163244
roots = [Path(item).expanduser() for item in DEFAULT_AGENT_LOG_DIRS]
@@ -308,14 +389,6 @@ def _is_verify_receipt(path: Path) -> bool:
308389
return path.name == "receipt.json" and "verify-runs" in path.parts
309390

310391

311-
def _is_noise_file(path: Path) -> bool:
312-
lowered_parts = tuple(part.lower() for part in path.parts)
313-
processed_handoff = "memory-handoffs" in lowered_parts and "processed" in lowered_parts
314-
return (
315-
path.name.lower() in DOCUMENTATION_NAMES or path.stem.lower() in GENERATED_SUGGESTION_STEMS or processed_handoff
316-
)
317-
318-
319392
def _is_successful_verify_receipt(payload: dict[str, Any]) -> bool:
320393
run_status = str(payload.get("status") or "completed")
321394
if run_status != "completed":
@@ -386,13 +459,29 @@ def _group_candidates(items: list[dict[str, Any]]) -> tuple[list[dict[str, Any]]
386459
for candidate_id in order:
387460
bucket = indexed[candidate_id]
388461
if len(bucket) == 1:
389-
result.append(bucket[0])
462+
single = dict(bucket[0])
463+
raw_evidence = single.get("evidence")
464+
if isinstance(raw_evidence, dict):
465+
evidence = dict(raw_evidence)
466+
evidence.setdefault("occurrence_count", 1)
467+
single["evidence"] = evidence
468+
result.append(single)
390469
continue
391470
grouped += len(bucket) - 1
392471
primary = dict(bucket[0])
393472
raw_evidence = primary.get("evidence")
394473
evidence = dict(raw_evidence) if isinstance(raw_evidence, dict) else {}
395474
evidence["children"] = [_evidence_child(occurrence) for occurrence in bucket]
475+
evidence["occurrence_count"] = len(bucket)
476+
source_paths = sorted(
477+
{
478+
str(occurrence.get("evidence", {}).get("path") or "")
479+
for occurrence in bucket
480+
if isinstance(occurrence.get("evidence"), dict) and occurrence["evidence"].get("path")
481+
}
482+
)
483+
if source_paths:
484+
evidence["source_paths"] = source_paths
396485
primary["evidence"] = evidence
397486
result.append(primary)
398487
return result, grouped
@@ -472,30 +561,33 @@ def _workflow_from_path(target: Path, path: Path) -> str:
472561
return parts[0]
473562

474563

475-
def _make_candidate(target: Path, match: Match) -> dict[str, Any]:
564+
def _make_candidate(target: Path, match: Match, *, damp_prose: bool = False) -> dict[str, Any]:
476565
try:
477566
source = str(match.path.resolve().relative_to(target))
478567
except ValueError:
479568
source = str(match.path)
480569
snippet = _short(match.line)
481570
text = f"{match.title}: {snippet}"
571+
severity = _damp_severity(match.severity) if damp_prose else match.severity
482572
return {
483-
"id": _candidate_id(source, match.friction_type, snippet, stable_source=True),
573+
"id": _candidate_id(source, match.friction_type, snippet, stable_source=False),
484574
"title": match.title,
485575
"text": text,
486576
"status": "candidate",
487577
"kind": "finding",
488578
"source": "friction-scan",
489579
"friction_type": match.friction_type,
490-
"severity": match.severity,
580+
"severity": severity,
491581
"workflow": _workflow_from_path(target, match.path),
492582
"evidence": {
493583
"path": source,
494584
"line": match.line_number,
495585
"snippet": snippet,
586+
"occurrence_count": 1,
496587
},
497588
"suggested_fix": "Review the evidence, decide whether this is actionable, then promote to a task, note, memory card, rule, or tool fix.",
498589
"detection": "regex",
590+
"prose_damped": damp_prose,
499591
}
500592

501593

@@ -732,6 +824,7 @@ def scan_payload(
732824
print("error: --max-candidates must be a positive integer", file=sys.stderr)
733825
return None, 2
734826

827+
scan_config = _load_scan_config(target)
735828
roots = _iter_source_roots(target, include_agent_logs=include_agent_logs, agent_logs_only=agent_logs_only)
736829
files, skipped_files = _iter_files(roots, since=since, max_files=max_files)
737830
families = (
@@ -746,23 +839,27 @@ def scan_payload(
746839
for path in files:
747840
if _is_verify_receipt(path) or path.name in {"run.json", "worker-results.json", "cell.json"}:
748841
continue
842+
if _is_denylisted_source(path, scan_config):
843+
dispositions["regex"]["rejected"] += 1
844+
rejected_noise += 1
845+
continue
749846
try:
750847
resolved_path = path.expanduser().resolve()
751848
except OSError:
752849
resolved_path = path
753-
reject_as_documentation = _is_noise_file(path)
754850
reject_as_historical = resolved_path in successful_verify_logs
851+
damp_prose = _should_damp_prose(path)
755852
matches = _scan_file(path)
756853
for match in matches:
757-
if reject_as_documentation or reject_as_historical:
854+
if reject_as_historical:
758855
dispositions["regex"]["rejected"] += 1
759856
rejected_noise += 1
760857
continue
761858
if _is_passing_zero_failure_line(match.line):
762859
dispositions["regex"]["rejected"] += 1
763860
rejected_noise += 1
764861
continue
765-
candidate = _make_candidate(target, match)
862+
candidate = _make_candidate(target, match, damp_prose=damp_prose)
766863
candidate["source_family"] = "regex"
767864
regex_candidates.append(candidate)
768865
families["regex"] = regex_candidates

src/brigade/repos_cmd/friction_fleet.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ def _signature_key(record: dict[str, Any]) -> str:
8585
def _occurrence_count(candidate: dict[str, Any]) -> int:
8686
evidence = candidate.get("evidence")
8787
if isinstance(evidence, dict):
88+
explicit = evidence.get("occurrence_count")
89+
if isinstance(explicit, int) and explicit > 0:
90+
return explicit
8891
children = evidence.get("children")
8992
if isinstance(children, list) and children:
9093
return len(children)

0 commit comments

Comments
 (0)