from __future__ import annotations import json from pathlib import Path from typing import Any import yaml from reuse_surface.registry import ( ROOT, level_at_least, load_index, load_index_at, parse_front_matter, parse_vector, registry_paths, ) def _availability_at_most(current: str, maximum: str) -> bool: from reuse_surface.registry import LEVEL_ORDERS order = LEVEL_ORDERS["availability"] return order.index(current) <= order.index(maximum) def cohort_filters_from_args(args: Any) -> dict[str, str | None]: filters: dict[str, str | None] = { "discovery_min": getattr(args, "discovery_min", None), "availability_min": getattr(args, "availability_min", None), "availability_max": getattr(args, "availability_max", None), "domain": getattr(args, "domain", None), } if getattr(args, "planning_min", None): filters["discovery_min"] = args.planning_min filters["availability_max"] = filters["availability_max"] or "A1" if getattr(args, "implementation_min", None): filters["availability_min"] = args.implementation_min return filters def select_cohort( filters: dict[str, str | None], index: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: data = index or load_index() matches: list[dict[str, Any]] = [] for item in data.get("capabilities", []): vector = parse_vector(item["vector"]) if filters.get("discovery_min") and not level_at_least( "discovery", vector["discovery"], filters["discovery_min"] ): continue if filters.get("availability_min") and not level_at_least( "availability", vector["availability"], filters["availability_min"] ): continue if filters.get("availability_max") and not _availability_at_most( vector["availability"], filters["availability_max"] ): continue if filters.get("domain") and item.get("domain") != filters["domain"]: continue matches.append(item) return matches def format_cohort_markdown( matches: list[dict[str, Any]], filters: dict[str, str | None], ) -> str: lines = ["# Capability cohort report", ""] active = {key: value for key, value in filters.items() if value} if active: lines.append("Filters:") for key, value in sorted(active.items()): lines.append(f"- `{key}`: `{value}`") lines.append("") if not matches: lines.append("_No capabilities matched._") return "\n".join(lines) + "\n" lines.append("| ID | Vector | Consumption modes |") lines.append("|---|---|---|") for item in matches: modes = ", ".join(item.get("consumption_modes", [])) lines.append(f"| `{item['id']}` | {item['vector']} | {modes} |") lines.append("") lines.append(f"**{len(matches)}** capabilit{'y' if len(matches) == 1 else 'ies'}.") return "\n".join(lines) + "\n" def format_cohort_json(matches: list[dict[str, Any]], filters: dict[str, str | None]) -> str: payload = { "count": len(matches), "filters": {key: value for key, value in filters.items() if value}, "capabilities": matches, } return json.dumps(payload, indent=2, sort_keys=True) def collect_gap_report( roster_path: Path, *, index: dict[str, Any] | None = None, ) -> dict[str, Any]: roster = yaml.safe_load(roster_path.read_text(encoding="utf-8")) repos = roster.get("repos", []) summary = roster.get("summary", {}) local_index = index or load_index() local_by_owner: dict[str, list[str]] = {} for row in local_index.get("capabilities", []): owner = row.get("owner") or "unknown" local_by_owner.setdefault(owner, []).append(row["id"]) publish_fail = [r for r in repos if r.get("publish_check") == "fail"] empty_scaffolds = [ r for r in repos if r.get("status") == "established" and r.get("capability_count", 0) == 0 ] unclassified = [r for r in empty_scaffolds if r.get("capability_status", "pending") == "pending"] explicit_none = [r for r in empty_scaffolds if r.get("capability_status") == "none"] covered = [r for r in repos if r.get("capability_status") in ("has", "none")] seeded = [r for r in repos if r.get("seed_from_reuse_surface")] dedup_pending = [ { "slug": owner, "local_ids": ids, } for owner, ids in sorted(local_by_owner.items()) if owner not in {"reuse-surface", "unknown"} ] return { "roster_path": str(roster_path), "summary": summary, "publish_fail": [ { "slug": r["slug"], "hub_registered": r.get("hub_registered"), "publish_note": r.get("publish_note"), } for r in publish_fail ], "empty_scaffold_count": len(empty_scaffolds), "empty_scaffolds": [r["slug"] for r in empty_scaffolds], "unclassified_count": len(unclassified), "unclassified": [r["slug"] for r in unclassified], "explicit_none_count": len(explicit_none), "explicit_none": [r["slug"] for r in explicit_none], "coverage_ratio": f"{len(covered)}/{len(repos)}" if repos else "0/0", "seeded_repos": [ { "slug": r["slug"], "seed_capability_ids": r.get("seed_capability_ids", []), "publish_check": r.get("publish_check"), } for r in seeded ], "dedup_pending_local_owners": dedup_pending, "local_capability_count": len(local_index.get("capabilities", [])), } def format_gap_markdown(report: dict[str, Any]) -> str: lines = ["# Registry gap report", ""] lines.append(f"**Roster:** `{report['roster_path']}`") summary = report.get("summary", {}) if summary: lines.append( f"**Workstation:** {summary.get('established', '?')}/" f"{summary.get('total', '?')} established; " f"publish pass {summary.get('publish_pass', '?')}/" f"{summary.get('total', '?')}" ) lines.append("") fails = report.get("publish_fail", []) lines.append(f"## Publish blocked ({len(fails)})") if fails: for item in fails: note = item.get("publish_note") or "" suffix = f" — {note}" if note else "" lines.append(f"- `{item['slug']}`{suffix}") else: lines.append("- none") lines.append("") dedup = report.get("dedup_pending_local_owners", []) lines.append(f"## Local index owner stubs ({len(dedup)})") if dedup: for item in dedup: ids = ", ".join(f"`{cap_id}`" for cap_id in item["local_ids"]) lines.append(f"- **{item['slug']}:** {ids}") else: lines.append("- none (owner rows migrated to canonical repos)") lines.append("") lines.append(f"**Capability coverage:** {report.get('coverage_ratio', '?')} " "(repos with a capability or an explicit no-capability marker)") lines.append("") unclassified = report.get("unclassified", []) lines.append(f"## Unclassified scaffolds ({report.get('unclassified_count', len(unclassified))})") if unclassified: for slug in unclassified: lines.append(f"- `{slug}`") else: lines.append("- none") lines.append("") explicit_none = report.get("explicit_none", []) lines.append(f"## Explicitly no-capability ({report.get('explicit_none_count', len(explicit_none))})") if explicit_none: for slug in explicit_none: lines.append(f"- `{slug}`") else: lines.append("- none") lines.append("") seeded = report.get("seeded_repos", []) lines.append(f"## Seed-ready repos ({len(seeded)})") for item in seeded: publish = item.get("publish_check", "?") lines.append(f"- `{item['slug']}` (publish: {publish})") lines.append("") lines.append( f"**Local reuse-surface capabilities:** {report.get('local_capability_count', 0)}" ) return "\n".join(lines) + "\n" def format_gap_json(report: dict[str, Any]) -> str: return json.dumps(report, indent=2, sort_keys=True) def default_roster_path() -> Path: return ROOT / "registry/federation/local-repo-roster.yaml" def collect_reuse_events( *, repo_root: Path = ROOT, hub_url: str | None = None ) -> list[dict[str, Any]]: """Merges the hub's reuse_events (ecosystem-wide, if reachable) with this repo's local JSONL fallback (covers facts recorded while the hub was down), deduped by (ts, consumer_repo, capability_id, source).""" import urllib.error from reuse_surface import hub_client events: list[dict[str, Any]] = [] try: status, payload = hub_client.hub_list_reuse_events(base_url=hub_url) if status == 200: events.extend(payload.get("events", [])) except (ValueError, urllib.error.URLError, TimeoutError, OSError): pass telemetry_path = repo_root / "registry" / "telemetry" / "plan-check-events.jsonl" if telemetry_path.exists(): for line in telemetry_path.read_text(encoding="utf-8").splitlines(): if line.strip(): events.append(json.loads(line)) seen: set[tuple[Any, ...]] = set() deduped: list[dict[str, Any]] = [] for event in events: key = (event.get("ts"), event.get("consumer_repo"), event.get("capability_id"), event.get("source")) if key not in seen: seen.add(key) deduped.append(event) return deduped def collect_reuse_report( events: list[dict[str, Any]], *, capability_id: str | None = None ) -> dict[str, Any]: """Per-capability consumer counts, outcome breakdown, last-used.""" by_capability: dict[str, dict[str, Any]] = {} for event in events: cap_id = event.get("capability_id") if not cap_id or (capability_id and cap_id != capability_id): continue bucket = by_capability.setdefault( cap_id, {"consumer_repos": set(), "outcomes": {}, "last_used": None}, ) bucket["consumer_repos"].add(event["consumer_repo"]) outcome = event.get("outcome") or "unspecified" bucket["outcomes"][outcome] = bucket["outcomes"].get(outcome, 0) + 1 ts = event.get("ts") if ts and (bucket["last_used"] is None or ts > bucket["last_used"]): bucket["last_used"] = ts rows = [ { "capability_id": cap_id, "consumer_count": len(bucket["consumer_repos"]), "consumer_repos": sorted(bucket["consumer_repos"]), "outcomes": bucket["outcomes"], "last_used": bucket["last_used"], } for cap_id, bucket in sorted(by_capability.items()) ] return {"capabilities": rows, "total_events": len(events)} def format_reuse_report_markdown(report: dict[str, Any]) -> str: lines = ["# Reuse telemetry report", ""] if not report["capabilities"]: lines.append("_No reuse events recorded yet._") return "\n".join(lines) + "\n" for row in report["capabilities"]: outcomes = ", ".join(f"{k}: {v}" for k, v in sorted(row["outcomes"].items())) lines.append(f"## `{row['capability_id']}`") lines.append(f"- Consumers ({row['consumer_count']}): {', '.join(row['consumer_repos'])}") lines.append(f"- Outcomes: {outcomes}") lines.append(f"- Last used: {row['last_used']}") lines.append("") lines.append(f"**{report['total_events']}** total event(s) across **{len(report['capabilities'])}** capability(ies).") return "\n".join(lines) + "\n" def format_reuse_report_json(report: dict[str, Any]) -> str: return json.dumps(report, indent=2, sort_keys=True) def collect_reused_by_suggestions( report: dict[str, Any], *, repo_root: Path = ROOT ) -> list[dict[str, Any]]: """Evidence-gated relation_add suggestions (REUSE-WP-0019-T05): only for capabilities this repo actually owns (in its own index), and only for consumer repos not already listed in relations.reused_by. Suggestions only -- applying them is a separate, explicit step (`report reuse --suggest-relations --apply`), never automatic.""" paths = registry_paths(repo_root) if not paths["index"].exists(): return [] index = load_index_at(paths["index"]) index_by_id = {row["id"]: row for row in index.get("capabilities", [])} suggestions: list[dict[str, Any]] = [] for row in report["capabilities"]: cap_id = row["capability_id"] entry_row = index_by_id.get(cap_id) if not entry_row: continue front_matter = parse_front_matter(repo_root / entry_row["path"]) existing = set(front_matter.get("relations", {}).get("reused_by", [])) for consumer_repo in row["consumer_repos"]: if consumer_repo in existing: continue suggestions.append( { "capability_id": cap_id, "kind": "relation_add", "detail": f"observed reuse by {consumer_repo} (outcomes: {row['outcomes']})", "value": {"type": "reused_by", "target": consumer_repo}, } ) return suggestions