feat(preview): sampling, metrics, governance, server mana data

Preview endpoint + fast caches; curated pins + role quotas + rarity/overlap tuning; catalog+preview metrics; governance enforcement flags; server mana/color identity fields; docs/tests/scripts updated.
This commit is contained in:
matt 2025-09-23 09:19:23 -07:00
parent 8f47dfbb81
commit c4a7fc48ea
40 changed files with 6092 additions and 17312 deletions

View file

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Fast path theme catalog presence & schema sanity validator.
Checks:
1. theme_list.json exists.
2. Loads JSON and ensures top-level keys present: themes (list), metadata_info (dict).
3. Basic field contract for each theme: id, theme, synergies (list), description.
4. Enforces presence of catalog_hash inside metadata_info for drift detection.
5. Optionally validates against Pydantic models if available (best effort).
Exit codes:
0 success
1 structural failure / missing file
2 partial validation warnings elevated via --strict
"""
from __future__ import annotations
import sys
import json
import argparse
import pathlib
import typing as t
THEME_LIST_PATH = pathlib.Path('config/themes/theme_list.json')
class Problem:
def __init__(self, level: str, message: str):
self.level = level
self.message = message
def __repr__(self):
return f"{self.level.upper()}: {self.message}"
def load_json(path: pathlib.Path) -> t.Any:
try:
return json.loads(path.read_text(encoding='utf-8') or '{}')
except FileNotFoundError:
raise
except Exception as e: # pragma: no cover
raise RuntimeError(f"parse_error: {e}")
def validate(data: t.Any) -> list[Problem]:
probs: list[Problem] = []
if not isinstance(data, dict):
probs.append(Problem('error','top-level not an object'))
return probs
themes = data.get('themes')
if not isinstance(themes, list) or not themes:
probs.append(Problem('error','themes list missing or empty'))
meta = data.get('metadata_info')
if not isinstance(meta, dict):
probs.append(Problem('error','metadata_info missing or not object'))
else:
if not meta.get('catalog_hash'):
probs.append(Problem('error','metadata_info.catalog_hash missing'))
if not meta.get('generated_at'):
probs.append(Problem('warn','metadata_info.generated_at missing'))
# Per theme spot check (limit to first 50 to keep CI snappy)
for i, th in enumerate(themes[:50] if isinstance(themes, list) else []):
if not isinstance(th, dict):
probs.append(Problem('error', f'theme[{i}] not object'))
continue
if not th.get('id'):
probs.append(Problem('error', f'theme[{i}] id missing'))
if not th.get('theme'):
probs.append(Problem('error', f'theme[{i}] theme missing'))
syns = th.get('synergies')
if not isinstance(syns, list) or not syns:
probs.append(Problem('warn', f'theme[{i}] synergies empty or not list'))
if 'description' not in th:
probs.append(Problem('warn', f'theme[{i}] description missing'))
return probs
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description='Validate fast path theme catalog build presence & schema.')
ap.add_argument('--strict-warn', action='store_true', help='Promote warnings to errors (fail CI).')
args = ap.parse_args(argv)
if not THEME_LIST_PATH.exists():
print('ERROR: theme_list.json missing at expected path.', file=sys.stderr)
return 1
try:
data = load_json(THEME_LIST_PATH)
except FileNotFoundError:
print('ERROR: theme_list.json missing.', file=sys.stderr)
return 1
except Exception as e:
print(f'ERROR: failed parsing theme_list.json: {e}', file=sys.stderr)
return 1
problems = validate(data)
errors = [p for p in problems if p.level=='error']
warns = [p for p in problems if p.level=='warn']
for p in problems:
stream = sys.stderr if p.level!='info' else sys.stdout
print(repr(p), file=stream)
if errors:
return 1
if args.strict_warn and warns:
return 2
print(f"Fast path validation ok: {len(errors)} errors, {len(warns)} warnings. Checked {min(len(data.get('themes', [])),50)} themes.")
return 0
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))