core/scripts/dump-api-schemas.py
Shay cdead696ed
feat(W-027): Workbench frontend shell — five-region grid + ten empty routes + live StatusFooter (ADR-0160 / ADR-0162) (#299)
- Five-region CSS grid shell (TopBar, LeftNav, Main, StatusFooter; Inspector collapsed)
- Ten placeholder routes with EmptyState cli/string variants and ApiErrorBoundary
- Extended EmptyState API: nextAction accepts string (button) | { kind: "cli", command }
- Updated CommandPalette: real fuzzy search over three commands (Chat, Proposals, Evals)
  with ↑/↓/Enter keyboard nav and useInRouterContext degradation for Branch 1 preview
- TanStack Query hooks for all W-026 endpoints; 30s polling on runtime/status
- TypeScript mirror of workbench/schemas.py at src/types/api.ts
- StatusFooter: mutation_mode badge, git_revision (copy on click), checkpoint_revision
  with amber warning + ADR-0157/ADR-0158 expansion note
- TopBar: CORE Workbench wordmark, ⌘K palette trigger, connection pill
- ApiErrorBoundary (class component) catches WorkbenchApiError → ErrorState
- scripts/dump-api-schemas.py: AST-based Python dataclass field extractor
- workbench-ui/api-schema-snapshot.json: checked-in drift sentinel
- 51 tests across 10 files (0 failures); enum-coverage 4/4; clean vite build
2026-05-26 12:09:12 -07:00

82 lines
2.6 KiB
Python

"""
Dump workbench/schemas.py dataclass field shapes to a JSON snapshot.
Usage (from repo root):
uv run python scripts/dump-api-schemas.py
Output: workbench-ui/api-schema-snapshot.json
The snapshot is used by workbench-ui/src/types/api.test.ts to detect
TypeScript ↔ Python schema drift.
"""
from __future__ import annotations
import ast
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).parent.parent
SCHEMAS_PATH = REPO_ROOT / "workbench" / "schemas.py"
SNAPSHOT_PATH = REPO_ROOT / "workbench-ui" / "api-schema-snapshot.json"
def annotation_to_str(node: ast.expr) -> str:
"""Convert an AST annotation node to a canonical string."""
return ast.unparse(node)
def extract_dataclasses(source: str) -> dict[str, dict[str, str]]:
"""Parse source and extract fields from @dataclass-decorated classes."""
tree = ast.parse(source)
result: dict[str, dict[str, str]] = {}
for node in ast.walk(tree):
if not isinstance(node, ast.ClassDef):
continue
# Check for @dataclass decorator (any form)
is_dataclass = any(
(isinstance(d, ast.Name) and d.id == "dataclass")
or (isinstance(d, ast.Attribute) and d.attr == "dataclass")
or (
isinstance(d, ast.Call)
and (
(isinstance(d.func, ast.Name) and d.func.id == "dataclass")
or (isinstance(d.func, ast.Attribute) and d.func.attr == "dataclass")
)
)
for d in node.decorator_list
)
if not is_dataclass:
continue
fields: dict[str, str] = {}
for stmt in node.body:
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
field_name = stmt.target.id
annotation = annotation_to_str(stmt.annotation)
fields[field_name] = annotation
result[node.name] = fields
return result
def main() -> None:
if not SCHEMAS_PATH.exists():
print(f"ERROR: {SCHEMAS_PATH} not found", file=sys.stderr)
sys.exit(1)
source = SCHEMAS_PATH.read_text(encoding="utf-8")
dataclasses = extract_dataclasses(source)
snapshot = {"dataclasses": {name: {"fields": fields} for name, fields in dataclasses.items()}}
SNAPSHOT_PATH.parent.mkdir(parents=True, exist_ok=True)
SNAPSHOT_PATH.write_text(json.dumps(snapshot, indent=2) + "\n", encoding="utf-8")
print(f"Snapshot written to {SNAPSHOT_PATH}")
print(f"Dataclasses found: {', '.join(dataclasses.keys())}")
if __name__ == "__main__":
main()