generated from coulomb/repo-seed
REUSE-WP-0015: dedup owner entries, add report gaps (T02/T03/T05)
Some checks failed
ci / validate-registry (push) Has been cancelled
Some checks failed
ci / validate-registry (push) Has been cancelled
Remove 17 owner-migrated capabilities from reuse-surface index (keep activity-core stub). Add report gaps CLI, roster stats + gaps CI steps. T01 remains operator-blocked on Gitea publish.
This commit is contained in:
@@ -22,8 +22,12 @@ from reuse_surface.hub_sync import (
|
||||
from reuse_surface.overlaps import find_overlaps
|
||||
from reuse_surface.reports import (
|
||||
cohort_filters_from_args,
|
||||
collect_gap_report,
|
||||
default_roster_path,
|
||||
format_cohort_json,
|
||||
format_cohort_markdown,
|
||||
format_gap_json,
|
||||
format_gap_markdown,
|
||||
select_cohort,
|
||||
)
|
||||
from reuse_surface.establish import (
|
||||
@@ -520,6 +524,19 @@ def cmd_report_cohorts(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_report_gaps(args: argparse.Namespace) -> int:
|
||||
roster_path = Path(args.roster).resolve() if args.roster else default_roster_path()
|
||||
if not roster_path.exists():
|
||||
print(f"error: roster not found: {roster_path}", file=sys.stderr)
|
||||
return 1
|
||||
report = collect_gap_report(roster_path)
|
||||
if args.format == "json":
|
||||
print(format_gap_json(report))
|
||||
else:
|
||||
print(format_gap_markdown(report), end="")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_export(args: argparse.Namespace) -> int:
|
||||
index = load_index()
|
||||
bundle: dict[str, Any] = {
|
||||
@@ -724,6 +741,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
cohorts.set_defaults(func=cmd_report_cohorts)
|
||||
|
||||
gaps = report_sub.add_parser(
|
||||
"gaps",
|
||||
help="roster publish blockers, empty scaffolds, and dedup stubs",
|
||||
)
|
||||
gaps.add_argument(
|
||||
"--roster",
|
||||
help="workstation roster YAML (default: registry/federation/local-repo-roster.yaml)",
|
||||
)
|
||||
gaps.add_argument(
|
||||
"--format",
|
||||
choices=["markdown", "json"],
|
||||
default="markdown",
|
||||
)
|
||||
gaps.set_defaults(func=cmd_report_gaps)
|
||||
|
||||
stats = subparsers.add_parser("stats", help="registry maturity and federation stats")
|
||||
stats.add_argument("--path", help="repo root (default: cwd)")
|
||||
stats.add_argument(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from reuse_surface.registry import level_at_least, load_index, parse_vector
|
||||
import yaml
|
||||
|
||||
from reuse_surface.registry import ROOT, level_at_least, load_index, parse_vector
|
||||
|
||||
|
||||
def _availability_at_most(current: str, maximum: str) -> bool:
|
||||
@@ -84,4 +87,125 @@ def format_cohort_json(matches: list[dict[str, Any]], filters: dict[str, str | N
|
||||
"filters": {key: value for key, value in filters.items() if value},
|
||||
"capabilities": matches,
|
||||
}
|
||||
return json.dumps(payload, indent=2, sort_keys=True)
|
||||
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
|
||||
]
|
||||
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],
|
||||
"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("")
|
||||
|
||||
empty_count = report.get("empty_scaffold_count", 0)
|
||||
lines.append(f"## Empty scaffolds ({empty_count})")
|
||||
slugs = report.get("empty_scaffolds", [])
|
||||
if slugs:
|
||||
for slug in slugs:
|
||||
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"
|
||||
Reference in New Issue
Block a user