diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f879c85..c714814 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,7 +1,7 @@ name: Validate Request Documents on: - workflow_run: + workflow_dispatch: pull_request: branches: - main diff --git a/claims/nyt/ai-backlash.yaml b/claims/nyt/ai-backlash.yaml new file mode 100644 index 0000000..f812b02 --- /dev/null +++ b/claims/nyt/ai-backlash.yaml @@ -0,0 +1,4 @@ +sourceUriDigest: "f217d823160d39ca379a09044e1242ac641c8b1d79a1d6f499c59f08a1b67500" +summary: "The global push for AI infrastructure is increasingly clashing with local environmental and economic limits. In the U.S., massive data center expansion has sparked significant resistance due to the immense electricity and water these facilities require, which threatens to strain power grids and increase costs for residents. This tension represents a broader international challenge: balancing the resource-heavy needs of the tech sector against community sustainability and energy security." +title: "From Indiana to Idaho, a Backlash Against A.I. Gathers Momentum" +uri: "https://www.nytimes.com/2026/04/27/technology/ai-artificial-intelligence-backlash.html" \ No newline at end of file diff --git a/proofs/nyt/ai-backlash/supports/idahonews.yaml b/proofs/nyt/ai-backlash/supports/idahonews.yaml new file mode 100644 index 0000000..d6895f5 --- /dev/null +++ b/proofs/nyt/ai-backlash/supports/idahonews.yaml @@ -0,0 +1,4 @@ +claimUriDigest: "a6aa62d59056d0f6b74c26c26c3a8c561f6ec43320626443d2759bc771181464" +supportsClaim: true +reviewedBy: "semmet95" +uri: "https://idahonews.com/news/nation-world/ai-data-centers-spark-local-backlash-across-the-us-artificial-intelligence-electricity-utilities-noise-land-use-tax-incentives" \ No newline at end of file diff --git a/scripts/post_requests.py b/scripts/post_requests.py index 21a9036..a5546d6 100644 --- a/scripts/post_requests.py +++ b/scripts/post_requests.py @@ -12,6 +12,7 @@ import urllib.error import urllib.request from pathlib import Path +import traceback # Shared utilities (try package import first, fallback to local module) try: @@ -61,58 +62,100 @@ def post(url: str, data: dict, api_key: str) -> tuple[int, str]: def main() -> int: + def _info(msg: str, *args) -> None: + print("INFO:", msg % args if args else msg) + + def _error(msg: str, *args) -> None: + print("ERROR:", msg % args if args else msg, file=sys.stderr) + + def _exception(msg: str, *args) -> None: + print("ERROR:", msg % args if args else msg, file=sys.stderr) + traceback.print_exc(file=sys.stderr) + base_url = os.environ.get("API_BASE_URL", "").rstrip("/") api_key = os.environ.get("API_KEY", "") if not base_url: - print("API_BASE_URL environment variable is not set", file=sys.stderr) + _error("API_BASE_URL environment variable is not set") return 1 if not api_key: - print("API_KEY environment variable is not set", file=sys.stderr) + _error("API_KEY environment variable is not set") return 1 files = [f for f in os.environ.get("ADDED_FILES", "").splitlines() if f.strip()] if not files: - print("No added files to process.") + _info("No added files to process.") return 0 + _info("POST run: %d file(s) to process", len(files)) + spec = load_oapi("oapi.yaml") schema_paths = extract_post_paths(spec) failed = False + allowed_exts = {".yaml", ".yml", ".json"} for f in files: f = f.strip() - parts = Path(f).parts - if not parts or parts[0] not in SCHEMA_MAP: + if not f: + continue + + # normalize path parts (skip '.' and '..') + p = Path(f) + parts = [part for part in p.parts if part not in (".", "..")] + if not parts: continue folder = parts[0] + if folder not in SCHEMA_MAP: + continue + + norm_path = Path(*parts) + if norm_path.suffix.lower() not in allowed_exts: + _info("Skipping non-document file: %s", str(norm_path)) + continue + schema_name = SCHEMA_MAP[folder] path = schema_paths.get(schema_name) if not path: - print(f"No POST path found for schema {schema_name}, skipping {f}") + _error("No POST path found for schema %s, skipping %s", schema_name, str(norm_path)) failed = True continue url = f"{base_url}{path}" + # warn if BASE_URL likely duplicates path prefix (common misconfiguration) + path_segments = [seg for seg in path.split("/") if seg] + if path_segments and base_url.rstrip("/").endswith("/" + path_segments[0]): + _info("Warning: BASE_URL '%s' may duplicate path segment '%s' when combined with '%s'", base_url, path_segments[0], path) + + if not norm_path.exists(): + _error("%s: File not found", str(norm_path)) + failed = True + continue + + try: + data = load_doc(str(norm_path)) + except Exception as e: + _exception("%s: Failed to parse: %s", str(norm_path), e) + failed = True + continue - if not Path(f).exists(): - print(f"{f}: File not found") + if not isinstance(data, dict): + _error("%s: Parsed document is not a JSON object; skipping", str(norm_path)) failed = True continue + _info("Posting %s -> %s", str(norm_path), url) try: - data = load_doc(f) + status, body = post(url, data, api_key) except Exception as e: - print(f"{f}: Failed to parse: {e}") + _exception("%s: Request failed: %s", str(norm_path), e) failed = True continue - status, body = post(url, data, api_key) if 200 <= status < 300: - print(f"{f} → {url} ({status})") + _info("%s → %s (%d)", str(norm_path), url, status) else: - print(f"{f} → {url} ({status}): {body[:200]}") + _error("%s → %s (%d): %s", str(norm_path), url, status, body[:200]) failed = True return 1 if failed else 0 diff --git a/scripts/validate.py b/scripts/validate.py index 463e654..c67ed3f 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 """Validate request documents against OpenAPI schemas.""" -import json import sys import os from pathlib import Path +import traceback +from referencing.jsonschema import DRAFT202012 +from referencing import Registry, Resource # Shared utilities (try package import first, fallback to local module) try: @@ -32,7 +34,10 @@ def resolve_schema(spec: dict, ref: str) -> dict: return current def validate(data: dict, schema: dict, spec: dict) -> list[str]: - registry = Registry().with_resource("oapi", Resource.from_contents(spec)) # type: ignore + registry = Registry().with_resource( + "oapi", + Resource.from_contents(spec, DRAFT202012) # explicit spec + ) validator = Draft202012Validator(schema, registry=registry) return [f"{e.json_path}: {e.message}" for e in validator.iter_errors(data)] @@ -45,21 +50,38 @@ def scan_tracked_files() -> list[str]: files.extend(Path(folder).rglob(ext)) return [str(f) for f in sorted(files)] + +def _info(msg: str, *args) -> None: + print("INFO:", msg % args if args else msg) + + +def _error(msg: str, *args) -> None: + print("ERROR:", msg % args if args else msg, file=sys.stderr) + + +def _exception(msg: str, *args) -> None: + print("ERROR:", msg % args if args else msg, file=sys.stderr) + traceback.print_exc(file=sys.stderr) + def main() -> int: files = sys.argv[1:] + if not files: - print("Validating all files...") + _info("Validating new docs...") files = scan_tracked_files() - return 0 spec = load_oapi("oapi.yaml") failed = False + _info("Validation run: %d file(s) to check", len(files)) + for f in files: f = f.strip() if not f: continue + _info("Validating %s", f) + parts = Path(f).parts if not parts or parts[0] not in SCHEMA_MAP: continue @@ -69,25 +91,25 @@ def main() -> int: schema = resolve_schema(spec, schema_ref) if not Path(f).exists(): - print(f"{f}: File not found") + _error("%s: File not found", f) failed = True continue try: data = load_doc(f) except Exception as e: - print(f"{f}: Failed to parse document: {e}") + _exception("%s: Failed to parse document: %s", f, e) failed = True continue errors = validate(data, schema, spec) if errors: - print(f"{f}:") + _error("%s: %d validation error(s)", f, len(errors)) for e in errors: - print(f" - {e}") + _error(" - %s", e) failed = True else: - print(f"{f}") + _info("%s: OK", f) return 1 if failed else 0 diff --git a/sources/nyt.yaml b/sources/nyt.yaml new file mode 100644 index 0000000..d04ee50 --- /dev/null +++ b/sources/nyt.yaml @@ -0,0 +1,4 @@ +name: "The New York Times" +summary: "The New York Times is dedicated to helping people understand and engage with the world through on-the-ground, expert and deeply reported independent journalism." +tags: "american" +uri: "https://www.nytimes.com" \ No newline at end of file