rafter-cli 0.8.6__tar.gz → 0.8.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/PKG-INFO +1 -1
  2. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/pyproject.toml +1 -1
  3. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/mcp_server.py +35 -0
  4. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/policy_loader.py +3 -3
  5. rafter_cli-0.8.7/rafter_cli/core/suppression_writer.py +111 -0
  6. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/rafter-security-skill.md +1 -1
  7. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/docs/finding-triage.md +3 -2
  8. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/README.md +0 -0
  9. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/__init__.py +0 -0
  10. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/__main__.py +0 -0
  11. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/__init__.py +0 -0
  12. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/agent.py +0 -0
  13. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/agent_components.py +0 -0
  14. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/backend.py +0 -0
  15. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/brief.py +0 -0
  16. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/ci.py +0 -0
  17. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/docs.py +0 -0
  18. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/hook.py +0 -0
  19. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/issues/__init__.py +0 -0
  20. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/issues/dedup.py +0 -0
  21. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/issues/github_client.py +0 -0
  22. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/issues/issue_builder.py +0 -0
  23. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/issues/issues_app.py +0 -0
  24. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/notify.py +0 -0
  25. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/policy.py +0 -0
  26. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/report.py +0 -0
  27. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/scan.py +0 -0
  28. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/skill.py +0 -0
  29. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/commands/skill_remote.py +0 -0
  30. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/__init__.py +0 -0
  31. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/audit_logger.py +0 -0
  32. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/command_interceptor.py +0 -0
  33. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/config_manager.py +0 -0
  34. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/config_schema.py +0 -0
  35. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/custom_patterns.py +0 -0
  36. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/docs_loader.py +0 -0
  37. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/hook_control.py +0 -0
  38. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/pattern_engine.py +0 -0
  39. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/core/risk_rules.py +0 -0
  40. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/__init__.py +0 -0
  41. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/agents/__init__.py +0 -0
  42. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/agents/rafter.md +0 -0
  43. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/continue-rules/rafter-code-review.md +0 -0
  44. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/continue-rules/rafter-secure-design.md +0 -0
  45. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/continue-rules/rafter-skill-review.md +0 -0
  46. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/continue-rules/rafter.md +0 -0
  47. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc +0 -0
  48. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc +0 -0
  49. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc +0 -0
  50. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/cursor-rules/rafter.mdc +0 -0
  51. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/pre-commit-hook.sh +0 -0
  52. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/pre-push-hook.sh +0 -0
  53. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/__init__.py +0 -0
  54. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/SKILL.md +0 -0
  55. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/__init__.py +0 -0
  56. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/docs/backend.md +0 -0
  57. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/docs/cli-reference.md +0 -0
  58. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/docs/guardrails.md +0 -0
  59. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter/docs/shift-left.md +0 -0
  60. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/SKILL.md +0 -0
  61. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/api.md +0 -0
  62. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md +0 -0
  63. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/cwe-top25.md +0 -0
  64. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/investigation-playbook.md +0 -0
  65. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md +0 -0
  66. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md +0 -0
  67. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md +0 -0
  68. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/api-design.md +0 -0
  69. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md +0 -0
  70. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/data-storage.md +0 -0
  71. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/dependencies.md +0 -0
  72. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/deployment.md +0 -0
  73. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/ingestion.md +0 -0
  74. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/standards-pointers.md +0 -0
  75. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-secure-design/docs/threat-modeling.md +0 -0
  76. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md +0 -0
  77. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/authorship-provenance.md +0 -0
  78. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/changelog-review.md +0 -0
  79. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/data-practices.md +0 -0
  80. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/malware-indicators.md +0 -0
  81. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/prompt-injection.md +0 -0
  82. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/skills/rafter-skill-review/docs/telemetry.md +0 -0
  83. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/windsurf-rules/rafter-code-review.md +0 -0
  84. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md +0 -0
  85. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md +0 -0
  86. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/resources/windsurf-rules/rafter.md +0 -0
  87. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/scanners/__init__.py +0 -0
  88. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/scanners/betterleaks.py +0 -0
  89. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/scanners/regex_scanner.py +0 -0
  90. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/scanners/secret_patterns.py +0 -0
  91. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/scanners/union.py +0 -0
  92. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/__init__.py +0 -0
  93. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/api.py +0 -0
  94. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/binary_manager.py +0 -0
  95. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/formatter.py +0 -0
  96. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/git.py +0 -0
  97. {rafter_cli-0.8.6 → rafter_cli-0.8.7}/rafter_cli/utils/skill_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rafter-cli
3
- Version: 0.8.6
3
+ Version: 0.8.7
4
4
  Summary: Rafter CLI — the default security agent for AI workflows. Free for individuals and open source.
5
5
  License: MIT
6
6
  Author: Rafter Team
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rafter-cli"
3
- version = "0.8.6"
3
+ version = "0.8.7"
4
4
  description = "Rafter CLI — the default security agent for AI workflows. Free for individuals and open source."
5
5
  authors = ["Rafter Team <hello@rafter.so>"]
6
6
  license = "MIT"
@@ -175,6 +175,18 @@ def handle_get_docs_resource() -> str:
175
175
  return json.dumps(handle_list_docs(), indent=2)
176
176
 
177
177
 
178
+ def handle_suppress_finding(
179
+ path: str,
180
+ rules: list[str] | None = None,
181
+ reason: str | None = None,
182
+ ) -> dict:
183
+ """Persist a false-positive suppression into the project's .rafter.yml."""
184
+ from ..core.suppression_writer import write_suppression
185
+
186
+ result = write_suppression([path], rules=rules, reason=reason)
187
+ return {"ok": True, **result}
188
+
189
+
178
190
  # ── MCP server factory ────────────────────────────────────────────────
179
191
 
180
192
 
@@ -249,6 +261,29 @@ def create_mcp_server():
249
261
  """
250
262
  return json.dumps(handle_get_doc(id_or_tag, refresh))
251
263
 
264
+ @mcp.tool()
265
+ def suppress_finding(
266
+ path: str,
267
+ rules: list[str] | None = None,
268
+ reason: str | None = None,
269
+ ) -> str:
270
+ """Triage a false positive by persisting a suppression rule into .rafter.yml.
271
+
272
+ Use when a scan_secrets finding (or a remote scan finding) is a confirmed
273
+ false positive — e.g. a test fixture or sample credential. Suppressed
274
+ findings still surface under '_suppressed' in scan output, so the decision
275
+ is reviewable and version-controlled. Always include a reason.
276
+
277
+ Args:
278
+ path: File path or glob to suppress findings in (e.g. 'test/fixtures/**').
279
+ rules: Specific rules to suppress, matched case-insensitively against a
280
+ finding's rule name OR rule id — e.g. 'AWS Access Key' (local pattern
281
+ name) or 'R-6D5E2' (remote SAST/SCA rule id). Omit to suppress all
282
+ rules for the path. Honored by both local scans and remote `rafter run`.
283
+ reason: Why this is a false positive — persisted with the rule. Strongly recommended.
284
+ """
285
+ return json.dumps(handle_suppress_finding(path, rules, reason))
286
+
252
287
  @mcp.resource("rafter://config")
253
288
  def config_resource() -> str:
254
289
  """Current Rafter configuration."""
@@ -30,13 +30,13 @@ POLICY_FILENAMES = [".rafter.yml", ".rafter.yaml"]
30
30
  _KNOWN_DOC_KEYS = {"id", "path", "url", "description", "tags", "cache"}
31
31
 
32
32
 
33
- def find_policy_file() -> Path | None:
34
- """Walk from cwd up to git root looking for a policy file.
33
+ def find_policy_file(start_dir: str | Path | None = None) -> Path | None:
34
+ """Walk from ``start_dir`` (default cwd) up to git root looking for a policy file.
35
35
 
36
36
  Returns the first candidate that exists, in the precedence order
37
37
  declared by ``POLICY_FILE_CANDIDATES``.
38
38
  """
39
- cwd = Path.cwd()
39
+ cwd = Path(start_dir) if start_dir else Path.cwd()
40
40
  root = get_git_root()
41
41
  stop = Path(root) if root else cwd.anchor and Path(cwd.anchor)
42
42
 
@@ -0,0 +1,111 @@
1
+ """Persist finding suppressions into the project's .rafter.yml ignore list.
2
+
3
+ Mirrors node/src/core/suppression-writer.ts — keep both in sync.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from ..utils.git import get_git_root
11
+ from .policy_loader import find_policy_file
12
+
13
+
14
+ def _rule_key(paths: list[str], rules: list[str] | None) -> str:
15
+ """Order- and duplicate-insensitive identity for an ignore rule.
16
+
17
+ Two rules are "the same" if they target the same set of paths and the
18
+ same set of rule names. Reason is excluded — re-suppressing the same
19
+ scope just updates the reason.
20
+ """
21
+ def norm(xs: list[str] | None) -> list[str]:
22
+ return sorted({str(x) for x in (xs or [])})
23
+
24
+ return json.dumps({"paths": norm(paths), "rules": norm(rules)}, sort_keys=True)
25
+
26
+
27
+ def write_suppression(
28
+ paths: list[str],
29
+ rules: list[str] | None = None,
30
+ reason: str | None = None,
31
+ cwd: str | Path | None = None,
32
+ ) -> dict:
33
+ """Persist a finding suppression into the project's .rafter.yml ignore list.
34
+
35
+ Resolves the policy file via the same precedence the loader uses; if none
36
+ exists, creates a canonical ``.rafter.yml`` at the git root (or ``cwd``).
37
+
38
+ Merge semantics: if an existing ignore rule targets the same paths + rules,
39
+ its reason is updated in place rather than appending a duplicate.
40
+
41
+ ``cwd`` overrides the base directory for resolution (defaults to the process cwd).
42
+
43
+ Returns a dict: {file, action, entry, suppression_count}.
44
+ """
45
+ import yaml
46
+
47
+ norm_paths = [str(p) for p in (paths or []) if str(p)]
48
+ if not norm_paths:
49
+ raise ValueError('"paths" must be a non-empty list of file paths or globs.')
50
+ norm_rules = [str(r) for r in rules if str(r)] if isinstance(rules, list) else None
51
+ norm_reason = reason.strip() if isinstance(reason, str) and reason.strip() else None
52
+ base_dir = str(cwd) if cwd else str(Path.cwd())
53
+
54
+ target = find_policy_file(base_dir)
55
+ raw: dict = {}
56
+
57
+ if target and Path(target).exists():
58
+ parsed = yaml.safe_load(Path(target).read_text())
59
+ raw = parsed if isinstance(parsed, dict) else {}
60
+ action = "appended"
61
+ else:
62
+ root = get_git_root() or base_dir
63
+ target = Path(root) / ".rafter.yml"
64
+ action = "created"
65
+
66
+ target = Path(target)
67
+ ignore_list = raw.get("ignore") if isinstance(raw.get("ignore"), list) else []
68
+
69
+ new_entry: dict = {"paths": norm_paths}
70
+ if norm_rules:
71
+ new_entry["rules"] = norm_rules
72
+ if norm_reason:
73
+ new_entry["reason"] = norm_reason
74
+
75
+ key = _rule_key(norm_paths, norm_rules)
76
+ existing = next(
77
+ (
78
+ e
79
+ for e in ignore_list
80
+ if isinstance(e, dict)
81
+ and isinstance(e.get("paths"), list)
82
+ and _rule_key(e["paths"], e.get("rules")) == key
83
+ ),
84
+ None,
85
+ )
86
+
87
+ if existing is not None:
88
+ if norm_reason:
89
+ existing["reason"] = norm_reason
90
+ else:
91
+ existing.pop("reason", None)
92
+ if action != "created":
93
+ action = "updated"
94
+ entry = dict(new_entry)
95
+ if existing.get("reason"):
96
+ entry["reason"] = existing["reason"]
97
+ else:
98
+ ignore_list.append(new_entry)
99
+ entry = new_entry
100
+
101
+ raw["ignore"] = ignore_list
102
+
103
+ target.parent.mkdir(parents=True, exist_ok=True)
104
+ target.write_text(yaml.safe_dump(raw, sort_keys=False, default_flow_style=False))
105
+
106
+ return {
107
+ "file": str(target),
108
+ "action": action,
109
+ "entry": entry,
110
+ "suppression_count": len(ignore_list),
111
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rafter-security
3
3
  description: Security toolkit for AI workflows. Use when scanning code or repos for vulnerabilities, auditing third-party skills/MCPs/agent configs before installing, evaluating shell commands before running them, or generating secure design questions for new features. Provides `rafter run` (remote SAST + SCA, needs RAFTER_API_KEY), `rafter secrets` (offline secrets-only), `rafter agent exec --dry-run` (command-risk classification), and `rafter skill review`.
4
- version: 0.8.6
4
+ version: 0.8.7
5
5
  homepage: https://rafter.so
6
6
  metadata:
7
7
  openclaw:
@@ -53,7 +53,7 @@ If the finding is a leaked secret that was committed:
53
53
 
54
54
  ## Suppression — When It's OK
55
55
 
56
- Suppress only when the finding is a real false positive *for this context*, with a written reason. Two mechanisms:
56
+ Suppress only when the finding is a real false positive *for this context*, with a written reason. Three mechanisms:
57
57
 
58
58
  - **`.rafter.yml`**: add an `ignore:` rule naming the path(s), the rule(s), and a `reason` (your evidence). Repo-tied; persists across scans:
59
59
  ```yaml
@@ -62,7 +62,8 @@ Suppress only when the finding is a real false positive *for this context*, with
62
62
  rules: ["AWS Access Key ID"] # omit `rules` to suppress every finding on those paths
63
63
  reason: "fake test keys — no live path"
64
64
  ```
65
- Match `rules` on the finding's **rule name** (case-insensitive), not a hashed `R-…` id. On a local `rafter scan`, suppressed findings move into a `_suppressed[]` array and don't affect the exit code — you only fail on a *non-suppressed* finding. Docs: https://docs.rafter.so/suppression
65
+ Each `rules` entry matches (case-insensitively) the finding's **rule name** (e.g. `AWS Access Key ID`) **or** its **rule id** (e.g. `R-6D5E2`) — use the name for local pattern findings, the id for remote SAST/SCA findings. Path globs are gitignore-style: `*` stays within a path segment, `**` crosses segments, a bare name matches the basename. The same `.rafter.yml ignore` block is honored by **local** scans and **remote `rafter run`** alike. Suppressed findings move into a `_suppressed[]` array and don't affect the exit code — you only fail on a *non-suppressed* finding. Docs: https://docs.rafter.so/suppression
66
+ - **MCP `suppress_finding` tool**: agents can triage a false positive directly through the MCP — it writes the same `.rafter.yml` `ignore` rule above (path, optional rule names, reason). No hand-editing required.
66
67
  - **Baseline**: `rafter agent baseline create` snapshots current findings; scan with `rafter scan --baseline` so only *new* findings surface. Good for adopting Rafter on a legacy codebase without a big bang.
67
68
 
68
69
  Never suppress by:
File without changes