mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
145 lines
4.5 KiB
Python
145 lines
4.5 KiB
Python
"""Diagnostics persistence — accumulate and retrieve diagnostics across commands.
|
|
|
|
Diagnostics are persisted as JSONL in project/diagnostics.jsonl, one
|
|
JSON object per line. Each entry has: severity, category, message, recoverable,
|
|
command, and timestamp.
|
|
|
|
The diagnostics file grows across the project lifecycle: warnings and errors
|
|
from analyze, triage, suspicious-apis, and other commands are accumulated
|
|
and retrievable via the `binary diagnostics` command.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from binary_analysis.projects.atomic import atomic_append_text
|
|
|
|
DIAGNOSTICS_FILENAME = "diagnostics.jsonl"
|
|
|
|
|
|
def _diagnostics_path(project_path: str) -> str:
|
|
"""Return the path to the diagnostics file within a project workspace."""
|
|
return os.path.join(project_path, DIAGNOSTICS_FILENAME)
|
|
|
|
|
|
def persist_diagnostics(
|
|
project_path: str,
|
|
diagnostics: list[dict[str, Any]],
|
|
command: str = "unknown",
|
|
) -> None:
|
|
"""Persist diagnostic entries to the project's diagnostics file.
|
|
|
|
Each diagnostic entry is augmented with a command field and timestamp
|
|
before being appended atomically to the JSONL file.
|
|
|
|
Args:
|
|
project_path: Absolute path to the project workspace directory.
|
|
diagnostics: List of diagnostic dicts to persist.
|
|
command: Name of the command that produced these diagnostics.
|
|
"""
|
|
if not diagnostics:
|
|
return
|
|
|
|
path = _diagnostics_path(project_path)
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
|
|
for diag in diagnostics:
|
|
entry = {
|
|
"severity": diag.get("severity", "INFO"),
|
|
"category": diag.get("category", "general"),
|
|
"message": diag.get("message", ""),
|
|
"recoverable": diag.get("recoverable", True),
|
|
"command": command,
|
|
"timestamp": timestamp,
|
|
}
|
|
# Preserve optional fields
|
|
if "component" in diag:
|
|
entry["component"] = diag["component"]
|
|
if "remediation" in diag:
|
|
entry["remediation"] = diag["remediation"]
|
|
|
|
line = json.dumps(entry, ensure_ascii=False)
|
|
atomic_append_text(path, line)
|
|
|
|
|
|
def load_diagnostics(project_path: str) -> list[dict[str, Any]]:
|
|
"""Load all accumulated diagnostics from the project's diagnostics file.
|
|
|
|
Returns an empty list if the file does not exist or is empty.
|
|
|
|
Args:
|
|
project_path: Absolute path to the project workspace directory.
|
|
|
|
Returns:
|
|
List of diagnostic dicts ordered by appearance in the file
|
|
(oldest first).
|
|
"""
|
|
path = _diagnostics_path(project_path)
|
|
if not os.path.exists(path):
|
|
return []
|
|
|
|
diagnostics: list[dict[str, Any]] = []
|
|
try:
|
|
with open(path, encoding="utf-8") as f:
|
|
for line_num, line in enumerate(f, start=1):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
entry = json.loads(line)
|
|
diagnostics.append(entry)
|
|
except json.JSONDecodeError:
|
|
# Skip corrupted lines but note in a diagnostic
|
|
diagnostics.append(
|
|
{
|
|
"severity": "WARNING",
|
|
"category": "diagnostics-file",
|
|
"message": f"Corrupted diagnostics entry at line {line_num}",
|
|
"recoverable": True,
|
|
"command": "diagnostics",
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
except OSError:
|
|
return []
|
|
|
|
return diagnostics
|
|
|
|
|
|
def clear_diagnostics(project_path: str) -> None:
|
|
"""Remove the diagnostics file (e.g., on project clean).
|
|
|
|
Args:
|
|
project_path: Absolute path to the project workspace directory.
|
|
"""
|
|
path = _diagnostics_path(project_path)
|
|
if os.path.exists(path):
|
|
os.unlink(path)
|
|
|
|
|
|
def get_diagnostics_summary(
|
|
diagnostics: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""Compute a summary of diagnostic entries.
|
|
|
|
Args:
|
|
diagnostics: List of diagnostic dicts.
|
|
|
|
Returns:
|
|
Dict with total count and breakdown by severity.
|
|
"""
|
|
by_severity: dict[str, int] = {"INFO": 0, "WARNING": 0, "ERROR": 0}
|
|
for d in diagnostics:
|
|
sev = d.get("severity", "INFO")
|
|
if sev in by_severity:
|
|
by_severity[sev] += 1
|
|
|
|
return {
|
|
"total": len(diagnostics),
|
|
"by_severity": by_severity,
|
|
}
|