Team Ai
Apppublic

evalstate/diffusers-pr-api

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
pr_search_scope.py704 linesDownload Raw Back to reports
1from __future__ import annotations2 3import json4from collections import defaultdict5from collections.abc import Mapping, Sequence6from dataclasses import asdict7from datetime import UTC, datetime8from pathlib import Path9from typing import Any10 11from slop_farmer.config import PrSearchRefreshOptions12from slop_farmer.data.parquet_io import read_json, read_parquet_rows13from slop_farmer.data.snapshot_source import resolve_snapshot_source_dir14from slop_farmer.reports.pr_heuristics import (15    compile_cluster_suppression_rules,16    suppressed_pull_request_reasons,17)18from slop_farmer.reports.pr_scope import (19    PrScopeCluster,20    PrScopeClusterOptions,21    _build_scope_profile,22    _feature_idf,23    _include_pull_request,24    _normalize_vector,25    _pairwise_comparisons,26    build_pr_scope_clusters,27)28 29FEATURE_VERSION = "pr_scope_v1"30CANDIDATE_FORMULA_VERSION = "scope_cluster_candidate_v1"31DEFAULT_CANDIDATE_LIMIT = 532 33 34def resolve_pr_search_snapshot_dir(options: PrSearchRefreshOptions) -> Path:35    return resolve_snapshot_source_dir(36        snapshot_dir=options.snapshot_dir,37        local_snapshots_root=options.output_dir.resolve() / "snapshots",38        hf_repo_id=options.hf_repo_id,39        hf_revision=options.hf_revision,40        hf_materialize_dir=options.hf_materialize_dir,41        hf_output_dir=options.output_dir,42    )43 44 45def load_pr_search_snapshot(snapshot_dir: Path) -> dict[str, Any]:46    manifest_path = snapshot_dir / "manifest.json"47    manifest = read_json(manifest_path) if manifest_path.exists() else {}48    pull_requests = read_parquet_rows(snapshot_dir / "pull_requests.parquet")49    pr_files = read_parquet_rows(snapshot_dir / "pr_files.parquet")50    contributors = read_parquet_rows(snapshot_dir / "new_contributors.parquet")51    repo = manifest.get("repo") or (pull_requests[0].get("repo") if pull_requests else None) or ""52    snapshot_id = manifest.get("snapshot_id") or snapshot_dir.name53    return {54        "repo": repo,55        "snapshot_id": snapshot_id,56        "manifest": manifest,57        "pull_requests": pull_requests,58        "pr_files": pr_files,59        "contributors": contributors,60    }61 62 63def build_pr_scope_search_artifacts(64    pull_requests: Sequence[Mapping[str, Any]],65    pr_files: Sequence[Mapping[str, Any]],66    *,67    options: PrScopeClusterOptions | None = None,68    suppression_rules: Sequence[Mapping[str, Any]] = (),69    limit_prs: int | None = None,70) -> dict[str, Any]:71    settings = options or PrScopeClusterOptions()72    suppressed_prs = suppressed_pull_request_reasons(73        pull_requests,74        pr_files,75        compile_cluster_suppression_rules(suppression_rules),76    )77    active_prs = [78        row79        for row in pull_requests80        if _include_pull_request(row, settings) and int(row["number"]) not in suppressed_prs81    ]82    active_prs.sort(key=lambda row: int(row["number"]))83    if limit_prs is not None:84        if limit_prs < 1:85            raise ValueError("--limit-prs must be at least 1")86        active_prs = active_prs[:limit_prs]87 88    active_numbers = {int(row["number"]) for row in active_prs if row.get("number") is not None}89    filtered_pr_files = [90        row91        for row in pr_files92        if row.get("pull_request_number") is not None93        and int(row["pull_request_number"]) in active_numbers94    ]95    files_by_pr: defaultdict[int, list[Mapping[str, Any]]] = defaultdict(list)96    for row in filtered_pr_files:97        files_by_pr[int(row["pull_request_number"])].append(row)98 99    profiles = [100        _build_scope_profile(row, files_by_pr.get(int(row["number"]), []), settings)101        for row in active_prs102    ]103    feature_idf = _feature_idf(profiles, settings) if profiles else {}104    for profile in profiles:105        profile.vector = _normalize_vector(106            {107                feature: weight * feature_idf[feature]108                for feature, weight in profile.raw_vector.items()109                if feature in feature_idf110            }111        )112 113    comparisons = _pairwise_comparisons(profiles, settings) if len(profiles) > 1 else []114    comparison_rows = {_pair_key(entry.left, entry.right): entry for entry in comparisons}115    neighbor_rankings = _neighbor_rankings(comparisons, settings)116    clusters = build_pr_scope_clusters(117        active_prs,118        filtered_pr_files,119        options=settings,120        suppression_rules=suppression_rules,121    )122 123    documents = [_document_row(row) for row in active_prs]124    features = [_feature_row(profile) for profile in profiles]125    neighbors = _neighbor_rows(neighbor_rankings)126    cluster_rows = [_cluster_row(cluster) for cluster in clusters]127    cluster_members = _cluster_member_rows(clusters)128    cluster_candidates = _cluster_candidate_rows(129        profiles=profiles,130        comparison_rows=comparison_rows,131        clusters=clusters,132    )133    settings_json = {134        **asdict(settings),135        "feature_version": FEATURE_VERSION,136        "candidate_formula_version": CANDIDATE_FORMULA_VERSION,137    }138    return {139        "documents": documents,140        "features": features,141        "run_artifact": {142            "feature_version": FEATURE_VERSION,143            "idf_json": feature_idf,144        },145        "neighbors": neighbors,146        "clusters": cluster_rows,147        "cluster_members": cluster_members,148        "cluster_candidates": cluster_candidates,149        "settings_json": settings_json,150    }151 152 153def build_scope_feature_idf_for_indexed_documents(154    indexed_documents: Sequence[Mapping[str, Any]],155    pr_files: Sequence[Mapping[str, Any]],156    *,157    options: PrScopeClusterOptions | None = None,158) -> dict[str, float]:159    settings = options or PrScopeClusterOptions()160    indexed_numbers = {161        int(row["pr_number"]) for row in indexed_documents if row.get("pr_number") is not None162    }163    files_by_pr: defaultdict[int, list[Mapping[str, Any]]] = defaultdict(list)164    for row in pr_files:165        pr_number = row.get("pull_request_number")166        if pr_number is None:167            continue168        number = int(pr_number)169        if number in indexed_numbers:170            files_by_pr[number].append(row)171    profiles = [172        _build_scope_profile(173            _document_to_profile_row(row),174            files_by_pr.get(int(row["pr_number"]), []),175            settings,176        )177        for row in indexed_documents178        if row.get("pr_number") is not None179    ]180    return _feature_idf(profiles, settings)181 182 183def build_scope_feature_for_pull_request(184    pr_row: Mapping[str, Any],185    pr_files: Sequence[Mapping[str, Any]],186    *,187    feature_idf: Mapping[str, float],188    options: PrScopeClusterOptions | None = None,189) -> dict[str, Any]:190    settings = options or PrScopeClusterOptions()191    profile = _build_scope_profile(pr_row, pr_files, settings)192    profile.vector = _normalize_vector(193        {194            feature: weight * feature_idf[feature]195            for feature, weight in profile.raw_vector.items()196            if feature in feature_idf197        }198    )199    return _feature_row(profile)200 201 202def rank_scope_feature_matches(203    query_feature: Mapping[str, Any],204    indexed_features: Sequence[Mapping[str, Any]],205    *,206    options: PrScopeClusterOptions | None = None,207    limit: int = 10,208) -> list[dict[str, Any]]:209    settings = options or PrScopeClusterOptions()210    rows: list[dict[str, Any]] = []211    query_pr_number = int(query_feature["pr_number"])212    for feature in indexed_features:213        if int(feature["pr_number"]) == query_pr_number:214            continue215        pair = scope_feature_pair_explanation(query_feature, feature, options=settings)216        if pair["similarity"] < settings.min_similarity:217            continue218        rows.append(pair)219    rows.sort(220        key=lambda row: (221            -float(row["similarity"]),222            -float(row["content_similarity"]),223            int(row["right_pr_number"]),224        )225    )226    return rows[:limit]227 228 229def rank_scope_cluster_candidates(230    *,231    similarity_rows: Sequence[Mapping[str, Any]],232    clusters: Sequence[Mapping[str, Any]],233    cluster_members: Mapping[str, Sequence[int]],234    assigned_cluster_ids: set[str] | None = None,235    limit: int = DEFAULT_CANDIDATE_LIMIT,236) -> list[dict[str, Any]]:237    similarities_by_pr = {238        int(row["right_pr_number"]): row239        for row in similarity_rows240        if row.get("right_pr_number") is not None241    }242    candidate_rows: list[dict[str, Any]] = []243    assigned = assigned_cluster_ids or set()244    for cluster in clusters:245        cluster_id = str(cluster["cluster_id"])246        member_rows = [247            (member_pr_number, similarities_by_pr.get(member_pr_number))248            for member_pr_number in cluster_members.get(cluster_id, ())249        ]250        member_similarities = [251            (member_pr_number, similarity_row)252            for member_pr_number, similarity_row in member_rows253            if similarity_row is not None and float(similarity_row["similarity"]) > 0.0254        ]255        if not member_similarities and cluster_id not in assigned:256            continue257        member_similarities.sort(key=lambda item: (-float(item[1]["similarity"]), item[0]))258        top_similarities = [float(entry["similarity"]) for _, entry in member_similarities[:3]]259        max_member_similarity = top_similarities[0] if top_similarities else 0.0260        avg_top_member_similarity = (261            sum(top_similarities) / len(top_similarities) if top_similarities else 0.0262        )263        matched_member_count = len(member_similarities)264        best_member_pr_number = member_similarities[0][0] if member_similarities else None265        best_match = member_similarities[0][1] if member_similarities else None266        candidate_score = (267            max_member_similarity * 0.60268            + avg_top_member_similarity * 0.30269            + min(matched_member_count, 3) / 3.0 * 0.10270        )271        evidence = {272            "matched_member_pr_numbers": [member for member, _ in member_similarities[:5]],273            "best_member_pr_number": best_member_pr_number,274            "best_shared_filenames": (275                list(best_match["shared_filenames"][:5]) if best_match is not None else []276            ),277            "best_shared_directories": (278                list(best_match["shared_directories"][:5]) if best_match is not None else []279            ),280            "reason": _candidate_reason(281                matched_member_count=matched_member_count,282                best_comparison=best_match,283            ),284        }285        candidate_rows.append(286            {287                "cluster_id": cluster_id,288                "candidate_score": candidate_score,289                "matched_member_count": matched_member_count,290                "best_member_pr_number": best_member_pr_number,291                "max_member_similarity": max_member_similarity,292                "avg_top_member_similarity": avg_top_member_similarity,293                "evidence": evidence,294                "assigned": cluster_id in assigned,295            }296        )297    candidate_rows.sort(298        key=lambda row: (299            -float(row["candidate_score"]),300            -int(row["matched_member_count"]),301            str(row["cluster_id"]),302        )303    )304    for rank, row in enumerate(candidate_rows[:limit], start=1):305        row["candidate_rank"] = rank306    return candidate_rows[:limit]307 308 309def scope_feature_pair_explanation(310    left_feature: Mapping[str, Any],311    right_feature: Mapping[str, Any],312    *,313    options: PrScopeClusterOptions | None = None,314) -> dict[str, Any]:315    settings = options or PrScopeClusterOptions()316    weight_total = (317        settings.content_weight318        + settings.size_weight319        + settings.breadth_weight320        + settings.concentration_weight321    )322    if weight_total <= 0.0:323        raise ValueError("PR scope similarity weights must sum to a positive value.")324 325    left_vector = _json_dict(left_feature.get("vector_json"))326    right_vector = _json_dict(right_feature.get("vector_json"))327    left_filenames = set(_json_list(left_feature.get("filenames_json")))328    right_filenames = set(_json_list(right_feature.get("filenames_json")))329    left_directories = set(_json_list(left_feature.get("directories_json")))330    right_directories = set(_json_list(right_feature.get("directories_json")))331    content_similarity = _cosine_similarity(left_vector, right_vector)332    if (333        content_similarity <= 0.0334        and not left_filenames.intersection(right_filenames)335        and not left_directories.intersection(right_directories)336    ):337        similarity = 0.0338    else:339        size_similarity = _ratio_similarity(340            int(left_feature.get("total_changed_lines") or 0),341            int(right_feature.get("total_changed_lines") or 0),342        )343        breadth_similarity = (344            _ratio_similarity(345                int(left_feature.get("file_count") or 0),346                int(right_feature.get("file_count") or 0),347            )348            + _ratio_similarity(349                int(left_feature.get("directory_count") or 0),350                int(right_feature.get("directory_count") or 0),351            )352        ) / 2.0353        concentration_similarity = max(354            0.0,355            1.0356            - abs(357                float(left_feature.get("dominant_dir_share") or 0.0)358                - float(right_feature.get("dominant_dir_share") or 0.0)359            ),360        )361        similarity = (362            content_similarity * settings.content_weight363            + size_similarity * settings.size_weight364            + breadth_similarity * settings.breadth_weight365            + concentration_similarity * settings.concentration_weight366        ) / weight_total367        return {368            "left_pr_number": int(left_feature["pr_number"]),369            "right_pr_number": int(right_feature["pr_number"]),370            "similarity": similarity,371            "content_similarity": content_similarity,372            "size_similarity": size_similarity,373            "breadth_similarity": breadth_similarity,374            "concentration_similarity": concentration_similarity,375            "shared_filenames": sorted(left_filenames & right_filenames)[:10],376            "shared_directories": sorted(377                left_directories & right_directories,378                key=lambda value: (-value.count("/"), value),379            )[:10],380        }381    return {382        "left_pr_number": int(left_feature["pr_number"]),383        "right_pr_number": int(right_feature["pr_number"]),384        "similarity": similarity,385        "content_similarity": content_similarity,386        "size_similarity": 0.0,387        "breadth_similarity": 0.0,388        "concentration_similarity": 0.0,389        "shared_filenames": [],390        "shared_directories": [],391    }392 393 394def scope_options_from_settings(settings_json: Mapping[str, Any] | None) -> PrScopeClusterOptions:395    if not settings_json:396        return PrScopeClusterOptions()397    defaults = asdict(PrScopeClusterOptions())398    values = {key: settings_json[key] for key in defaults if key in settings_json}399    return PrScopeClusterOptions(**values)400 401 402def iso_timestamp() -> str:403    return datetime.now(tz=UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")404 405 406def _document_row(row: Mapping[str, Any]) -> dict[str, Any]:407    return {408        "pr_number": int(row["number"]),409        "github_id": row.get("github_id"),410        "author_login": row.get("author_login"),411        "state": row.get("state"),412        "draft": bool(row.get("draft")),413        "merged": bool(row.get("merged")),414        "title": row.get("title") or "",415        "base_ref": row.get("base_ref"),416        "created_at": row.get("created_at"),417        "updated_at": row.get("updated_at"),418        "merged_at": row.get("merged_at"),419        "additions": int(row.get("additions") or 0),420        "deletions": int(row.get("deletions") or 0),421        "changed_files": int(row.get("changed_files") or 0),422        "comments_count": int(row.get("comments_count") or 0),423        "review_comments_count": int(row.get("review_comments_count") or 0),424        "html_url": row.get("html_url"),425    }426 427 428def _document_to_profile_row(row: Mapping[str, Any]) -> dict[str, Any]:429    return {430        "number": int(row["pr_number"]),431        "additions": int(row.get("additions") or 0),432        "deletions": int(row.get("deletions") or 0),433        "changed_files": int(row.get("changed_files") or 0),434    }435 436 437def _feature_row(profile: Any) -> dict[str, Any]:438    return {439        "pr_number": profile.number,440        "feature_version": FEATURE_VERSION,441        "total_changed_lines": profile.total_changed_lines,442        "file_count": profile.file_count,443        "directory_count": profile.directory_count,444        "dominant_dir_share": profile.dominant_dir_share,445        "filenames_json": sorted(profile.filenames),446        "directories_json": sorted(profile.directories),447        "vector_json": profile.vector,448    }449 450 451def _neighbor_rankings(452    comparisons: Sequence[Any], options: PrScopeClusterOptions453) -> dict[int, list[dict[str, Any]]]:454    ranked: defaultdict[int, list[tuple[float, int, Any]]] = defaultdict(list)455    for entry in comparisons:456        if entry.similarity < options.min_similarity:457            continue458        ranked[entry.left].append((entry.similarity, entry.right, entry))459        ranked[entry.right].append((entry.similarity, entry.left, entry))460 461    results: dict[int, list[dict[str, Any]]] = {}462    for pr_number, items in ranked.items():463        ordered = sorted(items, key=lambda item: (-item[0], item[1]))[: options.max_neighbors]464        results[pr_number] = [465            {466                "other_pr_number": other_pr_number,467                "rank": rank,468                "comparison": comparison,469            }470            for rank, (_, other_pr_number, comparison) in enumerate(ordered, start=1)471        ]472    return results473 474 475def _neighbor_rows(476    neighbor_rankings: Mapping[int, Sequence[Mapping[str, Any]]],477) -> list[dict[str, Any]]:478    rows: dict[tuple[int, int], dict[str, Any]] = {}479    for pr_number, ranked_neighbors in neighbor_rankings.items():480        for ranked_neighbor in ranked_neighbors:481            comparison = ranked_neighbor["comparison"]482            left_pr = min(pr_number, int(ranked_neighbor["other_pr_number"]))483            right_pr = max(pr_number, int(ranked_neighbor["other_pr_number"]))484            pair_key = (left_pr, right_pr)485            row = rows.get(pair_key)486            if row is None:487                row = {488                    "left_pr_number": left_pr,489                    "right_pr_number": right_pr,490                    "rank_from_left": None,491                    "rank_from_right": None,492                    "similarity": comparison.similarity,493                    "content_similarity": comparison.content_similarity,494                    "size_similarity": comparison.size_similarity,495                    "breadth_similarity": comparison.breadth_similarity,496                    "concentration_similarity": comparison.concentration_similarity,497                    "shared_filenames_json": comparison.shared_filenames,498                    "shared_directories_json": comparison.shared_directories,499                }500                rows[pair_key] = row501            if pr_number == left_pr:502                row["rank_from_left"] = int(ranked_neighbor["rank"])503            else:504                row["rank_from_right"] = int(ranked_neighbor["rank"])505    return [rows[key] for key in sorted(rows)]506 507 508def _cluster_row(cluster: PrScopeCluster) -> dict[str, Any]:509    return {510        "cluster_id": cluster.cluster_id,511        "representative_pr_number": cluster.representative_pr_number,512        "cluster_size": len(cluster.pr_numbers),513        "average_similarity": cluster.average_similarity,514        "summary": cluster.summary,515        "shared_filenames_json": cluster.shared_filenames,516        "shared_directories_json": cluster.shared_directories,517    }518 519 520def _cluster_member_rows(clusters: Sequence[PrScopeCluster]) -> list[dict[str, Any]]:521    rows: list[dict[str, Any]] = []522    for cluster in clusters:523        for pr_number in cluster.pr_numbers:524            rows.append(525                {526                    "cluster_id": cluster.cluster_id,527                    "pr_number": pr_number,528                    "member_role": (529                        "representative"530                        if pr_number == cluster.representative_pr_number531                        else "member"532                    ),533                }534            )535    rows.sort(536        key=lambda row: (537            row["cluster_id"],538            row["member_role"] != "representative",539            row["pr_number"],540        )541    )542    return rows543 544 545def _cluster_candidate_rows(546    *,547    profiles: Sequence[Any],548    comparison_rows: Mapping[tuple[int, int], Any],549    clusters: Sequence[PrScopeCluster],550) -> list[dict[str, Any]]:551    cluster_ids_by_pr: defaultdict[int, set[str]] = defaultdict(set)552    cluster_members: dict[str, list[int]] = {}553    for cluster in clusters:554        cluster_members[cluster.cluster_id] = list(cluster.pr_numbers)555        for pr_number in cluster.pr_numbers:556            cluster_ids_by_pr[pr_number].add(cluster.cluster_id)557 558    rows: list[dict[str, Any]] = []559    for profile in sorted(profiles, key=lambda item: item.number):560        candidates = _cluster_candidates_for_pr(561            pr_number=profile.number,562            comparison_rows=comparison_rows,563            clusters=clusters,564            assigned_cluster_ids=cluster_ids_by_pr.get(profile.number, set()),565            cluster_members=cluster_members,566        )567        rows.extend(candidates)568    return rows569 570 571def _cluster_candidates_for_pr(572    *,573    pr_number: int,574    comparison_rows: Mapping[tuple[int, int], Any],575    clusters: Sequence[PrScopeCluster],576    assigned_cluster_ids: set[str],577    cluster_members: Mapping[str, Sequence[int]],578) -> list[dict[str, Any]]:579    candidate_rows: list[dict[str, Any]] = []580    for cluster in clusters:581        member_similarities: list[tuple[int, Any]] = []582        for member_pr_number in cluster_members[cluster.cluster_id]:583            if member_pr_number == pr_number:584                continue585            comparison = comparison_rows.get(_pair_key(pr_number, member_pr_number))586            if comparison is None or comparison.similarity <= 0.0:587                continue588            member_similarities.append((member_pr_number, comparison))589        if not member_similarities and cluster.cluster_id not in assigned_cluster_ids:590            continue591        member_similarities.sort(key=lambda item: (-item[1].similarity, item[0]))592        top_similarities = [entry.similarity for _, entry in member_similarities[:3]]593        max_member_similarity = top_similarities[0] if top_similarities else 0.0594        avg_top_member_similarity = (595            sum(top_similarities) / len(top_similarities) if top_similarities else 0.0596        )597        matched_member_count = len(member_similarities)598        candidate_score = (599            max_member_similarity * 0.60600            + avg_top_member_similarity * 0.30601            + min(matched_member_count, 3) / 3.0 * 0.10602        )603        best_member_pr_number = member_similarities[0][0] if member_similarities else None604        best_comparison = member_similarities[0][1] if member_similarities else None605        evidence = {606            "matched_member_pr_numbers": [member for member, _ in member_similarities[:5]],607            "best_member_pr_number": best_member_pr_number,608            "best_shared_filenames": (609                list(best_comparison.shared_filenames[:5]) if best_comparison is not None else []610            ),611            "best_shared_directories": (612                list(best_comparison.shared_directories[:5]) if best_comparison is not None else []613            ),614            "reason": _candidate_reason(615                matched_member_count=matched_member_count,616                best_comparison=best_comparison,617            ),618        }619        candidate_rows.append(620            {621                "pr_number": pr_number,622                "cluster_id": cluster.cluster_id,623                "candidate_score": candidate_score,624                "matched_member_count": matched_member_count,625                "best_member_pr_number": best_member_pr_number,626                "max_member_similarity": max_member_similarity,627                "avg_top_member_similarity": avg_top_member_similarity,628                "evidence_json": evidence,629                "assigned": cluster.cluster_id in assigned_cluster_ids,630            }631        )632 633    candidate_rows.sort(634        key=lambda row: (635            -row["candidate_score"],636            -row["matched_member_count"],637            row["cluster_id"],638        )639    )640    for rank, row in enumerate(candidate_rows[:DEFAULT_CANDIDATE_LIMIT], start=1):641        row["candidate_rank"] = rank642    return candidate_rows[:DEFAULT_CANDIDATE_LIMIT]643 644 645def _candidate_reason(*, matched_member_count: int, best_comparison: Any | None) -> str:646    if best_comparison is None:647        return "cluster membership matches existing scope assignment"648    shared_filenames = (649        list(best_comparison.shared_filenames)650        if hasattr(best_comparison, "shared_filenames")651        else list(best_comparison.get("shared_filenames") or [])652    )653    shared_directories = (654        list(best_comparison.shared_directories)655        if hasattr(best_comparison, "shared_directories")656        else list(best_comparison.get("shared_directories") or [])657    )658    if matched_member_count >= 2:659        return "overlapping files and directories with multiple cluster members"660    if shared_filenames:661        return "overlapping changed files with a cluster member"662    if shared_directories:663        return "overlapping directories with a cluster member"664    return "similar change shape to a cluster member"665 666 667def _pair_key(left: int, right: int) -> tuple[int, int]:668    return (left, right) if left <= right else (right, left)669 670 671def _json_dict(raw: Any) -> dict[str, float]:672    if isinstance(raw, dict):673        return {str(key): float(value) for key, value in raw.items()}674    if isinstance(raw, str) and raw:675        payload = json.loads(raw)676        if isinstance(payload, dict):677            return {str(key): float(value) for key, value in payload.items()}678    return {}679 680 681def _json_list(raw: Any) -> list[str]:682    if isinstance(raw, list):683        return [str(item) for item in raw]684    if isinstance(raw, str) and raw:685        payload = json.loads(raw)686        if isinstance(payload, list):687            return [str(item) for item in payload]688    return []689 690 691def _cosine_similarity(left: Mapping[str, float], right: Mapping[str, float]) -> float:692    if not left or not right:693        return 0.0694    if len(left) > len(right):695        left, right = right, left696    return sum(weight * right.get(feature, 0.0) for feature, weight in left.items())697 698 699def _ratio_similarity(left: int, right: int) -> float:700    largest = max(left, right)701    if largest <= 0:702        return 1.0703    return min(left, right) / largest704