Skip to content

Commit e24a8cb

Browse files
tbitcsoz-agent
andcommitted
feat(auditor): merge REQ-357 \u2014 accepted_warnings audit suppression
Co-Authored-By: Oz <oz-agent@warp.dev>
2 parents 6236c8c + 6f79dd7 commit e24a8cb

3 files changed

Lines changed: 159 additions & 8 deletions

File tree

src/specsmith/auditor.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class AuditResult:
2323
passed: bool
2424
message: str
2525
fixable: bool = False
26+
suppressed: bool = False
2627

2728

2829
@dataclass
@@ -37,7 +38,7 @@ def passed(self) -> int:
3738

3839
@property
3940
def failed(self) -> int:
40-
return sum(1 for r in self.results if not r.passed)
41+
return sum(1 for r in self.results if not r.passed and not r.suppressed)
4142

4243
@property
4344
def fixable(self) -> int:
@@ -47,6 +48,39 @@ def fixable(self) -> int:
4748
def healthy(self) -> bool:
4849
return self.failed == 0
4950

51+
@property
52+
def suppressed_count(self) -> int:
53+
return sum(1 for r in self.results if r.suppressed)
54+
55+
56+
# ---------------------------------------------------------------------------
57+
# Suppression aliases (scaffold.yml accepted_warnings → AuditResult.name)
58+
# ---------------------------------------------------------------------------
59+
60+
_SUPPRESSION_ALIASES: dict[str, str] = {
61+
"scaffold_type_mismatch": "type-mismatch",
62+
"ledger_line_threshold": "ledger-size",
63+
"open_todo_count": "ledger-open-todos",
64+
"ledger_size": "ledger-size",
65+
}
66+
67+
68+
def _apply_accepted_warnings(report: AuditReport, accepted: list[str]) -> None:
69+
"""Mark audit results matching *accepted* warning names as suppressed.
70+
71+
Each entry in *accepted* is either a key in ``_SUPPRESSION_ALIASES`` or a
72+
direct ``AuditResult.name`` (exact match or prefix match up to ``:``).
73+
Matched results are set to ``suppressed=True`` and ``passed=True`` so they
74+
no longer count as failures.
75+
"""
76+
resolved: list[str] = [_SUPPRESSION_ALIASES.get(a, a) for a in accepted]
77+
for result in report.results:
78+
for name in resolved:
79+
if result.name == name or result.name.startswith(name + ":"):
80+
result.suppressed = True
81+
result.passed = True
82+
break
83+
5084

5185
# ---------------------------------------------------------------------------
5286
# Governance file existence checks
@@ -386,10 +420,7 @@ def check_ledger_health(root: Path) -> list[AuditResult]:
386420

387421
# Size check — uses configurable threshold (#145)
388422
threshold = _get_ledger_threshold(root)
389-
# Check audit_suppressions for opt-out
390-
raw = _read_scaffold_raw(root)
391-
suppressed = "ledger_size" in (raw.get("audit_suppressions") or [])
392-
if not suppressed and line_count > threshold:
423+
if line_count > threshold:
393424
results.append(
394425
AuditResult(
395426
name="ledger-size",
@@ -1171,6 +1202,15 @@ def run_audit(root: Path) -> AuditReport:
11711202
report.results.extend(check_industrial_artifacts(root))
11721203
report.results.extend(check_derived_artifacts(root))
11731204
report.results.extend(check_cross_repo_dependencies(root))
1205+
1206+
# Apply accepted_warnings / audit_suppressions from scaffold.yml (REQ-357)
1207+
raw = _read_scaffold_raw(root)
1208+
_accepted: list[str] = list(raw.get("accepted_warnings") or [])
1209+
# backward-compat: audit_suppressions list (pre-#188 ledger_size suppression)
1210+
_old_suppressed: list[str] = list(raw.get("audit_suppressions") or [])
1211+
_accepted = _accepted + _old_suppressed
1212+
if _accepted:
1213+
_apply_accepted_warnings(report, _accepted)
11741214
return report
11751215

11761216

@@ -1182,7 +1222,7 @@ def run_auto_fix(root: Path, report: AuditReport) -> list[str]:
11821222
fixed: list[str] = []
11831223

11841224
for result in report.results:
1185-
if result.passed:
1225+
if result.passed or result.suppressed:
11861226
continue
11871227

11881228
# Fix missing required files with minimal stubs

src/specsmith/cli.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,14 @@ def audit(fix: bool, project_dir: str) -> None:
441441
report = run_audit(root)
442442

443443
for r in report.results:
444-
icon = "[green]✓[/green]" if r.passed else "[red]✗[/red]"
445-
console.print(f" {icon} {r.message}")
444+
if r.suppressed:
445+
icon = "[dim]~[/dim]"
446+
elif r.passed:
447+
icon = "[green]✓[/green]"
448+
else:
449+
icon = "[red]✗[/red]"
450+
msg = r.message + " [dim](accepted)[/dim]" if r.suppressed else r.message
451+
console.print(f" {icon} {msg}")
446452

447453
console.print()
448454
if report.healthy:

tests/test_auditor.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,108 @@ def test_sync_parse_tests_md_preserves_letter_suffix(self, tmp_path: Path) -> No
199199
assert "TEST-NN-002a" in ids
200200
assert "TEST-NN-002b" in ids
201201
assert "TEST-NN-002" not in ids # must not be truncated
202+
203+
204+
# ---------------------------------------------------------------------------
205+
# REQ-357: accepted_warnings suppression
206+
# ---------------------------------------------------------------------------
207+
208+
209+
class TestAcceptedWarningsSuppression:
210+
"""Tests for REQ-357 — accepted_warnings suppression in auditor."""
211+
212+
def test_accepted_warnings_suppresses_type_mismatch(self, tmp_path: Path) -> None:
213+
"""scaffold_type_mismatch alias should suppress the type-mismatch check."""
214+
# Set up a minimal project with a type that will mismatch detection
215+
(tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8")
216+
(tmp_path / "LEDGER.md").write_text("# Ledger\nDone.\n", encoding="utf-8")
217+
docs = tmp_path / "docs"
218+
docs.mkdir()
219+
(docs / "TESTS.md").write_text("# Tests\n", encoding="utf-8")
220+
# Use a type that differs from what detect_project will infer.
221+
# "backend-frontend" is unlikely to match a near-empty tmp project.
222+
(tmp_path / "scaffold.yml").write_text(
223+
"name: test\n"
224+
"type: backend-frontend\n"
225+
"spec_version: 0.10.1\n"
226+
"vcs_platform: github\n"
227+
"accepted_warnings:\n"
228+
" - scaffold_type_mismatch\n",
229+
encoding="utf-8",
230+
)
231+
232+
report = run_audit(tmp_path)
233+
234+
# Find the type-mismatch result
235+
tm_results = [r for r in report.results if r.name == "type-mismatch"]
236+
if tm_results:
237+
assert tm_results[0].suppressed is True
238+
assert tm_results[0].passed is True
239+
# The suppressed result must not count as a failure
240+
type_mismatch_failures = [
241+
r for r in report.results if r.name == "type-mismatch" and not r.passed
242+
]
243+
assert len(type_mismatch_failures) == 0
244+
245+
def test_ledger_line_threshold_suppresses_ledger_size(self, tmp_path: Path) -> None:
246+
"""ledger_line_threshold alias should suppress ledger-size check."""
247+
(tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8")
248+
big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600))
249+
(tmp_path / "LEDGER.md").write_text(big_ledger, encoding="utf-8")
250+
(tmp_path / "scaffold.yml").write_text(
251+
"name: test\n"
252+
"type: cli-python\n"
253+
"spec_version: 0.10.1\n"
254+
"vcs_platform: github\n"
255+
"accepted_warnings:\n"
256+
" - ledger_line_threshold\n",
257+
encoding="utf-8",
258+
)
259+
260+
report = run_audit(tmp_path)
261+
262+
size_results = [r for r in report.results if r.name == "ledger-size"]
263+
assert len(size_results) == 1
264+
assert size_results[0].suppressed is True
265+
assert size_results[0].passed is True
266+
# Should not count toward failures
267+
assert all(
268+
r.passed or r.name != "ledger-size" for r in report.results
269+
)
270+
271+
def test_audit_suppressions_backward_compat(self, tmp_path: Path) -> None:
272+
"""Old audit_suppressions: [ledger_size] field should still suppress ledger-size."""
273+
(tmp_path / "AGENTS.md").write_text("# AGENTS\nShort.\n", encoding="utf-8")
274+
big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600))
275+
(tmp_path / "LEDGER.md").write_text(big_ledger, encoding="utf-8")
276+
(tmp_path / "scaffold.yml").write_text(
277+
"name: test\n"
278+
"type: cli-python\n"
279+
"spec_version: 0.10.1\n"
280+
"vcs_platform: github\n"
281+
"audit_suppressions:\n"
282+
" - ledger_size\n",
283+
encoding="utf-8",
284+
)
285+
286+
report = run_audit(tmp_path)
287+
288+
size_results = [r for r in report.results if r.name == "ledger-size"]
289+
assert len(size_results) == 1
290+
assert size_results[0].suppressed is True
291+
assert size_results[0].passed is True
292+
293+
def test_suppressed_count_property(self, tmp_path: Path) -> None:
294+
"""AuditReport.suppressed_count should reflect the number of suppressed results."""
295+
from specsmith.auditor import AuditReport, AuditResult, _apply_accepted_warnings
296+
297+
report = AuditReport(results=[
298+
AuditResult(name="ledger-size", passed=False, message="too big", fixable=True),
299+
AuditResult(name="type-mismatch", passed=False, message="mismatch"),
300+
AuditResult(name="other-check", passed=True, message="ok"),
301+
])
302+
_apply_accepted_warnings(report, ["ledger_line_threshold", "scaffold_type_mismatch"])
303+
304+
assert report.suppressed_count == 2
305+
assert report.failed == 0
306+
assert report.healthy is True

0 commit comments

Comments
 (0)