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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ jobs:
- name: Monitor issues and pull requests
env:
GITHUB_TOKEN: ${{ github.token }}
run: uv run ml4t-ecosystem monitor --output status/monitor.json
# Exit 1 means advisory findings; exit 2 means incomplete evidence.
run: uv run ml4t-ecosystem monitor --output status/monitor.json || test "$?" -eq 1
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ User-visible changes and defect fixes do.

## Issue and Pull-Request Monitoring

The ecosystem monitor covers all open issues and pull requests in the seven repositories.
The shared queue monitor covers all eleven repositories in `config/maintained-repositories.toml`.
It uses full owner/name identities, records actual observation times and PR revisions, and marks
incomplete API evidence explicitly. Stable qualification remains limited to the seven current
libraries in `config/libraries.toml`. Individual maintenance agents own review and executed
verification under their repository's native checks. Recommendations require maintainer approval.

- Classify each new item within one hour using the shared type, priority, status, affected-version,
and compatibility-impact vocabulary.
Expand Down
35 changes: 35 additions & 0 deletions config/maintained-repositories.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
schema_version = 1

[[repositories]]
key = "data"
repository = "ml4t/data"
[[repositories]]
key = "engineer"
repository = "ml4t/engineer"
[[repositories]]
key = "backtest"
repository = "ml4t/backtest"
[[repositories]]
key = "specs"
repository = "ml4t/specs"
[[repositories]]
key = "live"
repository = "ml4t/live"
[[repositories]]
key = "diagnostic"
repository = "ml4t/diagnostic"
[[repositories]]
key = "models"
repository = "ml4t/models"
[[repositories]]
key = "zipline-reloaded"
repository = "stefan-jansen/zipline-reloaded"
[[repositories]]
key = "pyfolio-reloaded"
repository = "stefan-jansen/pyfolio-reloaded"
[[repositories]]
key = "alphalens-reloaded"
repository = "stefan-jansen/alphalens-reloaded"
[[repositories]]
key = "empyrical-reloaded"
repository = "stefan-jansen/empyrical-reloaded"
14 changes: 14 additions & 0 deletions docs/standards/issues-and-pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ a substantive response or applies `status: pending-review` and states the next r

Library-specific labels may extend the vocabulary but may not redefine shared labels.

`uv run ml4t-ecosystem monitor --output status/monitor.json` collects the full open queues in
`config/maintained-repositories.toml`, using each repository's explicit GitHub owner and name.
This maintenance inventory includes the seven current libraries and four Reloaded libraries;
`config/libraries.toml` continues to define only the seven-library qualification scope.
The existing audit workflow runs nominally hourly. Each report records when evidence was actually
collected, complete paginated queues, and current PR head/base revisions. Collection is metadata
inspection, not a code review or executed verification.

The command exits zero when collection is complete with no response-target findings, one when
findings need attention, and two when API evidence is incomplete. An incomplete repository must not
be interpreted as an empty queue or used to infer that an item closed. The report retains evidence
errors alongside successfully collected repositories. Downstream advisory tools must key items by
full repository identity and number, and re-evaluate a changed PR revision before reusing a review.

## Implementation

Every non-automated user-visible change and defect fix has an associated issue. Its pull request uses
Expand Down
16 changes: 12 additions & 4 deletions src/ml4t_ecosystem/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ml4t_ecosystem.clients import DocumentationClient, GitHubClient, PyPIClient
from ml4t_ecosystem.config import load_config
from ml4t_ecosystem.labels import load_labels, sync_labels
from ml4t_ecosystem.monitor import monitor_all
from ml4t_ecosystem.maintenance import collect_queues, load_maintained_repositories
from ml4t_ecosystem.status import write_current_status
from ml4t_ecosystem.templates import sync_templates
from ml4t_ecosystem.workspaces import audit_workspaces
Expand Down Expand Up @@ -43,6 +43,12 @@ def _parser() -> argparse.ArgumentParser:

monitor = subparsers.add_parser("monitor", help="check issue and pull-request response targets")
monitor.add_argument("--output", type=Path)
monitor.add_argument(
"--inventory",
type=Path,
default=ROOT / "config/maintained-repositories.toml",
help="maintained owner/name inventory, separate from stable qualification",
)

snapshot = subparsers.add_parser("snapshot", help="retain validated current status")
snapshot.add_argument("--status-dir", type=Path, default=ROOT / "status")
Expand Down Expand Up @@ -81,12 +87,12 @@ def _monitor(args: argparse.Namespace) -> int:
raise SystemExit("GITHUB_TOKEN is required for accurate maintainer-response monitoring")
github = GitHubClient(token=token)
try:
findings = monitor_all(config, github)
report = collect_queues(config, load_maintained_repositories(args.inventory), github)
finally:
github.close()
payload = (
json.dumps(
{"schema_version": 1, "findings": [finding.to_dict() for finding in findings]},
report,
indent=2,
sort_keys=True,
)
Expand All @@ -97,7 +103,9 @@ def _monitor(args: argparse.Namespace) -> int:
args.output.write_text(payload, encoding="utf-8")
else:
print(payload, end="")
return 1 if findings else 0
if not report["complete"]:
return 2
return 1 if report["findings"] else 0


def _audit_workspaces(args: argparse.Namespace) -> int:
Expand Down
70 changes: 58 additions & 12 deletions src/ml4t_ecosystem/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import base64
import re
from typing import Any, Protocol

import httpx
Expand Down Expand Up @@ -50,6 +51,12 @@ def issue_comments(self, owner: str, repository: str, number: int) -> list[dict[
def pull_reviews(self, owner: str, repository: str, number: int) -> list[dict[str, Any]]: ...


class QueueGitHub(MonitorGitHub, Protocol):
"""Response monitoring plus the revisions required for owner review tasks."""

def pull_request(self, owner: str, repository: str, number: int) -> dict[str, Any]: ...


class GitHubClient:
"""Minimal read client for GitHub repository evidence."""

Expand Down Expand Up @@ -150,33 +157,72 @@ def private_vulnerability_reporting(self, owner: str, repository: str) -> bool |

def open_issues(self, owner: str, repository: str) -> list[dict[str, Any]]:
"""Return open issues and pull requests from the issues endpoint."""
result = self.get_json(
return self._pages(
f"/repos/{owner}/{repository}/issues",
params={"state": "open", "per_page": "100", "sort": "created"},
)
if not isinstance(result, list):
raise EvidenceError("GitHub issues response was not a list")
return [item for item in result if isinstance(item, dict)]

def issue_comments(self, owner: str, repository: str, number: int) -> list[dict[str, Any]]:
"""Return comments for an issue or pull request."""
result = self.get_json(
return self._pages(
f"/repos/{owner}/{repository}/issues/{number}/comments",
params={"per_page": "100"},
)
if not isinstance(result, list):
raise EvidenceError("GitHub comments response was not a list")
return [item for item in result if isinstance(item, dict)]

def pull_reviews(self, owner: str, repository: str, number: int) -> list[dict[str, Any]]:
"""Return reviews for a pull request."""
result = self.get_json(
return self._pages(
f"/repos/{owner}/{repository}/pulls/{number}/reviews",
params={"per_page": "100"},
)
if not isinstance(result, list):
raise EvidenceError("GitHub pull reviews response was not a list")
return [item for item in result if isinstance(item, dict)]

def _pages(self, path: str, *, params: dict[str, str]) -> list[dict[str, Any]]:
url = str(self._client.base_url.join(path).copy_merge_params(params))
visited: set[str] = set()
items: list[dict[str, Any]] = []
resource = "/" + "/".join(path.split("/")[4:])
canonical_path: str | None = None
while url:
target = httpx.URL(url)
valid_path = target.path == path or target.path == canonical_path
if (
not valid_path
and canonical_path is None
and re.fullmatch(r"/repositories/[0-9]+" + re.escape(resource), target.path)
):
canonical_path = target.path
valid_path = True
if (
(target.scheme, target.host, target.port) != ("https", "api.github.com", None)
or not valid_path
or url in visited
):
raise EvidenceError(f"GitHub {path} returned an invalid pagination link")
visited.add(url)
try:
response = self._client.get(url)
response.raise_for_status()
page = response.json()
except (httpx.HTTPError, ValueError) as error:
raise EvidenceError(f"GitHub {path} page unavailable: {error}") from error
if not isinstance(page, list) or any(not isinstance(item, dict) for item in page):
raise EvidenceError(
f"GitHub {path.rsplit('/', 1)[-1]} response was not a list of objects"
)
items.extend(page)
url = response.links.get("next", {}).get("url", "")
return items

def pull_request(self, owner: str, repository: str, number: int) -> dict[str, Any]:
"""Return current pull-request revisions and material status."""
path = f"/repos/{owner}/{repository}/pulls/{number}"
try:
result = self.get_json(path)
except (httpx.HTTPError, ValueError) as error:
raise EvidenceError(f"GitHub {path} unavailable: {error}") from error
if not isinstance(result, dict):
raise EvidenceError(f"GitHub {path} response was not an object")
return result


class PyPIClient:
Expand Down
Loading
Loading