#!/usr/bin/env python3 """Library for validating and resolving evidence-backed framework profiles.""" import json from pathlib import Path from lookup import ( 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 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", []), }