#!/usr/bin/env python3 """Resolve framework-class profiles without replacing ordinary API lookup.""" import argparse import json import sys from pathlib import Path from lookup import ( DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize, search_exact, slice_entry, ) SKILL_ROOT = Path(__file__).resolve().parents[1] DEFAULT_INDEX = SKILL_ROOT / "data" / "framework_index.json" REQUIRED_FRAMEWORK_KEYS = { "qualified_name", "scope", "module", "kind", "lifecycle", "configuration", "hooks", "state_apis", "execution", "result_apis", "contracts", } REFERENCE_SECTIONS = ( "configuration", "hooks", "state_apis", "execution", "result_apis", ) def load_index(index_path): try: data = json.loads(index_path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as error: raise ValueError(f"failed to load {index_path}: {error}") from error if not isinstance(data, dict): raise ValueError("framework index root must be an object") if data.get("schema_version") != 1: raise ValueError("unsupported framework index schema_version") frameworks = data.get("frameworks") if not isinstance(frameworks, list): raise ValueError("frameworks must be an array") return data def evidence_set(item): evidence = item.get("evidence", []) if isinstance(item, dict) else [] if not isinstance(evidence, list): return set() return {str(source) for source in evidence} def api_key(row): return f"{row.get('page', '')}#{row.get('anchor', '')}" def indexed_source_keys(rows): return {api_key(row) for row in rows if row.get("page") and row.get("anchor")} def validate_source_ids(data, rows): known_sources = indexed_source_keys(rows) errors = [] for framework_index, framework in enumerate(data.get("frameworks", [])): if not isinstance(framework, dict): errors.append(f"frameworks[{framework_index}] must be an object") continue for source in evidence_set(framework): if source not in known_sources: errors.append( f"frameworks[{framework_index}].evidence references unknown source {source}" ) for section in ("lifecycle", "contracts"): entries = framework.get(section, []) if not isinstance(entries, list): continue for entry_index, entry in enumerate(entries): for source in evidence_set(entry): if source not in known_sources: errors.append( f"{section}[{entry_index}].evidence references unknown source {source}" ) return errors def resolve_reference(reference, framework, rows, codegen_root, path): if not isinstance(reference, dict) or not reference.get("api"): return [f"{path} must contain an api"] qualified_name = reference["api"] matches = [ row for row in search_exact(rows, qualified_name) if normalize(row.get("scope", "")) == normalize(framework["scope"]) and normalize(row.get("module", "")) == normalize(framework["module"]) ] if not matches: return [ f"{path}.api {qualified_name!r} is not indexed in " f"{framework['scope']}/{framework['module']}" ] evidence = evidence_set(reference) if not evidence: return [f"{path}.evidence must contain at least one source id"] matching_evidence = [row for row in matches if api_key(row) in evidence] if not matching_evidence: expected = ", ".join(api_key(row) for row in matches) return [ f"{path}.evidence does not identify an indexed overload; expected one of {expected}" ] for row in matching_evidence: if not slice_entry( codegen_root, row["page"], row["signature"], row["anchor"] ): return [ f"{path}.api {qualified_name!r} points to a missing reference body " f"{api_key(row)}" ] return [] def validate_framework(framework, rows, codegen_root, index): errors = [] if not isinstance(framework, dict): return [f"frameworks[{index}] must be an object"] missing = sorted(REQUIRED_FRAMEWORK_KEYS - set(framework)) if missing: errors.append(f"frameworks[{index}] missing keys: {', '.join(missing)}") return errors if framework.get("kind") != "framework": errors.append(f"frameworks[{index}].kind must be 'framework'") root_matches = [ row for row in search_exact(rows, framework["qualified_name"]) if row.get("kind") == "class" and normalize(row.get("scope", "")) == normalize(framework["scope"]) and normalize(row.get("module", "")) == normalize(framework["module"]) ] if not root_matches: errors.append( f"{framework['qualified_name']} is not an indexed class in " f"{framework['scope']}/{framework['module']}" ) for section in REFERENCE_SECTIONS: entries = framework.get(section) if not isinstance(entries, list): errors.append(f"{framework['qualified_name']}.{section} must be an array") continue for entry_index, entry in enumerate(entries): errors.extend( resolve_reference( entry, framework, rows, codegen_root, f"{framework['qualified_name']}.{section}[{entry_index}]", ) ) for phase_index, phase in enumerate(framework.get("lifecycle", [])): if not isinstance(phase, dict): errors.append( f"{framework['qualified_name']}.lifecycle[{phase_index}] must be an object" ) continue for member_index, member in enumerate(phase.get("members", [])): matches = [ row for row in search_exact(rows, member) if normalize(row.get("scope", "")) == normalize(framework["scope"]) and normalize(row.get("module", "")) == normalize(framework["module"]) ] if not matches: errors.append( f"{framework['qualified_name']}.lifecycle[{phase_index}].members" f"[{member_index}] {member!r} is not indexed" ) for contract_index, contract in enumerate(framework.get("contracts", [])): callback = contract.get("callback") if isinstance(contract, dict) else None if not callback: errors.append( f"{framework['qualified_name']}.contracts[{contract_index}] must contain callback" ) continue matches = [ row for row in search_exact(rows, callback) if normalize(row.get("scope", "")) == normalize(framework["scope"]) and normalize(row.get("module", "")) == normalize(framework["module"]) ] if not matches: errors.append( f"{framework['qualified_name']}.contracts[{contract_index}].callback " f"{callback!r} is not indexed" ) for source in evidence_set(contract): if source not in indexed_source_keys(rows): errors.append( f"{framework['qualified_name']}.contracts[{contract_index}] " f"references unknown source {source}" ) return errors def validate_index(data, rows, codegen_root): errors = validate_source_ids(data, rows) names = set() for index, framework in enumerate(data.get("frameworks", [])): if isinstance(framework, dict): name = framework.get("qualified_name") if name in names: errors.append(f"duplicate framework qualified_name: {name}") names.add(name) errors.extend(validate_framework(framework, rows, codegen_root, index)) return errors def find_framework(data, name): requested = normalize(name) return next( ( framework for framework in data.get("frameworks", []) if isinstance(framework, dict) and normalize(framework.get("qualified_name", "")) == requested ), None, ) def parse_config(values): config = {} errors = [] for value in values: key, separator, raw_value = value.partition("=") if not separator or not key.strip() or not raw_value.strip(): errors.append( f"invalid --config {value!r}; expected a non-empty key=value pair" ) continue config[key.strip()] = raw_value.strip() return config, errors def scaffold_packet(framework, config): configured_names = { entry["api"].rsplit(".", 1)[-1] for entry in framework["configuration"] } diagnostics = [] for key in config: if key not in configured_names: diagnostics.append( { "code": "UNKNOWN_CONFIGURATION", "severity": "error", "message": ( f"{key} is not a configuration member of " f"{framework['qualified_name']}" ), "next_action": "Use a configuration field listed by the framework profile.", } ) for entry in framework["configuration"]: if entry.get("contract_evidence_required") and entry["api"].rsplit(".", 1)[-1] in config: diagnostics.append( { "code": "CONTRACT_MODE_UNRESOLVED", "severity": "error", "message": ( f"{entry['api']} is a candidate discriminator, but no " "configuration-to-return-schema mapping is recorded." ), "next_action": "Add a mode contract before generating callback fields.", "source_ids": entry.get("evidence", []), } ) contract_status = "resolved" for contract in framework["contracts"]: if contract.get("status") != "resolved": contract_status = contract.get("status", "incomplete") diagnostics.append( { "code": "CONTRACT_INCOMPLETE", "severity": "error", "message": contract.get("reason", "framework contract is incomplete"), "next_action": contract.get("required_next_step", "Resolve the contract."), "source_ids": contract.get("evidence", []), } ) return { "status": "resolved", "scaffold_status": "resolved", "contract_status": contract_status, "framework": framework["qualified_name"], "scope": framework["scope"], "module": framework["module"], "configured_values": config, "lifecycle": framework["lifecycle"], "configuration": framework["configuration"], "hooks": framework["hooks"], "state_apis": framework["state_apis"], "execution": framework["execution"], "result_apis": framework["result_apis"], "diagnostics": diagnostics, "evidence": framework.get("evidence", []), } def print_text(packet): print(f"framework: {packet['framework']}") print(f"scope/module: {packet['scope']}/{packet['module']}") print(f"scaffold_status: {packet['scaffold_status']}") print(f"contract_status: {packet['contract_status']}") print("lifecycle: " + " -> ".join(item["phase"] for item in packet["lifecycle"])) print("configuration: " + ", ".join(item["api"] for item in packet["configuration"])) required_hooks = [item["api"] for item in packet["hooks"] if item.get("required")] print("required_hooks: " + (", ".join(required_hooks) or "none")) print("state_apis: " + ", ".join(item["api"] for item in packet["state_apis"])) print("execution: " + ", ".join(item["api"] for item in packet["execution"])) print("result_apis: " + ", ".join(item["api"] for item in packet["result_apis"])) if packet["configured_values"]: print("configured_values: " + ", ".join( f"{key}={value}" for key, value in packet["configured_values"].items() )) if packet["diagnostics"]: print("diagnostics:") for diagnostic in packet["diagnostics"]: print(f"- {diagnostic['code']}: {diagnostic['message']}") def main(argv=None): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser( description="Resolve framework-class scaffolds for TSL API consumers.", allow_abbrev=False, ) action = parser.add_mutually_exclusive_group(required=True) action.add_argument("--framework", help="exact framework qualified name") action.add_argument( "--check", action="store_true", help="validate all framework profile references" ) parser.add_argument( "--config", action="append", default=[], metavar="KEY=VALUE", help="raw framework configuration; may be repeated", ) parser.add_argument("--format", choices=("text", "json"), default="text") parser.add_argument("--index", metavar="PATH", help="framework_index.json path") parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv path") args = parser.parse_args(argv) index_path = Path(args.index) if args.index else DEFAULT_INDEX tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV try: data = load_index(index_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 codegen_root = codegen_root_for_tsv(tsv_path) errors = validate_index(data, rows, codegen_root) if args.check: if errors: for error in errors: print(f"ERROR: {error}", file=sys.stderr) return 1 print(f"OK: {len(data['frameworks'])} framework profile(s) validated") return 0 if errors: packet = { "status": "data_error", "framework": args.framework, "diagnostics": [ { "code": "PROFILE_REFERENCE_ERROR", "severity": "error", "message": error, "next_action": "Fix framework_index.json or rebuild the API index.", } for error in errors ], } if args.format == "json": print(json.dumps(packet, ensure_ascii=False, indent=2)) else: print(f"Framework profile data is invalid for {args.framework!r}.") for diagnostic in packet["diagnostics"]: print(f"- {diagnostic['code']}: {diagnostic['message']}") return 1 framework = find_framework(data, args.framework) if framework is None: packet = { "status": "not_found", "framework": args.framework, "diagnostics": [ { "code": "FRAMEWORK_NOT_FOUND", "severity": "error", "message": f"No framework profile named {args.framework!r}.", "next_action": "Use --check or add a framework profile.", } ], } if args.format == "json": print(json.dumps(packet, ensure_ascii=False, indent=2)) else: print(f"No framework profile named {args.framework!r}.") return 0 config, config_errors = parse_config(args.config) packet = scaffold_packet(framework, config) for error in config_errors: packet["diagnostics"].append( { "code": "INVALID_CONFIGURATION", "severity": "error", "message": error, "next_action": "Use KEY=VALUE syntax with non-empty values.", } ) if args.format == "json": print(json.dumps(packet, ensure_ascii=False, indent=2)) else: print_text(packet) return 0 if __name__ == "__main__": raise SystemExit(main())