This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new f52b9526 feat(dashboard): add pyproject.toml, src/ layout, and pytest 
suite to security-tracker-stats-dashboard (#536)
f52b9526 is described below

commit f52b9526c674870c4a3527c2c08e2856212ee666
Author: Justin Mclean <[email protected]>
AuthorDate: Sat Jun 27 10:43:06 2026 +1000

    feat(dashboard): add pyproject.toml, src/ layout, and pytest suite to 
security-tracker-stats-dashboard (#536)
    
    Extract pure rendering helpers from render.py into
    src/security_tracker_stats_dashboard/core.py (parse_dt, bucket
    categorisation, eval_predicate, triage helpers, HTML serialisers).
    Add pyproject.toml (hatchling, uv, dev group: pytest>=8.0), and a
    100-test pytest suite in tests/test_core.py covering bucket
    categorisation, time-to-triage computation, and the render pipeline.
    
    The render.py monolith now imports from core and removes all duplicate
    local definitions (eval_predicate, bucket helpers, triage regex, etc.);
    scope_labels is threaded through ctx instead of reading a global.
    Pre-existing fetch_*.py scripts reformatted by ruff for workspace
    compliance.
    
    All validation commands pass:
      uv run --directory tools/security-tracker-stats-dashboard --group dev 
pytest
      bash -n tools/security-tracker-stats-dashboard/run.sh
    
    Closes the test-coverage gap named in specs/security-reporting.md Known
    Gaps. Adds the project to the root uv workspace members.
    
    Generated-by: Claude (Opus 4.7)
---
 pyproject.toml                                     |   1 +
 .../fetch_bodies.py                                |  34 +-
 .../fetch_events.py                                |  49 +-
 .../fetch_issues.py                                |  25 +-
 .../security-tracker-stats-dashboard/fetch_prs.py  |  41 +-
 .../fetch_roster.py                                |  14 +-
 .../pyproject.toml                                 |  57 ++
 tools/security-tracker-stats-dashboard/render.py   | 887 ++++++---------------
 .../security_tracker_stats_dashboard/__init__.py   |  16 +
 .../src/security_tracker_stats_dashboard/core.py   | 525 ++++++++++++
 .../tests/__init__.py                              |  16 +
 .../tests/test_core.py                             | 619 ++++++++++++++
 tools/security-tracker-stats-dashboard/uv.lock     | 204 +++++
 tools/spec-loop/specs/security-reporting.md        |   4 +-
 uv.lock                                            |  16 +
 15 files changed, 1789 insertions(+), 719 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index a6f83f13..83aeb815 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -109,6 +109,7 @@ members = [
   "tools/privacy-llm/checker",
   "tools/privacy-llm/redactor",
   "tools/sandbox-lint",
+  "tools/security-tracker-stats-dashboard",
   "tools/skill-and-tool-validator",
   "tools/skill-evals",
   "tools/spec-status-index",
diff --git a/tools/security-tracker-stats-dashboard/fetch_bodies.py 
b/tools/security-tracker-stats-dashboard/fetch_bodies.py
index 6e346afd..d8497442 100644
--- a/tools/security-tracker-stats-dashboard/fetch_bodies.py
+++ b/tools/security-tracker-stats-dashboard/fetch_bodies.py
@@ -25,11 +25,11 @@ import os
 import subprocess
 from concurrent.futures import ThreadPoolExecutor, as_completed
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-REPO = os.environ.get('TRACKER_STATS_REPO', 'airflow-s/airflow-s')
-OUT = f'{ROOT}/issue_extra.json'
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+REPO = os.environ.get("TRACKER_STATS_REPO", "airflow-s/airflow-s")
+OUT = f"{ROOT}/issue_extra.json"
 
-with open(f'{ROOT}/issues.json') as f:
+with open(f"{ROOT}/issues.json") as f:
     issues = json.load(f)
 
 # Resume support
@@ -39,22 +39,32 @@ if os.path.exists(OUT):
         cache = json.load(f)
     print(f"resume: {len(cache)} cached")
 
-todo = [i['number'] for i in issues if str(i['number']) not in cache]
+todo = [i["number"] for i in issues if str(i["number"]) not in cache]
 print(f"to fetch: {len(todo)}")
 
 
 def fetch(n):
     try:
         r = subprocess.run(
-            ['gh', 'issue', 'view', str(n), '--repo', REPO,
-             '--json', 'number,body,closedByPullRequestsReferences'],
-            capture_output=True, text=True, timeout=60,
+            [
+                "gh",
+                "issue",
+                "view",
+                str(n),
+                "--repo",
+                REPO,
+                "--json",
+                "number,body,closedByPullRequestsReferences",
+            ],
+            capture_output=True,
+            text=True,
+            timeout=60,
         )
         if r.returncode != 0:
-            return n, {'error': r.stderr.strip()}
+            return n, {"error": r.stderr.strip()}
         return n, json.loads(r.stdout)
     except Exception as e:
-        return n, {'error': str(e)}
+        return n, {"error": str(e)}
 
 
 done = 0
@@ -65,10 +75,10 @@ with ThreadPoolExecutor(max_workers=10) as ex:
         cache[str(n)] = data
         done += 1
         if done % 25 == 0:
-            with open(OUT, 'w') as f:
+            with open(OUT, "w") as f:
                 json.dump(cache, f)
             print(f"  {done}/{len(todo)}")
 
-with open(OUT, 'w') as f:
+with open(OUT, "w") as f:
     json.dump(cache, f)
 print(f"done: cached {len(cache)} → {OUT}")
diff --git a/tools/security-tracker-stats-dashboard/fetch_events.py 
b/tools/security-tracker-stats-dashboard/fetch_events.py
index 3d4bdc9b..9bd9f673 100644
--- a/tools/security-tracker-stats-dashboard/fetch_events.py
+++ b/tools/security-tracker-stats-dashboard/fetch_events.py
@@ -19,48 +19,60 @@
 
 """Fetch per-issue label-history events. Resumes from cache."""
 
-import json
-import subprocess
 import concurrent.futures
 import datetime as dt
+import json
 import os
+import subprocess
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-REPO = os.environ.get('TRACKER_STATS_REPO', 'airflow-s/airflow-s')
-EVENTS_DIR = f'{ROOT}/events'
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+REPO = os.environ.get("TRACKER_STATS_REPO", "airflow-s/airflow-s")
+EVENTS_DIR = f"{ROOT}/events"
 
-with open(f'{ROOT}/issues.json') as f:
+with open(f"{ROOT}/issues.json") as f:
     issues = json.load(f)
 
-numbers = [i['number'] for i in issues]
+numbers = [i["number"] for i in issues]
+
+
 # Map issue number -> last-updated epoch so the cache can be refreshed when an
 # issue was relabeled / closed after its events were last fetched.
 def _iso_to_epoch(s):
     if not s:
         return None
     try:
-        return dt.datetime.fromisoformat(s.replace('Z', '+00:00')).timestamp()
+        return dt.datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
     except ValueError:
         return None
-UPDATED_AT = {i['number']: _iso_to_epoch(i.get('updatedAt')) for i in issues}
+
+
+UPDATED_AT = {i["number"]: _iso_to_epoch(i.get("updatedAt")) for i in issues}
 print(f"Fetching events for {len(numbers)} issues...")
 
 os.makedirs(EVENTS_DIR, exist_ok=True)
 
+
 def fetch_one(n):
-    out_path = f'{EVENTS_DIR}/{n}.json'
+    out_path = f"{EVENTS_DIR}/{n}.json"
     if os.path.exists(out_path) and os.path.getsize(out_path) > 0:
         # Trust the cache only if it was written after the issue was last
         # updated; otherwise the issue changed since and the events are stale.
         upd = UPDATED_AT.get(n)
         if upd is None or os.path.getmtime(out_path) >= upd:
-            return (n, True, 'cached')
+            return (n, True, "cached")
     try:
         r = subprocess.run(
-            ['gh', 'api', f'repos/{REPO}/issues/{n}/events',
-             '--paginate',
-             '--jq', '[.[] | select(.event == "labeled" or .event == 
"unlabeled" or .event == "closed" or .event == "reopened") | {event, label: 
(.label.name // null), created_at}]'],
-            capture_output=True, text=True, timeout=60
+            [
+                "gh",
+                "api",
+                f"repos/{REPO}/issues/{n}/events",
+                "--paginate",
+                "--jq",
+                '[.[] | select(.event == "labeled" or .event == "unlabeled" or 
.event == "closed" or .event == "reopened") | {event, label: (.label.name // 
null), created_at}]',
+            ],
+            capture_output=True,
+            text=True,
+            timeout=60,
         )
         if r.returncode != 0:
             return (n, False, r.stderr[:200])
@@ -69,19 +81,20 @@ def fetch_one(n):
         idx = 0
         merged = []
         while idx < len(out):
-            while idx < len(out) and out[idx] in ' \n\r\t':
+            while idx < len(out) and out[idx] in " \n\r\t":
                 idx += 1
             if idx >= len(out):
                 break
             obj, n2 = decoder.raw_decode(out, idx)
             merged.extend(obj)
             idx = n2
-        with open(out_path, 'w') as f:
+        with open(out_path, "w") as f:
             json.dump(merged, f)
-        return (n, True, f'{len(merged)} events')
+        return (n, True, f"{len(merged)} events")
     except Exception as e:
         return (n, False, str(e)[:200])
 
+
 with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
     results = list(ex.map(fetch_one, numbers))
 
diff --git a/tools/security-tracker-stats-dashboard/fetch_issues.py 
b/tools/security-tracker-stats-dashboard/fetch_issues.py
index f71de349..b32460b1 100644
--- a/tools/security-tracker-stats-dashboard/fetch_issues.py
+++ b/tools/security-tracker-stats-dashboard/fetch_issues.py
@@ -23,22 +23,35 @@ import json
 import os
 import subprocess
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-REPO = os.environ.get('TRACKER_STATS_REPO', 'airflow-s/airflow-s')
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+REPO = os.environ.get("TRACKER_STATS_REPO", "airflow-s/airflow-s")
 
 os.makedirs(ROOT, exist_ok=True)
 
 print(f"Fetching issue list from {REPO} (state=all, limit 1000) ...")
 r = subprocess.run(
-    ['gh', 'issue', 'list', '--repo', REPO, '--state', 'all', '--limit', 
'1000',
-     '--json', 
'number,title,state,stateReason,createdAt,closedAt,labels,comments'],
-    capture_output=True, text=True, timeout=300,
+    [
+        "gh",
+        "issue",
+        "list",
+        "--repo",
+        REPO,
+        "--state",
+        "all",
+        "--limit",
+        "1000",
+        "--json",
+        "number,title,state,stateReason,createdAt,closedAt,labels,comments",
+    ],
+    capture_output=True,
+    text=True,
+    timeout=300,
 )
 if r.returncode != 0:
     raise SystemExit(f"gh failed: {r.stderr}")
 
 issues = json.loads(r.stdout)
-with open(f'{ROOT}/issues.json', 'w') as f:
+with open(f"{ROOT}/issues.json", "w") as f:
     json.dump(issues, f)
 
 print(f"Wrote {len(issues)} issues to {ROOT}/issues.json")
diff --git a/tools/security-tracker-stats-dashboard/fetch_prs.py 
b/tools/security-tracker-stats-dashboard/fetch_prs.py
index 4b545397..a371cb87 100644
--- a/tools/security-tracker-stats-dashboard/fetch_prs.py
+++ b/tools/security-tracker-stats-dashboard/fetch_prs.py
@@ -30,31 +30,31 @@ import re
 import subprocess
 from concurrent.futures import ThreadPoolExecutor, as_completed
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-UPSTREAM = os.environ.get('TRACKER_STATS_UPSTREAM_REPO', 'apache/airflow')
-if UPSTREAM in ('', 'none', 'null'):
-    print('TRACKER_STATS_UPSTREAM_REPO is empty/none - skipping PR fetch.')
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+UPSTREAM = os.environ.get("TRACKER_STATS_UPSTREAM_REPO", "apache/airflow")
+if UPSTREAM in ("", "none", "null"):
+    print("TRACKER_STATS_UPSTREAM_REPO is empty/none - skipping PR fetch.")
     raise SystemExit(0)
 
-EXTRA = f'{ROOT}/issue_extra.json'
-OUT = f'{ROOT}/prs.json'
+EXTRA = f"{ROOT}/issue_extra.json"
+OUT = f"{ROOT}/prs.json"
 
 with open(EXTRA) as f:
     extra = json.load(f)
 
 PR_PAT = re.compile(
-    
rf'{re.escape(UPSTREAM)}#(\d+)|https://github\.com/{re.escape(UPSTREAM)}/pull/(\d+)',
+    
rf"{re.escape(UPSTREAM)}#(\d+)|https://github\.com/{re.escape(UPSTREAM)}/pull/(\d+)",
     re.I,
 )
 
 
 def extract_prs(v):
     nums = set()
-    cb = v.get('closedByPullRequestsReferences') or []
+    cb = v.get("closedByPullRequestsReferences") or []
     for ref in cb:
-        if ref.get('repository', {}).get('nameWithOwner') == UPSTREAM:
-            nums.add(ref['number'])
-    body = v.get('body') or ''
+        if ref.get("repository", {}).get("nameWithOwner") == UPSTREAM:
+            nums.add(ref["number"])
+    body = v.get("body") or ""
     # Only parse the "PR with the fix" field portion if we can find it,
     # but also accept apache/airflow PR mentions anywhere in the body
     # (the spec allows either).
@@ -74,7 +74,7 @@ for issue_n, v in extra.items():
     all_prs.update(prs)
 
 # Save the issue_to_prs linkage map alongside
-with open(f'{ROOT}/issue_to_prs.json', 'w') as f:
+with open(f"{ROOT}/issue_to_prs.json", "w") as f:
     json.dump(issue_to_prs, f)
 print(f"unique {UPSTREAM} PRs to fetch: {len(all_prs)}")
 
@@ -92,15 +92,16 @@ print(f"to fetch: {len(todo)}")
 def fetch(n):
     try:
         r = subprocess.run(
-            ['gh', 'pr', 'view', str(n), '--repo', UPSTREAM,
-             '--json', 'number,createdAt,mergedAt,state'],
-            capture_output=True, text=True, timeout=60,
+            ["gh", "pr", "view", str(n), "--repo", UPSTREAM, "--json", 
"number,createdAt,mergedAt,state"],
+            capture_output=True,
+            text=True,
+            timeout=60,
         )
         if r.returncode != 0:
-            return n, {'error': r.stderr.strip()}
+            return n, {"error": r.stderr.strip()}
         return n, json.loads(r.stdout)
     except Exception as e:
-        return n, {'error': str(e)}
+        return n, {"error": str(e)}
 
 
 done = 0
@@ -111,11 +112,11 @@ with ThreadPoolExecutor(max_workers=12) as ex:
         cache[str(n)] = data
         done += 1
         if done % 25 == 0:
-            with open(OUT, 'w') as f:
+            with open(OUT, "w") as f:
                 json.dump(cache, f)
             print(f"  {done}/{len(todo)}")
 
-with open(OUT, 'w') as f:
+with open(OUT, "w") as f:
     json.dump(cache, f)
-errs = sum(1 for v in cache.values() if 'error' in v)
+errs = sum(1 for v in cache.values() if "error" in v)
 print(f"done: cached {len(cache)} PRs ({errs} errors) → {OUT}")
diff --git a/tools/security-tracker-stats-dashboard/fetch_roster.py 
b/tools/security-tracker-stats-dashboard/fetch_roster.py
index 9d3aef25..7375cc60 100644
--- a/tools/security-tracker-stats-dashboard/fetch_roster.py
+++ b/tools/security-tracker-stats-dashboard/fetch_roster.py
@@ -22,21 +22,23 @@
 import os
 import subprocess
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-REPO = os.environ.get('TRACKER_STATS_REPO', 'airflow-s/airflow-s')
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+REPO = os.environ.get("TRACKER_STATS_REPO", "airflow-s/airflow-s")
 
 os.makedirs(ROOT, exist_ok=True)
 
 r = subprocess.run(
-    ['gh', 'api', f'repos/{REPO}/collaborators', '--jq', '.[].login', 
'--paginate'],
-    capture_output=True, text=True, timeout=60,
+    ["gh", "api", f"repos/{REPO}/collaborators", "--jq", ".[].login", 
"--paginate"],
+    capture_output=True,
+    text=True,
+    timeout=60,
 )
 if r.returncode != 0:
     raise SystemExit(f"gh failed: {r.stderr}")
 
 logins = [ln.strip() for ln in r.stdout.splitlines() if ln.strip()]
-with open(f'{ROOT}/roster.txt', 'w') as f:
+with open(f"{ROOT}/roster.txt", "w") as f:
     for ln in sorted(set(logins)):
-        f.write(ln + '\n')
+        f.write(ln + "\n")
 
 print(f"Wrote {len(set(logins))} roster handles to {ROOT}/roster.txt")
diff --git a/tools/security-tracker-stats-dashboard/pyproject.toml 
b/tools/security-tracker-stats-dashboard/pyproject.toml
new file mode 100644
index 00000000..4ba1154f
--- /dev/null
+++ b/tools/security-tracker-stats-dashboard/pyproject.toml
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "security-tracker-stats-dashboard"
+version = "0.1.0"
+description = "Rendering helpers for the security-tracker statistics 
dashboard."
+readme = "README.md"
+requires-python = ">=3.9"
+license = { text = "Apache-2.0" }
+dependencies = []
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/security_tracker_stats_dashboard"]
+
+[tool.ruff]
+line-length = 110
+target-version = "py39"
+src = ["src", "tests"]
+
+[tool.ruff.lint]
+select = ["E", "W", "F", "I", "B", "UP", "SIM", "C4", "RUF"]
+ignore = ["E501"]
+
+[tool.mypy]
+python_version = "3.10"
+files = ["src", "tests"]
+check_untyped_defs = true
+no_implicit_optional = true
+
+[tool.pytest.ini_options]
+minversion = "8.0"
+addopts = "-ra -q"
+testpaths = ["tests"]
+
+[dependency-groups]
+dev = [
+    "pytest>=8.0",
+]
diff --git a/tools/security-tracker-stats-dashboard/render.py 
b/tools/security-tracker-stats-dashboard/render.py
index 3112c03e..e3961053 100644
--- a/tools/security-tracker-stats-dashboard/render.py
+++ b/tools/security-tracker-stats-dashboard/render.py
@@ -46,18 +46,46 @@ null, those three charts are omitted and the snapshot 
back-fill rule is
 disabled.
 """
 
-import calendar
+import datetime as dt
 import json
 import os
 import re
 import statistics
-import datetime as dt
+import sys
 from collections import defaultdict
 
+# Make the src/ package importable when render.py is run as a standalone 
script.
+_HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_HERE, "src"))
+
+from security_tracker_stats_dashboard.core import (  # noqa: E402
+    _minimal_yaml_load,
+    build_triage_regex,
+    deep_merge,
+    eval_predicate,
+    is_bot_body,
+    iter_months,
+    iter_quarters,
+    iter_weeks,
+    js_array,
+    js_quotes,
+    mean_or_none,
+    milestone_x,
+    month_end,
+    month_label,
+    month_of,
+    parse_dt,
+    quarter_end,
+    quarter_label,
+    quarter_of,
+    week_end,
+    week_label,
+    week_of,
+)
+
 # --- YAML loader ----------------------------------------------------
 # Prefer pyyaml when available (handles every edge case). When it's not
-# installed, fall back to a tiny subset parser that covers the schema in
-# default-config.yaml only.
+# installed, fall back to the pure-Python subset parser from core.
 try:
     import yaml  # type: ignore
 
@@ -65,311 +93,55 @@ try:
         return yaml.safe_load(text)
 
 except ImportError:
+
     def yaml_load(text):
         return _minimal_yaml_load(text)
 
 
-def _minimal_yaml_load(text):
-    """Tiny YAML subset parser sufficient for default-config.yaml.
-
-    Supports: nested block mappings, block sequences (`- ...`), inline
-    flow lists `[a, b, "c d"]`, string scalars (with optional quotes),
-    integers, floats, booleans, null. Comments start at `#` outside of
-    quoted strings. No anchors, no merge keys, no flow mappings.
-    """
-    lines = []
-    for raw in text.splitlines():
-        # Strip comments outside of quotes.
-        in_q = None
-        out = []
-        i = 0
-        while i < len(raw):
-            ch = raw[i]
-            if in_q:
-                out.append(ch)
-                if ch == '\\' and i + 1 < len(raw):
-                    out.append(raw[i + 1])
-                    i += 2
-                    continue
-                if ch == in_q:
-                    in_q = None
-                i += 1
-                continue
-            if ch in ('"', "'"):
-                in_q = ch
-                out.append(ch)
-                i += 1
-                continue
-            if ch == '#':
-                break
-            out.append(ch)
-            i += 1
-        line = ''.join(out).rstrip()
-        if line.strip():
-            lines.append(line)
-
-    # Parse using indentation stack.
-    def indent_of(s):
-        return len(s) - len(s.lstrip(' '))
-
-    def scalar(s):
-        s = s.strip()
-        if not s:
-            return None
-        if s.lower() in ('null', '~'):
-            return None
-        if s.lower() == 'true':
-            return True
-        if s.lower() == 'false':
-            return False
-        if s.startswith('"') and s.endswith('"') and len(s) >= 2:
-            return s[1:-1].encode().decode('unicode_escape')
-        if s.startswith("'") and s.endswith("'") and len(s) >= 2:
-            return s[1:-1]
-        if s.startswith('[') and s.endswith(']'):
-            inner = s[1:-1].strip()
-            if not inner:
-                return []
-            return [scalar(x) for x in _split_flow_list(inner)]
-        try:
-            if '.' in s or 'e' in s or 'E' in s:
-                return float(s)
-            return int(s)
-        except ValueError:
-            return s
-
-    def _split_flow_list(inner):
-        parts = []
-        cur = []
-        in_q = None
-        depth = 0
-        for ch in inner:
-            if in_q:
-                cur.append(ch)
-                if ch == in_q:
-                    in_q = None
-                continue
-            if ch in ('"', "'"):
-                in_q = ch
-                cur.append(ch)
-                continue
-            if ch == '[':
-                depth += 1
-                cur.append(ch)
-                continue
-            if ch == ']':
-                depth -= 1
-                cur.append(ch)
-                continue
-            if ch == ',' and depth == 0:
-                parts.append(''.join(cur).strip())
-                cur = []
-                continue
-            cur.append(ch)
-        if cur:
-            parts.append(''.join(cur).strip())
-        return parts
-
-    def parse_block(idx, base_indent):
-        # Returns (value, next_idx). Inspects the first non-empty line
-        # at >= base_indent to decide mapping vs. sequence.
-        if idx >= len(lines):
-            return None, idx
-        first = lines[idx]
-        ind = indent_of(first)
-        if ind < base_indent:
-            return None, idx
-        if first.lstrip().startswith('- '):
-            return parse_seq(idx, ind)
-        return parse_map(idx, ind)
-
-    def parse_map(idx, base_indent):
-        out = {}
-        while idx < len(lines):
-            line = lines[idx]
-            ind = indent_of(line)
-            if ind < base_indent:
-                break
-            if ind > base_indent:
-                # Shouldn't happen at top of map.
-                break
-            stripped = line.strip()
-            if stripped.startswith('- '):
-                break
-            # key: value or key:
-            if ':' not in stripped:
-                idx += 1
-                continue
-            # Split on the first ':' that isn't inside quotes.
-            key, _, rest = _split_key_value(stripped)
-            rest = rest.strip()
-            idx += 1
-            if rest == '' or rest is None:
-                # Block child.
-                if idx < len(lines) and indent_of(lines[idx]) > base_indent:
-                    child, idx = parse_block(idx, indent_of(lines[idx]))
-                    out[key] = child
-                else:
-                    out[key] = None
-            else:
-                out[key] = scalar(rest)
-        return out, idx
-
-    def _split_key_value(stripped):
-        in_q = None
-        for i, ch in enumerate(stripped):
-            if in_q:
-                if ch == in_q:
-                    in_q = None
-                continue
-            if ch in ('"', "'"):
-                in_q = ch
-                continue
-            if ch == ':':
-                key = stripped[:i].strip()
-                rest = stripped[i + 1 :]
-                # Unquote key.
-                if (key.startswith('"') and key.endswith('"')) or (
-                    key.startswith("'") and key.endswith("'")
-                ):
-                    key = key[1:-1]
-                return key, ':', rest
-        return stripped, None, ''
-
-    def parse_seq(idx, base_indent):
-        out = []
-        while idx < len(lines):
-            line = lines[idx]
-            ind = indent_of(line)
-            if ind < base_indent:
-                break
-            if ind > base_indent:
-                break
-            stripped = line.strip()
-            if not stripped.startswith('- '):
-                break
-            after_dash = stripped[2:].rstrip()
-            # Item indent = base_indent + 2 (for "- ")
-            item_inner_indent = base_indent + 2
-            idx += 1
-            if after_dash == '':
-                # Block item, child lines.
-                if idx < len(lines) and indent_of(lines[idx]) > base_indent:
-                    child, idx = parse_block(idx, indent_of(lines[idx]))
-                    out.append(child)
-                else:
-                    out.append(None)
-                continue
-            if ':' in after_dash and not (
-                after_dash.startswith('"') or after_dash.startswith("'")
-            ):
-                # Inline first key-value of a mapping item. Treat the "- "
-                # as introducing a mapping whose first key is on this line.
-                key, _, rest = _split_key_value(after_dash)
-                rest = rest.strip()
-                item = {}
-                if rest == '':
-                    if idx < len(lines) and indent_of(lines[idx]) > 
item_inner_indent:
-                        child, idx = parse_block(idx, indent_of(lines[idx]))
-                        item[key] = child
-                    else:
-                        item[key] = None
-                else:
-                    item[key] = scalar(rest)
-                # Continue absorbing further keys at item_inner_indent.
-                while idx < len(lines):
-                    nline = lines[idx]
-                    nind = indent_of(nline)
-                    if nind < item_inner_indent:
-                        break
-                    if nind > item_inner_indent:
-                        break
-                    nstripped = nline.strip()
-                    if nstripped.startswith('- '):
-                        break
-                    if ':' not in nstripped:
-                        idx += 1
-                        continue
-                    nkey, _, nrest = _split_key_value(nstripped)
-                    nrest = nrest.strip()
-                    idx += 1
-                    if nrest == '':
-                        if idx < len(lines) and indent_of(lines[idx]) > 
item_inner_indent:
-                            child, idx = parse_block(idx, 
indent_of(lines[idx]))
-                            item[nkey] = child
-                        else:
-                            item[nkey] = None
-                    else:
-                        item[nkey] = scalar(nrest)
-                out.append(item)
-            else:
-                out.append(scalar(after_dash))
-        return out, idx
-
-    val, _ = parse_block(0, 0)
-    return val
-
-
 # --- Config loading -------------------------------------------------
 
-ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
-OUT_PATH = os.environ.get('TRACKER_STATS_OUT', '/tmp/airflow_s_monthly.html')
+ROOT = os.environ.get("TRACKER_STATS_CACHE", "/tmp/tracker-stats-cache")
+OUT_PATH = os.environ.get("TRACKER_STATS_OUT", "/tmp/airflow_s_monthly.html")
 HERE = os.path.dirname(os.path.abspath(__file__))
-DEFAULT_CONFIG_PATH = os.path.join(HERE, 'default-config.yaml')
-
-
-def deep_merge(base, overlay):
-    """Deep-merge overlay into base. Lists are REPLACED (not concatenated)."""
-    if overlay is None:
-        return base
-    if not isinstance(base, dict) or not isinstance(overlay, dict):
-        return overlay
-    out = dict(base)
-    for k, v in overlay.items():
-        if k in out and isinstance(out[k], dict) and isinstance(v, dict):
-            out[k] = deep_merge(out[k], v)
-        else:
-            out[k] = v
-    return out
+DEFAULT_CONFIG_PATH = os.path.join(HERE, "default-config.yaml")
 
 
 def load_config():
     with open(DEFAULT_CONFIG_PATH) as f:
         cfg = yaml_load(f.read()) or {}
-    overlay_path = os.environ.get('TRACKER_STATS_CONFIG')
+    overlay_path = os.environ.get("TRACKER_STATS_CONFIG")
     if overlay_path and os.path.exists(overlay_path):
         with open(overlay_path) as f:
             overlay = yaml_load(f.read()) or {}
         cfg = deep_merge(cfg, overlay)
     # Env-var quick overrides.
-    if os.environ.get('TRACKER_STATS_BUCKETS'):
-        cfg['buckets'] = os.environ['TRACKER_STATS_BUCKETS']
-    if 'TRACKER_STATS_START' in os.environ:
-        v = os.environ['TRACKER_STATS_START']
-        cfg['start'] = v if v else None
-    if 'TRACKER_STATS_UPSTREAM_REPO' in os.environ:
-        v = os.environ['TRACKER_STATS_UPSTREAM_REPO']
-        cfg['upstream_repo'] = None if v in ('', 'none', 'null') else v
+    if os.environ.get("TRACKER_STATS_BUCKETS"):
+        cfg["buckets"] = os.environ["TRACKER_STATS_BUCKETS"]
+    if "TRACKER_STATS_START" in os.environ:
+        v = os.environ["TRACKER_STATS_START"]
+        cfg["start"] = v if v else None
+    if "TRACKER_STATS_UPSTREAM_REPO" in os.environ:
+        v = os.environ["TRACKER_STATS_UPSTREAM_REPO"]
+        cfg["upstream_repo"] = None if v in ("", "none", "null") else v
     return cfg
 
 
 CONFIG = load_config()
 
-BUCKETS_MODE = CONFIG.get('buckets', 'monthly')
-if BUCKETS_MODE not in ('monthly', 'quarterly', 'weekly'):
-    raise SystemExit(
-        f"buckets must be 'monthly', 'quarterly' or 'weekly', got 
{BUCKETS_MODE!r}")
-
-START_OVERRIDE = CONFIG.get('start')
-UPSTREAM_REPO = CONFIG.get('upstream_repo')
-SCOPE_LABELS = set(CONFIG.get('scope_labels') or [])
-MILESTONES = CONFIG.get('milestones') or []
-CATEGORIES_CFG = CONFIG.get('categories') or []
-TRIAGE_KW = CONFIG.get('triage', {}).get('keywords') or []
-BOT_PREFIXES = tuple(CONFIG.get('triage', {}).get('bot_prefixes') or [])
+BUCKETS_MODE = CONFIG.get("buckets", "monthly")
+if BUCKETS_MODE not in ("monthly", "quarterly", "weekly"):
+    raise SystemExit(f"buckets must be 'monthly', 'quarterly' or 'weekly', got 
{BUCKETS_MODE!r}")
+
+START_OVERRIDE = CONFIG.get("start")
+UPSTREAM_REPO = CONFIG.get("upstream_repo")
+SCOPE_LABELS = set(CONFIG.get("scope_labels") or [])
+MILESTONES = CONFIG.get("milestones") or []
+CATEGORIES_CFG = CONFIG.get("categories") or []
+TRIAGE_KW = CONFIG.get("triage", {}).get("keywords") or []
+BOT_PREFIXES = tuple(CONFIG.get("triage", {}).get("bot_prefixes") or [])
 # Label identifying the "rejected without tracker" ledger issue. When
 # null (or no such issue exists) the rejection stat is omitted / shows 0.
-REJECTIONS_LEDGER_LABEL = CONFIG.get('rejections_ledger_label')
+REJECTIONS_LEDGER_LABEL = CONFIG.get("rejections_ledger_label")
 
 # Distinct category names in the order they FIRST appear in CATEGORIES_CFG
 # (multiple rules can share a name to express disjoint branches of the
@@ -377,30 +149,30 @@ REJECTIONS_LEDGER_LABEL = 
CONFIG.get('rejections_ledger_label')
 _seen = set()
 CATS_DEFAULT_ORDER = []
 for c in CATEGORIES_CFG:
-    if c['name'] not in _seen:
-        _seen.add(c['name'])
-        CATS_DEFAULT_ORDER.append(c['name'])
-STACK_ORDER = CONFIG.get('stack_order') or CATS_DEFAULT_ORDER
+    if c["name"] not in _seen:
+        _seen.add(c["name"])
+        CATS_DEFAULT_ORDER.append(c["name"])
+STACK_ORDER = CONFIG.get("stack_order") or CATS_DEFAULT_ORDER
 # CATS used for snapshot counting is the distinct-name set. Plotting uses
 # STACK_ORDER (which may re-order them for visual layering).
 CATS = list(CATS_DEFAULT_ORDER)
 CAT_COLORS = {}
 for c in CATEGORIES_CFG:
-    CAT_COLORS.setdefault(c['name'], c.get('color', '#888888'))
+    CAT_COLORS.setdefault(c["name"], c.get("color", "#888888"))
 
 
 # --- Cache load -----------------------------------------------------
 
-with open(f'{ROOT}/issues.json') as f:
+with open(f"{ROOT}/issues.json") as f:
     all_issues = json.load(f)
-with open(f'{ROOT}/roster.txt') as f:
+with open(f"{ROOT}/roster.txt") as f:
     roster = {ln.strip() for ln in f if ln.strip()}
-with open(f'{ROOT}/issue_extra.json') as f:
+with open(f"{ROOT}/issue_extra.json") as f:
     issue_extra = json.load(f)
 
 
 def _issue_label_names(issue):
-    return {(l.get('name') if isinstance(l, dict) else l) for l in 
(issue.get('labels') or [])}
+    return {(lbl.get("name") if isinstance(lbl, dict) else lbl) for lbl in 
(issue.get("labels") or [])}
 
 
 # Partition out the "rejected without tracker" ledger issue(s). The ledger
@@ -410,17 +182,15 @@ def _issue_label_names(issue):
 # (`all_issues`) only for the rejection-comment parse; `issues` is the
 # tracker-only list every downstream loop iterates.
 if REJECTIONS_LEDGER_LABEL:
-    ledger_issues = [i for i in all_issues
-                     if REJECTIONS_LEDGER_LABEL in _issue_label_names(i)]
-    issues = [i for i in all_issues
-              if REJECTIONS_LEDGER_LABEL not in _issue_label_names(i)]
+    ledger_issues = [i for i in all_issues if REJECTIONS_LEDGER_LABEL in 
_issue_label_names(i)]
+    issues = [i for i in all_issues if REJECTIONS_LEDGER_LABEL not in 
_issue_label_names(i)]
 else:
     ledger_issues = []
     issues = list(all_issues)
 
 prs_cache = {}
 if UPSTREAM_REPO:
-    prs_path = f'{ROOT}/prs.json'
+    prs_path = f"{ROOT}/prs.json"
     if os.path.exists(prs_path):
         with open(prs_path) as f:
             prs_cache = json.load(f)
@@ -432,108 +202,17 @@ if UPSTREAM_REPO:
     # of `apache/airflow` still matches the historical pre-existing
     # `apache/airflow#NNN` references byte-for-byte.
     repo_re = re.escape(UPSTREAM_REPO)
-    PR_PAT = re.compile(
-        rf'{repo_re}#(\d+)|https://github\.com/{repo_re}/pull/(\d+)', re.I
-    )
+    PR_PAT = 
re.compile(rf"{repo_re}#(\d+)|https://github\.com/{repo_re}/pull/(\d+)", re.I)
 else:
     PR_PAT = None
 
 
-# --- helpers --------------------------------------------------------
-
-def parse_dt(s):
-    if not s:
-        return None
-    return dt.datetime.fromisoformat(s.replace('Z', '+00:00'))
-
-
-# --- Bucket abstraction --------------------------------------------
-
-def month_of(d):
-    return d.year, d.month
-
-
-def quarter_of(d):
-    return d.year, (d.month - 1) // 3 + 1
-
-
-def month_label(y, m):
-    return f"{y}-{m:02d}"
-
-
-def quarter_label(y, q):
-    return f"{y}-Q{q}"
-
-
-def month_end(y, m):
-    last_day = calendar.monthrange(y, m)[1]
-    return dt.datetime(y, m, last_day, 23, 59, 59, tzinfo=dt.timezone.utc)
-
-
-def quarter_end(y, q):
-    # q in {1,2,3,4}
-    last_month = q * 3
-    last_day = calendar.monthrange(y, last_month)[1]
-    return dt.datetime(y, last_month, last_day, 23, 59, 59, 
tzinfo=dt.timezone.utc)
-
-
-def iter_months(y0, m0, y1, m1):
-    y, m = y0, m0
-    while (y, m) <= (y1, m1):
-        yield y, m
-        m += 1
-        if m == 13:
-            m = 1
-            y += 1
-
-
-def iter_quarters(y0, q0, y1, q1):
-    y, q = y0, q0
-    while (y, q) <= (y1, q1):
-        yield y, q
-        q += 1
-        if q == 5:
-            q = 1
-            y += 1
-
-
-# Weekly buckets key on ISO (year, week) — note the ISO year can differ from
-# the calendar year in late December / early January, but (iso_year, iso_week)
-# tuples remain chronologically ordered, so the existing <=-based iteration
-# and bucketing carry over unchanged.
-def week_of(d):
-    iso = d.isocalendar()
-    return iso[0], iso[1]
-
-
-def week_label(y, w):
-    return f"{y}-W{w:02d}"
-
-
-def week_end(y, w):
-    # ISO weeks run Monday (day 1) .. Sunday (day 7); end at Sunday 23:59:59.
-    sunday = dt.date.fromisocalendar(y, w, 7)
-    return dt.datetime(sunday.year, sunday.month, sunday.day,
-                       23, 59, 59, tzinfo=dt.timezone.utc)
-
-
-def iter_weeks(y0, w0, y1, w1):
-    # Step by 7 days from the Monday of the start week through the end week;
-    # ISO years have 52 or 53 weeks, so stepping by date avoids that edge.
-    cur = dt.date.fromisocalendar(y0, w0, 1)
-    last = dt.date.fromisocalendar(y1, w1, 1)
-    while cur <= last:
-        iso = cur.isocalendar()
-        yield iso[0], iso[1]
-        cur += dt.timedelta(days=7)
-
-
-if BUCKETS_MODE == 'monthly':
+if BUCKETS_MODE == "monthly":
     bucket_of = month_of
     bucket_label = month_label
     bucket_end = month_end
     bucket_iter = iter_months
-elif BUCKETS_MODE == 'weekly':
+elif BUCKETS_MODE == "weekly":
     bucket_of = week_of
     bucket_label = week_label
     bucket_end = week_end
@@ -547,18 +226,18 @@ else:
 
 # --- index issues + buckets ----------------------------------------
 
-issues_by_n = {i['number']: i for i in issues}
-earliest = min(parse_dt(i['createdAt']) for i in issues)
+issues_by_n = {i["number"]: i for i in issues}
+earliest = min(parse_dt(i["createdAt"]) for i in issues)
 
 if START_OVERRIDE:
-    if BUCKETS_MODE == 'monthly':
-        y0, m0 = (int(x) for x in START_OVERRIDE.split('-'))
+    if BUCKETS_MODE == "monthly":
+        y0, m0 = (int(x) for x in START_OVERRIDE.split("-"))
         start_key = (y0, m0)
-    elif BUCKETS_MODE == 'weekly':
-        y_part, w_part = START_OVERRIDE.split('-W')
+    elif BUCKETS_MODE == "weekly":
+        y_part, w_part = START_OVERRIDE.split("-W")
         start_key = (int(y_part), int(w_part))
     else:
-        y_part, q_part = START_OVERRIDE.split('-Q')
+        y_part, q_part = START_OVERRIDE.split("-Q")
         start_key = (int(y_part), int(q_part))
 else:
     start_key = bucket_of(earliest)
@@ -597,19 +276,18 @@ print(f"buckets in range ({BUCKETS_MODE}): {n_buckets}")
 # than smearing it across buckets — that keeps the per-bucket series
 # faithful (only real dated rejections appear in it) while still
 # surfacing the historical lump in the summary / HTML header.
-REJECTION_MARKER = '<!-- rejection v1 -->'
-REJECTION_BACKFILL_RE = re.compile(
-    r'<!--\s*rejection-backfill\s+v1\s+count:\s*(\d+)\s*-->', re.I)
-REJECTION_DATE_RE = re.compile(r'^\s*date:\s*(\d{4})-(\d{2})-(\d{2})\s*$', 
re.M)
+REJECTION_MARKER = "<!-- rejection v1 -->"
+REJECTION_BACKFILL_RE = 
re.compile(r"<!--\s*rejection-backfill\s+v1\s+count:\s*(\d+)\s*-->", re.I)
+REJECTION_DATE_RE = re.compile(r"^\s*date:\s*(\d{4})-(\d{2})-(\d{2})\s*$", 
re.M)
 
-rejections_by_b = defaultdict(int)   # bucket label -> dated rejection count
-rejections_dated = []                # every dated rejection datetime (in + 
out of range)
+rejections_by_b = defaultdict(int)  # bucket label -> dated rejection count
+rejections_dated = []  # every dated rejection datetime (in + out of range)
 rejections_dated_total = 0
 rejections_backfill_total = 0
 
 for li in ledger_issues:
-    for c in (li.get('comments') or []):
-        body = c.get('body') or ''
+    for c in li.get("comments") or []:
+        body = c.get("body") or ""
         # Historical backfill marker (undated lump).
         for m in REJECTION_BACKFILL_RE.finditer(body):
             try:
@@ -630,8 +308,7 @@ for li in ledger_issues:
                 # No / malformed date line — skip this entry defensively.
                 continue
             try:
-                d = dt.datetime(int(dm.group(1)), int(dm.group(2)),
-                                int(dm.group(3)), tzinfo=dt.timezone.utc)
+                d = dt.datetime(int(dm.group(1)), int(dm.group(2)), 
int(dm.group(3)), tzinfo=dt.timezone.utc)
             except ValueError:
                 continue
             rejections_dated.append(d)
@@ -650,7 +327,7 @@ rejections_total = rejections_dated_total + 
rejections_backfill_total
 # Per-issue events
 events_by_n = {}
 for n in issues_by_n:
-    p = f'{ROOT}/events/{n}.json'
+    p = f"{ROOT}/events/{n}.json"
     if os.path.exists(p) and os.path.getsize(p) > 0:
         with open(p) as f:
             events_by_n[n] = json.load(f)
@@ -660,15 +337,16 @@ for n in issues_by_n:
 
 # --- tracker -> linked PR list (from body parse + closedBy) --------
 
+
 def extract_prs_for_issue(n):
     if not UPSTREAM_REPO:
         return set()
     v = issue_extra.get(str(n)) or {}
     nums = set()
-    for ref in (v.get('closedByPullRequestsReferences') or []):
-        if ref.get('repository', {}).get('nameWithOwner') == UPSTREAM_REPO:
-            nums.add(ref['number'])
-    body = v.get('body') or ''
+    for ref in v.get("closedByPullRequestsReferences") or []:
+        if ref.get("repository", {}).get("nameWithOwner") == UPSTREAM_REPO:
+            nums.add(ref["number"])
+    body = v.get("body") or ""
     if PR_PAT is not None:
         for m in PR_PAT.findall(body):
             x = m[0] or m[1]
@@ -683,12 +361,12 @@ issue_prs = {n: extract_prs_for_issue(n) for n in 
issues_by_n}
 def pr_meta(num):
     """Return dict(createdAt=dt, mergedAt=dt|None, state=str) or None."""
     v = prs_cache.get(str(num))
-    if not v or 'error' in v:
+    if not v or "error" in v:
         return None
     return {
-        'createdAt': parse_dt(v.get('createdAt')),
-        'mergedAt': parse_dt(v.get('mergedAt')),
-        'state': v.get('state'),
+        "createdAt": parse_dt(v.get("createdAt")),
+        "mergedAt": parse_dt(v.get("mergedAt")),
+        "state": v.get("state"),
     }
 
 
@@ -701,21 +379,19 @@ def tracker_pr_signals(n):
         meta = pr_meta(prn)
         if meta is None:
             continue
-        c = meta['createdAt']
-        if c is not None:
-            if earliest_created is None or c < earliest_created:
-                earliest_created = c
-                earliest_created_pr = prn
-        mt = meta['mergedAt']
-        if mt is not None:
-            if earliest_merged_ts is None or mt < earliest_merged_ts:
-                earliest_merged_ts = mt
-                earliest_merged_pr = prn
+        c = meta["createdAt"]
+        if c is not None and (earliest_created is None or c < 
earliest_created):
+            earliest_created = c
+            earliest_created_pr = prn
+        mt = meta["mergedAt"]
+        if mt is not None and (earliest_merged_ts is None or mt < 
earliest_merged_ts):
+            earliest_merged_ts = mt
+            earliest_merged_pr = prn
     return {
-        'first_pr_created': earliest_created,
-        'first_pr_created_num': earliest_created_pr,
-        'first_pr_merged': earliest_merged_ts,
-        'first_pr_merged_num': earliest_merged_pr,
+        "first_pr_created": earliest_created,
+        "first_pr_created_num": earliest_created_pr,
+        "first_pr_merged": earliest_merged_ts,
+        "first_pr_merged_num": earliest_merged_pr,
     }
 
 
@@ -724,116 +400,52 @@ tracker_signals = {n: tracker_pr_signals(n) for n in 
issues_by_n}
 
 # --- label timeline replay ------------------------------------------
 
+
 def labels_open_at(issue, ts):
-    n = issue['number']
-    created = parse_dt(issue['createdAt'])
+    n = issue["number"]
+    created = parse_dt(issue["createdAt"])
     if ts < created:
         return None, None
     labels = set()
     is_open = True
     for e in events_by_n.get(n, []):
-        et = parse_dt(e['created_at'])
+        et = parse_dt(e["created_at"])
         if et > ts:
             break
-        if e['event'] == 'labeled' and e.get('label'):
-            labels.add(e['label'])
-        elif e['event'] == 'unlabeled' and e.get('label'):
-            labels.discard(e['label'])
-        elif e['event'] == 'closed':
+        if e["event"] == "labeled" and e.get("label"):
+            labels.add(e["label"])
+        elif e["event"] == "unlabeled" and e.get("label"):
+            labels.discard(e["label"])
+        elif e["event"] == "closed":
             is_open = False
-        elif e['event'] == 'reopened':
+        elif e["event"] == "reopened":
             is_open = True
     # The issue-events API can omit the 'closed' event for some (often
     # older) issues, which would leave is_open stuck True and miscount a
     # closed issue as open. Trust the authoritative closedAt from the
     # issue payload as a backstop (the cum_closed series already does).
-    closed_at = parse_dt(issue.get('closedAt'))
-    if issue.get('state') == 'CLOSED' and closed_at and closed_at <= ts:
+    closed_at = parse_dt(issue.get("closedAt"))
+    if issue.get("state") == "CLOSED" and closed_at and closed_at <= ts:
         is_open = False
     return labels, is_open
 
 
-# --- Predicate evaluator -------------------------------------------
-
-def eval_predicate(pred, ctx):
-    """Evaluate a category predicate against a snapshot context.
-
-    `ctx` keys:
-        labels (set), is_open (bool), state_reason (str|None),
-        pr_merged_by_snapshot (bool).
-    """
-    if not isinstance(pred, dict):
-        return False
-    for key, val in pred.items():
-        if key == 'any_of':
-            if not any(eval_predicate(p, ctx) for p in val):
-                return False
-        elif key == 'all_of':
-            if isinstance(val, list):
-                if not all(eval_predicate(p, ctx) for p in val):
-                    return False
-            elif isinstance(val, dict):
-                if not eval_predicate(val, ctx):
-                    return False
-            else:
-                return False
-        elif key == 'state':
-            want_open = (val == 'open')
-            if ctx['is_open'] != want_open:
-                return False
-        elif key == 'state_reason':
-            if ctx['state_reason'] != val:
-                return False
-        elif key == 'any_label':
-            if not any(l in ctx['labels'] for l in val):
-                return False
-        elif key == 'all_labels':
-            if not all(l in ctx['labels'] for l in val):
-                return False
-        elif key == 'not_label':
-            if val in ctx['labels']:
-                return False
-        elif key == 'not_any_label':
-            if any(l in ctx['labels'] for l in val):
-                return False
-        elif key == 'no_scope_label':
-            has_scope = bool(ctx['labels'] & SCOPE_LABELS)
-            if val and has_scope:
-                return False
-            if not val and not has_scope:
-                return False
-        elif key == 'has_scope_label':
-            has_scope = bool(ctx['labels'] & SCOPE_LABELS)
-            if val and not has_scope:
-                return False
-            if not val and has_scope:
-                return False
-        elif key == 'pr_merged_by_snapshot':
-            if val and not ctx['pr_merged_by_snapshot']:
-                return False
-            if not val and ctx['pr_merged_by_snapshot']:
-                return False
-        else:
-            # Unknown key — fail safe.
-            return False
-    return True
-
-
 def classify_per_config(labels, is_open, ts, n):
     issue = issues_by_n[n]
-    state_reason = issue.get('stateReason')
+    state_reason = issue.get("stateReason")
     sig = tracker_signals.get(n, {})
-    fm = sig.get('first_pr_merged')
+    fm = sig.get("first_pr_merged")
     pr_merged_by_snapshot = bool(UPSTREAM_REPO and fm is not None and fm <= ts)
     ctx = {
-        'labels': labels,
-        'is_open': is_open,
-        'state_reason': state_reason,
-        'pr_merged_by_snapshot': pr_merged_by_snapshot,
+        "labels": labels,
+        "is_open": is_open,
+        "state_reason": state_reason,
+        "pr_merged_by_snapshot": pr_merged_by_snapshot,
+        "scope_labels": SCOPE_LABELS,
     }
     for cat in CATEGORIES_CFG:
-        if eval_predicate(cat['predicate'], ctx):
-            return cat['name']
+        if eval_predicate(cat["predicate"], ctx):
+            return cat["name"]
     return None
 
 
@@ -849,13 +461,13 @@ for bi, b in enumerate(buckets):
         labels, is_open = labels_open_at(i, ts)
         if labels is None:
             continue
-        cat = classify_per_config(labels, is_open, ts, i['number'])
+        cat = classify_per_config(labels, is_open, ts, i["number"])
         if cat is None:
             continue
         counts[cat][bi] += 1
 
-        if cat == 'open_pr_merged' and is_open and 'pr merged' not in labels:
-            backfill_trackers.add(i['number'])
+        if cat == "open_pr_merged" and is_open and "pr merged" not in labels:
+            backfill_trackers.add(i["number"])
 
 # --- cumulative opened / closed ------------------------------------
 
@@ -867,10 +479,10 @@ for bi, b in enumerate(buckets):
     op = 0
     cl = 0
     for i in issues:
-        ca = parse_dt(i['createdAt'])
+        ca = parse_dt(i["createdAt"])
         if ca and ca <= ts:
             op += 1
-        cz = parse_dt(i.get('closedAt'))
+        cz = parse_dt(i.get("closedAt"))
         if cz and cz <= ts:
             cl += 1
     cum_opened[bi] = op
@@ -888,17 +500,16 @@ cum_rejected = []
 for b in buckets:
     be = bucket_end(*b)
     ts = NOW if be > NOW else be
-    cum_rejected.append(
-        rejections_backfill_total + sum(1 for rd in rejections_dated if rd <= 
ts))
+    cum_rejected.append(rejections_backfill_total + sum(1 for rd in 
rejections_dated if rd <= ts))
 cum_reported = [o + r for o, r in zip(cum_opened, cum_rejected)]
 
 # --- Opened-in-bucket vs untriaged-at-bucket-end ------------------
 
 opened_in_b = [0] * n_buckets
-untriaged_at_bend = counts.get('open_untriaged', [0] * n_buckets)
+untriaged_at_bend = counts.get("open_untriaged", [0] * n_buckets)
 
 for i in issues:
-    ca = parse_dt(i['createdAt'])
+    ca = parse_dt(i["createdAt"])
     if ca is None:
         continue
     cb = bucket_of(ca)
@@ -914,28 +525,7 @@ reported_in_b = [o + r for o, r in zip(opened_in_b, 
rejected_series)]
 
 # --- triage / response ---------------------------------------------
 
-# Build the triage regex from config. Keep word-boundary wrapping for
-# the all-caps keywords so they don't match substrings inside other
-# words (mirrors the original handwritten regex).
-_kw_parts = []
-for kw in TRIAGE_KW:
-    if kw.isupper() and ' ' not in kw and '-' not in kw:
-        _kw_parts.append(rf'\b{re.escape(kw)}\b')
-    elif kw.isalpha() and kw.islower() and ' ' not in kw:
-        _kw_parts.append(rf'\b{re.escape(kw)}\b')
-    else:
-        _kw_parts.append(re.escape(kw))
-TRIAGE_RE = re.compile('|'.join(_kw_parts), re.IGNORECASE) if _kw_parts else 
None
-
-
-def is_bot_body(body):
-    if not body:
-        return False
-    b = body.lstrip()
-    for p in BOT_PREFIXES:
-        if b.startswith(p):
-            return True
-    return False
+TRIAGE_RE = build_triage_regex(TRIAGE_KW)
 
 
 triage_hours_by_b = defaultdict(list)
@@ -945,26 +535,22 @@ n_no_triage = 0
 all_triage_hours = []
 
 for i in issues:
-    created = parse_dt(i['createdAt'])
+    created = parse_dt(i["createdAt"])
     blbl = bucket_label(*bucket_of(created))
-    comments = i.get('comments', []) or []
+    comments = i.get("comments", []) or []
 
     first_roster = None
     first_roster_keyword = None
     for c in comments:
-        author = (c.get('author') or {}).get('login')
+        author = (c.get("author") or {}).get("login")
         if not author or author not in roster:
             continue
-        if is_bot_body(c.get('body') or ''):
+        if is_bot_body(c.get("body") or "", BOT_PREFIXES):
             continue
-        ct = parse_dt(c['createdAt'])
+        ct = parse_dt(c["createdAt"])
         if first_roster is None:
             first_roster = ct
-        if (
-            first_roster_keyword is None
-            and TRIAGE_RE is not None
-            and TRIAGE_RE.search(c.get('body') or '')
-        ):
+        if first_roster_keyword is None and TRIAGE_RE is not None and 
TRIAGE_RE.search(c.get("body") or ""):
             first_roster_keyword = ct
         if first_roster is not None and first_roster_keyword is not None:
             break
@@ -984,10 +570,6 @@ for i in issues:
     all_triage_hours.append(hours)
 
 
-def mean_or_none(xs):
-    return round(statistics.mean(xs), 2) if xs else None
-
-
 def per_b_series(by_b):
     ys = []
     ns = []
@@ -1016,41 +598,40 @@ rel_by_b = defaultdict(list)
 
 def first_label_time(n, label):
     for e in events_by_n.get(n, []):
-        if e['event'] == 'labeled' and e.get('label') == label:
-            return parse_dt(e['created_at'])
+        if e["event"] == "labeled" and e.get("label") == label:
+            return parse_dt(e["created_at"])
     return None
 
 
 if UPSTREAM_REPO:
     for i in issues:
-        n = i['number']
-        created = parse_dt(i['createdAt'])
+        n = i["number"]
+        created = parse_dt(i["createdAt"])
         sig = tracker_signals.get(n, {})
 
-        first_pr_c = sig.get('first_pr_created')
-        first_pr_m = sig.get('first_pr_merged')
+        first_pr_c = sig.get("first_pr_created")
+        first_pr_m = sig.get("first_pr_merged")
 
         if first_pr_c and created and first_pr_c >= created:
             days = (first_pr_c - created).total_seconds() / 86400
             prc_by_b[bucket_label(*bucket_of(created))].append(days)
 
         if first_pr_m is not None:
-            prn = sig.get('first_pr_merged_num')
+            prn = sig.get("first_pr_merged_num")
             meta = pr_meta(prn) if prn else None
-            if meta and meta['createdAt'] and meta['mergedAt'] and 
meta['mergedAt'] >= meta['createdAt']:
-                days = (meta['mergedAt'] - meta['createdAt']).total_seconds() 
/ 86400
-                
prm_by_b[bucket_label(*bucket_of(meta['createdAt']))].append(days)
+            if meta and meta["createdAt"] and meta["mergedAt"] and 
meta["mergedAt"] >= meta["createdAt"]:
+                days = (meta["mergedAt"] - meta["createdAt"]).total_seconds() 
/ 86400
+                
prm_by_b[bucket_label(*bucket_of(meta["createdAt"]))].append(days)
 
         if first_pr_m is not None:
-            announced = (first_label_time(n, 'announced - emails sent')
-                         or first_label_time(n, 'announced'))
+            announced = first_label_time(n, "announced - emails sent") or 
first_label_time(n, "announced")
             rel_ts = announced
             if rel_ts is None:
-                ca = parse_dt(i.get('closedAt'))
-                state_reason = i.get('stateReason')
-                cur_labels = {l['name'] for l in i.get('labels', [])}
-                is_closed_completed = (i.get('state') == 'CLOSED' and 
state_reason == 'COMPLETED')
-                has_cve = 'cve allocated' in cur_labels
+                ca = parse_dt(i.get("closedAt"))
+                state_reason = i.get("stateReason")
+                cur_labels = {lb["name"] for lb in i.get("labels", [])}
+                is_closed_completed = i.get("state") == "CLOSED" and 
state_reason == "COMPLETED"
+                has_cve = "cve allocated" in cur_labels
                 if ca and is_closed_completed and has_cve:
                     rel_ts = ca
             if rel_ts is not None and rel_ts > first_pr_m:
@@ -1079,9 +660,10 @@ def overall_n(by_b):
 # --- KPIs ----------------------------------------------------------
 
 total = len(issues)
-open_now = sum(1 for i in issues if i.get('state') == 'OPEN')
+open_now = sum(1 for i in issues if i.get("state") == "OPEN")
 closed_now = total - open_now
 
+
 def latest(cat):
     return counts[cat][-1] if cat in counts else 0
 
@@ -1089,68 +671,51 @@ def latest(cat):
 print(f"total trackers: {total}")
 print(f"open: {open_now}, closed: {closed_now}")
 print(f"fixed_released (latest bucket): {latest('fixed_released')}")
-print(f"open_untriaged: {latest('open_untriaged')}, open_triaged: 
{latest('open_triaged')}, "
-      f"open_pr_merged: {latest('open_pr_merged')}, closed_other: 
{latest('closed_other')}")
-print(f"triage median {triage_median}h, mean {triage_mean}h, n={triage_n} "
-      f"(fallback={n_fallback_triage}, none={n_no_triage})")
+print(
+    f"open_untriaged: {latest('open_untriaged')}, open_triaged: 
{latest('open_triaged')}, "
+    f"open_pr_merged: {latest('open_pr_merged')}, closed_other: 
{latest('closed_other')}"
+)
+print(
+    f"triage median {triage_median}h, mean {triage_mean}h, n={triage_n} "
+    f"(fallback={n_fallback_triage}, none={n_no_triage})"
+)
 if REJECTIONS_LEDGER_LABEL:
-    print(f"rejected without tracker: {rejections_dated_total} dated + "
-          f"{rejections_backfill_total} historical = {rejections_total} "
-          f"(ledger issues: {len(ledger_issues)})")
+    print(
+        f"rejected without tracker: {rejections_dated_total} dated + "
+        f"{rejections_backfill_total} historical = {rejections_total} "
+        f"(ledger issues: {len(ledger_issues)})"
+    )
 
 if UPSTREAM_REPO:
     print()
     print("PR-driven mean-time series:")
     for name, by_b in [
-        ('c_prc', prc_by_b),
-        ('c_prm', prm_by_b),
-        ('c_rel', rel_by_b),
+        ("c_prc", prc_by_b),
+        ("c_prm", prm_by_b),
+        ("c_rel", rel_by_b),
     ]:
-        print(f"  {name}: n={overall_n(by_b)} median={overall_median(by_b)} "
-              f"buckets_with_data={n_buckets_with_data(by_b)}")
+        print(
+            f"  {name}: n={overall_n(by_b)} median={overall_median(by_b)} "
+            f"buckets_with_data={n_buckets_with_data(by_b)}"
+        )
 
 print()
-print(f"open_pr_merged back-fill: {len(backfill_trackers)} trackers were 
re-classified "
-      f"from open_triaged -> open_pr_merged in at least one historical bucket "
-      f"because of the PR-merge-date rule")
+print(
+    f"open_pr_merged back-fill: {len(backfill_trackers)} trackers were 
re-classified "
+    f"from open_triaged -> open_pr_merged in at least one historical bucket "
+    f"because of the PR-merge-date rule"
+)
 print()
-print(f"Latest bucket ({bucket_labels[-1]}) opened-vs-untriaged: "
-      f"opened_in_b={opened_in_b[-1]}, 
untriaged_at_bend={untriaged_at_bend[-1]}")
+print(
+    f"Latest bucket ({bucket_labels[-1]}) opened-vs-untriaged: "
+    f"opened_in_b={opened_in_b[-1]}, untriaged_at_bend={untriaged_at_bend[-1]}"
+)
 
 
 # --- Render HTML ---------------------------------------------------
 
-def js_array(xs, fmt_null='null'):
-    parts = []
-    for x in xs:
-        if x is None:
-            parts.append(fmt_null)
-        elif isinstance(x, float):
-            parts.append(f"{x:.2f}" if not (x == int(x)) else f"{int(x)}")
-        else:
-            parts.append(str(x))
-    return '[' + ', '.join(parts) + ']'
-
-
-def js_quotes(xs):
-    return '[' + ', '.join(f'"{x}"' for x in xs) + ']'
-
-
-def milestone_x(milestone_date):
-    """Map a milestone date (YYYY-MM-DD) onto a bucket-axis label."""
-    y = int(milestone_date[:4])
-    mo = int(milestone_date[5:7])
-    if BUCKETS_MODE == 'monthly':
-        return f"{y}-{mo:02d}"
-    if BUCKETS_MODE == 'weekly':
-        d = dt.date(y, mo, int(milestone_date[8:10]))
-        iso = d.isocalendar()
-        return f"{iso[0]}-W{iso[1]:02d}"
-    return f"{y}-Q{(mo - 1) // 3 + 1}"
-
-
 # Title prefix differs between bucket modes for clarity.
-bucket_word = {'monthly': 'month', 'weekly': 'week'}.get(BUCKETS_MODE, 
'quarter')
+bucket_word = {"monthly": "month", "weekly": "week"}.get(BUCKETS_MODE, 
"quarter")
 
 # Build stacked-band traces in STACK_ORDER. With the default config that
 # resolves to `fixed_released, open_pr_merged, open_triaged,
@@ -1159,36 +724,43 @@ stacked_traces = []
 for cat in STACK_ORDER:
     if cat not in counts:
         continue
-    color = CAT_COLORS.get(cat, '#888888')
+    color = CAT_COLORS.get(cat, "#888888")
     ys = js_array(counts[cat])
     stacked_traces.append(
         f"  {{x: buckets, y: {ys},  name: '{cat}',  stackgroup: 'one', "
         f"type: 'scatter', mode: 'lines', line: {{color: '{color}', width: 
0}}, "
         f"fillcolor: '{color}', hoveron: 'points+fills'}}"
     )
-stacked_block = ',\n'.join(stacked_traces)
+stacked_block = ",\n".join(stacked_traces)
 
 # Milestone shapes + annotations (multi-milestone capable).
 ms_shapes = []
 ms_annots = []
 for ms in MILESTONES:
-    ms_date = ms.get('date')
-    ms_label = ms.get('label') or 'milestone'
+    ms_date = ms.get("date")
+    ms_label = ms.get("label") or "milestone"
     if not ms_date:
         continue
-    x_val = milestone_x(str(ms_date))
+    x_val = milestone_x(str(ms_date), BUCKETS_MODE)
     ms_shapes.append(
-        "{type: 'line', xref: 'x', yref: 'paper', x0: '" + x_val
-        + "', x1: '" + x_val
+        "{type: 'line', xref: 'x', yref: 'paper', x0: '"
+        + x_val
+        + "', x1: '"
+        + x_val
         + "', y0: 0, y1: 1, line: {color: '#888', width: 1.5, dash: 'dash'}}"
     )
     ms_annots.append(
-        "{xref: 'x', yref: 'paper', x: '" + x_val
-        + "', y: 1.04, xanchor: 'left', text: '↓ " + ms_label + " (" + 
str(ms_date) + ")', "
+        "{xref: 'x', yref: 'paper', x: '"
+        + x_val
+        + "', y: 1.04, xanchor: 'left', text: '↓ "
+        + ms_label
+        + " ("
+        + str(ms_date)
+        + ")', "
         + "showarrow: false, font: {size: 11, color: '#666'}}"
     )
-shapes_js = '[' + ', '.join(ms_shapes) + ']'
-annots_js = '[' + ', '.join(ms_annots) + ']'
+shapes_js = "[" + ", ".join(ms_shapes) + "]"
+annots_js = "[" + ", ".join(ms_annots) + "]"
 
 
 # Build the optional PR-charts HTML and JS sections.
@@ -1207,8 +779,8 @@ if UPSTREAM_REPO:
         f"{js_array(rel_ys)}, {js_array(rel_ns)}, 'd', '#d35400');"
     )
 else:
-    pr_cards_html = ''
-    pr_charts_js = ''
+    pr_cards_html = ""
+    pr_charts_js = ""
 
 
 # Build the optional "rejected (no tracker)" header banner, chart card,
@@ -1217,8 +789,8 @@ else:
 if REJECTIONS_LEDGER_LABEL and (rejections_total or ledger_issues):
     rej_header_html = (
         '<div class="banner">Reports rejected without a tracker: '
-        f'<strong>{rejections_total}</strong> '
-        f'({rejections_dated_total} dated + {rejections_backfill_total} 
historical)</div>\n'
+        f"<strong>{rejections_total}</strong> "
+        f"({rejections_dated_total} dated + {rejections_backfill_total} 
historical)</div>\n"
     )
     rej_cards_html = '<div class="card full"><div 
id="c_rejected"></div></div>\n'
     rej_chart_js = (
@@ -1236,9 +808,9 @@ if REJECTIONS_LEDGER_LABEL and (rejections_total or 
ledger_issues):
         f"}});"
     )
 else:
-    rej_header_html = ''
-    rej_cards_html = ''
-    rej_chart_js = ''
+    rej_header_html = ""
+    rej_cards_html = ""
+    rej_chart_js = ""
 
 # Extra cumulative traces (rejected + reported) appended to the c_cum
 # chart. Only emitted when the rejections stat is active; otherwise an
@@ -1247,30 +819,35 @@ else:
 # verbatim into the HTML f-string via {cum_rej_traces}.
 if REJECTIONS_LEDGER_LABEL and (rejections_total or ledger_issues):
     cum_rej_traces = (
-        ",\n  {x: buckets, y: " + js_array(cum_rejected) +
-        ", name: 'cumulative rejected (no tracker)', type: 'scatter', "
+        ",\n  {x: buckets, y: "
+        + js_array(cum_rejected)
+        + ", name: 'cumulative rejected (no tracker)', type: 'scatter', "
         "mode: 'lines+markers', connectgaps: true, "
         "line: {color: '#7f8c8d', dash: 'dot'}},\n"
-        "  {x: buckets, y: " + js_array(cum_reported) +
-        ", name: 'reported (opened + rejected)', type: 'scatter', "
+        "  {x: buckets, y: "
+        + js_array(cum_reported)
+        + ", name: 'reported (opened + rejected)', type: 'scatter', "
         "mode: 'lines+markers', connectgaps: true, "
         "line: {color: '#9467bd', width: 3}}"
     )
 else:
-    cum_rej_traces = ''
+    cum_rej_traces = ""
 
 # Extra "reported" trace (opened + rejected per bucket) appended to the
 # opened-vs-untriaged chart. Same activation rule as cum_rej_traces so it
 # is omitted (and the chart keeps its two original lines) when no ledger.
 if REJECTIONS_LEDGER_LABEL and (rejections_total or ledger_issues):
     rep_ou_trace = (
-        ",\n  {x: buckets, y: " + js_array(reported_in_b) +
-        ", name: 'reported in " + bucket_word + " (opened + rejected)', "
+        ",\n  {x: buckets, y: "
+        + js_array(reported_in_b)
+        + ", name: 'reported in "
+        + bucket_word
+        + " (opened + rejected)', "
         "type: 'scatter', mode: 'lines+markers', connectgaps: true, "
         "line: {color: '#9467bd', width: 3}}"
     )
 else:
-    rep_ou_trace = ''
+    rep_ou_trace = ""
 
 
 HTML = f"""<!DOCTYPE html>
@@ -1374,7 +951,7 @@ meanChart('c_resp',   'Mean time to first response 
(hours)',  {js_array(resp_ys)
 </html>
 """
 
-with open(OUT_PATH, 'w') as f:
+with open(OUT_PATH, "w") as f:
     f.write(HTML)
 
 print(f"\nWrote {OUT_PATH} ({len(HTML)} bytes)")
diff --git 
a/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/__init__.py
 
b/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/__init__.py
new file mode 100644
index 00000000..13a83393
--- /dev/null
+++ 
b/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git 
a/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/core.py
 
b/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/core.py
new file mode 100644
index 00000000..21656c44
--- /dev/null
+++ 
b/tools/security-tracker-stats-dashboard/src/security_tracker_stats_dashboard/core.py
@@ -0,0 +1,525 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Pure rendering helpers for the security-tracker statistics dashboard.
+
+All functions here are dependency-free (stdlib only) and have no side
+effects, making them straightforwardly unit-testable without a cache
+directory or live GitHub credentials.
+"""
+
+from __future__ import annotations
+
+import calendar
+import datetime as dt
+import re
+import statistics
+from collections.abc import Iterator
+from typing import Any
+
+# ---------------------------------------------------------------------------
+# Datetime helpers
+# ---------------------------------------------------------------------------
+
+
+def parse_dt(s: str | None) -> dt.datetime | None:
+    """Parse an ISO-8601 timestamp string to an aware datetime, or None."""
+    if not s:
+        return None
+    return dt.datetime.fromisoformat(s.replace("Z", "+00:00"))
+
+
+# ---------------------------------------------------------------------------
+# Bucket abstraction (monthly / quarterly)
+# ---------------------------------------------------------------------------
+
+
+def month_of(d: dt.datetime) -> tuple[int, int]:
+    """Return (year, month) bucket key for a datetime."""
+    return d.year, d.month
+
+
+def quarter_of(d: dt.datetime) -> tuple[int, int]:
+    """Return (year, quarter) bucket key for a datetime.  Quarter in 1..4."""
+    return d.year, (d.month - 1) // 3 + 1
+
+
+def month_label(y: int, m: int) -> str:
+    """Format a monthly bucket key as "YYYY-MM"."""
+    return f"{y}-{m:02d}"
+
+
+def quarter_label(y: int, q: int) -> str:
+    """Format a quarterly bucket key as "YYYY-Qn"."""
+    return f"{y}-Q{q}"
+
+
+def month_end(y: int, m: int) -> dt.datetime:
+    """Return the last instant of the given calendar month (UTC)."""
+    last_day = calendar.monthrange(y, m)[1]
+    return dt.datetime(y, m, last_day, 23, 59, 59, tzinfo=dt.timezone.utc)
+
+
+def quarter_end(y: int, q: int) -> dt.datetime:
+    """Return the last instant of the given calendar quarter (UTC)."""
+    last_month = q * 3
+    last_day = calendar.monthrange(y, last_month)[1]
+    return dt.datetime(y, last_month, last_day, 23, 59, 59, 
tzinfo=dt.timezone.utc)
+
+
+def iter_months(y0: int, m0: int, y1: int, m1: int) -> Iterator[tuple[int, 
int]]:
+    """Yield (year, month) tuples from (y0, m0) to (y1, m1) inclusive."""
+    y, m = y0, m0
+    while (y, m) <= (y1, m1):
+        yield y, m
+        m += 1
+        if m == 13:
+            m = 1
+            y += 1
+
+
+def iter_quarters(y0: int, q0: int, y1: int, q1: int) -> Iterator[tuple[int, 
int]]:
+    """Yield (year, quarter) tuples from (y0, q0) to (y1, q1) inclusive."""
+    y, q = y0, q0
+    while (y, q) <= (y1, q1):
+        yield y, q
+        q += 1
+        if q == 5:
+            q = 1
+            y += 1
+
+
+# Weekly buckets key on ISO (year, week) — note the ISO year can differ from
+# the calendar year in late December / early January, but (iso_year, iso_week)
+# tuples remain chronologically ordered, so the existing <=-based iteration
+# and bucketing carry over unchanged.
+def week_of(d: dt.datetime) -> tuple[int, int]:
+    iso = d.isocalendar()
+    return iso[0], iso[1]
+
+
+def week_label(y: int, w: int) -> str:
+    return f"{y}-W{w:02d}"
+
+
+def week_end(y: int, w: int) -> dt.datetime:
+    # ISO weeks run Monday (day 1) .. Sunday (day 7); end at Sunday 23:59:59.
+    sunday = dt.date.fromisocalendar(y, w, 7)
+    return dt.datetime(sunday.year, sunday.month, sunday.day, 23, 59, 59, 
tzinfo=dt.timezone.utc)
+
+
+def iter_weeks(y0: int, w0: int, y1: int, w1: int) -> Iterator[tuple[int, 
int]]:
+    # Step by 7 days from the Monday of the start week through the end week;
+    # ISO years have 52 or 53 weeks, so stepping by date avoids that edge.
+    cur = dt.date.fromisocalendar(y0, w0, 1)
+    last = dt.date.fromisocalendar(y1, w1, 1)
+    while cur <= last:
+        iso = cur.isocalendar()
+        yield iso[0], iso[1]
+        cur += dt.timedelta(days=7)
+
+
+def milestone_x(milestone_date: str, buckets_mode: str = "monthly") -> str:
+    """Map a milestone date string (YYYY-MM-DD) to a bucket-axis label."""
+    y = int(milestone_date[:4])
+    mo = int(milestone_date[5:7])
+    if buckets_mode == "monthly":
+        return f"{y}-{mo:02d}"
+    if buckets_mode == "weekly":
+        d = dt.date(y, mo, int(milestone_date[8:10]))
+        iso = d.isocalendar()
+        return f"{iso[0]}-W{iso[1]:02d}"
+    return f"{y}-Q{(mo - 1) // 3 + 1}"
+
+
+# ---------------------------------------------------------------------------
+# Configuration helpers
+# ---------------------------------------------------------------------------
+
+
+def deep_merge(base: Any, overlay: Any) -> Any:
+    """Deep-merge *overlay* into *base*.  Lists are REPLACED, not 
concatenated."""
+    if overlay is None:
+        return base
+    if not isinstance(base, dict) or not isinstance(overlay, dict):
+        return overlay
+    out = dict(base)
+    for k, v in overlay.items():
+        if k in out and isinstance(out[k], dict) and isinstance(v, dict):
+            out[k] = deep_merge(out[k], v)
+        else:
+            out[k] = v
+    return out
+
+
+def _minimal_yaml_load(text: str) -> Any:
+    """Tiny YAML subset parser sufficient for default-config.yaml.
+
+    Supports: nested block mappings, block sequences (``- ...``), inline
+    flow lists ``[a, b, "c d"]``, string scalars (with optional quotes),
+    integers, floats, booleans, null.  Comments start at ``#`` outside
+    quoted strings.  No anchors, no merge keys, no flow mappings.
+    """
+    lines: list[str] = []
+    for raw in text.splitlines():
+        in_q = None
+        out: list[str] = []
+        i = 0
+        while i < len(raw):
+            ch = raw[i]
+            if in_q:
+                out.append(ch)
+                if ch == "\\" and i + 1 < len(raw):
+                    out.append(raw[i + 1])
+                    i += 2
+                    continue
+                if ch == in_q:
+                    in_q = None
+                i += 1
+                continue
+            if ch in ('"', "'"):
+                in_q = ch
+                out.append(ch)
+                i += 1
+                continue
+            if ch == "#":
+                break
+            out.append(ch)
+            i += 1
+        line = "".join(out).rstrip()
+        if line.strip():
+            lines.append(line)
+
+    def indent_of(s: str) -> int:
+        return len(s) - len(s.lstrip(" "))
+
+    def scalar(s: str) -> Any:
+        s = s.strip()
+        if not s:
+            return None
+        if s.lower() in ("null", "~"):
+            return None
+        if s.lower() == "true":
+            return True
+        if s.lower() == "false":
+            return False
+        if s.startswith('"') and s.endswith('"') and len(s) >= 2:
+            return s[1:-1].encode().decode("unicode_escape")
+        if s.startswith("'") and s.endswith("'") and len(s) >= 2:
+            return s[1:-1]
+        if s.startswith("[") and s.endswith("]"):
+            inner = s[1:-1].strip()
+            if not inner:
+                return []
+            return [scalar(x) for x in _split_flow_list(inner)]
+        try:
+            if "." in s or "e" in s or "E" in s:
+                return float(s)
+            return int(s)
+        except ValueError:
+            return s
+
+    def _split_flow_list(inner: str) -> list[str]:
+        parts: list[str] = []
+        cur: list[str] = []
+        in_q2 = None
+        depth = 0
+        for ch in inner:
+            if in_q2:
+                cur.append(ch)
+                if ch == in_q2:
+                    in_q2 = None
+                continue
+            if ch in ('"', "'"):
+                in_q2 = ch
+                cur.append(ch)
+                continue
+            if ch == "[":
+                depth += 1
+                cur.append(ch)
+                continue
+            if ch == "]":
+                depth -= 1
+                cur.append(ch)
+                continue
+            if ch == "," and depth == 0:
+                parts.append("".join(cur).strip())
+                cur = []
+                continue
+            cur.append(ch)
+        if cur:
+            parts.append("".join(cur).strip())
+        return parts
+
+    def parse_block(idx: int, base_indent: int) -> tuple[Any, int]:
+        if idx >= len(lines):
+            return None, idx
+        first = lines[idx]
+        ind = indent_of(first)
+        if ind < base_indent:
+            return None, idx
+        if first.lstrip().startswith("- "):
+            return parse_seq(idx, ind)
+        return parse_map(idx, ind)
+
+    def _split_key_value(stripped: str) -> tuple[str, str, str]:
+        in_q3 = None
+        for i, ch in enumerate(stripped):
+            if in_q3:
+                if ch == in_q3:
+                    in_q3 = None
+                continue
+            if ch in ('"', "'"):
+                in_q3 = ch
+                continue
+            if ch == ":":
+                key = stripped[:i].strip()
+                rest = stripped[i + 1 :]
+                if (key.startswith('"') and key.endswith('"')) or 
(key.startswith("'") and key.endswith("'")):
+                    key = key[1:-1]
+                return key, ":", rest
+        return stripped, "", ""
+
+    def parse_map(idx: int, base_indent: int) -> tuple[dict, int]:
+        out: dict[str, Any] = {}
+        while idx < len(lines):
+            line = lines[idx]
+            ind = indent_of(line)
+            if ind < base_indent:
+                break
+            if ind > base_indent:
+                break
+            stripped = line.strip()
+            if stripped.startswith("- "):
+                break
+            if ":" not in stripped:
+                idx += 1
+                continue
+            key, _, rest = _split_key_value(stripped)
+            rest = rest.strip()
+            idx += 1
+            if rest == "" or rest is None:
+                if idx < len(lines) and indent_of(lines[idx]) > base_indent:
+                    child, idx = parse_block(idx, indent_of(lines[idx]))
+                    out[key] = child
+                else:
+                    out[key] = None
+            else:
+                out[key] = scalar(rest)
+        return out, idx
+
+    def parse_seq(idx: int, base_indent: int) -> tuple[list, int]:
+        out: list[Any] = []
+        while idx < len(lines):
+            line = lines[idx]
+            ind = indent_of(line)
+            if ind < base_indent:
+                break
+            if ind > base_indent:
+                break
+            stripped = line.strip()
+            if not stripped.startswith("- "):
+                break
+            after_dash = stripped[2:].rstrip()
+            item_inner_indent = base_indent + 2
+            idx += 1
+            if after_dash == "":
+                if idx < len(lines) and indent_of(lines[idx]) > base_indent:
+                    child, idx = parse_block(idx, indent_of(lines[idx]))
+                    out.append(child)
+                else:
+                    out.append(None)
+                continue
+            if ":" in after_dash and not (after_dash.startswith('"') or 
after_dash.startswith("'")):
+                key, _, rest = _split_key_value(after_dash)
+                rest = rest.strip()
+                item: dict[str, Any] = {}
+                if rest == "":
+                    if idx < len(lines) and indent_of(lines[idx]) > 
item_inner_indent:
+                        child, idx = parse_block(idx, indent_of(lines[idx]))
+                        item[key] = child
+                    else:
+                        item[key] = None
+                else:
+                    item[key] = scalar(rest)
+                while idx < len(lines):
+                    nline = lines[idx]
+                    nind = indent_of(nline)
+                    if nind < item_inner_indent:
+                        break
+                    if nind > item_inner_indent:
+                        break
+                    nstripped = nline.strip()
+                    if nstripped.startswith("- "):
+                        break
+                    if ":" not in nstripped:
+                        idx += 1
+                        continue
+                    nkey, _, nrest = _split_key_value(nstripped)
+                    nrest = nrest.strip()
+                    idx += 1
+                    if nrest == "":
+                        if idx < len(lines) and indent_of(lines[idx]) > 
item_inner_indent:
+                            child, idx = parse_block(idx, 
indent_of(lines[idx]))
+                            item[nkey] = child
+                        else:
+                            item[nkey] = None
+                    else:
+                        item[nkey] = scalar(nrest)
+                out.append(item)
+            else:
+                out.append(scalar(after_dash))
+        return out, idx
+
+    val, _ = parse_block(0, 0)
+    return val
+
+
+# ---------------------------------------------------------------------------
+# Category predicate evaluator
+# ---------------------------------------------------------------------------
+
+
+def eval_predicate(pred: Any, ctx: dict[str, Any]) -> bool:
+    """Evaluate a category predicate against a snapshot context dict.
+
+    Expected *ctx* keys:
+      - ``labels``               : set of label strings present at snapshot 
time
+      - ``is_open``              : bool — tracker is open at snapshot time
+      - ``state_reason``         : str | None — GitHub stateReason
+      - ``pr_merged_by_snapshot``: bool — a linked upstream PR is merged by 
snapshot
+      - ``scope_labels``         : set of label strings that are scope labels
+                                   (optional; defaults to empty set)
+
+    Returns True when the predicate matches the context.
+    """
+    if not isinstance(pred, dict):
+        return False
+    scope_labels: set[str] = ctx.get("scope_labels") or set()  # type: 
ignore[assignment]
+    for key, val in pred.items():
+        if key == "any_of":
+            if not any(eval_predicate(p, ctx) for p in val):
+                return False
+        elif key == "all_of":
+            if isinstance(val, list):
+                if not all(eval_predicate(p, ctx) for p in val):
+                    return False
+            elif isinstance(val, dict):
+                if not eval_predicate(val, ctx):
+                    return False
+            else:
+                return False
+        elif key == "state":
+            want_open = val == "open"
+            if ctx["is_open"] != want_open:
+                return False
+        elif key == "state_reason":
+            if ctx["state_reason"] != val:
+                return False
+        elif key == "any_label":
+            if not any(lb in ctx["labels"] for lb in val):
+                return False
+        elif key == "all_labels":
+            if not all(lb in ctx["labels"] for lb in val):
+                return False
+        elif key == "not_label":
+            if val in ctx["labels"]:
+                return False
+        elif key == "not_any_label":
+            if any(lb in ctx["labels"] for lb in val):
+                return False
+        elif key == "no_scope_label":
+            has_scope = bool(ctx["labels"] & scope_labels)
+            if val and has_scope:
+                return False
+            if not val and not has_scope:
+                return False
+        elif key == "has_scope_label":
+            has_scope = bool(ctx["labels"] & scope_labels)
+            if val and not has_scope:
+                return False
+            if not val and has_scope:
+                return False
+        elif key == "pr_merged_by_snapshot":
+            if val and not ctx["pr_merged_by_snapshot"]:
+                return False
+            if not val and ctx["pr_merged_by_snapshot"]:
+                return False
+        else:
+            return False
+    return True
+
+
+# ---------------------------------------------------------------------------
+# Statistics helpers
+# ---------------------------------------------------------------------------
+
+
+def mean_or_none(xs: list[float]) -> float | None:
+    """Return the mean of *xs* rounded to 2 decimal places, or None if 
empty."""
+    return round(statistics.mean(xs), 2) if xs else None
+
+
+# ---------------------------------------------------------------------------
+# HTML / JS serialisation helpers
+# ---------------------------------------------------------------------------
+
+
+def js_array(xs: list[Any], fmt_null: str = "null") -> str:
+    """Serialise a Python list to a JavaScript array literal string."""
+    parts = []
+    for x in xs:
+        if x is None:
+            parts.append(fmt_null)
+        elif isinstance(x, float):
+            parts.append(f"{x:.2f}" if x != int(x) else f"{int(x)}")
+        else:
+            parts.append(str(x))
+    return "[" + ", ".join(parts) + "]"
+
+
+def js_quotes(xs: list[str]) -> str:
+    """Serialise a list of strings to a JavaScript array of quoted string 
literals."""
+    return "[" + ", ".join(f'"{x}"' for x in xs) + "]"
+
+
+# ---------------------------------------------------------------------------
+# Triage helpers
+# ---------------------------------------------------------------------------
+
+
+def build_triage_regex(keywords: list[str]) -> re.Pattern[str] | None:
+    """Compile a triage keyword list into a single OR-pattern regex."""
+    if not keywords:
+        return None
+    parts = []
+    for kw in keywords:
+        if (kw.isupper() and " " not in kw and "-" not in kw) or (
+            kw.isalpha() and kw.islower() and " " not in kw
+        ):
+            parts.append(rf"\b{re.escape(kw)}\b")
+        else:
+            parts.append(re.escape(kw))
+    return re.compile("|".join(parts), re.IGNORECASE)
+
+
+def is_bot_body(body: str | None, bot_prefixes: tuple[str, ...] = ()) -> bool:
+    """Return True when *body* begins with one of the known bot comment 
prefixes."""
+    if not body:
+        return False
+    b = body.lstrip()
+    return any(b.startswith(p) for p in bot_prefixes)
diff --git a/tools/security-tracker-stats-dashboard/tests/__init__.py 
b/tools/security-tracker-stats-dashboard/tests/__init__.py
new file mode 100644
index 00000000..13a83393
--- /dev/null
+++ b/tools/security-tracker-stats-dashboard/tests/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tools/security-tracker-stats-dashboard/tests/test_core.py 
b/tools/security-tracker-stats-dashboard/tests/test_core.py
new file mode 100644
index 00000000..5374ace5
--- /dev/null
+++ b/tools/security-tracker-stats-dashboard/tests/test_core.py
@@ -0,0 +1,619 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime as dt
+
+import pytest
+
+from security_tracker_stats_dashboard.core import (
+    _minimal_yaml_load,
+    build_triage_regex,
+    deep_merge,
+    eval_predicate,
+    is_bot_body,
+    iter_months,
+    iter_quarters,
+    iter_weeks,
+    js_array,
+    js_quotes,
+    mean_or_none,
+    milestone_x,
+    month_end,
+    month_label,
+    month_of,
+    parse_dt,
+    quarter_end,
+    quarter_label,
+    quarter_of,
+    week_end,
+    week_label,
+    week_of,
+)
+
+# ---------------------------------------------------------------------------
+# parse_dt
+# ---------------------------------------------------------------------------
+
+
+class TestParseDt:
+    def test_none_returns_none(self):
+        assert parse_dt(None) is None
+
+    def test_empty_returns_none(self):
+        assert parse_dt("") is None
+
+    def test_z_suffix(self):
+        result = parse_dt("2024-03-15T10:30:00Z")
+        assert result == dt.datetime(2024, 3, 15, 10, 30, 0, 
tzinfo=dt.timezone.utc)
+
+    def test_plus_offset(self):
+        result = parse_dt("2024-03-15T10:30:00+00:00")
+        assert result == dt.datetime(2024, 3, 15, 10, 30, 0, 
tzinfo=dt.timezone.utc)
+
+    def test_aware(self):
+        result = parse_dt("2025-01-01T00:00:00Z")
+        assert result is not None
+        assert result.tzinfo is not None
+
+
+# ---------------------------------------------------------------------------
+# Bucket categorisation — monthly
+# ---------------------------------------------------------------------------
+
+
+class TestMonthOf:
+    def test_basic(self):
+        d = dt.datetime(2024, 6, 15, tzinfo=dt.timezone.utc)
+        assert month_of(d) == (2024, 6)
+
+    def test_january(self):
+        d = dt.datetime(2025, 1, 1, tzinfo=dt.timezone.utc)
+        assert month_of(d) == (2025, 1)
+
+    def test_december(self):
+        d = dt.datetime(2024, 12, 31, tzinfo=dt.timezone.utc)
+        assert month_of(d) == (2024, 12)
+
+
+class TestQuarterOf:
+    def test_q1(self):
+        d = dt.datetime(2024, 2, 15, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 1)
+
+    def test_q2(self):
+        d = dt.datetime(2024, 4, 1, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 2)
+
+    def test_q3(self):
+        d = dt.datetime(2024, 9, 30, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 3)
+
+    def test_q4(self):
+        d = dt.datetime(2024, 12, 1, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 4)
+
+    def test_boundary_month_3_is_q1(self):
+        d = dt.datetime(2024, 3, 31, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 1)
+
+    def test_boundary_month_4_is_q2(self):
+        d = dt.datetime(2024, 4, 1, tzinfo=dt.timezone.utc)
+        assert quarter_of(d) == (2024, 2)
+
+
+class TestMonthLabel:
+    def test_basic(self):
+        assert month_label(2024, 3) == "2024-03"
+
+    def test_two_digit_month(self):
+        assert month_label(2024, 11) == "2024-11"
+
+    def test_january_zero_padded(self):
+        assert month_label(2025, 1) == "2025-01"
+
+
+class TestQuarterLabel:
+    def test_q1(self):
+        assert quarter_label(2024, 1) == "2024-Q1"
+
+    def test_q4(self):
+        assert quarter_label(2023, 4) == "2023-Q4"
+
+
+class TestMonthEnd:
+    def test_january(self):
+        result = month_end(2024, 1)
+        assert result.day == 31
+        assert result.hour == 23
+        assert result.minute == 59
+        assert result.tzinfo == dt.timezone.utc
+
+    def test_february_leap(self):
+        result = month_end(2024, 2)
+        assert result.day == 29
+
+    def test_february_nonleap(self):
+        result = month_end(2023, 2)
+        assert result.day == 28
+
+    def test_april(self):
+        result = month_end(2024, 4)
+        assert result.day == 30
+
+
+class TestQuarterEnd:
+    def test_q1(self):
+        result = quarter_end(2024, 1)
+        assert result.month == 3
+        assert result.day == 31
+
+    def test_q2(self):
+        result = quarter_end(2024, 2)
+        assert result.month == 6
+        assert result.day == 30
+
+    def test_q3(self):
+        result = quarter_end(2024, 3)
+        assert result.month == 9
+        assert result.day == 30
+
+    def test_q4(self):
+        result = quarter_end(2024, 4)
+        assert result.month == 12
+        assert result.day == 31
+
+    def test_aware(self):
+        assert quarter_end(2024, 1).tzinfo == dt.timezone.utc
+
+
+# ---------------------------------------------------------------------------
+# iter_months / iter_quarters
+# ---------------------------------------------------------------------------
+
+
+class TestIterMonths:
+    def test_single(self):
+        assert list(iter_months(2024, 3, 2024, 3)) == [(2024, 3)]
+
+    def test_spans_year(self):
+        result = list(iter_months(2024, 11, 2025, 2))
+        assert result == [(2024, 11), (2024, 12), (2025, 1), (2025, 2)]
+
+    def test_empty_when_start_after_end(self):
+        assert list(iter_months(2025, 1, 2024, 12)) == []
+
+    def test_full_year(self):
+        result = list(iter_months(2024, 1, 2024, 12))
+        assert len(result) == 12
+        assert result[0] == (2024, 1)
+        assert result[-1] == (2024, 12)
+
+
+class TestIterQuarters:
+    def test_single(self):
+        assert list(iter_quarters(2024, 2, 2024, 2)) == [(2024, 2)]
+
+    def test_spans_year(self):
+        result = list(iter_quarters(2024, 3, 2025, 2))
+        assert result == [(2024, 3), (2024, 4), (2025, 1), (2025, 2)]
+
+    def test_empty_when_start_after_end(self):
+        assert list(iter_quarters(2025, 1, 2024, 4)) == []
+
+    def test_full_year(self):
+        result = list(iter_quarters(2024, 1, 2024, 4))
+        assert len(result) == 4
+
+
+# ---------------------------------------------------------------------------
+# week_of / week_label / week_end / iter_weeks  (ISO-week buckets)
+# ---------------------------------------------------------------------------
+
+
+class TestWeekOf:
+    def test_basic(self):
+        # 2026-01-01 is a Thursday -> ISO week 1 of 2026.
+        assert week_of(dt.datetime(2026, 1, 1)) == (2026, 1)
+
+    def test_iso_year_behind_calendar_year(self):
+        # 2023-01-01 is a Sunday -> still ISO week 52 of 2022.
+        assert week_of(dt.datetime(2023, 1, 1)) == (2022, 52)
+
+    def test_iso_year_ahead_of_calendar_year(self):
+        # 2024-12-30 is a Monday -> ISO week 1 of 2025.
+        assert week_of(dt.datetime(2024, 12, 30)) == (2025, 1)
+
+
+class TestWeekLabel:
+    def test_zero_padded(self):
+        assert week_label(2026, 3) == "2026-W03"
+
+    def test_two_digit_week(self):
+        assert week_label(2025, 52) == "2025-W52"
+
+
+class TestWeekEnd:
+    def test_sunday_end_of_week(self):
+        # ISO 2026-W01 runs Mon 2025-12-29 .. Sun 2026-01-04.
+        result = week_end(2026, 1)
+        assert (result.year, result.month, result.day) == (2026, 1, 4)
+        assert (result.hour, result.minute, result.second) == (23, 59, 59)
+
+    def test_is_utc(self):
+        assert week_end(2025, 52).tzinfo == dt.timezone.utc
+
+    def test_week_52(self):
+        # ISO 2025-W52 ends Sun 2025-12-28.
+        result = week_end(2025, 52)
+        assert (result.year, result.month, result.day) == (2025, 12, 28)
+
+
+class TestIterWeeks:
+    def test_single(self):
+        assert list(iter_weeks(2026, 1, 2026, 1)) == [(2026, 1)]
+
+    def test_spans_iso_year_rollover(self):
+        # 2025-W52 -> 2026-W01 -> 2026-W02; the ISO year flips mid-range.
+        result = list(iter_weeks(2025, 52, 2026, 2))
+        assert result == [(2025, 52), (2026, 1), (2026, 2)]
+
+    def test_empty_when_start_after_end(self):
+        assert list(iter_weeks(2026, 5, 2026, 2)) == []
+
+    def test_consecutive_count(self):
+        result = list(iter_weeks(2026, 1, 2026, 10))
+        assert len(result) == 10
+
+
+# ---------------------------------------------------------------------------
+# milestone_x
+# ---------------------------------------------------------------------------
+
+
+class TestMilestoneX:
+    def test_monthly(self):
+        assert milestone_x("2024-03-15") == "2024-03"
+
+    def test_quarterly_q1(self):
+        assert milestone_x("2024-02-01", buckets_mode="quarterly") == "2024-Q1"
+
+    def test_quarterly_q2(self):
+        assert milestone_x("2024-04-01", buckets_mode="quarterly") == "2024-Q2"
+
+    def test_quarterly_q4(self):
+        assert milestone_x("2024-11-30", buckets_mode="quarterly") == "2024-Q4"
+
+    def test_monthly_default(self):
+        assert milestone_x("2025-01-01") == "2025-01"
+
+    def test_weekly(self):
+        # 2026-01-15 is a Thursday -> ISO week 3 of 2026.
+        assert milestone_x("2026-01-15", buckets_mode="weekly") == "2026-W03"
+
+    def test_weekly_iso_year_rollover(self):
+        # 2024-12-30 is a Monday -> ISO week 1 of 2025.
+        assert milestone_x("2024-12-30", buckets_mode="weekly") == "2025-W01"
+
+
+# ---------------------------------------------------------------------------
+# deep_merge
+# ---------------------------------------------------------------------------
+
+
+class TestDeepMerge:
+    def test_overlay_none_returns_base(self):
+        base = {"a": 1}
+        assert deep_merge(base, None) == {"a": 1}
+
+    def test_non_dict_overlay_replaces(self):
+        assert deep_merge({"a": 1}, "scalar") == "scalar"
+
+    def test_simple_merge(self):
+        result = deep_merge({"a": 1, "b": 2}, {"b": 3, "c": 4})
+        assert result == {"a": 1, "b": 3, "c": 4}
+
+    def test_nested_merge(self):
+        base = {"x": {"a": 1, "b": 2}}
+        overlay = {"x": {"b": 99, "c": 3}}
+        result = deep_merge(base, overlay)
+        assert result == {"x": {"a": 1, "b": 99, "c": 3}}
+
+    def test_list_replaced_not_concatenated(self):
+        base = {"items": [1, 2, 3]}
+        overlay = {"items": [4, 5]}
+        result = deep_merge(base, overlay)
+        assert result["items"] == [4, 5]
+
+    def test_deep_nested(self):
+        base = {"a": {"b": {"c": 1}}}
+        overlay = {"a": {"b": {"d": 2}}}
+        result = deep_merge(base, overlay)
+        assert result == {"a": {"b": {"c": 1, "d": 2}}}
+
+
+# ---------------------------------------------------------------------------
+# _minimal_yaml_load
+# ---------------------------------------------------------------------------
+
+
+class TestMinimalYamlLoad:
+    def test_simple_mapping(self):
+        text = "key: value\nnum: 42\n"
+        result = _minimal_yaml_load(text)
+        assert result == {"key": "value", "num": 42}
+
+    def test_boolean(self):
+        result = _minimal_yaml_load("flag: true\n")
+        assert result == {"flag": True}
+
+    def test_null(self):
+        result = _minimal_yaml_load("x: null\n")
+        assert result == {"x": None}
+
+    def test_nested(self):
+        text = "outer:\n  inner: 1\n"
+        result = _minimal_yaml_load(text)
+        assert result == {"outer": {"inner": 1}}
+
+    def test_sequence(self):
+        text = "items:\n  - a\n  - b\n"
+        result = _minimal_yaml_load(text)
+        assert result == {"items": ["a", "b"]}
+
+    def test_inline_list(self):
+        result = _minimal_yaml_load("tags: [alpha, beta]\n")
+        assert result == {"tags": ["alpha", "beta"]}
+
+    def test_comment_stripped(self):
+        result = _minimal_yaml_load("key: value  # comment\n")
+        assert result == {"key": "value"}
+
+    def test_float(self):
+        result = _minimal_yaml_load("v: 3.14\n")
+        assert result == {"v": pytest.approx(3.14)}
+
+    def test_quoted_string(self):
+        result = _minimal_yaml_load("s: 'hello world'\n")
+        assert result == {"s": "hello world"}
+
+    def test_mapping_in_sequence(self):
+        text = "cats:\n  - name: foo\n    color: red\n"
+        result = _minimal_yaml_load(text)
+        assert result == {"cats": [{"name": "foo", "color": "red"}]}
+
+
+# ---------------------------------------------------------------------------
+# eval_predicate
+# ---------------------------------------------------------------------------
+
+
+def _ctx(labels=None, is_open=True, state_reason=None, pr_merged=False, 
scope_labels=None):
+    return {
+        "labels": set(labels or []),
+        "is_open": is_open,
+        "state_reason": state_reason,
+        "pr_merged_by_snapshot": pr_merged,
+        "scope_labels": set(scope_labels or []),
+    }
+
+
+class TestEvalPredicate:
+    def test_non_dict_pred_returns_false(self):
+        assert eval_predicate("bad", _ctx()) is False
+
+    def test_empty_pred_matches_anything(self):
+        assert eval_predicate({}, _ctx()) is True
+
+    def test_state_open(self):
+        assert eval_predicate({"state": "open"}, _ctx(is_open=True)) is True
+        assert eval_predicate({"state": "open"}, _ctx(is_open=False)) is False
+
+    def test_state_closed(self):
+        assert eval_predicate({"state": "closed"}, _ctx(is_open=False)) is True
+
+    def test_state_reason(self):
+        assert eval_predicate({"state_reason": "COMPLETED"}, 
_ctx(state_reason="COMPLETED")) is True
+        assert eval_predicate({"state_reason": "COMPLETED"}, 
_ctx(state_reason="NOT_PLANNED")) is False
+
+    def test_any_label(self):
+        assert eval_predicate({"any_label": ["a", "b"]}, _ctx(labels=["b"])) 
is True
+        assert eval_predicate({"any_label": ["a", "b"]}, _ctx(labels=["c"])) 
is False
+
+    def test_all_labels(self):
+        assert eval_predicate({"all_labels": ["a", "b"]}, _ctx(labels=["a", 
"b", "c"])) is True
+        assert eval_predicate({"all_labels": ["a", "b"]}, _ctx(labels=["a"])) 
is False
+
+    def test_not_label(self):
+        assert eval_predicate({"not_label": "bad"}, _ctx(labels=["good"])) is 
True
+        assert eval_predicate({"not_label": "bad"}, _ctx(labels=["bad"])) is 
False
+
+    def test_not_any_label(self):
+        assert eval_predicate({"not_any_label": ["a", "b"]}, 
_ctx(labels=["c"])) is True
+        assert eval_predicate({"not_any_label": ["a", "b"]}, 
_ctx(labels=["a"])) is False
+
+    def test_pr_merged_by_snapshot(self):
+        assert eval_predicate({"pr_merged_by_snapshot": True}, 
_ctx(pr_merged=True)) is True
+        assert eval_predicate({"pr_merged_by_snapshot": True}, 
_ctx(pr_merged=False)) is False
+
+    def test_no_scope_label_true(self):
+        ctx = _ctx(labels=["bug"], scope_labels=["area/api", "area/core"])
+        assert eval_predicate({"no_scope_label": True}, ctx) is True
+
+    def test_no_scope_label_false_when_scope_present(self):
+        ctx = _ctx(labels=["area/api"], scope_labels=["area/api", "area/core"])
+        assert eval_predicate({"no_scope_label": True}, ctx) is False
+
+    def test_has_scope_label(self):
+        ctx = _ctx(labels=["area/api"], scope_labels=["area/api"])
+        assert eval_predicate({"has_scope_label": True}, ctx) is True
+        ctx2 = _ctx(labels=["bug"], scope_labels=["area/api"])
+        assert eval_predicate({"has_scope_label": True}, ctx2) is False
+
+    def test_any_of(self):
+        pred = {"any_of": [{"state": "open"}, {"any_label": ["wontfix"]}]}
+        assert eval_predicate(pred, _ctx(is_open=True)) is True
+        assert eval_predicate(pred, _ctx(is_open=False, labels=["wontfix"])) 
is True
+        assert eval_predicate(pred, _ctx(is_open=False)) is False
+
+    def test_all_of_list(self):
+        pred = {"all_of": [{"state": "closed"}, {"state_reason": "COMPLETED"}]}
+        assert eval_predicate(pred, _ctx(is_open=False, 
state_reason="COMPLETED")) is True
+        assert eval_predicate(pred, _ctx(is_open=False, 
state_reason="NOT_PLANNED")) is False
+
+    def test_unknown_key_returns_false(self):
+        assert eval_predicate({"unknown_key": "value"}, _ctx()) is False
+
+    def test_composite(self):
+        pred = {
+            "state": "closed",
+            "state_reason": "COMPLETED",
+            "any_label": ["cve allocated"],
+        }
+        ctx = _ctx(is_open=False, state_reason="COMPLETED", labels=["cve 
allocated"])
+        assert eval_predicate(pred, ctx) is True
+        ctx2 = _ctx(is_open=False, state_reason="COMPLETED", labels=[])
+        assert eval_predicate(pred, ctx2) is False
+
+
+# ---------------------------------------------------------------------------
+# mean_or_none
+# ---------------------------------------------------------------------------
+
+
+class TestMeanOrNone:
+    def test_empty(self):
+        assert mean_or_none([]) is None
+
+    def test_single(self):
+        assert mean_or_none([5.0]) == 5.0
+
+    def test_multiple(self):
+        assert mean_or_none([1.0, 2.0, 3.0]) == 2.0
+
+    def test_rounds_to_two_places(self):
+        result = mean_or_none([1.0, 2.0])
+        assert result == 1.5
+        result2 = mean_or_none([1.0, 2.0, 4.0])
+        assert result2 == pytest.approx(2.33, abs=0.01)
+
+
+# ---------------------------------------------------------------------------
+# build_triage_regex
+# ---------------------------------------------------------------------------
+
+
+class TestBuildTriageRegex:
+    def test_empty_returns_none(self):
+        assert build_triage_regex([]) is None
+
+    def test_single_keyword(self):
+        pat = build_triage_regex(["triage"])
+        assert pat is not None
+        assert pat.search("please triage this") is not None
+        assert pat.search("TRIAGE this") is not None
+
+    def test_uppercase_word_boundary(self):
+        pat = build_triage_regex(["CVE"])
+        assert pat is not None
+        # \b matches before '-', so "CVE-2024" still triggers
+        assert pat.search("CVE-2024-1234") is not None
+        # Does NOT match inside a longer word token
+        assert pat.search("XCVE") is None
+        assert pat.search("CVEx") is None
+        assert pat.search("is a CVE here") is not None
+
+    def test_phrase_keyword(self):
+        pat = build_triage_regex(["needs triage"])
+        assert pat is not None
+        assert pat.search("this needs triage today") is not None
+
+    def test_multiple_keywords(self):
+        pat = build_triage_regex(["triage", "CVE", "needs review"])
+        assert pat is not None
+        assert pat.search("triage this") is not None
+        assert pat.search("see the CVE today") is not None
+        assert pat.search("needs review please") is not None
+        assert pat.search("nothing here") is None
+
+
+# ---------------------------------------------------------------------------
+# is_bot_body
+# ---------------------------------------------------------------------------
+
+
+class TestIsBotBody:
+    def test_none_returns_false(self):
+        assert is_bot_body(None) is False
+
+    def test_empty_returns_false(self):
+        assert is_bot_body("") is False
+
+    def test_no_prefix_match(self):
+        assert is_bot_body("regular comment", ("<!-- bot -->",)) is False
+
+    def test_prefix_match(self):
+        assert is_bot_body("<!-- bot --> auto generated", ("<!-- bot -->",)) 
is True
+
+    def test_leading_whitespace_stripped(self):
+        assert is_bot_body("  <!-- bot -->body", ("<!-- bot -->",)) is True
+
+    def test_multiple_prefixes(self):
+        assert is_bot_body("[BOT] hello", ("[BOT]", "<!-- auto -->")) is True
+        assert is_bot_body("<!-- auto --> hi", ("[BOT]", "<!-- auto -->")) is 
True
+
+    def test_default_empty_prefixes(self):
+        assert is_bot_body("<!-- bot -->") is False
+
+
+# ---------------------------------------------------------------------------
+# js_array / js_quotes
+# ---------------------------------------------------------------------------
+
+
+class TestJsArray:
+    def test_empty(self):
+        assert js_array([]) == "[]"
+
+    def test_integers(self):
+        assert js_array([1, 2, 3]) == "[1, 2, 3]"
+
+    def test_none_default(self):
+        assert js_array([1, None, 3]) == "[1, null, 3]"
+
+    def test_none_custom(self):
+        assert js_array([None], fmt_null="undefined") == "[undefined]"
+
+    def test_float_with_decimal(self):
+        assert js_array([1.5]) == "[1.50]"
+
+    def test_float_whole_number(self):
+        assert js_array([2.0]) == "[2]"
+
+    def test_mixed(self):
+        result = js_array([1, None, 2.5])
+        assert result == "[1, null, 2.50]"
+
+
+class TestJsQuotes:
+    def test_empty(self):
+        assert js_quotes([]) == "[]"
+
+    def test_single(self):
+        assert js_quotes(["hello"]) == '["hello"]'
+
+    def test_multiple(self):
+        assert js_quotes(["a", "b", "c"]) == '["a", "b", "c"]'
diff --git a/tools/security-tracker-stats-dashboard/uv.lock 
b/tools/security-tracker-stats-dashboard/uv.lock
new file mode 100644
index 00000000..cc990e74
--- /dev/null
+++ b/tools/security-tracker-stats-dashboard/uv.lock
@@ -0,0 +1,204 @@
+version = 1
+revision = 3
+requires-python = ">=3.9"
+resolution-markers = [
+    "python_full_version >= '3.10'",
+    "python_full_version < '3.10'",
+]
+
+[options]
+exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included 
for backwards compatibility when using relative exclude-newer values.
+exclude-newer-span = "P7D"
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz";,
 hash = 
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size 
= 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl";,
 hash = 
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size 
= 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz";,
 hash = 
"sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size 
= 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl";,
 hash = 
"sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size 
= 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version < '3.10'",
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz";,
 hash = 
"sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size 
= 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl";,
 hash = 
"sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size 
= 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.10'",
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz";,
 hash = 
"sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size 
= 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl";,
 hash = 
"sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size 
= 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz";,
 hash = 
"sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size 
= 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl";,
 hash = 
"sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size 
= 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz";,
 hash = 
"sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size 
= 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl";,
 hash = 
"sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size 
= 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz";,
 hash = 
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size 
= 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl";,
 hash = 
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size 
= 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version < '3.10'",
+]
+dependencies = [
+    { name = "colorama", marker = "python_full_version < '3.10' and 
sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
+    { name = "iniconfig", version = "2.1.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.10'" },
+    { name = "packaging", marker = "python_full_version < '3.10'" },
+    { name = "pluggy", marker = "python_full_version < '3.10'" },
+    { name = "pygments", marker = "python_full_version < '3.10'" },
+    { name = "tomli", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz";,
 hash = 
"sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size 
= 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl";,
 hash = 
"sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size 
= 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.3"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.10'",
+]
+dependencies = [
+    { name = "colorama", marker = "python_full_version >= '3.10' and 
sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
+    { name = "iniconfig", version = "2.3.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.10'" },
+    { name = "packaging", marker = "python_full_version >= '3.10'" },
+    { name = "pluggy", marker = "python_full_version >= '3.10'" },
+    { name = "pygments", marker = "python_full_version >= '3.10'" },
+    { name = "tomli", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz";,
 hash = 
"sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size 
= 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl";,
 hash = 
"sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size 
= 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+]
+
+[[package]]
+name = "security-tracker-stats-dashboard"
+version = "0.1.0"
+source = { editable = "." }
+
+[package.dev-dependencies]
+dev = [
+    { name = "pytest", version = "8.4.2", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.10'" },
+    { name = "pytest", version = "9.0.3", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.10'" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pytest", specifier = ">=8.0" }]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz";,
 hash = 
"sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size 
= 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl";,
 hash = 
"sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size 
= 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl";,
 hash = 
"sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size 
= 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+    { url = 
"https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size 
= 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+    { url = 
"https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size 
= 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+    { url = 
"https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size 
= 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size 
= 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+    { url = 
"https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl";,
 hash = 
"sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size 
= 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+    { url = 
"https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl";,
 hash = 
"sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size 
= 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+    { url = 
"https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl";,
 hash = 
"sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size 
= 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl";,
 hash = 
"sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size 
= 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+    { url = 
"https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl";,
 hash = 
"sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size 
= 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size 
= 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+    { url = 
"https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size 
= 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+    { url = 
"https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size 
= 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+    { url = 
"https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size 
= 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl";,
 hash = 
"sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size 
= 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+    { url = 
"https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl";,
 hash = 
"sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size 
= 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+    { url = 
"https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl";,
 hash = 
"sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size 
= 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+    { url = 
"https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl";,
 hash = 
"sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size 
= 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+    { url = 
"https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl";,
 hash = 
"sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size 
= 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size 
= 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+    { url = 
"https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size 
= 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size 
= 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size 
= 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+    { url = 
"https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl";,
 hash = 
"sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size 
= 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl";,
 hash = 
"sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size 
= 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+    { url = 
"https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl";,
 hash = 
"sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size 
= 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+    { url = 
"https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl";,
 hash = 
"sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size 
= 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+    { url = 
"https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl";,
 hash = 
"sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size 
= 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size 
= 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+    { url = 
"https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size 
= 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+    { url = 
"https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size 
= 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+    { url = 
"https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size 
= 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl";,
 hash = 
"sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size 
= 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+    { url = 
"https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl";,
 hash = 
"sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size 
= 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl";,
 hash = 
"sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size 
= 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+    { url = 
"https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl";,
 hash = 
"sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size 
= 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+    { url = 
"https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl";,
 hash = 
"sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size 
= 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+    { url = 
"https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size 
= 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size 
= 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+    { url = 
"https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size 
= 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+    { url = 
"https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size 
= 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl";,
 hash = 
"sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size 
= 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+    { url = 
"https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl";,
 hash = 
"sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size 
= 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl";,
 hash = 
"sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size 
= 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl";,
 hash = 
"sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size 
= 14583, upload-time = "2026-03-25T20:22:03.012Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz";,
 hash = 
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size 
= 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl";,
 hash = 
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size 
= 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
diff --git a/tools/spec-loop/specs/security-reporting.md 
b/tools/spec-loop/specs/security-reporting.md
index 43227e57..7ef9ca82 100644
--- a/tools/spec-loop/specs/security-reporting.md
+++ b/tools/spec-loop/specs/security-reporting.md
@@ -66,6 +66,7 @@ health without navigating the tracker issue-by-issue.
 ## Validation
 
 ```bash
+uv run --directory tools/security-tracker-stats-dashboard --group dev pytest
 bash -n tools/security-tracker-stats-dashboard/run.sh
 shellcheck tools/security-tracker-stats-dashboard/run.sh
 ```
@@ -73,5 +74,4 @@ shellcheck tools/security-tracker-stats-dashboard/run.sh
 ## Known gaps
 
 - `experimental` — no adopter pilot has run the dashboard end-to-end.
-  The tool's test coverage and CI integration are tracked as follow-on
-  work items.
+- CI integration is a follow-on item.
diff --git a/uv.lock b/uv.lock
index 9148330f..2b82e2b7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -27,6 +27,7 @@ members = [
     "preflight-audit",
     "redactor",
     "sandbox-lint",
+    "security-tracker-stats-dashboard",
     "skill-and-tool-validator",
     "skill-evals",
     "spec-status-index",
@@ -783,6 +784,21 @@ name = "sandbox-lint"
 version = "0.1.0"
 source = { editable = "tools/sandbox-lint" }
 
+[[package]]
+name = "security-tracker-stats-dashboard"
+version = "0.1.0"
+source = { editable = "tools/security-tracker-stats-dashboard" }
+
+[package.dev-dependencies]
+dev = [
+    { name = "pytest" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pytest", specifier = ">=8.0" }]
+
 [[package]]
 name = "skill-and-tool-validator"
 version = "0.1.0"

Reply via email to