diff --git a/reuse_surface/federation.py b/reuse_surface/federation.py index 3c8c03e..f736667 100644 --- a/reuse_surface/federation.py +++ b/reuse_surface/federation.py @@ -183,6 +183,26 @@ def resolve_source_index_path( return _write_remote_cache(source["repo"], url, content, cache_dir), warnings +SCOPE_BLOCK_KEYS = {"type", "title", "description", "keywords"} + + +def _shape_hint(item: dict[str, Any]) -> str: + """Name the likely mistake when an index row has no id. + + The fenced `capability` blocks in a repo's SCOPE.md use + type/title/description/keywords, which is close enough to an index row to + be copied into one by mistake — and then the member silently contributes + nothing. Say so instead of leaving the author to guess. + """ + if SCOPE_BLOCK_KEYS & set(item): + return ( + " — looks like a SCOPE.md capability block" + " (type/title/description/keywords); an index row needs" + " id/name/summary/vector/domain/status/owner/path" + ) + return "" + + def _read_index_entries( index_path: Path, repo: str ) -> tuple[list[Any], list[str]]: @@ -234,7 +254,10 @@ def compose_federated_index( continue cap_id = item.get("id") if not cap_id: - warnings.append(f"{source['repo']}: capability #{position} has no id, skipped") + warnings.append( + f"{source['repo']}: capability #{position} has no id, skipped" + f"{_shape_hint(item)}" + ) continue if cap_id in seen_ids: warnings.append( diff --git a/tests/test_federation.py b/tests/test_federation.py index 1119100..1978c7a 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -283,3 +283,33 @@ def test_malformed_source_still_listed_with_zero_count(tmp_path, monkeypatch): ) remote = next(s for s in federated["sources"] if s["repo"] == "remote-repo") assert remote["count"] == 0 + + +def test_scope_shaped_row_names_the_mistake(tmp_path, monkeypatch): + """A SCOPE.md capability block copied into an index should say so.""" + body = """ +version: 1 +domain: helix_forge +capabilities: + - type: library + title: Evidence-to-target binding + description: Links evidence items to structured targets. + keywords: [evidence, binding] +""" + _, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch) + hint = next(w for w in warnings if "remote-repo" in w) + assert "SCOPE.md capability block" in hint + assert "id/name/summary" in hint + + +def test_plain_missing_id_gets_no_scope_hint(tmp_path, monkeypatch): + body = """ +version: 1 +capabilities: + - name: Has a name but no id + summary: Something +""" + _, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch) + hint = next(w for w in warnings if "remote-repo" in w) + assert "has no id" in hint + assert "SCOPE.md" not in hint diff --git a/workplans/REUSE-WP-0020-coulombcore-retirement-cutover.md b/workplans/REUSE-WP-0020-coulombcore-retirement-cutover.md index f7f24be..5f7549c 100644 --- a/workplans/REUSE-WP-0020-coulombcore-retirement-cutover.md +++ b/workplans/REUSE-WP-0020-coulombcore-retirement-cutover.md @@ -312,9 +312,19 @@ blocks that belong in `SCOPE.md` use `type` / `title` / `description` / one from the wrong place. That makes it a systemic trap rather than a one-off mistake, so the Terminology -section now documents the distinction explicitly. Worth a follow-up: `validate` -could detect SCOPE-shaped rows in an index and say so by name, and `report gaps` -could sweep the federation for other members with the same confusion. +section now documents the distinction explicitly, and the compose warning now +names it: + +``` +evidence-binder: capability #0 has no id, skipped — looks like a SCOPE.md +capability block (type/title/description/keywords); an index row needs +id/name/summary/vector/domain/status/owner/path +``` + +**Federation swept 2026-08-21.** Composed all 61 sources with `--refresh`: +`evidence-binder` is the **only** affected member. The other ten zero-count +repos are genuine empty scaffolds and produced no warnings, so the confusion has +not spread. ## Fix Wall-Clock Rot In The Plan-Check Test Suite