reuse-surface/reuse_surface/reports.py
tegwick bca7165e02
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
ci / validate-registry (push) Has been cancelled
Build and Publish Container Image / build-and-push (push) Successful in 1m23s
REUSE-WP-0019-T05: reuse telemetry aggregation into R-axis evidence
reuse_surface/reports.py: collect_reuse_events() merges the hub's
GET /v1/reuse-events (if reachable) with this repo's local JSONL fallback,
deduped. collect_reuse_report() aggregates per-capability consumer counts,
outcome breakdown, and last-used. collect_reused_by_suggestions() proposes
evidence-gated relation_add patches -- only for capabilities this repo
owns, only for consumer repos not already listed -- reusing the existing
patches.py:apply_patches mechanism (relation_add already isn't in
SAFE_DETERMINISTIC_KINDS, so it was already never auto-applied by
maintain --auto).

New CLI: reuse-surface report reuse [--capability-id] [--format]
[--suggest-relations] [--apply]. --apply requires --suggest-relations and
is the only thing that writes -- nothing happens automatically from
telemetry alone.

schemas/capability.schema.yaml: added relations.reused_by as a new
repoSlugList type, distinct from the existing capability-id relations,
since reused-by targets are consumer repo slugs.

specs/CapabilityMaturityStandard.md Sec8.9: what observed-reuse evidence
counts toward R2->R3 (single corroborating consumer) vs R3->R4+ (multiple
independent consumers) and what it never substitutes for.

19 new pytest cases, 162 total pass. Live-verified with synthetic local
events against a real capability entry: --suggest-relations --apply
correctly wrote relations.reused_by via the real apply_patches path
(reverted after, since it was a smoke test).

Deliberately deferred: surfacing consumer counts in the catalog/graph --
graph.py's relation model is capability-to-capability edges, a different
namespace than repo-slug reused_by targets; catalog.py doesn't currently
parse full front matter per entry. Left for a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 22:47:51 +02:00

362 lines
No EOL
13 KiB
Python

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