From dd00b27b4ce77180b01c5756c2a3841819fcac76 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 16 Oct 2026 21:01:43 +0200 Subject: [PATCH] fix(registry): reject owner index drift and duplicate discovery rows Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786 --- reuse_surface/cli.py | 50 +++++++++++++++++-- tests/test_index_parity.py | 91 +++++++++++++++++++++++++++++++++++ workplans/ADHOC-2026-10-16.md | 31 ++++++++++++ 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 tests/test_index_parity.py create mode 100644 workplans/ADHOC-2026-10-16.md diff --git a/reuse_surface/cli.py b/reuse_surface/cli.py index 39ba2de..5c47322 100644 --- a/reuse_surface/cli.py +++ b/reuse_surface/cli.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +from collections import Counter import json import sys from pathlib import Path @@ -87,6 +88,7 @@ from reuse_surface.registry import ( parse_vector, registry_paths, resolve_repo_root, + vectors_match, ) @@ -114,12 +116,48 @@ def _check_index_drift( repo_root: Path, ) -> list[str]: warnings: list[str] = [] - indexed_paths = {item["path"] for item in index.get("capabilities", [])} + if not isinstance(index, dict) or not isinstance(index.get("capabilities"), list): + return ["index drift: index must contain a capabilities list"] + rows = [] + for position, row in enumerate(index["capabilities"]): + if not isinstance(row, dict) or any( + not isinstance(row.get(field), str) or not row[field] + for field in ("id", "path") + ): + warnings.append(f"index drift: row {position} requires nonempty string id and path") + continue + rows.append(row) + indexed_paths = {item["path"] for item in rows} file_paths = {str(path.relative_to(repo_root)) for path in entry_paths} for path in sorted(file_paths - indexed_paths): warnings.append(f"index drift: entry file not indexed: {path}") for path in sorted(indexed_paths - file_paths): warnings.append(f"index drift: index references missing file: {path}") + for field in ("id", "path"): + for value, count in sorted(Counter(row[field] for row in rows).items()): + if count > 1: + warnings.append(f"index drift: duplicate {field}: {value} ({count} rows)") + for row in rows: + path = repo_root / row["path"] + if not path.is_file(): + continue + try: + entry = parse_front_matter(path) + except (ValueError, yaml.YAMLError): + continue # The entry validation pass reports malformed source. + # The index projects the owner entry; index-only changes cannot replace it. + for field in ("id", "name", "summary", "domain", "status", "owner", "tags"): + if row.get(field) != entry.get(field): + warnings.append(f"index drift: {row['path']}: {field} differs from entry") + availability = entry.get("availability") + modes = availability.get("consumption_modes") if isinstance(availability, dict) else None + if row.get("consumption_modes") != modes: + warnings.append(f"index drift: {row['path']}: consumption_modes differs from entry") + try: + if not vectors_match(row["vector"], entry): + warnings.append(f"index drift: {row['path']}: vector differs from entry") + except (KeyError, TypeError, AttributeError): + warnings.append(f"index drift: {row['path']}: vector cannot be compared to entry") return warnings @@ -145,7 +183,7 @@ def _run_validate( for path in paths: try: data = parse_front_matter(path) - except ValueError as exc: + except (ValueError, yaml.YAMLError) as exc: errors.append(str(exc)) continue for error in sorted(validator.iter_errors(data), key=lambda e: e.path): @@ -155,8 +193,12 @@ def _run_validate( if not target: index_path = registry_paths(repo_root)["index"] if index_path.exists(): - index = load_index_at(index_path) - warnings.extend(_check_index_drift(paths, index, repo_root)) + try: + index = load_index_at(index_path) + except (ValueError, yaml.YAMLError) as exc: + errors.append(f"{index_path}: {exc}") + else: + warnings.extend(_check_index_drift(paths, index, repo_root)) if relations and repo_root == ROOT: warnings.extend(check_relations()) return errors, warnings, paths diff --git a/tests/test_index_parity.py b/tests/test_index_parity.py new file mode 100644 index 0000000..3a2774b --- /dev/null +++ b/tests/test_index_parity.py @@ -0,0 +1,91 @@ +"""Exercise the same strict native validator used by the existing Forgejo job.""" +from __future__ import annotations + +import copy +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parent.parent + + +def fixture_registry(tmp_path): + index = yaml.safe_load((ROOT / "registry/indexes/capabilities.yaml").read_text()) + row = copy.deepcopy(index["capabilities"][0]) + source = ROOT / row["path"] + target = tmp_path / row["path"] + target.parent.mkdir(parents=True) + target.write_bytes(source.read_bytes()) + index["capabilities"] = [row] + index_path = tmp_path / "registry/indexes/capabilities.yaml" + index_path.parent.mkdir(parents=True) + return index_path, index + + +def validate(root): + return subprocess.run( + [sys.executable, "-m", "reuse_surface.cli", "validate", "--root", str(root), + "--fail-on-warnings"], cwd=ROOT, capture_output=True, text=True, + ) + + +@pytest.mark.parametrize("field,value", [ + ("id", "capability.synthetic.stale"), ("name", "stale name"), + ("summary", "stale summary"), ("domain", "stale-domain"), + ("status", "retired"), ("owner", "wrong-owner"), + ("tags", ["stale"]), ("consumption_modes", ["stale"]), + ("vector", "D0 / A0 / C0 / R0"), +]) +def test_existing_strict_validator_refuses_stale_projection(tmp_path, field, value): + path, index = fixture_registry(tmp_path) + path.write_text(yaml.safe_dump(index)) + assert validate(tmp_path).returncode == 0 + original_entry = (tmp_path / index["capabilities"][0]["path"]).read_bytes() + index["capabilities"][0][field] = value + path.write_text(yaml.safe_dump(index)) + result = validate(tmp_path) + assert result.returncode == 1, result.stdout + result.stderr + assert f"{field} differs from entry" in result.stderr + assert (tmp_path / index["capabilities"][0]["path"]).read_bytes() == original_entry + + +def test_duplicate_id_and_path_cannot_satisfy_exactly_once(tmp_path): + path, index = fixture_registry(tmp_path) + index["capabilities"].append(copy.deepcopy(index["capabilities"][0])) + path.write_text(yaml.safe_dump(index)) + result = validate(tmp_path) + assert result.returncode == 1 + assert "duplicate id:" in result.stderr + assert "duplicate path:" in result.stderr + + +def test_equivalent_vector_whitespace_is_allowed(tmp_path): + path, index = fixture_registry(tmp_path) + index["capabilities"][0]["vector"] = index["capabilities"][0]["vector"].replace(" ", "") + path.write_text(yaml.safe_dump(index)) + assert validate(tmp_path).returncode == 0 + + +@pytest.mark.parametrize("source", ["null", "[]", "availability: null", "availability: []", "availability: ["]) +def test_malformed_frontmatter_returns_failure_without_traceback(tmp_path, source): + path, index = fixture_registry(tmp_path) + path.write_text(yaml.safe_dump(index)) + (tmp_path / index["capabilities"][0]["path"]).write_text("---\n" + source + "\n---\n") + result = validate(tmp_path) + assert result.returncode == 1 + assert "Traceback" not in result.stderr + assert "error:" in result.stderr + + +@pytest.mark.parametrize("rows", [None, {}, [None], [[]], [{}], [{"id": [], "path": "x"}], [{"id": "x", "path": None}]]) +def test_unvalidated_index_shape_returns_failure_without_traceback(tmp_path, rows): + path, index = fixture_registry(tmp_path) + index["capabilities"] = rows + path.write_text(yaml.safe_dump(index)) + result = validate(tmp_path) + assert result.returncode == 1 + assert "Traceback" not in result.stderr + assert "index drift:" in result.stderr diff --git a/workplans/ADHOC-2026-10-16.md b/workplans/ADHOC-2026-10-16.md new file mode 100644 index 0000000..333d601 --- /dev/null +++ b/workplans/ADHOC-2026-10-16.md @@ -0,0 +1,31 @@ +--- +id: REUSE-WP-ADHOC-2026-10-16 +type: workplan +title: "Refuse owner/index drift in the existing native registry validator" +domain: infotech +repo: reuse-surface +status: finished +owner: codex +created: "2026-10-16" +updated: "2026-10-16" +flavor: implementation +--- + +## Refuse owner/index drift in the existing native registry validator + +```task +id: REUSE-WP-ADHOC-2026-10-16-T01 +status: done +priority: medium +``` + +The existing strict validator accepted stale index metadata and duplicate IDs/paths. Extend its existing parity warnings and add native CLI regression tests, including malformed frontmatter/index shape without tracebacks. Existing mandatory Forgejo validation consumes this source; fleet adoption and hosted freshness are unverified. + +Completed 2026-10-16 under the founder's NetKingdom target implementation demand +(CUST-WP-0080, TSA-11). Source review and exact before/after native CLI evidence: +`the-custodian/docs/changes/netkingdom-target-2026-10-16/improvement-loop-next/`. +The current 60-provider cohort has all 70 reviewed IDs exactly once; 3 field +mismatches across 2 capabilities become zero with the source corrections. +Native hosted refresh/readback and the accepted Activity/rein schedule remain +separate gates. No production change, model call, credential access or external +message was performed.