"""Dead field analysis and JSON report generation.""" import json from datetime import datetime, timezone from typing import Any from src.config import STUDIO_BASE_URL RANGE_MAP: dict[int, str] = {1: 'lastMonth', 3: 'lastThreeMonths', 6: 'lastSixMonths'} def _studio_url(graph_id: str, type_name: str, field_name: str, range_label: str) -> str: return f'{STUDIO_BASE_URL}/{graph_id}/variant/prod/insights/schema/{type_name}.{field_name}?coordinateKind=OBJECT_FIELD&insightsTab=objectFields&range={range_label}' def find_dead_fields( all_queries: dict[str, str], all_mutations: dict[str, str], all_objects: dict[str, dict[str, Any]], usage_cache: dict[str, dict[str, dict[str, int]] | None], graph_id: str, months: int, ) -> tuple[list[dict[str, str]], list[dict[str, str]], list[dict[str, str]]]: """Identify dead (0-operation) fields by comparing schema against usage data. Returns: (dead_queries, dead_mutations, dead_object_fields) """ dead_queries: list[dict[str, str]] = [] dead_mutations: list[dict[str, str]] = [] dead_object_fields: list[dict[str, str]] = [] range_label = RANGE_MAP.get(months, 'lastThreeMonths') query_usage = usage_cache.get('Query') or {} for field_name, repo in sorted(all_queries.items()): if field_name not in query_usage or query_usage[field_name]['operations'] == 0: dead_queries.append({ 'field': field_name, 'subgraph': repo, 'studio_url': _studio_url(graph_id, 'Query', field_name, range_label), }) mutation_usage = usage_cache.get('Mutation') or {} for field_name, repo in sorted(all_mutations.items()): if field_name not in mutation_usage or mutation_usage[field_name]['operations'] == 0: dead_mutations.append({ 'field': field_name, 'subgraph': repo, 'studio_url': _studio_url(graph_id, 'Mutation', field_name, range_label), }) for type_name, info in sorted(all_objects.items()): type_usage = usage_cache.get(type_name) or {} for field_name in sorted(info['fields']): if field_name not in type_usage or type_usage[field_name]['operations'] == 0: dead_object_fields.append({ 'type': type_name, 'field': field_name, 'subgraph': info['repo'], 'studio_url': _studio_url(graph_id, type_name, field_name, range_label), }) return dead_queries, dead_mutations, dead_object_fields def find_dead_objects( all_objects: dict[str, dict[str, Any]], dead_object_fields: list[dict[str, str]], graph_id: str, ) -> list[dict[str, Any]]: """Identify object types where every field is dead.""" dead_pairs = {(e['type'], e['field']) for e in dead_object_fields} dead_objects: list[dict[str, Any]] = [] for type_name, info in sorted(all_objects.items()): fields = info['fields'] if fields and all((type_name, f) in dead_pairs for f in fields): dead_objects.append({ 'object': type_name, 'subgraph': info.get('subgraph') or info.get('repo'), 'field_count': len(fields), 'studio_url': f'{STUDIO_BASE_URL}/{graph_id}/variant/prod/schema/reference/objects/{type_name}', }) return dead_objects def write_report( output_path: str, since_str: str, months: int, graph_id: str, subgraphs: list[str], totals: dict[str, int], dead_queries: list[dict[str, str]], dead_mutations: list[dict[str, str]], dead_object_fields: list[dict[str, str]], dead_objects: list[dict[str, Any]] | None = None, ) -> None: """Write the audit report as JSON.""" summary: dict[str, Any] = { 'queries': {'dead': len(dead_queries), 'total': totals['queries']}, 'mutations': {'dead': len(dead_mutations), 'total': totals['mutations']}, 'object_fields': {'dead': len(dead_object_fields), 'total': totals['object_fields']}, } if 'objects' in totals: summary['objects'] = {'dead': len(dead_objects or []), 'total': totals['objects']} report: dict[str, Any] = { 'generated_at': datetime.now(timezone.utc).isoformat(), 'since': since_str, 'months': months, 'graph_id': graph_id, 'repos': subgraphs, 'summary': summary, 'dead_queries': dead_queries, 'dead_mutations': dead_mutations, 'dead_object_fields': dead_object_fields, } if dead_objects is not None: report['dead_objects'] = dead_objects with open(output_path, 'w') as fp: json.dump(report, fp, indent=2) print(f'\nJSON report written to {output_path}')