feat: comprehensive registry improvements
Community docs: - CODE_OF_CONDUCT.md (Contributor Covenant v2.1) - SECURITY.md (vulnerability reporting policy) - CHANGELOG.md (initial release notes) - CODEOWNERS (per-type review ownership) GitHub config: - Issue templates: bug-report, pricing-correction, documentation - FUNDING.yml (GitHub Sponsors) Validation enhancements: - Cross-reference check: hand [[requires]] → integration existence - Routing alias collisions as warnings (errors with --strict) - --strict flag to promote warnings to errors - --type filter to validate single content type - CI: add taplo format check and lychee link check jobs Developer experience: - Makefile with validate, fmt, and scaffold targets - Scaffold templates for all 5 content types - .pre-commit-config.yaml (trailing whitespace, TOML check, validate) - docs/content-guide.md (naming, descriptions, prompts, decision guide) Content quality: - schema.toml: add last_verified field for model pricing - CONTRIBUTING.md: add pricing verification guide with source links
This commit is contained in:
1 parent
6cc5c745be
commit
a8b7c9d08b
20 files changed
+670
-18
No files matched your search
+95
-18
@@ -3,6 +3,9 @@
|
||||
|
||||
Usage:
|
||||
python scripts/validate.py
|
||||
python scripts/validate.py --strict
|
||||
python scripts/validate.py --type providers
|
||||
python scripts/validate.py --strict --type agents
|
||||
|
||||
Validates:
|
||||
- Provider TOML files (required fields, valid tiers, non-negative costs, no duplicates)
|
||||
@@ -11,11 +14,13 @@ Validates:
|
||||
- Integration TOML files (required fields: id, name, [transport])
|
||||
- Skill TOML files (required fields: [skill].name, [runtime].type)
|
||||
- aliases.toml parsing
|
||||
- Cross-reference: hand [[requires]] integration references
|
||||
- Routing alias collision detection (WARNING, ERROR with --strict)
|
||||
|
||||
Exit code 0 on success, 1 on any validation error.
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -42,6 +47,8 @@ REQUIRED_MODEL_FIELDS = {
|
||||
"input_cost_per_m", "output_cost_per_m",
|
||||
}
|
||||
|
||||
VALID_CONTENT_TYPES = {"providers", "agents", "hands", "integrations", "skills", "plugins", "aliases"}
|
||||
|
||||
|
||||
def load_toml(filepath: Path) -> tuple[dict | None, str | None]:
|
||||
"""Load a TOML file, return (data, error)."""
|
||||
@@ -126,8 +133,14 @@ def validate_agent_file(filepath: Path) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def validate_hand_file(filepath: Path) -> list[str]:
|
||||
"""Validate a HAND.toml file."""
|
||||
def validate_hand_file(filepath: Path, integration_ids: set[str]) -> list[str]:
|
||||
"""Validate a HAND.toml file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the HAND.toml file.
|
||||
integration_ids: Set of known integration IDs (filenames without .toml
|
||||
from the integrations/ directory) for cross-reference validation.
|
||||
"""
|
||||
data, err = load_toml(filepath)
|
||||
if err:
|
||||
return [err]
|
||||
@@ -152,6 +165,19 @@ def validate_hand_file(filepath: Path) -> list[str]:
|
||||
if "agent" not in data:
|
||||
errors.append(f"{rel}: Missing [agent] section")
|
||||
|
||||
# Cross-reference: check that [[requires]] with requirement_type = "integration"
|
||||
# reference existing integration TOML files
|
||||
requires_list = data.get("requires", [])
|
||||
if isinstance(requires_list, list):
|
||||
for req in requires_list:
|
||||
if isinstance(req, dict) and req.get("requirement_type") == "integration":
|
||||
req_key = req.get("key", "")
|
||||
if req_key and req_key not in integration_ids:
|
||||
errors.append(
|
||||
f"{rel}: [[requires]] references integration '{req_key}' "
|
||||
f"but no integrations/{req_key}.toml exists"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -233,16 +259,52 @@ def validate_aliases_file(filepath: Path) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate all TOML files in the LibreFang registry."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Treat all warnings as errors.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
choices=sorted(VALID_CONTENT_TYPES),
|
||||
dest="content_type",
|
||||
default=None,
|
||||
help="Validate only the specified content type (e.g. providers, agents, hands).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def should_validate(content_type: str, filter_type: str | None) -> bool:
|
||||
"""Check whether a content type should be validated given the --type filter."""
|
||||
if filter_type is None:
|
||||
return True
|
||||
return content_type == filter_type
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
root = script_dir.parent
|
||||
|
||||
all_errors = []
|
||||
stats = {}
|
||||
|
||||
# --- Collect integration IDs for cross-reference validation ---
|
||||
integrations_dir = root / "integrations"
|
||||
integration_ids: set[str] = set()
|
||||
if integrations_dir.is_dir():
|
||||
for fp in integrations_dir.glob("*.toml"):
|
||||
integration_ids.add(fp.stem)
|
||||
|
||||
# --- Providers ---
|
||||
providers_dir = root / "providers"
|
||||
if providers_dir.is_dir():
|
||||
if providers_dir.is_dir() and should_validate("providers", args.content_type):
|
||||
toml_files = sorted(providers_dir.glob("*.toml"))
|
||||
total_models = 0
|
||||
global_model_ids = {}
|
||||
@@ -273,7 +335,7 @@ def main():
|
||||
|
||||
# --- Agents ---
|
||||
agents_dir = root / "agents"
|
||||
if agents_dir.is_dir():
|
||||
if agents_dir.is_dir() and should_validate("agents", args.content_type):
|
||||
agent_dirs = sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in agent_dirs:
|
||||
agent_toml = d / "agent.toml"
|
||||
@@ -285,19 +347,18 @@ def main():
|
||||
|
||||
# --- Hands ---
|
||||
hands_dir = root / "hands"
|
||||
if hands_dir.is_dir():
|
||||
if hands_dir.is_dir() and should_validate("hands", args.content_type):
|
||||
hand_dirs = sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in hand_dirs:
|
||||
hand_toml = d / "HAND.toml"
|
||||
if hand_toml.exists():
|
||||
all_errors.extend(validate_hand_file(hand_toml))
|
||||
all_errors.extend(validate_hand_file(hand_toml, integration_ids))
|
||||
else:
|
||||
all_errors.append(f"hands/{d.name}: Missing HAND.toml")
|
||||
stats["hands"] = len(hand_dirs)
|
||||
|
||||
# --- Integrations ---
|
||||
integrations_dir = root / "integrations"
|
||||
if integrations_dir.is_dir():
|
||||
if integrations_dir.is_dir() and should_validate("integrations", args.content_type):
|
||||
int_files = sorted(integrations_dir.glob("*.toml"))
|
||||
for fp in int_files:
|
||||
all_errors.extend(validate_integration_file(fp))
|
||||
@@ -305,7 +366,7 @@ def main():
|
||||
|
||||
# --- Skills ---
|
||||
skills_dir = root / "skills"
|
||||
if skills_dir.is_dir():
|
||||
if skills_dir.is_dir() and should_validate("skills", args.content_type):
|
||||
skill_dirs = sorted([d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in skill_dirs:
|
||||
skill_toml = d / "skill.toml"
|
||||
@@ -317,7 +378,7 @@ def main():
|
||||
|
||||
# --- Plugins ---
|
||||
plugins_dir = root / "plugins"
|
||||
if plugins_dir.is_dir():
|
||||
if plugins_dir.is_dir() and should_validate("plugins", args.content_type):
|
||||
plugin_dirs = sorted([d for d in plugins_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
|
||||
for d in plugin_dirs:
|
||||
plugin_toml = d / "plugin.toml"
|
||||
@@ -342,14 +403,18 @@ def main():
|
||||
stats["plugins"] = len(plugin_dirs)
|
||||
|
||||
# --- Aliases ---
|
||||
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
|
||||
if should_validate("aliases", args.content_type):
|
||||
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
|
||||
|
||||
# --- Cross-type: duplicate routing aliases ---
|
||||
# --- Cross-type: duplicate routing aliases (now treated as ERRORS) ---
|
||||
# Only run this cross-type check when no --type filter is active,
|
||||
# or when filtering to agents or hands specifically.
|
||||
all_aliases = {} # alias -> (type, name)
|
||||
warnings = []
|
||||
alias_collisions = []
|
||||
run_alias_check = args.content_type is None or args.content_type in ("agents", "hands")
|
||||
|
||||
# Collect agent routing aliases
|
||||
if agents_dir.is_dir():
|
||||
if run_alias_check and agents_dir.is_dir():
|
||||
for d in sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")]):
|
||||
data, _ = load_toml(d / "agent.toml")
|
||||
if data:
|
||||
@@ -358,12 +423,12 @@ def main():
|
||||
key = alias.lower()
|
||||
if key in all_aliases:
|
||||
prev_type, prev_name = all_aliases[key]
|
||||
warnings.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and agent/{d.name}")
|
||||
alias_collisions.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and agent/{d.name}")
|
||||
else:
|
||||
all_aliases[key] = ("agent", d.name)
|
||||
|
||||
# Collect hand routing aliases
|
||||
if hands_dir.is_dir():
|
||||
if run_alias_check and hands_dir.is_dir():
|
||||
for d in sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")]):
|
||||
data, _ = load_toml(d / "HAND.toml")
|
||||
if data:
|
||||
@@ -372,13 +437,25 @@ def main():
|
||||
key = alias.lower()
|
||||
if key in all_aliases:
|
||||
prev_type, prev_name = all_aliases[key]
|
||||
warnings.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and hand/{d.name}")
|
||||
alias_collisions.append(f"Routing alias '{alias}' used by both {prev_type}/{prev_name} and hand/{d.name}")
|
||||
else:
|
||||
all_aliases[key] = ("hand", d.name)
|
||||
|
||||
# --- Collect warnings ---
|
||||
warnings = list(alias_collisions)
|
||||
|
||||
# --- In --strict mode, promote warnings to errors ---
|
||||
if args.strict and warnings:
|
||||
all_errors.extend(warnings)
|
||||
warnings = []
|
||||
|
||||
# --- Report ---
|
||||
print("=" * 60)
|
||||
print("LibreFang Registry Validation")
|
||||
if args.content_type:
|
||||
print(f" Filter: --type {args.content_type}")
|
||||
if args.strict:
|
||||
print(" Mode: --strict (warnings are errors)")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
|
||||
Reference in new issue
Block a user