diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad13eef..cab6bca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,9 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip - run: pip install -e ".[dev]" + - name: Lint with Ruff + if: matrix.python-version == '3.12' + run: ruff check . - run: pytest -q # No services, no API keys, no network: it reads local files, so CI runs the real thing. - name: Smoke the CLI against the bundled fixture session diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fee1eed..53cbe72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,8 @@ Thanks for considering a contribution — issues and pull requests are welcome. ## Making a change - Keep pull requests focused and describe **what** changed and **why**. -- Run the project's format, lint, and test steps (see the README) before pushing. +- Run `ruff check .` and the project's test steps (see the README) before pushing. +- Formatting is not currently enforced; Ruff is used for linting. - Add or update tests when you change behavior. - Update docs or the CHANGELOG when relevant. diff --git a/agentrace/checks.py b/agentrace/checks.py index b431308..4204050 100644 --- a/agentrace/checks.py +++ b/agentrace/checks.py @@ -16,8 +16,8 @@ from __future__ import annotations import re +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable from .parse import AgentRun @@ -68,7 +68,7 @@ def check_refused_or_gave_up(run: AgentRun) -> list[Finding]: ] head = run.result[:1500] for p in patterns: - m = re.search(p, head, re.I) + m = re.search(p, head, re.IGNORECASE) if m: return [ Finding( @@ -93,11 +93,11 @@ def check_destructive_command(run: AgentRun) -> list[Finding]: r"\bkubectl\s+delete\b", r":\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:", ] - caution = re.compile(r"\b(?:dry[- ]?run|backup|verify first|check first|confirm first)\b|--dry-run", re.I) + caution = re.compile(r"\b(?:dry[- ]?run|backup|verify first|check first|confirm first)\b|--dry-run", re.IGNORECASE) head = run.result[:1500] for pattern in patterns: - m = re.search(pattern, head, re.I) + m = re.search(pattern, head, re.IGNORECASE) if not m: continue start = max(0, m.start() - 140) @@ -132,7 +132,7 @@ def check_unverified_claim(run: AgentRun) -> list[Finding]: ] hits = [] for p in hedges: - for m in re.finditer(p, run.result, re.I): + for m in re.finditer(p, run.result, re.IGNORECASE): hits.append(_context(run.result, m.start())) if hits: return [ @@ -160,7 +160,7 @@ def check_absence_as_evidence(run: AgentRun) -> list[Finding]: r"\bboard is empty\b", ] for p in patterns: - m = re.search(p, run.result, re.I) + m = re.search(p, run.result, re.IGNORECASE) if m: return [ Finding( @@ -208,7 +208,7 @@ def check_url_without_verification(run: AgentRun) -> list[Finding]: urls = re.findall(r"https?://[^\s)\]<>\"']+", run.result) if len(urls) < 5: return [] - verified = re.search(r"\b(?:verified|confirmed|checked|fetched|HTTP 200|status 200)\b", run.result, re.I) + verified = re.search(r"\b(?:verified|confirmed|checked|fetched|HTTP 200|status 200)\b", run.result, re.IGNORECASE) if not verified: return [ Finding( @@ -236,7 +236,7 @@ def check_prompt_hygiene(run: AgentRun) -> list[Finding]: r"\b(?:output|outputs|return|returns|format|formatted|respond|reply|write|writing|report|" r"summar[iy]|list|table|json|csv|markdown|schema|fields|columns|deliver)\b", run.prompt, - re.I, + re.IGNORECASE, ) # Length alone is not the defect. "Run the suite and report every failing test as node ids @@ -274,7 +274,7 @@ def check_unquantified(run: AgentRun) -> list[Finding]: r"\b(?:count|counts|how many|number of|enumerate|enumerat(?:e|ing)|" r"list (?:every|all|each)|report every|every \w+ as)\b", run.prompt, - re.I, + re.IGNORECASE, ) if not asks_to_count: return [] @@ -289,7 +289,7 @@ def check_unquantified(run: AgentRun) -> list[Finding]: vague = re.search( r"\b(?:several|some|a few|a number of|various|numerous|many|multiple)\b", result, - re.I, + re.IGNORECASE, ) if not vague: return [] diff --git a/agentrace/parse.py b/agentrace/parse.py index 261f2f0..dafff69 100644 --- a/agentrace/parse.py +++ b/agentrace/parse.py @@ -16,10 +16,10 @@ from __future__ import annotations import json +from collections.abc import Iterator from dataclasses import dataclass, field from datetime import UTC, datetime from pathlib import Path -from typing import Iterator @dataclass @@ -77,7 +77,7 @@ def _ts(value: str | None) -> datetime | None: if not value: return None try: - parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + parsed = datetime.fromisoformat(value) # Treat missing offsets as UTC, independently of the reader's local timezone. return parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed except ValueError: diff --git a/tests/test_agentrace.py b/tests/test_agentrace.py index 5386784..ec90a7d 100644 --- a/tests/test_agentrace.py +++ b/tests/test_agentrace.py @@ -11,23 +11,21 @@ from __future__ import annotations import json -from datetime import datetime, timezone - -import pytest +from datetime import UTC, datetime from agentrace.checks import analyse from agentrace.parse import AgentRun, parse_session def _run(result: str = "x" * 500, prompt: str = "y" * 500, **kw) -> AgentRun: - defaults = dict( - tool_use_id="toolu_test123456", - description="test run", - prompt=prompt, - result=result, - started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc), - ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=timezone.utc), - ) + defaults = { + "tool_use_id": "toolu_test123456", + "description": "test run", + "prompt": prompt, + "result": result, + "started_at": datetime(2026, 7, 16, 12, 0, tzinfo=UTC), + "ended_at": datetime(2026, 7, 16, 12, 1, tzinfo=UTC), + } defaults.update(kw) return AgentRun(**defaults) @@ -292,8 +290,8 @@ def test_unquantified_clean_when_result_has_counts(): def test_slow_run_is_flagged(): r = _run( - started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc), - ended_at=datetime(2026, 7, 16, 12, 30, tzinfo=timezone.utc), + started_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC), + ended_at=datetime(2026, 7, 16, 12, 30, tzinfo=UTC), ) assert "slow_run" in _codes(r) diff --git a/tests/test_destructive_command.py b/tests/test_destructive_command.py index 451b624..7ab5d10 100644 --- a/tests/test_destructive_command.py +++ b/tests/test_destructive_command.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from agentrace.checks import analyse from agentrace.parse import AgentRun @@ -10,8 +10,8 @@ def _run(result: str) -> AgentRun: description="test run", prompt="Review the operation and report what should be run.", result=result, - started_at=datetime(2026, 7, 16, 12, 0, tzinfo=timezone.utc), - ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=timezone.utc), + started_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC), + ended_at=datetime(2026, 7, 16, 12, 1, tzinfo=UTC), )