feat(tsl-api): add parameter value domain lookup

This commit is contained in:
csh
2026-08-22 14:15:54 +08:00
parent 476f999139
commit b101b13bba
17 changed files with 2551 additions and 400 deletions
+138 -4
View File
@@ -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 来源标记;
简单成员名可能返回多个 ownerqualified_name 可消歧;重载会全部返回
简单成员名可能返回多个 ownerqualified_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: