✨ feat(tsl-api-reference): add class and framework API lookup
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Query compact class surfaces and optional framework lifecycle profiles."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from framework_lookup import (
|
||||
DEFAULT_INDEX,
|
||||
load_index,
|
||||
scaffold_packet,
|
||||
validate_index,
|
||||
)
|
||||
from lookup import DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize
|
||||
|
||||
|
||||
def class_rows(rows):
|
||||
return [row for row in rows if row.get("kind") == "class"]
|
||||
|
||||
|
||||
def filter_rows(rows, scope=None, module=None):
|
||||
filtered = rows
|
||||
if scope:
|
||||
filtered = [
|
||||
row for row in filtered if normalize(row.get("scope", "")) == normalize(scope)
|
||||
]
|
||||
if module:
|
||||
filtered = [
|
||||
row
|
||||
for row in filtered
|
||||
if normalize(row.get("module", "")) == normalize(module)
|
||||
]
|
||||
return filtered
|
||||
|
||||
|
||||
def find_classes(rows, name, scope=None, module=None):
|
||||
requested = normalize(name)
|
||||
matches = [
|
||||
row
|
||||
for row in class_rows(rows)
|
||||
if requested
|
||||
in {
|
||||
normalize(row.get("name", "")),
|
||||
normalize(row.get("qualified_name", "")),
|
||||
}
|
||||
]
|
||||
return filter_rows(matches, scope, module)
|
||||
|
||||
|
||||
def find_profile(data, row):
|
||||
for framework in data.get("frameworks", []):
|
||||
if not isinstance(framework, dict):
|
||||
continue
|
||||
if (
|
||||
normalize(framework.get("qualified_name", ""))
|
||||
== normalize(row.get("qualified_name", ""))
|
||||
and normalize(framework.get("scope", "")) == normalize(row.get("scope", ""))
|
||||
and normalize(framework.get("module", ""))
|
||||
== normalize(row.get("module", ""))
|
||||
):
|
||||
return framework
|
||||
return None
|
||||
|
||||
|
||||
def class_members(rows, row):
|
||||
owner = normalize(row.get("qualified_name", "") or row.get("name", ""))
|
||||
members = [
|
||||
candidate
|
||||
for candidate in rows
|
||||
if candidate.get("kind") != "class"
|
||||
and normalize(candidate.get("scope", "")) == normalize(row.get("scope", ""))
|
||||
and normalize(candidate.get("module", "")) == normalize(row.get("module", ""))
|
||||
and normalize(candidate.get("owner", "")) == owner
|
||||
]
|
||||
return sorted(
|
||||
members,
|
||||
key=lambda item: (
|
||||
normalize(item.get("kind", "")),
|
||||
normalize(item.get("qualified_name", "") or item.get("name", "")),
|
||||
normalize(item.get("signature", "")),
|
||||
item.get("anchor", ""),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def source_id(row):
|
||||
return f"{row.get('page', '')}#{row.get('anchor', '')}"
|
||||
|
||||
|
||||
def compact_class(row, profile_available=False):
|
||||
return {
|
||||
"name": row.get("name", ""),
|
||||
"qualified_name": row.get("qualified_name", "") or row.get("name", ""),
|
||||
"scope": row.get("scope", ""),
|
||||
"module": row.get("module", ""),
|
||||
"summary": row.get("summary", ""),
|
||||
"profile_available": profile_available,
|
||||
"evidence": [source_id(row)],
|
||||
}
|
||||
|
||||
|
||||
def compact_member(row):
|
||||
return {
|
||||
"name": row.get("name", ""),
|
||||
"qualified_name": row.get("qualified_name", "") or row.get("name", ""),
|
||||
"signature": row.get("signature", ""),
|
||||
"kind": row.get("kind", ""),
|
||||
"binding": row.get("binding", ""),
|
||||
"visibility": row.get("visibility", ""),
|
||||
"summary": row.get("summary", ""),
|
||||
"evidence": [source_id(row)],
|
||||
}
|
||||
|
||||
|
||||
def member_summary(members):
|
||||
counts = Counter(member.get("kind", "unknown") or "unknown" for member in members)
|
||||
return dict(sorted(counts.items()))
|
||||
|
||||
|
||||
def class_packet(rows, row, profile, config, include_members):
|
||||
members = class_members(rows, row)
|
||||
packet = {
|
||||
"status": "resolved",
|
||||
"class": compact_class(row, profile is not None),
|
||||
"member_summary": member_summary(members),
|
||||
"profile_status": "resolved" if profile is not None else "not_profiled",
|
||||
"profile": None,
|
||||
"diagnostics": [],
|
||||
}
|
||||
if include_members:
|
||||
packet["members"] = [compact_member(member) for member in members]
|
||||
if profile is not None:
|
||||
packet["profile"] = scaffold_packet(profile, config)
|
||||
packet["diagnostics"] = packet["profile"]["diagnostics"]
|
||||
elif config:
|
||||
packet["diagnostics"].append(
|
||||
{
|
||||
"code": "CLASS_NOT_PROFILED",
|
||||
"severity": "error",
|
||||
"message": "Configuration contracts require a curated framework profile.",
|
||||
"next_action": "Query class members directly or add an evidence-backed profile.",
|
||||
}
|
||||
)
|
||||
return packet
|
||||
|
||||
|
||||
def ambiguous_packet(matches, profiles):
|
||||
return {
|
||||
"status": "ambiguous",
|
||||
"candidates": [
|
||||
compact_class(row, find_profile(profiles, row) is not None) for row in matches
|
||||
],
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "AMBIGUOUS_CLASS",
|
||||
"severity": "error",
|
||||
"message": "Multiple classes share this name across scopes or modules.",
|
||||
"next_action": "Pass --scope and --module, or use a qualified class name.",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def print_class_text(packet):
|
||||
if packet["status"] != "resolved":
|
||||
print(f"status: {packet['status']}")
|
||||
for candidate in packet.get("candidates", []):
|
||||
print(
|
||||
f"- {candidate['qualified_name']} "
|
||||
f"({candidate['scope']}/{candidate['module']})"
|
||||
)
|
||||
for diagnostic in packet.get("diagnostics", []):
|
||||
print(f"- {diagnostic['code']}: {diagnostic['message']}")
|
||||
return
|
||||
|
||||
item = packet["class"]
|
||||
print(f"class: {item['qualified_name']}")
|
||||
print(f"scope/module: {item['scope']}/{item['module']}")
|
||||
print(f"summary: {item['summary']}")
|
||||
print(
|
||||
"members: "
|
||||
+ ", ".join(
|
||||
f"{kind}={count}" for kind, count in packet["member_summary"].items()
|
||||
)
|
||||
)
|
||||
print(f"profile_status: {packet['profile_status']}")
|
||||
profile = packet.get("profile")
|
||||
if profile:
|
||||
print(f"scaffold_status: {profile['scaffold_status']}")
|
||||
print(f"contract_status: {profile['contract_status']}")
|
||||
print(
|
||||
"lifecycle: "
|
||||
+ " -> ".join(phase["phase"] for phase in profile["lifecycle"])
|
||||
)
|
||||
required_hooks = [
|
||||
hook["api"] for hook in profile["hooks"] if hook.get("required")
|
||||
]
|
||||
print("required_hooks: " + (", ".join(required_hooks) or "none"))
|
||||
if packet.get("members"):
|
||||
print("member_list:")
|
||||
for member in packet["members"]:
|
||||
print(
|
||||
f"- {member['qualified_name']}\t{member['kind']}\t"
|
||||
f"{member['signature']}\t{member['evidence'][0]}"
|
||||
)
|
||||
if packet["diagnostics"]:
|
||||
print("diagnostics:")
|
||||
for diagnostic in packet["diagnostics"]:
|
||||
print(f"- {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def print_list_text(packet):
|
||||
print(f"# {packet['count']} indexed classes")
|
||||
print("qualified_name\tscope\tmodule\tprofile\tsummary")
|
||||
for item in packet["classes"]:
|
||||
print(
|
||||
"\t".join(
|
||||
[
|
||||
item["qualified_name"],
|
||||
item["scope"],
|
||||
item["module"],
|
||||
"framework" if item["profile_available"] else "generic",
|
||||
item["summary"],
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Query indexed TSL classes and curated framework profiles.",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
action = parser.add_mutually_exclusive_group(required=True)
|
||||
action.add_argument("--class", dest="class_name", help="exact class name")
|
||||
action.add_argument("--list", action="store_true", help="list indexed classes")
|
||||
action.add_argument("--check", action="store_true", help="validate framework profiles")
|
||||
parser.add_argument("--scope", help="filter class scope")
|
||||
parser.add_argument("--module", help="filter class module")
|
||||
parser.add_argument(
|
||||
"--config", action="append", default=[], metavar="KEY=VALUE", help="raw config"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-members", action="store_true", help="include the compact member list"
|
||||
)
|
||||
parser.add_argument("--format", choices=("text", "json"), default="text")
|
||||
parser.add_argument("--profiles", metavar="PATH", help="framework profile index")
|
||||
parser.add_argument("--tsv", metavar="PATH", help="function index")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.config and not args.class_name:
|
||||
parser.error("--config requires --class")
|
||||
if args.include_members and not args.class_name:
|
||||
parser.error("--include-members requires --class")
|
||||
|
||||
profile_path = Path(args.profiles) if args.profiles else DEFAULT_INDEX
|
||||
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
|
||||
try:
|
||||
profiles = load_index(profile_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
|
||||
|
||||
errors = validate_index(profiles, rows, codegen_root_for_tsv(tsv_path))
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
indexed_classes = filter_rows(class_rows(rows), args.scope, args.module)
|
||||
if args.check:
|
||||
print(
|
||||
f"OK: {len(class_rows(rows))} classes indexed; "
|
||||
f"{len(profiles['frameworks'])} framework profile(s) validated"
|
||||
)
|
||||
return 0
|
||||
|
||||
if args.list:
|
||||
items = [
|
||||
compact_class(row, find_profile(profiles, row) is not None)
|
||||
for row in sorted(
|
||||
indexed_classes,
|
||||
key=lambda item: (
|
||||
normalize(item.get("qualified_name", "") or item.get("name", "")),
|
||||
normalize(item.get("scope", "")),
|
||||
normalize(item.get("module", "")),
|
||||
),
|
||||
)
|
||||
]
|
||||
packet = {"status": "resolved", "count": len(items), "classes": items}
|
||||
if args.format == "json":
|
||||
print(json.dumps(packet, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print_list_text(packet)
|
||||
return 0
|
||||
|
||||
matches = find_classes(rows, args.class_name, args.scope, args.module)
|
||||
if not matches:
|
||||
packet = {
|
||||
"status": "not_found",
|
||||
"class": args.class_name,
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "CLASS_NOT_FOUND",
|
||||
"severity": "error",
|
||||
"message": f"No indexed class named {args.class_name!r}.",
|
||||
"next_action": "Use --list or query an API directly with lookup.py.",
|
||||
}
|
||||
],
|
||||
}
|
||||
elif len(matches) > 1:
|
||||
packet = ambiguous_packet(matches, profiles)
|
||||
else:
|
||||
config = {}
|
||||
config_errors = []
|
||||
for value in args.config:
|
||||
key, separator, raw_value = value.partition("=")
|
||||
if not separator or not key.strip() or not raw_value.strip():
|
||||
config_errors.append(f"invalid configuration {value!r}")
|
||||
else:
|
||||
config[key.strip()] = raw_value.strip()
|
||||
row = matches[0]
|
||||
packet = class_packet(
|
||||
rows,
|
||||
row,
|
||||
find_profile(profiles, row),
|
||||
config,
|
||||
args.include_members,
|
||||
)
|
||||
packet["diagnostics"].extend(
|
||||
{
|
||||
"code": "INVALID_CONFIGURATION",
|
||||
"severity": "error",
|
||||
"message": error,
|
||||
"next_action": "Use KEY=VALUE syntax with non-empty values.",
|
||||
}
|
||||
for error in config_errors
|
||||
)
|
||||
|
||||
if args.format == "json":
|
||||
print(json.dumps(packet, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print_class_text(packet)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,454 @@
|
||||
#!/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())
|
||||
@@ -73,8 +73,8 @@ HELP_EPILOG = """\
|
||||
lookup.py --name arrDropDuplicate
|
||||
|
||||
已知确切名称时可以直接用 --name,跳过第 1 步。
|
||||
只查询一个 API scope 时使用 --scope,例如 --scope builtin、--scope dotnet、
|
||||
--scope third 或 --scope deprecated。
|
||||
只查询一个 API scope 时使用 --scope,例如 --scope builtin、
|
||||
--scope dotnet 或 --scope module。
|
||||
|
||||
退出码:
|
||||
0 取回成功;--name 无匹配也是 0(打印提示,不算错误)
|
||||
@@ -423,7 +423,7 @@ def main(argv=None):
|
||||
type=non_empty,
|
||||
metavar="SCOPE",
|
||||
help="只查询指定 scope,大小写不敏感;作用于 --name 和 --kw。"
|
||||
"内置索引当前提供 builtin、dotnet、third 与 deprecated",
|
||||
"内置索引当前提供 builtin、dotnet 与 module",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
|
||||
Reference in New Issue
Block a user