the-custodian/docs/changes/netkingdom-target-2026-10-16/improvement-loop-next/reuse-native-validator-r2-integrated.patch
codex 6cf0012a6e
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s
Reject generated-only commits / generated-commit-guard (push) Successful in 12s
docs: preserve reviewed NetKingdom session artifacts
2026-11-01 23:09:51 +01:00

191 lines
7.8 KiB
Diff

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