📝 docs(tsl-api): resolve backtesting return contract
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"""Library for validating and resolving evidence-backed framework profiles."""
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from lookup import (
|
||||
@@ -87,6 +88,17 @@ def validate_source_ids(data, rows):
|
||||
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
|
||||
|
||||
|
||||
@@ -209,6 +221,54 @@ def validate_framework(framework, rows, codegen_root, index):
|
||||
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
|
||||
|
||||
|
||||
@@ -225,6 +285,47 @@ def validate_index(data, rows, codegen_root):
|
||||
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]
|
||||
@@ -245,7 +346,15 @@ def scaffold_packet(framework, config):
|
||||
}
|
||||
)
|
||||
for entry in framework["configuration"]:
|
||||
if entry.get("contract_evidence_required") and entry["api"].rsplit(".", 1)[-1] in config:
|
||||
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",
|
||||
@@ -258,6 +367,29 @@ def scaffold_packet(framework, config):
|
||||
"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":
|
||||
@@ -271,6 +403,15 @@ def scaffold_packet(framework, config):
|
||||
"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",
|
||||
@@ -285,6 +426,7 @@ def scaffold_packet(framework, config):
|
||||
"state_apis": framework["state_apis"],
|
||||
"execution": framework["execution"],
|
||||
"result_apis": framework["result_apis"],
|
||||
"contracts": contracts,
|
||||
"diagnostics": diagnostics,
|
||||
"evidence": framework.get("evidence", []),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user