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

404 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
HELP_EPILOG = """\
查询顺序:
1. 先按 class 取类摘要或 Framework Profile
class_lookup.py --class TStringList
class_lookup.py --class TSBackTesting --format json
2. 需要成员正文时,再按返回的 qualified_name 调用 lookup.py --name
输出状态:
status resolved、not_found 或 ambiguous
profile_status resolved 表示有 Framework Profilenot_profiled 表示普通 class
scaffold_status 生命周期和成员引用是否完整,仅在有 Profile 时输出
contract_status 回调字段契约是否完整;非 resolved 时不得生成回调字段
diagnostics 消歧、配置或契约缺口及下一步动作
--check 校验全部 Framework Profile 对 class、成员正文和证据的引用。
使用自定义 --tsv 时默认不加载 Framework Profile;普通 class 仍可正常查询。若该索引
另有 curated Profile,必须同时显式传入 --profiles。
退出码:
0 查询或校验完成;not_found、ambiguous 通过输出状态表达
1 function/framework 索引缺失、格式错误、为空或引用校验失败
2 参数不合法
"""
def empty_profile_index():
return {"schema_version": 1, "frameworks": []}
def profile_path_for(args):
if args.profiles:
return Path(args.profiles)
if args.tsv:
return None
return DEFAULT_INDEX
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="查询 TSL class 摘要及其证据化的 Framework Profile。",
epilog=HELP_EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False,
)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--class", dest="class_name", help="精确 class 名或完全限定名")
action.add_argument("--list", action="store_true", help="列出索引中的 class")
action.add_argument("--check", action="store_true", help="校验全部 Framework Profile 引用")
parser.add_argument("--scope", help="按 class scope 过滤")
parser.add_argument("--module", help="按 class module 过滤")
parser.add_argument(
"--config",
action="append",
default=[],
metavar="KEY=VALUE",
help="传入原始框架配置,可重复",
)
parser.add_argument(
"--include-members", action="store_true", help="在类摘要中包含紧凑成员清单"
)
parser.add_argument(
"--format", choices=("text", "json"), default="text", help="输出格式"
)
parser.add_argument("--profiles", metavar="PATH", help="Framework Profile 索引路径")
parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv 路径")
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")
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
profile_path = profile_path_for(args)
try:
profiles = (
load_index(profile_path) if profile_path else empty_profile_index()
)
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())