Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,18 @@ jobs:
python-version: "3.12"

# GitHub-friendly SARIF (warning+), plus metrics.json / report.md.
# Runs against the committed synthetic fixture, not the stand-only sts_audit
# corpus, so CI stays green (and leaks nothing) once sts_audit is removed.
- name: Export audit artifacts
run: PYTHONPATH=. python3 -m report.cli --min-level warning
run: PYTHONPATH=. python3 -m report.cli --min-level warning --findings viz/fixtures/findings.json

# Catches Python/JS-template regressions in the dashboards (no unit tests there).
# Points at viz/fixtures (synthetic golden set) for the same reason as above.
# The arch dashboard renders empty-state here (graph.json/runtime.json are stand-only).
- name: Build dashboards (smoke)
run: |
set -euo pipefail
python3 viz/build_dashboard.py
python3 viz/build_dashboard.py viz/fixtures
python3 viz/build_arch_dashboard.py

- name: Upload artifacts
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ __pycache__/
# viz generated/vendored artifacts (reproducible via viz/build_dashboard.py)
viz/plotly.min.js
viz/sts-dashboard.html
viz/arch-dashboard.html
viz/preview.png
viz/theme-*.png

Expand Down
20 changes: 16 additions & 4 deletions viz/build_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,13 +603,25 @@ def main(argv=None):
global STS
ap = argparse.ArgumentParser(
description="Build the interactive audit dashboard from an audit-output folder.")
ap.add_argument("data_dir", nargs="?", default="artifacts",
ap.add_argument("data_dir", nargs="?", default=None,
help="folder with findings.json + health-report.md (default: artifacts). "
"A relative path resolves from the repo root.")
args = ap.parse_args(argv)
STS = args.data_dir if os.path.isabs(args.data_dir) else os.path.join(ROOT, args.data_dir)
if not os.path.isdir(STS):
ap.error(f"data dir not found: {STS}")
# An EXPLICIT dir must have its inputs (a caller asked to render *that* folder —
# missing files there is an error, so CI's `build_dashboard.py viz/fixtures` fails
# loudly if the fixture is gone). The IMPLICIT default `artifacts/` is empty until a
# stand run populates it, so a missing input there is "no work" — skip cleanly.
explicit = args.data_dir is not None
data_dir = args.data_dir if explicit else "artifacts"
STS = data_dir if os.path.isabs(data_dir) else os.path.join(ROOT, data_dir)
missing = [f for f in ("findings.json", "health-report.md")
if not os.path.isfile(os.path.join(STS, f))]
if missing:
msg = f"{STS} is missing {', '.join(missing)}"
if explicit:
ap.error(msg)
print(f"build_dashboard: {msg} — nothing to render, skipping.")
return
Comment on lines +619 to +624

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make fixture smoke fail when inputs are missing

When the caller supplies the new fixture path, treating missing inputs as success makes the dashboard smoke a no-op: report.cli only requires findings.json, so if viz/fixtures/health-report.md is deleted/renamed or the CI path points at an empty directory, the workflow's python3 viz/build_dashboard.py viz/fixtures still exits 0 here without rendering the dashboard. Limit the clean skip to the default empty artifacts/ case, or make CI assert the fixture files exist before relying on the smoke.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 78132ca (your first option). The clean skip now applies only to the implicit default artifacts/ (empty until a stand run populates it). An explicitly-supplied dir missing findings.json/health-report.md is now a hard error (argparse exit 2), so CI's build_dashboard.py viz/fixtures fails loudly if the fixture is ever deleted/renamed — no more silent no-op.

Verified: explicit valid → render (exit 0); default empty artifacts/ → skip (exit 0); explicit dir missing a file / nonexistent → exit 2.


Generated by Claude Code

data = collect()
out = HTML.replace("%PLOTLY%", _plotly_tag()) \
.replace("%DATA%", _json_for_script(data)) \
Expand Down
30 changes: 30 additions & 0 deletions viz/fixtures/findings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"_note": "GOLDEN smoke fixture for the dashboard/report CI (viz/build_dashboard.py + report.cli). Fully SYNTHETIC — invented module/file/handler names, not from any real audited project. Its only job is to exercise the render/export code paths (every tool, tier, source family, category, and OWN leak-shape) so CI catches Python/JS-template regressions without depending on the stand-only sts_audit corpus. Keep it small and stable.",
"coverage": {
"tools": ["own-check", "roslyn", "codeql", "infersharp"],
"total": 14,
"kept": 14,
"suppressed": 0,
"suppressed_by": {},
"analysis_skipped": 0,
"analysis_skipped_by": {},
"by_category": {"1": 3, "2": 2, "3": 2, "4": 3, "5": 1, "6": 2, "7": 1},
"uncategorized_rules": {}
},
"findings": [
{"tool": "own-check", "path": "Acme.Portfolio/ViewModels/PortfolioViewModel.cs", "line": 42, "rule": "OWN001", "category": 2, "category_name": "subscription-leak", "resource": "subscription token", "suppressed": false, "suppress_reason": "", "message": "event 'quotes.Updated' is subscribed (handler '(s, e) => Refresh()') but never unsubscribed; its source is an injected dependency whose lifetime is unknown, so it may outlive and keep 'PortfolioViewModel' alive (possible leak) [resource: subscription token]"},
{"tool": "own-check", "path": "Acme.Portfolio/ViewModels/PortfolioViewModel.cs", "line": 58, "rule": "OWN001", "category": 2, "category_name": "subscription-leak", "resource": "subscription token", "suppressed": false, "suppress_reason": "", "message": "event 'service.Ticked' is subscribed (handler 'OnTicked') but never unsubscribed; its source outlives the view model, so it may keep 'PortfolioViewModel' alive (possible leak) [resource: subscription token]"},
{"tool": "own-check", "path": "Acme.Portfolio/Data/PortfolioRepository.cs", "line": 19, "rule": "OWN020", "category": 1, "category_name": "idisposable-leak", "resource": "connection", "suppressed": false, "suppress_reason": "", "message": "field '_conn' (DbConnection) is created but never disposed; it holds an unmanaged handle for the lifetime of 'PortfolioRepository' (possible leak) [resource: connection]"},
{"tool": "own-check", "path": "Acme.Market/Services/QuoteFeed.cs", "line": 77, "rule": "OWN021", "category": 1, "category_name": "idisposable-leak", "resource": "stream", "suppressed": false, "suppress_reason": "", "message": "local 'buffer' (MemoryStream) is created but never disposed; wrap it in a using statement (possible leak) [resource: stream]"},
{"tool": "own-check", "path": "Acme.Portfolio/Reports/PdfExporter.cs", "line": 61, "rule": "OWN-REGION", "category": 7, "category_name": "region-escape", "resource": "region handle", "suppressed": false, "suppress_reason": "", "message": "a rented buffer escapes its region via the returned closure; ownership is ambiguous and it may never be released (possible leak) [resource: region handle]"},
{"tool": "roslyn", "path": "Acme.Portfolio/ViewModels/PortfolioViewModel.cs", "line": 33, "rule": "INPC020", "category": 3, "category_name": "inpc-correctness", "resource": "", "suppressed": false, "suppress_reason": "", "message": "prefer a nameof() expression over the string literal for the raised property name"},
{"tool": "roslyn", "path": "Acme.Portfolio/ViewModels/PositionViewModel.cs", "line": 14, "rule": "INPC003", "category": 3, "category_name": "inpc-correctness", "resource": "", "suppressed": false, "suppress_reason": "", "message": "notify when the 'Quantity' property value changes"},
{"tool": "roslyn", "path": "Acme.Market/Services/QuoteFeed.cs", "line": 90, "rule": "MA0006", "category": 4, "category_name": "general-quality", "resource": "", "suppressed": false, "suppress_reason": "", "message": "use String.Equals instead of the equality operator for culture-aware comparison"},
{"tool": "roslyn", "path": "Acme.UI/Controls/GaugeControl.cs", "line": 120, "rule": "WPF0041", "category": 5, "category_name": "wpf-freezable", "resource": "", "suppressed": false, "suppress_reason": "", "message": "set the DataContext in the constructor rather than in a loaded handler"},
{"tool": "roslyn", "path": "Acme.Portfolio/Data/PortfolioRepository.cs", "line": 48, "rule": "CA2000", "category": 1, "category_name": "idisposable-leak", "resource": "", "suppressed": false, "suppress_reason": "", "message": "call System.IDisposable.Dispose on the object before all references to it are out of scope"},
{"tool": "roslyn", "path": "Acme.UI/Controls/GaugeControl.cs", "line": 7, "rule": "RCS1037", "category": 4, "category_name": "general-quality", "resource": "", "suppressed": false, "suppress_reason": "", "message": "remove trailing white-space"},
{"tool": "roslyn", "path": "Acme.Portfolio/Reports/PdfExporter.cs", "line": 25, "rule": "RCS1090", "category": 6, "category_name": "architecture", "resource": "", "suppressed": false, "suppress_reason": "", "message": "add a ConfigureAwait(false) call to the awaited task"},
{"tool": "codeql", "path": "Acme.Market/Services/QuoteFeed.cs", "line": 132, "rule": "cs/dereferenced-value-may-be-null", "category": 6, "category_name": "architecture", "resource": "", "suppressed": false, "suppress_reason": "", "message": "this expression may be null at this access and would raise a NullReferenceException"},
{"tool": "infersharp", "path": "Acme.Portfolio/Reports/PdfExporter.cs", "line": 88, "rule": "NULL_DEREFERENCE", "category": 4, "category_name": "general-quality", "resource": "", "suppressed": false, "suppress_reason": "", "message": "object returned by the factory could be null and is dereferenced without a guard"}
]
}
19 changes: 19 additions & 0 deletions viz/fixtures/health-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Acme (synthetic) — health report

> GOLDEN smoke fixture for `viz/build_dashboard.py`. Fully synthetic; not a real
> project. Mirrors the shape `build_dashboard.py` parses (the summary line below and
> the module pain table) so the dashboard renders in CI without the stand-only
> `sts_audit` corpus. Numbers are illustrative, not a real audit.

## Summary

**14 findings** (5 high-confidence, 9 candidate). High-confidence = flagged by ≥2 independent tools at the same spot.

## Module pain

| module | pain | findings | high-conf | top category |
|---|---:|---:|---:|---|
| `Acme.Portfolio` | 128.0 | 6 | 3 | subscription-leak |
| `Acme.Portfolio/Reports` | 74.0 | 3 | 1 | architecture |
| `Acme.Market` | 63.0 | 3 | 1 | general-quality |
| `Acme.UI` | 21.0 | 2 | 0 | wpf-freezable |
Loading