✨ feat(tsl-api): add parameter value domain lookup
This commit is contained in:
@@ -6,6 +6,16 @@ import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from value_domains import ( # noqa: E402
|
||||
ApiHint,
|
||||
ValueDomainCatalog,
|
||||
domains_path_for_index,
|
||||
)
|
||||
|
||||
TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$")
|
||||
PLAIN_H2_RE = re.compile(r"^##(?!#)\s+")
|
||||
BARE_ENTRY_RE = re.compile(r"^(#{3,4})(?!#)\s+`(.+?)`\s*$")
|
||||
@@ -26,6 +36,7 @@ CLASS_MEMBER_LABELS = {
|
||||
UNIT_DIRECT_LABELS = {"function", "var", "const", "class"}
|
||||
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_TSV = SKILL_ROOT / "data" / "function_index.tsv"
|
||||
DEFAULT_VALUE_DOMAINS = SKILL_ROOT / "data" / "value_domains.json"
|
||||
DEFAULT_CODEGEN_ROOT = SKILL_ROOT / "references" / "codegen"
|
||||
REQUIRED_COLUMNS = (
|
||||
"name",
|
||||
@@ -77,13 +88,14 @@ HELP_EPILOG = """\
|
||||
不存在的 scope 会列出当前索引实际提供的值并返回 2。
|
||||
|
||||
结果:
|
||||
--kw 输出候选摘要表;选定后仍须运行 --name
|
||||
--kw 输出候选摘要表;参数取值域可补充召回相关 API,选定后仍须运行 --name
|
||||
--name 输出完整条目正文和 scope/module、page#anchor 来源标记;
|
||||
简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回
|
||||
简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回;
|
||||
若该 API 有参数取值域,同时显示动态目录解析器和已核实值边界
|
||||
|
||||
退出码:
|
||||
0 查询完成;--name 无匹配和 --kw 零候选也通过正文提示表达
|
||||
1 function_index.tsv 缺失、格式错误,或候选指向的条目正文找不到
|
||||
1 function_index.tsv/value_domains.json 缺失、格式错误,或候选指向的条目正文找不到
|
||||
2 参数不合法(缺少动作、空值、--limit 小于 1、使用短选项或缩写)
|
||||
"""
|
||||
|
||||
@@ -366,6 +378,91 @@ def format_row(row):
|
||||
)
|
||||
|
||||
|
||||
def row_identity(row):
|
||||
return (
|
||||
normalize(row.get("qualified_name", "") or row.get("name", "")),
|
||||
normalize(row.get("signature", "")),
|
||||
normalize(row.get("scope", "")),
|
||||
row.get("page", ""),
|
||||
row.get("anchor", ""),
|
||||
)
|
||||
|
||||
|
||||
def signature_parameters(signature):
|
||||
match = re.search(r"\((.*)\)", signature)
|
||||
if not match:
|
||||
return set()
|
||||
parameters = set()
|
||||
for raw_parameter in match.group(1).split(","):
|
||||
parameter = raw_parameter.strip().strip("[]").strip()
|
||||
parameter = re.sub(r"^(?:const|out|var)\s+", "", parameter, flags=re.I)
|
||||
parameter = parameter.split("=", maxsplit=1)[0].strip()
|
||||
parameter = parameter.split(":", maxsplit=1)[0].strip()
|
||||
if parameter:
|
||||
parameters.add(normalize(parameter))
|
||||
return parameters
|
||||
|
||||
|
||||
def rows_for_hint(rows, hint):
|
||||
api = normalize(hint.api)
|
||||
parameter = normalize(hint.parameter)
|
||||
return [
|
||||
row
|
||||
for row in rows
|
||||
if api
|
||||
in {
|
||||
normalize(row.get("name", "")),
|
||||
normalize(row.get("qualified_name", "")),
|
||||
}
|
||||
and normalize(row.get("scope", "")) == normalize(hint.scope)
|
||||
and normalize(row.get("module", "")) == normalize(hint.module)
|
||||
and parameter in signature_parameters(row.get("signature", ""))
|
||||
]
|
||||
|
||||
|
||||
def merge_keyword_matches(rows, normal_matches, hints):
|
||||
merged = []
|
||||
annotations = {}
|
||||
seen = set()
|
||||
for hint in hints:
|
||||
for row in rows_for_hint(rows, hint):
|
||||
key = row_identity(row)
|
||||
annotations.setdefault(key, []).append(hint)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
merged.append(row)
|
||||
for row in normal_matches:
|
||||
key = row_identity(row)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
merged.append(row)
|
||||
return merged, annotations
|
||||
|
||||
|
||||
def format_api_hint(hint: ApiHint) -> str:
|
||||
parts = [f" 参数取值域:{hint.target}", hint.summary()]
|
||||
verification = hint.verification_text()
|
||||
if verification:
|
||||
parts.append(verification)
|
||||
if hint.sources:
|
||||
parts.append(f"来源:{', '.join(hint.sources)}")
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def top_value_domain_matches(matches, *, score_window=50):
|
||||
matches = tuple(matches)
|
||||
if not matches:
|
||||
return ()
|
||||
minimum_score = matches[0].score - score_window
|
||||
return tuple(match for match in matches if match.score >= minimum_score)
|
||||
|
||||
|
||||
def value_domain_keyword_matches(catalog, terms):
|
||||
if len(terms) == 1 and re.fullmatch(r"\s*-?\d{1,2}\s*", terms[0]):
|
||||
return ()
|
||||
return top_value_domain_matches(catalog.search(terms))
|
||||
|
||||
|
||||
def codegen_root_for_tsv(tsv_path):
|
||||
if tsv_path.parent.name == "data":
|
||||
candidate = tsv_path.parent.parent / "references" / "codegen"
|
||||
@@ -423,6 +520,12 @@ def main(argv=None):
|
||||
help="显式指定 function_index.tsv;默认只使用本 skill 内置的 "
|
||||
"data/function_index.tsv,不从工作目录回退查找旧索引",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--value-domains",
|
||||
metavar="PATH",
|
||||
help="显式指定 value_domains.json;默认使用所选索引同目录的数据,"
|
||||
"自定义索引旁没有该文件时不加载参数取值域",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scope",
|
||||
type=non_empty,
|
||||
@@ -451,11 +554,25 @@ def main(argv=None):
|
||||
)
|
||||
return 1
|
||||
|
||||
domain_path = (
|
||||
Path(args.value_domains)
|
||||
if args.value_domains
|
||||
else domains_path_for_index(
|
||||
tsv_path,
|
||||
default_index=DEFAULT_TSV,
|
||||
default_domains=DEFAULT_VALUE_DOMAINS,
|
||||
)
|
||||
)
|
||||
try:
|
||||
rows = load_rows(tsv_path)
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(f"ERROR: failed to load {tsv_path}: {error}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
catalog = ValueDomainCatalog.load(domain_path) if domain_path else None
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(f"ERROR: failed to load {domain_path}: {error}", file=sys.stderr)
|
||||
return 1
|
||||
if not rows:
|
||||
print(
|
||||
f"ERROR: {tsv_path} has no entries; reinstall tsl-api-reference "
|
||||
@@ -499,17 +616,34 @@ def main(argv=None):
|
||||
entries.append((row, body))
|
||||
for row, body in entries:
|
||||
print(body)
|
||||
if catalog:
|
||||
domain_text = catalog.describe_api(
|
||||
row.get("qualified_name", "") or row.get("name", ""),
|
||||
scope=row.get("scope", ""),
|
||||
module=row.get("module", ""),
|
||||
parameters=signature_parameters(row.get("signature", "")),
|
||||
)
|
||||
if domain_text:
|
||||
print(f"\n{domain_text}", end="")
|
||||
print(
|
||||
f"\n<!-- {row['scope']}/{row['module']} · "
|
||||
f"{row['page']}#{row['anchor']} -->\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
matches = search_keyword(rows, args.kw)
|
||||
normal_matches = search_keyword(rows, args.kw)
|
||||
hints = (
|
||||
catalog.api_hints(value_domain_keyword_matches(catalog, args.kw))
|
||||
if catalog
|
||||
else ()
|
||||
)
|
||||
matches, annotations = merge_keyword_matches(rows, normal_matches, hints)
|
||||
print(f"# {len(matches)} matches for: {' '.join(args.kw)}")
|
||||
print("qualified_name\tdeclaration\tsignature\tpage#anchor\tsummary")
|
||||
for row in matches[: args.limit]:
|
||||
print(format_row(row))
|
||||
for hint in annotations.get(row_identity(row), ()):
|
||||
print(format_api_hint(hint))
|
||||
if len(matches) > args.limit:
|
||||
print(f"... {len(matches) - args.limit} more; refine keywords or raise --limit")
|
||||
if not matches:
|
||||
|
||||
Reference in New Issue
Block a user