Files
playbook/skills/tsl-api-reference/scripts/framework_lookup.py
T

455 lines
16 KiB
Python

#!/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())