#!/usr/bin/env python3 """Library for validating and resolving evidence-backed framework profiles.""" import json from copy import deepcopy 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}" ) if section == "contracts" and isinstance(entry, dict): for schema_index, schema in enumerate( entry.get("return_schemas", []) ): for source in evidence_set(schema): if source not in known_sources: errors.append( f"contracts[{entry_index}].return_schemas" f"[{schema_index}].evidence references unknown source " f"{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}" ) if not isinstance(contract, dict) or contract.get("status") != "resolved": continue discriminator = contract.get("mode_discriminator") if not isinstance(discriminator, dict) or not discriminator.get("api"): errors.append( f"{framework['qualified_name']}.contracts[{contract_index}] resolved " "contracts must contain mode_discriminator.api" ) continue configured_apis = { normalize(entry.get("api", "")) for entry in framework.get("configuration", []) if isinstance(entry, dict) } if normalize(discriminator["api"]) not in configured_apis: errors.append( f"{framework['qualified_name']}.contracts[{contract_index}]." f"mode_discriminator.api {discriminator['api']!r} is not a " "configuration member" ) schemas = contract.get("return_schemas") if not isinstance(schemas, list) or not schemas: errors.append( f"{framework['qualified_name']}.contracts[{contract_index}] resolved " "contracts must contain return_schemas" ) continue discriminator_field = discriminator["api"].rsplit(".", 1)[-1] seen_values = set() for schema_index, schema in enumerate(schemas): path = ( f"{framework['qualified_name']}.contracts[{contract_index}]." f"return_schemas[{schema_index}]" ) if not isinstance(schema, dict): errors.append(f"{path} must be an object") continue value = schema.get("when", {}).get(discriminator_field) if value is None or str(value).strip() == "": errors.append(f"{path}.when must select {discriminator_field}") elif str(value) in seen_values: errors.append(f"{path}.when duplicates {discriminator_field}={value}") else: seen_values.add(str(value)) if not isinstance(schema.get("fields"), list) or not schema["fields"]: errors.append(f"{path}.fields must be a non-empty array") if not evidence_set(schema): errors.append(f"{path}.evidence must contain at least one source id") 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 contract_mode_mapping(contract, configuration_api): discriminator = contract.get("mode_discriminator", {}) return ( contract.get("status") == "resolved" and normalize(discriminator.get("api", "")) == normalize(configuration_api) and isinstance(contract.get("return_schemas"), list) ) def resolved_contract_packet(contract, config): packet = deepcopy(contract) discriminator = packet.get("mode_discriminator") packet["selected_return_schema"] = None if not isinstance(discriminator, dict) or not discriminator.get("api"): return packet field = discriminator["api"].rsplit(".", 1)[-1] schemas = packet.get("return_schemas", []) values = [ str(schema.get("when", {}).get(field)) for schema in schemas if schema.get("when", {}).get(field) is not None ] packet["mode_discriminator"] = { **discriminator, "field": field, "values": values, } if field in config: requested = str(config[field]) packet["selected_return_schema"] = next( ( schema for schema in schemas if str(schema.get("when", {}).get(field)) == requested ), None, ) return packet 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"]: field = entry["api"].rsplit(".", 1)[-1] if not entry.get("contract_evidence_required") or field not in config: continue mapped_contracts = [ contract for contract in framework["contracts"] if contract_mode_mapping(contract, entry["api"]) ] if not mapped_contracts: 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", []), } ) continue requested = str(config[field]) available = { str(schema.get("when", {}).get(field)) for contract in mapped_contracts for schema in contract.get("return_schemas", []) if schema.get("when", {}).get(field) is not None } if requested not in available: diagnostics.append( { "code": "CONTRACT_MODE_UNKNOWN", "severity": "error", "message": ( f"{entry['api']}={requested!r} has no recorded return schema." ), "next_action": ( "Use one of the documented mode values: " + ", ".join(sorted(available)) ), "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", []), } ) contracts = [ resolved_contract_packet(contract, config) for contract in framework["contracts"] ] if any( diagnostic["code"] in {"CONTRACT_MODE_UNKNOWN", "CONTRACT_MODE_UNRESOLVED"} for diagnostic in diagnostics ): contract_status = "incomplete" 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"], "contracts": contracts, "diagnostics": diagnostics, "evidence": framework.get("evidence", []), }