433 lines
16 KiB
Python
433 lines
16 KiB
Python
#!/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", []),
|
|
}
|