#!/usr/bin/env python3 """Query compact class surfaces and optional framework lifecycle profiles.""" import argparse import json import sys from collections import Counter from pathlib import Path from framework_lookup import ( DEFAULT_INDEX, load_index, scaffold_packet, validate_index, ) from lookup import DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize def class_rows(rows): return [row for row in rows if row.get("kind") == "class"] def filter_rows(rows, scope=None, module=None): filtered = rows if scope: filtered = [ row for row in filtered if normalize(row.get("scope", "")) == normalize(scope) ] if module: filtered = [ row for row in filtered if normalize(row.get("module", "")) == normalize(module) ] return filtered def find_classes(rows, name, scope=None, module=None): requested = normalize(name) matches = [ row for row in class_rows(rows) if requested in { normalize(row.get("name", "")), normalize(row.get("qualified_name", "")), } ] return filter_rows(matches, scope, module) def find_profile(data, row): for framework in data.get("frameworks", []): if not isinstance(framework, dict): continue if ( normalize(framework.get("qualified_name", "")) == normalize(row.get("qualified_name", "")) and normalize(framework.get("scope", "")) == normalize(row.get("scope", "")) and normalize(framework.get("module", "")) == normalize(row.get("module", "")) ): return framework return None def class_members(rows, row): owner = normalize(row.get("qualified_name", "") or row.get("name", "")) members = [ candidate for candidate in rows if candidate.get("kind") != "class" and normalize(candidate.get("scope", "")) == normalize(row.get("scope", "")) and normalize(candidate.get("module", "")) == normalize(row.get("module", "")) and normalize(candidate.get("owner", "")) == owner ] return sorted( members, key=lambda item: ( normalize(item.get("kind", "")), normalize(item.get("qualified_name", "") or item.get("name", "")), normalize(item.get("signature", "")), item.get("anchor", ""), ), ) def source_id(row): return f"{row.get('page', '')}#{row.get('anchor', '')}" def compact_class(row, profile_available=False): return { "name": row.get("name", ""), "qualified_name": row.get("qualified_name", "") or row.get("name", ""), "scope": row.get("scope", ""), "module": row.get("module", ""), "summary": row.get("summary", ""), "profile_available": profile_available, "evidence": [source_id(row)], } def compact_member(row): return { "name": row.get("name", ""), "qualified_name": row.get("qualified_name", "") or row.get("name", ""), "signature": row.get("signature", ""), "kind": row.get("kind", ""), "binding": row.get("binding", ""), "visibility": row.get("visibility", ""), "summary": row.get("summary", ""), "evidence": [source_id(row)], } def member_summary(members): counts = Counter(member.get("kind", "unknown") or "unknown" for member in members) return dict(sorted(counts.items())) def class_packet(rows, row, profile, config, include_members): members = class_members(rows, row) packet = { "status": "resolved", "class": compact_class(row, profile is not None), "member_summary": member_summary(members), "profile_status": "resolved" if profile is not None else "not_profiled", "profile": None, "diagnostics": [], } if include_members: packet["members"] = [compact_member(member) for member in members] if profile is not None: packet["profile"] = scaffold_packet(profile, config) packet["diagnostics"] = packet["profile"]["diagnostics"] elif config: packet["diagnostics"].append( { "code": "CLASS_NOT_PROFILED", "severity": "error", "message": "Configuration contracts require a curated framework profile.", "next_action": "Query class members directly or add an evidence-backed profile.", } ) return packet def ambiguous_packet(matches, profiles): return { "status": "ambiguous", "candidates": [ compact_class(row, find_profile(profiles, row) is not None) for row in matches ], "diagnostics": [ { "code": "AMBIGUOUS_CLASS", "severity": "error", "message": "Multiple classes share this name across scopes or modules.", "next_action": "Pass --scope and --module, or use a qualified class name.", } ], } def print_class_text(packet): if packet["status"] != "resolved": print(f"status: {packet['status']}") for candidate in packet.get("candidates", []): print( f"- {candidate['qualified_name']} " f"({candidate['scope']}/{candidate['module']})" ) for diagnostic in packet.get("diagnostics", []): print(f"- {diagnostic['code']}: {diagnostic['message']}") return item = packet["class"] print(f"class: {item['qualified_name']}") print(f"scope/module: {item['scope']}/{item['module']}") print(f"summary: {item['summary']}") print( "members: " + ", ".join( f"{kind}={count}" for kind, count in packet["member_summary"].items() ) ) print(f"profile_status: {packet['profile_status']}") profile = packet.get("profile") if profile: print(f"scaffold_status: {profile['scaffold_status']}") print(f"contract_status: {profile['contract_status']}") print( "lifecycle: " + " -> ".join(phase["phase"] for phase in profile["lifecycle"]) ) required_hooks = [ hook["api"] for hook in profile["hooks"] if hook.get("required") ] print("required_hooks: " + (", ".join(required_hooks) or "none")) if packet.get("members"): print("member_list:") for member in packet["members"]: print( f"- {member['qualified_name']}\t{member['kind']}\t" f"{member['signature']}\t{member['evidence'][0]}" ) if packet["diagnostics"]: print("diagnostics:") for diagnostic in packet["diagnostics"]: print(f"- {diagnostic['code']}: {diagnostic['message']}") def print_list_text(packet): print(f"# {packet['count']} indexed classes") print("qualified_name\tscope\tmodule\tprofile\tsummary") for item in packet["classes"]: print( "\t".join( [ item["qualified_name"], item["scope"], item["module"], "framework" if item["profile_available"] else "generic", item["summary"], ] ) ) def main(argv=None): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser( description="Query indexed TSL classes and curated framework profiles.", allow_abbrev=False, ) action = parser.add_mutually_exclusive_group(required=True) action.add_argument("--class", dest="class_name", help="exact class name") action.add_argument("--list", action="store_true", help="list indexed classes") action.add_argument("--check", action="store_true", help="validate framework profiles") parser.add_argument("--scope", help="filter class scope") parser.add_argument("--module", help="filter class module") parser.add_argument( "--config", action="append", default=[], metavar="KEY=VALUE", help="raw config" ) parser.add_argument( "--include-members", action="store_true", help="include the compact member list" ) parser.add_argument("--format", choices=("text", "json"), default="text") parser.add_argument("--profiles", metavar="PATH", help="framework profile index") parser.add_argument("--tsv", metavar="PATH", help="function index") args = parser.parse_args(argv) if args.config and not args.class_name: parser.error("--config requires --class") if args.include_members and not args.class_name: parser.error("--include-members requires --class") profile_path = Path(args.profiles) if args.profiles else DEFAULT_INDEX tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV try: profiles = load_index(profile_path) rows = load_rows(tsv_path) except (OSError, UnicodeError, ValueError) as error: print(f"ERROR: {error}", file=sys.stderr) return 1 if not rows: print(f"ERROR: {tsv_path} has no entries", file=sys.stderr) return 1 errors = validate_index(profiles, rows, codegen_root_for_tsv(tsv_path)) if errors: for error in errors: print(f"ERROR: {error}", file=sys.stderr) return 1 indexed_classes = filter_rows(class_rows(rows), args.scope, args.module) if args.check: print( f"OK: {len(class_rows(rows))} classes indexed; " f"{len(profiles['frameworks'])} framework profile(s) validated" ) return 0 if args.list: items = [ compact_class(row, find_profile(profiles, row) is not None) for row in sorted( indexed_classes, key=lambda item: ( normalize(item.get("qualified_name", "") or item.get("name", "")), normalize(item.get("scope", "")), normalize(item.get("module", "")), ), ) ] packet = {"status": "resolved", "count": len(items), "classes": items} if args.format == "json": print(json.dumps(packet, ensure_ascii=False, indent=2)) else: print_list_text(packet) return 0 matches = find_classes(rows, args.class_name, args.scope, args.module) if not matches: packet = { "status": "not_found", "class": args.class_name, "diagnostics": [ { "code": "CLASS_NOT_FOUND", "severity": "error", "message": f"No indexed class named {args.class_name!r}.", "next_action": "Use --list or query an API directly with lookup.py.", } ], } elif len(matches) > 1: packet = ambiguous_packet(matches, profiles) else: config = {} config_errors = [] for value in args.config: key, separator, raw_value = value.partition("=") if not separator or not key.strip() or not raw_value.strip(): config_errors.append(f"invalid configuration {value!r}") else: config[key.strip()] = raw_value.strip() row = matches[0] packet = class_packet( rows, row, find_profile(profiles, row), config, args.include_members, ) packet["diagnostics"].extend( { "code": "INVALID_CONFIGURATION", "severity": "error", "message": error, "next_action": "Use KEY=VALUE syntax with non-empty values.", } for error in config_errors ) if args.format == "json": print(json.dumps(packet, ensure_ascii=False, indent=2)) else: print_class_text(packet) return 0 if __name__ == "__main__": raise SystemExit(main())