mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-07-27 18:57:50 +00:00
a8099f52fe
- add local external resources registry - add research domain contract and per-repository research domains - add raw fact snapshot governance and validation scripts Validation: - make test - git diff --check HEAD~1..HEAD Note: - welcome workflow failed because its action input names are stale; core content CI passed.
177 lines
7.9 KiB
Python
177 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the local external resources registry."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
REGISTRY_DIR = ROOT / "assets" / "external-resources"
|
|
MANIFEST = REGISTRY_DIR / "resources.yml"
|
|
SCHEMA = REGISTRY_DIR / "schema.yml"
|
|
ID_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
|
|
|
|
def load_yaml(path: Path) -> object:
|
|
with path.open(encoding="utf-8") as fh:
|
|
return yaml.safe_load(fh)
|
|
|
|
|
|
def as_dict(value: object, label: str, errors: list[str]) -> dict:
|
|
if isinstance(value, dict):
|
|
return value
|
|
errors.append(f"{label}: expected mapping")
|
|
return {}
|
|
|
|
|
|
def as_list(value: object, label: str, errors: list[str]) -> list:
|
|
if isinstance(value, list):
|
|
return value
|
|
errors.append(f"{label}: expected list")
|
|
return []
|
|
|
|
|
|
def main() -> int:
|
|
errors: list[str] = []
|
|
|
|
if not MANIFEST.is_file():
|
|
print(f"EXTERNAL_RESOURCE_ERRORS\n{MANIFEST.relative_to(ROOT)}: missing manifest")
|
|
return 1
|
|
if not SCHEMA.is_file():
|
|
print(f"EXTERNAL_RESOURCE_ERRORS\n{SCHEMA.relative_to(ROOT)}: missing schema")
|
|
return 1
|
|
|
|
manifest = as_dict(load_yaml(MANIFEST), str(MANIFEST.relative_to(ROOT)), errors)
|
|
schema = as_dict(load_yaml(SCHEMA), str(SCHEMA.relative_to(ROOT)), errors)
|
|
|
|
resource_schema = as_dict(schema.get("resource"), "schema.yml resource", errors)
|
|
required_resource_fields = set(as_list(resource_schema.get("required_fields"), "schema.yml required_fields", errors))
|
|
enums = as_dict(schema.get("enums"), "schema.yml enums", errors)
|
|
allowed_status = set(as_list(enums.get("status"), "schema.yml enums.status", errors))
|
|
allowed_verification = set(as_list(enums.get("verification_status"), "schema.yml enums.verification_status", errors))
|
|
allowed_risk_flags = set(as_list(enums.get("risk_flags"), "schema.yml enums.risk_flags", errors))
|
|
allowed_resource_types = set(as_list(enums.get("resource_type"), "schema.yml enums.resource_type", errors))
|
|
allowed_link_kinds = set(as_dict(as_dict(schema.get("link"), "schema.yml link", errors).get("kinds"), "schema.yml link.kinds", errors))
|
|
|
|
ids: list[str] = []
|
|
total_resources = 0
|
|
risk_counter: Counter[str] = Counter()
|
|
|
|
categories = as_list(manifest.get("categories"), "resources.yml categories", errors)
|
|
for index, category_ref in enumerate(categories, start=1):
|
|
category_ref = as_dict(category_ref, f"resources.yml categories[{index}]", errors)
|
|
category_id = category_ref.get("id")
|
|
category_file = category_ref.get("file")
|
|
expected_count = category_ref.get("count")
|
|
if not category_id or not isinstance(category_id, str):
|
|
errors.append(f"resources.yml categories[{index}].id: missing string")
|
|
continue
|
|
if not category_file or not isinstance(category_file, str):
|
|
errors.append(f"resources.yml categories[{index}].file: missing string")
|
|
continue
|
|
|
|
path = REGISTRY_DIR / category_file
|
|
if not path.is_file():
|
|
errors.append(f"{path.relative_to(ROOT)}: missing category file")
|
|
continue
|
|
|
|
payload = as_dict(load_yaml(path), str(path.relative_to(ROOT)), errors)
|
|
category = as_dict(payload.get("category"), f"{path.relative_to(ROOT)} category", errors)
|
|
if category.get("id") != category_id:
|
|
errors.append(f"{path.relative_to(ROOT)}: category.id does not match resources.yml")
|
|
|
|
resources = as_list(payload.get("resources"), f"{path.relative_to(ROOT)} resources", errors)
|
|
if expected_count != len(resources):
|
|
errors.append(
|
|
f"{path.relative_to(ROOT)}: expected {expected_count} resources from manifest, found {len(resources)}"
|
|
)
|
|
|
|
for row_index, resource in enumerate(resources, start=1):
|
|
label = f"{path.relative_to(ROOT)} resources[{row_index}]"
|
|
resource = as_dict(resource, label, errors)
|
|
missing_fields = sorted(field for field in required_resource_fields if field not in resource)
|
|
for field in missing_fields:
|
|
errors.append(f"{label}.{field}: missing required field")
|
|
|
|
resource_id = resource.get("id")
|
|
if not isinstance(resource_id, str) or not ID_PATTERN.match(resource_id):
|
|
errors.append(f"{label}.id: must be lowercase kebab-case")
|
|
elif not resource_id.startswith(f"{category_id}-"):
|
|
errors.append(f"{label}.id: must start with category prefix {category_id}-")
|
|
else:
|
|
ids.append(resource_id)
|
|
|
|
resource_type = resource.get("resource_type")
|
|
if resource_type not in allowed_resource_types:
|
|
errors.append(f"{label}.resource_type: unknown value {resource_type!r}")
|
|
|
|
status = resource.get("status")
|
|
if status not in allowed_status:
|
|
errors.append(f"{label}.status: unknown value {status!r}")
|
|
|
|
verification_status = resource.get("verification_status")
|
|
if verification_status not in allowed_verification:
|
|
errors.append(f"{label}.verification_status: unknown value {verification_status!r}")
|
|
|
|
link = as_dict(resource.get("link"), f"{label}.link", errors)
|
|
link_kind = link.get("kind")
|
|
if link_kind not in allowed_link_kinds:
|
|
errors.append(f"{label}.link.kind: unknown value {link_kind!r}")
|
|
elif link_kind == "external_url":
|
|
url = link.get("url")
|
|
domain = link.get("domain")
|
|
parsed = urlparse(str(url))
|
|
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
errors.append(f"{label}.link.url: must be http(s) URL")
|
|
if not isinstance(domain, str) or not domain:
|
|
errors.append(f"{label}.link.domain: missing domain")
|
|
elif parsed.netloc and domain != parsed.netloc.lower():
|
|
errors.append(f"{label}.link.domain: does not match URL netloc")
|
|
elif link_kind in {"internal_reference", "search_instruction"}:
|
|
if not link.get("locator"):
|
|
errors.append(f"{label}.link.locator: required for {link_kind}")
|
|
if status != "needs-review":
|
|
errors.append(f"{label}.status: non-URL locator must remain needs-review")
|
|
elif link_kind == "missing":
|
|
if status != "needs-review":
|
|
errors.append(f"{label}.status: missing link must remain needs-review")
|
|
|
|
risk_flags = as_list(resource.get("risk_flags"), f"{label}.risk_flags", errors)
|
|
unknown_flags = sorted(flag for flag in risk_flags if flag not in allowed_risk_flags)
|
|
for flag in unknown_flags:
|
|
errors.append(f"{label}.risk_flags: unknown value {flag!r}")
|
|
risk_counter.update(risk_flags)
|
|
total_resources += 1
|
|
|
|
duplicate_ids = sorted(resource_id for resource_id, count in Counter(ids).items() if count > 1)
|
|
for resource_id in duplicate_ids:
|
|
errors.append(f"duplicate resource id: {resource_id}")
|
|
|
|
stats = as_dict(manifest.get("stats"), "resources.yml stats", errors)
|
|
if stats.get("total_resources") != total_resources:
|
|
errors.append(f"resources.yml stats.total_resources: expected {total_resources}")
|
|
manifest_risks = as_dict(stats.get("risk_flags"), "resources.yml stats.risk_flags", errors)
|
|
if dict(sorted(risk_counter.items())) != manifest_risks:
|
|
errors.append("resources.yml stats.risk_flags: does not match category files")
|
|
|
|
if errors:
|
|
print("EXTERNAL_RESOURCE_ERRORS")
|
|
for error in errors:
|
|
print(error)
|
|
print(f"TOTAL={len(errors)}")
|
|
return 1
|
|
|
|
print(f"OK external resources checked: {total_resources} resources, {len(categories)} categories")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|