feat(tsl-api-reference): improve tagged API discovery

This commit is contained in:
csh
2026-07-31 12:37:59 +08:00
parent 736d1a8ad7
commit 29d110110b
5 changed files with 136 additions and 67 deletions
+104 -46
View File
@@ -3,6 +3,7 @@
import argparse
import re
import sys
import unicodedata
from pathlib import Path
TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$")
@@ -43,6 +44,25 @@ OPTIONAL_COLUMNS = (
"owner",
"qualified_name",
)
SEARCH_FIELD_WEIGHTS = (
("name", 120),
("qualified_name", 120),
("signature", 90),
("tags", 70),
("summary", 50),
("module", 30),
("scope", 30),
("owner", 25),
("kind", 20),
("binding", 20),
("visibility", 20),
)
# Complete coverage in curated semantic text must outrank incidental short
# aliases in composite identifiers, such as "index" and "max".
SEMANTIC_COVERAGE_BONUSES = (
("tags", 240),
("summary", 200),
)
HELP_EPILOG = """\
检索分两步,--kw 的摘要不能直接作为 API 事实:
@@ -53,6 +73,7 @@ HELP_EPILOG = """\
lookup.py --name arrDropDuplicate
已知确切名称时可以直接用 --name,跳过第 1 步。
只查询一个 API scope 时使用 --scope,例如 --scope builtin 或 --scope dotnet。
退出码:
0 取回成功;--name 无匹配也是 0(打印提示,不算错误)
@@ -67,20 +88,6 @@ def non_empty(value):
return value
def find_tsv(start):
"""Walk upward from start and find a bundled or legacy function_index.tsv."""
for directory in [start, *start.parents]:
candidates = [
directory / "data" / "function_index.tsv",
directory / "function_index.tsv",
directory / "docs" / "tsl" / "codegen" / "function_index.tsv",
]
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def load_rows(tsv_path):
lines = tsv_path.read_text(encoding="utf-8").splitlines()
if not lines:
@@ -108,6 +115,45 @@ def load_rows(tsv_path):
return rows
def normalize(value):
return unicodedata.normalize("NFKC", value).casefold().strip()
def field_match_score(value, variant, weight):
normalized = normalize(value)
if not normalized or not variant:
return 0
if normalized == variant:
return weight + 20
if variant in normalized:
return weight
return 0
def term_score(row, term):
best = 0
for field, weight in SEARCH_FIELD_WEIGHTS:
score = field_match_score(row.get(field, ""), term, weight)
if score:
best = max(best, score)
return best
def field_covers_terms(row, field, terms):
value = normalize(row.get(field, ""))
return bool(value) and all(term in value for term in terms)
def keyword_sort_key(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 search_exact(rows, name):
key = name.casefold()
return [
@@ -122,26 +168,19 @@ def search_exact(rows, name):
def search_keyword(rows, terms):
lowered_terms = [term.lower() for term in terms]
matches = []
normalized_terms = [normalize(term) for term in terms]
ranked = []
for row in rows:
haystack = " ".join(
[
row.get("name", ""),
row.get("signature", ""),
row.get("module", ""),
row.get("tags", ""),
row.get("summary", ""),
row.get("kind", ""),
row.get("binding", ""),
row.get("visibility", ""),
row.get("owner", ""),
row.get("qualified_name", ""),
]
).casefold()
if all(term in haystack for term in lowered_terms):
matches.append(row)
return matches
scores = [term_score(row, term) for term in normalized_terms]
if scores and all(scores):
coverage_bonus = sum(
bonus
for field, bonus in SEMANTIC_COVERAGE_BONUSES
if field_covers_terms(row, field, normalized_terms)
)
ranked.append((sum(scores) + coverage_bonus, row))
ranked.sort(key=lambda item: (-item[0], *keyword_sort_key(item[1])))
return [row for _, row in ranked]
def slug(text):
@@ -334,12 +373,6 @@ def codegen_root_for_tsv(tsv_path):
return DEFAULT_CODEGEN_ROOT
def default_tsv_path():
if DEFAULT_TSV.is_file():
return DEFAULT_TSV
return find_tsv(Path.cwd())
def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
@@ -372,15 +405,24 @@ def main(argv=None):
"--kw",
nargs="+",
type=non_empty,
help="按关键词检索候选清单多个词是 AND 关系(全部命中才返回)"
"行搜索 name、signature、module、tags、summary、kind、binding、"
"visibility、owner 和 qualified_name 这些列的拼接文本,"
"大小写不敏感且按子串匹配。只返回摘要表格,不含条目正文",
help="按关键词检索候选清单多个查询词是 AND 关系;"
"词在 name、signature、tags、summary、module、scope、kind、binding、"
"visibility、owner 和 qualified_name 中做字面子串匹配,按字段加权并稳定排序。"
"同义词由索引的 tags 字段提供。"
"大小写不敏感,只返回摘要表格,不含条目正文",
)
parser.add_argument(
"--tsv",
metavar="PATH",
help="显式指定 function_index.tsv;默认使用本 skill 内置的 data/function_index.tsv",
help="显式指定 function_index.tsv;默认使用本 skill 内置的 "
"data/function_index.tsv,不从工作目录回退查找旧索引",
)
parser.add_argument(
"--scope",
type=non_empty,
metavar="SCOPE",
help="只查询指定 scope,大小写不敏感;作用于 --name 和 --kw。"
"内置索引当前提供 builtin 与 dotnet",
)
parser.add_argument(
"--limit",
@@ -394,8 +436,8 @@ def main(argv=None):
if args.limit < 1:
parser.error("--limit must be >= 1")
tsv_path = Path(args.tsv) if args.tsv else default_tsv_path()
if not tsv_path or not tsv_path.is_file():
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
if not tsv_path.is_file():
print(
"ERROR: function_index.tsv not found; reinstall tsl-api-reference "
"or pass --tsv PATH",
@@ -415,6 +457,20 @@ def main(argv=None):
file=sys.stderr,
)
return 1
if args.scope is not None:
requested_scope = normalize(args.scope)
available_scopes = sorted(
{row["scope"] for row in rows if row.get("scope")},
key=normalize,
)
if requested_scope not in {normalize(scope) for scope in available_scopes}:
print(
f"ERROR: unknown scope {args.scope!r}; available scopes: "
f"{', '.join(available_scopes)}",
file=sys.stderr,
)
return 2
rows = [row for row in rows if normalize(row["scope"]) == requested_scope]
codegen_root = codegen_root_for_tsv(tsv_path)
if args.name is not None:
@@ -450,6 +506,8 @@ def main(argv=None):
print(format_row(row))
if len(matches) > args.limit:
print(f"... {len(matches) - args.limit} more; refine keywords or raise --limit")
if not matches:
print("No matching API candidates; do not infer an API name.")
return 0