evalstate/diffusers-pr-api
0
1from __future__ import annotations2 3import json4from collections import Counter, defaultdict5from datetime import UTC, datetime, timedelta6from pathlib import Path7from typing import Any8 9from slop_farmer.config import DashboardDataOptions10from slop_farmer.data.parquet_io import read_json, read_parquet_rows11from slop_farmer.data.snapshot_paths import (12 ResolvedAnalysisReportPath,13 resolve_default_dashboard_analysis_report,14)15from slop_farmer.data.snapshot_source import resolve_snapshot_source_dir16 17 18def run_dashboard_data(options: DashboardDataOptions) -> Path:19 snapshot_dir = _resolve_snapshot_dir(options)20 manifest = _read_optional_json(snapshot_dir / "manifest.json")21 issues = read_parquet_rows(snapshot_dir / "issues.parquet")22 pull_requests = read_parquet_rows(snapshot_dir / "pull_requests.parquet")23 analysis_path = _resolve_analysis_input(snapshot_dir, options.analysis_input)24 analysis = _read_optional_json(analysis_path.path) if analysis_path is not None else {}25 contributor_report = _read_optional_json(26 options.contributors_input or snapshot_dir / "new-contributors-report.json"27 )28 pr_scope_report = _read_optional_json(29 options.pr_scope_input or snapshot_dir / "pr-scope-clusters.json"30 )31 32 repo = (33 manifest.get("repo")34 or (pull_requests[0]["repo"] if pull_requests else None)35 or (issues[0]["repo"] if issues else None)36 or ""37 )38 snapshot_id = manifest.get("snapshot_id") or snapshot_dir.name39 reference_time = _reference_time(snapshot_id, pull_requests)40 cutoff = reference_time - timedelta(days=options.window_days)41 42 issue_map = {int(row["number"]): row for row in issues if row.get("number") is not None}43 pr_map = {int(row["number"]): row for row in pull_requests if row.get("number") is not None}44 recent_pull_requests = []45 for row in pull_requests:46 created_at = _coerce_datetime(row.get("created_at"))47 if created_at is not None and created_at >= cutoff:48 recent_pull_requests.append(row)49 recent_pull_requests.sort(key=lambda row: row.get("created_at") or "", reverse=True)50 recent_numbers = {51 int(row["number"]) for row in recent_pull_requests if row.get("number") is not None52 }53 54 clusters, memberships = _cluster_rows(analysis, issue_map, pr_map, recent_numbers)55 pr_scope_clusters = _pr_scope_cluster_rows(pr_scope_report, pr_map, recent_numbers)56 contributors = _contributor_rows(contributor_report, recent_pull_requests, memberships)57 prs = _pr_rows(recent_pull_requests, memberships)58 59 summary = {60 "repo": repo,61 "snapshot_id": snapshot_id,62 "generated_at": datetime.now(tz=UTC)63 .replace(microsecond=0)64 .isoformat()65 .replace("+00:00", "Z"),66 "window_days": options.window_days,67 "reference_time": reference_time.isoformat().replace("+00:00", "Z"),68 "pr_count": len(prs),69 "open_pr_count": sum(1 for row in prs if row["state"] == "open"),70 "merged_pr_count": sum(1 for row in prs if row["merged"]),71 "cluster_count": len(clusters),72 "clustered_pr_count": sum(1 for row in prs if row["cluster_id"]),73 "contributor_count": len(contributors),74 "analysis_available": bool(analysis),75 "analysis_source": None if analysis_path is None else analysis_path.source,76 "analysis_variant": None if analysis_path is None else analysis_path.variant,77 "analysis_snapshot_id": (78 None79 if analysis_path is None80 else (81 analysis_path.snapshot_id82 or (83 str(analysis.get("snapshot_id")).strip()84 if analysis.get("snapshot_id") is not None85 else None86 )87 )88 ),89 "analysis_id": None if analysis_path is None else analysis_path.analysis_id,90 "contributors_available": bool(contributor_report),91 "pr_scope_available": bool(pr_scope_report),92 "pr_scope_cluster_count": len(pr_scope_clusters),93 }94 95 output_dir = options.output_dir.resolve()96 output_dir.mkdir(parents=True, exist_ok=True)97 _write_json(summary, output_dir / "summary.json")98 _write_json(clusters, output_dir / "clusters.json")99 _write_json(pr_scope_clusters, output_dir / "pr_scope_clusters.json")100 _write_json(prs, output_dir / "prs.json")101 _write_json(contributors, output_dir / "contributors.json")102 return output_dir103 104 105def _resolve_snapshot_dir(options: DashboardDataOptions) -> Path:106 snapshots_root = (107 options.snapshot_root.resolve()108 if options.snapshot_root is not None109 else (Path("data") / "snapshots").resolve()110 )111 return resolve_snapshot_source_dir(112 snapshot_dir=options.snapshot_dir,113 local_snapshots_root=snapshots_root,114 hf_repo_id=options.hf_repo_id,115 hf_revision=options.hf_revision,116 hf_materialize_dir=options.hf_materialize_dir,117 hf_output_dir=snapshots_root.parent,118 )119 120 121def _resolve_analysis_input(122 snapshot_dir: Path, override_path: Path | None123) -> ResolvedAnalysisReportPath | None:124 if override_path is not None:125 resolved = override_path.resolve()126 if not resolved.exists():127 raise FileNotFoundError(f"Dashboard analysis input not found: {resolved}")128 return ResolvedAnalysisReportPath(129 path=resolved,130 variant=_analysis_variant_for_path(resolved),131 source="override",132 )133 return resolve_default_dashboard_analysis_report(snapshot_dir)134 135 136def _read_optional_json(path: Path) -> dict[str, Any]:137 if path.exists():138 return read_json(path)139 return {}140 141 142def _write_json(payload: Any, path: Path) -> None:143 path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")144 145 146def _reference_time(snapshot_id: str, pull_requests: list[dict[str, Any]]) -> datetime:147 parsed = _parse_snapshot_id(snapshot_id)148 if parsed is not None:149 return parsed150 timestamps = [151 timestamp152 for row in pull_requests153 for timestamp in (154 _coerce_datetime(row.get("updated_at")),155 _coerce_datetime(row.get("created_at")),156 )157 if timestamp is not None158 ]159 if timestamps:160 return max(timestamps)161 return datetime.now(tz=UTC)162 163 164def _parse_snapshot_id(value: str) -> datetime | None:165 try:166 return datetime.strptime(value, "%Y%m%dT%H%M%SZ").replace(tzinfo=UTC)167 except ValueError:168 return None169 170 171def _coerce_datetime(value: Any) -> datetime | None:172 if not value or not isinstance(value, str):173 return None174 try:175 return datetime.fromisoformat(value.replace("Z", "+00:00"))176 except ValueError:177 return None178 179 180def _coerce_int(value: Any) -> int | None:181 if value is None:182 return None183 try:184 return int(value)185 except (TypeError, ValueError):186 return None187 188 189def _excerpt(value: Any, limit: int = 240) -> str | None:190 if not value or not isinstance(value, str):191 return None192 compact = " ".join(value.split())193 if len(compact) <= limit:194 return compact195 return compact[: limit - 1].rstrip() + "…"196 197 198def _analysis_variant_for_path(path: Path) -> str:199 if path.name == "analysis-report-hybrid.json":200 return "hybrid"201 if path.name == "analysis-report.json":202 return "deterministic"203 return "override"204 205 206def _cluster_rows(207 analysis: dict[str, Any],208 issue_map: dict[int, dict[str, Any]],209 pr_map: dict[int, dict[str, Any]],210 recent_numbers: set[int],211) -> tuple[list[dict[str, Any]], dict[int, list[dict[str, str]]]]:212 rows: list[dict[str, Any]] = []213 memberships: dict[int, list[dict[str, str]]] = defaultdict(list)214 for cluster in analysis.get("meta_bugs", []):215 pr_numbers = [_coerce_int(value) for value in cluster.get("pr_numbers", [])]216 pr_numbers = [value for value in pr_numbers if value is not None]217 recent_pr_numbers = [number for number in pr_numbers if number in recent_numbers]218 outside_window_pr_numbers = [219 number for number in pr_numbers if number not in recent_numbers220 ]221 if not recent_pr_numbers:222 continue223 canonical_pr_number = _coerce_int(cluster.get("canonical_pr_number"))224 canonical_issue_number = _coerce_int(cluster.get("canonical_issue_number"))225 cluster_id = str(cluster.get("cluster_id") or f"cluster-{recent_pr_numbers[0]}")226 title = _cluster_title(227 cluster, issue_map, pr_map, canonical_issue_number, canonical_pr_number228 )229 recent_authors = sorted(230 {231 str(pr_map[number].get("author_login"))232 for number in recent_pr_numbers233 if number in pr_map and pr_map[number].get("author_login")234 }235 )236 last_activity_at = max(237 (238 pr_map[number].get("updated_at") or pr_map[number].get("created_at")239 for number in recent_pr_numbers240 if number in pr_map241 ),242 default=None,243 )244 row = {245 "cluster_id": cluster_id,246 "title": title,247 "summary": cluster.get("summary"),248 "status": cluster.get("status"),249 "confidence": cluster.get("confidence"),250 "canonical_issue_number": canonical_issue_number,251 "canonical_pr_number": canonical_pr_number,252 "issue_numbers": [253 _coerce_int(value)254 for value in cluster.get("issue_numbers", [])255 if _coerce_int(value) is not None256 ],257 "pr_numbers": pr_numbers,258 "recent_pr_numbers": recent_pr_numbers,259 "pr_count": len(pr_numbers),260 "recent_pr_count": len(recent_pr_numbers),261 "outside_window_prs": [262 _pr_member_stub(number, pr_map.get(number, {}))263 for number in outside_window_pr_numbers264 ],265 "authors": recent_authors,266 "last_activity_at": last_activity_at,267 "evidence_types": list(cluster.get("evidence_types", [])),268 "pr_similarity": _cluster_similarity_map(cluster, canonical_pr_number),269 "pairwise_similarity": _cluster_pairwise_similarity(cluster),270 "github_url": _cluster_github_url(271 issue_map, pr_map, canonical_issue_number, canonical_pr_number272 ),273 }274 rows.append(row)275 for number in recent_pr_numbers:276 role = "canonical" if canonical_pr_number == number else "member"277 memberships[number].append({"cluster_id": cluster_id, "role": role})278 rows.sort(279 key=lambda row: (280 -int(row["recent_pr_count"]),281 -int(row["pr_count"]),282 -(float(row["confidence"]) if row["confidence"] is not None else 0.0),283 row["last_activity_at"] or "",284 ),285 reverse=False,286 )287 return rows, memberships288 289 290def _cluster_title(291 cluster: dict[str, Any],292 issue_map: dict[int, dict[str, Any]],293 pr_map: dict[int, dict[str, Any]],294 canonical_issue_number: int | None,295 canonical_pr_number: int | None,296) -> str:297 if canonical_issue_number is not None and canonical_issue_number in issue_map:298 return str(299 issue_map[canonical_issue_number].get("title") or f"Issue #{canonical_issue_number}"300 )301 if canonical_pr_number is not None and canonical_pr_number in pr_map:302 return str(pr_map[canonical_pr_number].get("title") or f"PR #{canonical_pr_number}")303 summary = cluster.get("summary")304 if summary:305 return str(summary)306 cluster_id = cluster.get("cluster_id") or "cluster"307 return str(cluster_id)308 309 310def _cluster_github_url(311 issue_map: dict[int, dict[str, Any]],312 pr_map: dict[int, dict[str, Any]],313 canonical_issue_number: int | None,314 canonical_pr_number: int | None,315) -> str | None:316 if canonical_issue_number is not None and canonical_issue_number in issue_map:317 return issue_map[canonical_issue_number].get("html_url")318 if canonical_pr_number is not None and canonical_pr_number in pr_map:319 return pr_map[canonical_pr_number].get("html_url")320 return None321 322 323def _cluster_similarity_map(324 cluster: dict[str, Any], canonical_pr_number: int | None325) -> dict[str, dict[str, float]]:326 if canonical_pr_number is None:327 return {}328 scores: dict[str, dict[str, float]] = {}329 for comparison in cluster.get("pr_comparisons", []):330 left = _coerce_int(comparison.get("left_pr_number"))331 right = _coerce_int(comparison.get("right_pr_number"))332 if left != canonical_pr_number and right != canonical_pr_number:333 continue334 other = right if left == canonical_pr_number else left335 if other is None:336 continue337 scores[str(other)] = {338 "patch_similarity": float(comparison.get("patch_similarity") or 0.0),339 "code_similarity": float(comparison.get("code_similarity") or 0.0),340 "size_similarity": float(comparison.get("size_similarity") or 0.0),341 "file_overlap": float(comparison.get("file_overlap") or 0.0),342 "area_overlap": float(comparison.get("area_overlap") or 0.0),343 }344 return scores345 346 347def _cluster_pairwise_similarity(cluster: dict[str, Any]) -> list[dict[str, Any]]:348 rows: list[dict[str, Any]] = []349 for comparison in cluster.get("pr_comparisons", []):350 left = _coerce_int(comparison.get("left_pr_number"))351 right = _coerce_int(comparison.get("right_pr_number"))352 if left is None or right is None:353 continue354 rows.append(355 {356 "left_pr_number": left,357 "right_pr_number": right,358 "patch_similarity": float(comparison.get("patch_similarity") or 0.0),359 "code_similarity": float(comparison.get("code_similarity") or 0.0),360 "size_similarity": float(comparison.get("size_similarity") or 0.0),361 "file_overlap": float(comparison.get("file_overlap") or 0.0),362 "area_overlap": float(comparison.get("area_overlap") or 0.0),363 }364 )365 return rows366 367 368def _pr_scope_cluster_rows(369 pr_scope_report: dict[str, Any],370 pr_map: dict[int, dict[str, Any]],371 recent_numbers: set[int],372) -> list[dict[str, Any]]:373 rows: list[dict[str, Any]] = []374 for cluster in pr_scope_report.get("pr_scope_clusters", []):375 pr_numbers = [_coerce_int(value) for value in cluster.get("pr_numbers", [])]376 pr_numbers = [value for value in pr_numbers if value is not None]377 recent_pr_numbers = [number for number in pr_numbers if number in recent_numbers]378 outside_window_pr_numbers = [379 number for number in pr_numbers if number not in recent_numbers380 ]381 if not recent_pr_numbers:382 continue383 representative_pr_number = _coerce_int(cluster.get("representative_pr_number"))384 recent_authors = sorted(385 {386 str(pr_map[number].get("author_login"))387 for number in recent_pr_numbers388 if number in pr_map and pr_map[number].get("author_login")389 }390 )391 last_activity_at = max(392 (393 pr_map[number].get("updated_at") or pr_map[number].get("created_at")394 for number in recent_pr_numbers395 if number in pr_map396 ),397 default=None,398 )399 representative = pr_map.get(representative_pr_number or -1, {})400 rows.append(401 {402 "kind": "pr_scope",403 "cluster_id": str(cluster.get("cluster_id") or f"pr-scope-{recent_pr_numbers[0]}"),404 "title": _pr_scope_title(cluster, pr_map, representative_pr_number),405 "summary": cluster.get("summary"),406 "representative_pr_number": representative_pr_number,407 "representative_title": representative.get("title"),408 "representative_url": representative.get("html_url"),409 "pr_numbers": pr_numbers,410 "recent_pr_numbers": recent_pr_numbers,411 "pr_count": len(pr_numbers),412 "recent_pr_count": len(recent_pr_numbers),413 "outside_window_prs": [414 _pr_member_stub(number, pr_map.get(number, {}))415 for number in outside_window_pr_numbers416 ],417 "authors": recent_authors,418 "last_activity_at": last_activity_at,419 "average_similarity": float(cluster.get("average_similarity") or 0.0),420 "shared_filenames": list(cluster.get("shared_filenames") or []),421 "shared_directories": list(cluster.get("shared_directories") or []),422 "pairwise": _pr_scope_pairwise_rows(cluster),423 }424 )425 rows.sort(426 key=lambda row: (427 -int(row["recent_pr_count"]),428 -int(row["pr_count"]),429 -(float(row["average_similarity"]) if row["average_similarity"] is not None else 0.0),430 row["last_activity_at"] or "",431 str(row["cluster_id"]),432 )433 )434 return rows435 436 437def _pr_scope_title(438 cluster: dict[str, Any],439 pr_map: dict[int, dict[str, Any]],440 representative_pr_number: int | None,441) -> str:442 if representative_pr_number is not None and representative_pr_number in pr_map:443 title = pr_map[representative_pr_number].get("title")444 if title:445 return f"Scope: {title}"446 shared_filenames = [str(value) for value in (cluster.get("shared_filenames") or []) if value]447 if shared_filenames:448 return f"Scope: {shared_filenames[0]}"449 shared_directories = [450 str(value) for value in (cluster.get("shared_directories") or []) if value451 ]452 if shared_directories:453 return f"Scope: {shared_directories[0]}"454 summary = cluster.get("summary")455 if summary:456 return str(summary)457 return str(cluster.get("cluster_id") or "pr-scope")458 459 460def _pr_scope_pairwise_rows(cluster: dict[str, Any]) -> list[dict[str, Any]]:461 rows: list[dict[str, Any]] = []462 for comparison in cluster.get("pairwise", []):463 left = _coerce_int(comparison.get("left_pr_number"))464 right = _coerce_int(comparison.get("right_pr_number"))465 if left is None or right is None:466 continue467 rows.append(468 {469 "left_pr_number": left,470 "right_pr_number": right,471 "similarity": float(comparison.get("similarity") or 0.0),472 "content_similarity": float(comparison.get("content_similarity") or 0.0),473 "size_similarity": float(comparison.get("size_similarity") or 0.0),474 "breadth_similarity": float(comparison.get("breadth_similarity") or 0.0),475 "concentration_similarity": float(476 comparison.get("concentration_similarity") or 0.0477 ),478 "shared_filenames": list(comparison.get("shared_filenames") or []),479 "shared_directories": list(comparison.get("shared_directories") or []),480 }481 )482 return rows483 484 485def _pr_member_stub(number: int, row: dict[str, Any]) -> dict[str, Any]:486 html_url = row.get("html_url")487 return {488 "number": number,489 "title": row.get("title"),490 "author": row.get("author_login"),491 "state": row.get("state"),492 "merged": bool(row.get("merged")),493 "draft": bool(row.get("draft")),494 "created_at": row.get("created_at"),495 "updated_at": row.get("updated_at"),496 "changed_files": _coerce_int(row.get("changed_files")),497 "additions": _coerce_int(row.get("additions")),498 "deletions": _coerce_int(row.get("deletions")),499 "html_url": html_url,500 "files_url": f"{html_url}/files" if html_url else None,501 }502 503 504def _pr_rows(505 pull_requests: list[dict[str, Any]],506 memberships: dict[int, list[dict[str, str]]],507) -> list[dict[str, Any]]:508 rows = []509 for row in pull_requests:510 number = _coerce_int(row.get("number"))511 if number is None:512 continue513 cluster_memberships = memberships.get(number, [])514 primary_membership = cluster_memberships[0] if cluster_memberships else None515 html_url = row.get("html_url")516 rows.append(517 {518 "number": number,519 "title": row.get("title"),520 "author": row.get("author_login"),521 "state": row.get("state"),522 "author_association": row.get("author_association"),523 "merged": bool(row.get("merged")),524 "draft": bool(row.get("draft")),525 "created_at": row.get("created_at"),526 "updated_at": row.get("updated_at"),527 "changed_files": _coerce_int(row.get("changed_files")),528 "additions": _coerce_int(row.get("additions")),529 "deletions": _coerce_int(row.get("deletions")),530 "comments_count": _coerce_int(row.get("comments_count")),531 "review_comments_count": _coerce_int(row.get("review_comments_count")),532 "labels": list(row.get("labels") or []),533 "body_excerpt": _excerpt(row.get("body")),534 "cluster_id": primary_membership["cluster_id"] if primary_membership else None,535 "cluster_role": primary_membership["role"] if primary_membership else None,536 "cluster_ids": [membership["cluster_id"] for membership in cluster_memberships],537 "html_url": html_url,538 "files_url": f"{html_url}/files" if html_url else None,539 "conversation_url": html_url,540 }541 )542 return rows543 544 545def _contributor_rows(546 contributor_report: dict[str, Any],547 pull_requests: list[dict[str, Any]],548 memberships: dict[int, list[dict[str, str]]],549) -> list[dict[str, Any]]:550 recent_pr_counts = Counter(551 str(row.get("author_login")) for row in pull_requests if row.get("author_login")552 )553 recent_associations = _recent_repo_associations(pull_requests)554 recent_cluster_counts = Counter(555 str(row.get("author_login"))556 for row in pull_requests557 if row.get("author_login")558 for _membership in memberships.get(_coerce_int(row.get("number")) or -1, [])559 )560 report_rows = contributor_report.get("contributors", [])561 if not report_rows:562 rows = [563 {564 "author": author,565 "name": None,566 "profile_url": f"https://github.com/{author}",567 "repo_pull_requests_url": None,568 "repo_issues_url": None,569 "snapshot_pr_count": count,570 "snapshot_issue_count": 0,571 "recent_pr_count": count,572 "cluster_count": recent_cluster_counts.get(author, 0),573 "repo_association": recent_associations.get(author),574 "new_to_repo": None,575 "first_seen_in_snapshot": None,576 "report_reason": None,577 "known_contributor": _is_known_repo_association(recent_associations.get(author)),578 "follow_through_score": None,579 "breadth_score": None,580 "automation_risk_signal": None,581 "heuristic_note": None,582 "account_age_days": None,583 "quality_score": None,584 "public_pr_count_42d": None,585 "public_repo_count_42d": None,586 }587 for author, count in recent_pr_counts.items()588 ]589 rows.sort(key=lambda row: (-int(row["recent_pr_count"]), row["author"]))590 return rows591 592 rows = []593 for contributor in report_rows:594 author = contributor.get("author_login")595 if not author:596 continue597 recent_pr_count = recent_pr_counts.get(str(author), 0)598 if recent_pr_count == 0 and not contributor.get("snapshot_pr_count"):599 continue600 rows.append(601 {602 "author": author,603 "name": contributor.get("name"),604 "profile_url": contributor.get("profile_url"),605 "repo_pull_requests_url": contributor.get("repo_pull_requests_url"),606 "repo_issues_url": contributor.get("repo_issues_url"),607 "snapshot_pr_count": _coerce_int(contributor.get("snapshot_pr_count")) or 0,608 "snapshot_issue_count": _coerce_int(contributor.get("snapshot_issue_count")) or 0,609 "recent_pr_count": recent_pr_count,610 "cluster_count": recent_cluster_counts.get(str(author), 0),611 "repo_association": contributor.get("repo_association")612 or recent_associations.get(str(author)),613 "new_to_repo": contributor.get("new_to_repo"),614 "first_seen_in_snapshot": contributor.get("first_seen_in_snapshot"),615 "report_reason": contributor.get("report_reason"),616 "known_contributor": _known_contributor(contributor),617 "follow_through_score": contributor.get("follow_through_score"),618 "breadth_score": contributor.get("breadth_score"),619 "automation_risk_signal": contributor.get("automation_risk_signal"),620 "heuristic_note": contributor.get("heuristic_note"),621 "account_age_days": _coerce_int(contributor.get("account_age_days")),622 "quality_score": None,623 "public_pr_count_42d": _coerce_int(624 (contributor.get("activity") or {}).get("visible_authored_pr_count")625 ),626 "public_repo_count_42d": _coerce_int(627 (contributor.get("activity") or {}).get("distinct_repos_with_authored_prs")628 ),629 }630 )631 rows.sort(632 key=lambda row: (633 -int(row["recent_pr_count"]),634 -int(row["snapshot_pr_count"]),635 -int(row["cluster_count"]),636 str(row["author"]),637 )638 )639 return rows640 641 642def _known_contributor(contributor: dict[str, Any]) -> bool:643 return _is_known_repo_association(contributor.get("repo_association"))644 645 646def _recent_repo_associations(pull_requests: list[dict[str, Any]]) -> dict[str, str | None]:647 grouped: dict[str, set[str]] = defaultdict(set)648 for row in pull_requests:649 login = str(row.get("author_login") or "").strip()650 association = str(row.get("author_association") or "").strip()651 if not login or not association:652 continue653 grouped[login].add(association)654 return {login: _select_repo_association(sorted(values)) for login, values in grouped.items()}655 656 657def _select_repo_association(values: list[str]) -> str | None:658 if not values:659 return None660 priority = {661 "OWNER": 70,662 "MEMBER": 60,663 "COLLABORATOR": 50,664 "CONTRIBUTOR": 40,665 "FIRST_TIME_CONTRIBUTOR": 30,666 "FIRST_TIMER": 20,667 "NONE": 10,668 }669 return max(values, key=lambda value: (priority.get(value, 0), value))670 671 672def _is_known_repo_association(value: Any) -> bool:673 return str(value or "") in {"OWNER", "MEMBER", "COLLABORATOR"}674 