feat(tsl-codegen): refine generation and metadata enrichment

This commit is contained in:
csh
2026-08-11 18:11:32 +08:00
parent 7bd965de89
commit 7cb8b130bb
6 changed files with 1728 additions and 6 deletions
@@ -0,0 +1,778 @@
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).parents[1] / "scripts" / "enrich_metadata.py"
def load_script():
spec = importlib.util.spec_from_file_location("tsl_enrich_metadata", SCRIPT)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class SourceCatalogTest(unittest.TestCase):
def setUp(self):
self.module = load_script()
self.temp_dir = tempfile.TemporaryDirectory()
self.docs = Path(self.temp_dir.name)
def tearDown(self):
self.temp_dir.cleanup()
def write_corpus(self, corpus, rows, descriptions):
root = self.docs / corpus
pages = root / "pages"
pages.mkdir(parents=True)
manifest = [
"id\ttitle\turl\tpath\tsource_bytes\tutf8_bytes\tsha256\tstatus"
]
for page_id, title in rows:
manifest.append(
f"{page_id}\t{title}\thttp://example/{page_id}\t"
f"pages/{page_id}.html\t1\t1\tsha\tok"
)
description = descriptions[page_id]
(pages / f"{page_id}.html").write_text(
"<div id='help_content'><h3>"
+ title
+ "</h3><div><div class='DescriteTitle'>简述</div></div>"
+ f"<div>{description}</div></div>",
encoding="utf-8",
)
(root / "manifest.tsv").write_text(
"\n".join(manifest) + "\n", encoding="utf-8"
)
def test_exact_matching_preserves_underscores_and_prefers_scope_corpus(self):
self.write_corpus(
"tsl_base",
[("1", "alpha_test")],
{"1": "基础解释器说明"},
)
self.write_corpus(
"net_function",
[("2", "alpha_test"), ("3", "alphaTest")],
{"2": "NET 说明", "3": "无下划线的另一个函数"},
)
catalog = self.module.SourceCatalog.from_docs_root(self.docs)
builtin = self.module.EntryContext(
name="alpha_test",
signature="alpha_test()",
kind="function",
scope="builtin",
page="builtin/demo.md",
summary="原说明",
)
dotnet = self.module.EntryContext(
name="alpha_test",
signature="alpha_test()",
kind="function",
scope="dotnet",
page="dotnet/demo.md",
summary="原说明",
)
self.assertEqual("tsl_base", catalog.match(builtin)[0].corpus)
self.assertEqual("net_function", catalog.match(dotnet)[0].corpus)
self.assertNotIn(
"alphaTest", {record.title for record in catalog.match(dotnet)}
)
def test_duplicate_titles_remain_visible_for_audit(self):
self.write_corpus(
"net_function",
[("1", "same_name"), ("2", "same_name")],
{"1": "说明一", "2": "说明二"},
)
catalog = self.module.SourceCatalog.from_docs_root(self.docs)
entry = self.module.EntryContext(
name="same_name",
signature="same_name()",
kind="function",
scope="dotnet",
page="dotnet/demo.md",
summary="原说明",
)
self.assertEqual(["1", "2"], [item.page_id for item in catalog.match(entry)])
class DescriptionQualityTest(unittest.TestCase):
def setUp(self):
self.module = load_script()
def entry(self, **overrides):
values = {
"name": "profitRatio",
"signature": "profitRatio(report_date)",
"kind": "function",
"scope": "dotnet",
"page": "dotnet/financial/profitability.md",
"summary": "总资产收益率(%)。",
}
values.update(overrides)
return self.module.EntryContext(**values)
def test_removes_only_terminal_sentence_punctuation(self):
entry = self.entry(summary="总资产收益率(%)。")
self.assertEqual(
"返回总资产收益率(%)",
self.module.improve_description(entry, []),
)
def test_preserves_formula_and_identifiers(self):
entry = self.entry(
summary="总资产报酬率(%)=利润总额/平均资产总额*100。"
)
self.assertEqual(
"计算总资产报酬率(%),公式为利润总额/平均资产总额*100",
self.module.improve_description(entry, []),
)
def test_rejects_source_process_prose(self):
entry = self.entry(summary="返回指定值")
source = self.module.SourceRecord(
corpus="net_function",
page_id="1",
title="profitRatio",
page=Path("1.html"),
description="Windows 已验证通过,Linux 返回 not found",
)
self.assertEqual(
"返回指定值", self.module.improve_description(entry, [source])
)
def test_current_environment_can_be_real_api_semantics(self):
self.assertEqual(
"获取当前环境时间系统参数",
self.module.safe_source_description("获取当前环境时间系统参数。"),
)
def test_property_access_is_stated_explicitly(self):
entry = self.entry(
name="Host",
signature="Host",
kind="property",
scope="builtin",
page="builtin/object/ftp.md",
summary="远程服务器地址。",
access="read/write",
owner="FTP",
)
self.assertEqual(
"获取或设置远程服务器地址",
self.module.improve_description(entry, []),
)
def test_boolean_property_description_is_idempotent(self):
entry = self.entry(
name="UseTLS",
signature="UseTLS",
kind="property",
scope="builtin",
page="builtin/object/imap.md",
summary="是否采用 SSL 连接",
access="read/write",
owner="IMAP",
)
first = self.module.improve_description(entry, [])
repeated = self.module.improve_description(
self.module.EntryContext(
**{**entry.__dict__, "summary": first}
),
[],
)
self.assertEqual("控制是否采用 SSL 连接", first)
self.assertEqual(first, repeated)
def test_low_confidence_sentence_is_kept_except_terminal_punctuation(self):
entry = self.entry(
name="opaqueApi",
page="dotnet.md",
summary="根据调用上下文处理结果,具体规则取决于输入。",
)
self.assertEqual(
"根据调用上下文处理结果,具体规则取决于输入",
self.module.improve_description(entry, []),
)
def test_short_description_is_not_replaced_by_ambiguous_source_text(self):
entry = self.entry(
name="clRed",
page="builtin/color.md",
summary="红色",
)
sources = [
self.module.SourceRecord(
corpus="net_function",
page_id="1",
title="clRed",
page=Path("1.html"),
description="定义",
)
]
self.assertEqual("红色", self.module.improve_description(entry, sources))
def test_existing_action_word_is_normalized_without_duplicate_verb(self):
getter = self.entry(
name="getCValue",
page="builtin/color.md",
summary="得到颜色的 CMYK 模式 C 值。",
)
writer = self.entry(
name="write",
page="builtin/cgi.md",
summary="输出字符串。",
)
self.assertEqual(
"获取颜色的 CMYK 模式 C 值",
self.module.improve_description(getter, []),
)
self.assertEqual("输出字符串", self.module.improve_description(writer, []))
def test_equation_inside_algorithm_sentence_is_not_rewritten_as_formula(self):
entry = self.entry(
name="se_Gauss",
page="builtin/optimization.md",
summary="用高斯消去法求解线性方程组 AX = B",
)
self.assertEqual(
"用高斯消去法求解线性方程组 AX = B",
self.module.improve_description(entry, []),
)
def test_builtin_class_gets_a_purpose_description(self):
entry = self.entry(
name="TStringList",
signature="TStringList",
kind="class",
scope="builtin",
page="builtin/object/tstringlist.md",
summary="TStringList 内置对象",
)
self.assertEqual(
"提供字符串集合存储、查找、排序和名称值管理能力的内置对象",
self.module.improve_description(entry, []),
)
def test_long_property_explanation_is_not_wrapped_in_access_boilerplate(self):
entry = self.entry(
name="CommaTextW",
signature="CommaTextW",
kind="property",
scope="builtin",
page="builtin/object/tstringlist.md",
summary=(
"功能同 CommaText,区别是在读取时返回宽字节字符串,"
"而 CommaText 返回多字节字符串"
),
access="read/write",
owner="TStringList",
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_short_read_and_load_phrases_are_normalized(self):
read_entry = self.entry(
name="read",
page="builtin/object/tstream.md",
summary="读出内容",
)
load_entry = self.entry(
name="loadFromFile",
page="builtin/object/tstringlist.md",
summary="从指定的文件中装载内容",
)
self.assertEqual(
"读取内容", self.module.improve_description(read_entry, [])
)
self.assertEqual(
"从指定的文件中加载内容",
self.module.improve_description(load_entry, []),
)
def test_embedded_read_out_is_normalized_without_duplicate_prefix(self):
entry = self.entry(
name="readExcelSheets",
page="dotnet.md",
summary="从Excel文件中读出Sheets列表。",
)
self.assertEqual(
"从Excel文件中读取Sheets列表",
self.module.improve_description(entry, []),
)
def test_existing_decomposition_verb_is_not_prefixed(self):
entry = self.entry(
name="decodeGraphGroup",
page="builtin/graph.md",
summary="分解图形组合并写入输出参数",
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_comparison_expression_is_not_rewritten_as_formula(self):
entry = self.entry(
name="stockStepAmount",
page="dotnet/financial/stock-capital-flow.md",
summary="分档区间 V1<=Value<V2 的成交金额",
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_financial_action_sentence_is_not_prefixed_with_return(self):
for summary in (
"提取股票的财务比率,与系统证券相关",
"根据报告期获取财务指标",
"将日线数据转换为周线数据",
"统计指定报告期的基金数量",
"生成指定区间的分析结果",
):
with self.subTest(summary=summary):
entry = self.entry(
name="annualRatio",
page="dotnet/financial/financial-analysis.md",
summary=summary,
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_financial_action_clauses_are_not_prefixed_with_return(self):
for summary in (
"用于生成投资组合结果名称",
"如果存在对应记录则返回证券代码",
"对矩阵进行正交处理",
"从净值表中获取基金列表",
"由交易价格倒推出隐含波动率",
"按照周期读取数据",
"与系统参数证券和日期相关",
):
with self.subTest(summary=summary):
entry = self.entry(
name="financialHelper",
page="dotnet/financial/financial-analysis.md",
summary=summary,
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_financial_boolean_description_uses_judgment_wording(self):
entry = self.entry(
name="isBankBond",
page="dotnet/financial/bond-basic-info.md",
summary="是否银行间债券。如果是,返回1,否则返回0",
)
self.assertEqual(
"判断是否银行间债券。如果是,返回1,否则返回0",
self.module.improve_description(entry, []),
)
def test_function_boilerplate_is_removed_from_action_description(self):
entry = self.entry(
name="cb_ytm",
page="dotnet/financial/convertible-bond.md",
summary="该函数采用牛顿迭代法获取可转债的到期收益率",
)
self.assertEqual(
"采用牛顿迭代法获取可转债的到期收益率",
self.module.improve_description(entry, []),
)
def test_financial_numbered_explanation_is_not_prefixed_with_return(self):
entry = self.entry(
name="financialMode",
page="dotnet/financial/financial-analysis.md",
summary="1 表示合并报表,2 表示母公司报表",
)
self.assertEqual(
entry.summary, self.module.improve_description(entry, [])
)
def test_related_data_placeholder_is_made_explicit(self):
entry = self.entry(
name="cb_downPeriodConversionPeriod",
page="dotnet/financial/convertible-bond.md",
summary="CB_DownPeriodConversionPeriod 相关函数",
)
self.assertEqual(
"返回 CB_DownPeriodConversionPeriod 对应的数据",
self.module.improve_description(entry, []),
)
def test_reviewed_generic_descriptions_use_precise_purpose_text(self):
cases = (
(
"sf_Normal",
"统计分布相关函数。",
"计算正态分布函数值",
),
(
"readFile",
"文件访问函数相关函数。",
"读取本地文件中的数据",
),
(
"unicodeEsc2",
"多语言支持函数相关函数。",
"将字符串转换为 \\uxxxx 形式的 Unicode 编码串",
),
)
for name, summary, expected in cases:
with self.subTest(name=name):
entry = self.entry(name=name, page="dotnet.md", summary=summary)
self.assertEqual(
expected, self.module.improve_description(entry, [])
)
def test_reviewed_awkward_descriptions_are_rewritten_directly(self):
cases = (
(
"encodeGraph",
"返回根据指定的类型、名称、数据以及属性数组生成的图形。",
"根据指定的类型、名称、数据和属性数组生成图形并返回",
),
(
"dupeString",
"返回将指定的字符串AText重复ACount次后的字符串",
"将字符串 AText 重复 ACount 次并返回结果",
),
(
"stockpjcj",
"返回:区间平均成交,区间成交金额/区间成交量",
"返回区间平均成交价,计算公式为区间成交金额 / 区间成交量",
),
)
for name, summary, expected in cases:
with self.subTest(name=name):
entry = self.entry(
name=name, page="builtin/string.md", summary=summary
)
self.assertEqual(
expected, self.module.improve_description(entry, [])
)
class TagDerivationTest(unittest.TestCase):
def setUp(self):
self.module = load_script()
def test_array_deduplication_gets_controlled_chinese_and_english_aliases(self):
entry = self.module.EntryContext(
name="arrDropDuplicate",
signature="arrDropDuplicate(data)",
kind="function",
scope="builtin",
page="builtin/array.md",
summary="删除数组中的重复元素",
)
page = self.module.PageContext(
title="Builtin / 数组", path="builtin/array.md"
)
tags = self.module.derive_tags(entry, page)
self.assertTrue(
{"数组", "列表", "去重", "删除重复", "array", "deduplicate"}
<= set(tags)
)
self.assertLessEqual(len(tags), 12)
self.assertNotIn("函数", tags)
self.assertNotIn("builtin", tags)
def test_file_digest_gets_digest_and_hash_aliases(self):
entry = self.module.EntryContext(
name="getMsgDigest",
signature="getMsgDigest(s, mode)",
kind="function",
scope="builtin",
page="builtin/string.md",
summary="计算字符串摘要,支持 CRC32、MD5、SHA 和 SM3",
)
page = self.module.PageContext(
title="Builtin / 字符串", path="builtin/string.md"
)
tags = self.module.derive_tags(entry, page)
self.assertTrue(
{"字符串", "摘要", "哈希", "散列", "digest", "hash"}
<= set(tags)
)
def test_existing_tags_are_preserved_first_and_output_is_idempotent(self):
entry = self.module.EntryContext(
name="readFile",
signature="readFile(alias, file_name)",
kind="function",
scope="builtin",
page="builtin/file.md",
summary="读取文件内容",
tags=("自定义", "读取"),
)
page = self.module.PageContext(
title="Builtin / 文件", path="builtin/file.md"
)
first = self.module.derive_tags(entry, page)
second = self.module.derive_tags(
self.module.EntryContext(**{**entry.__dict__, "tags": tuple(first)}),
page,
)
self.assertEqual(["自定义", "读取"], first[:2])
self.assertEqual(first, second)
def test_substrings_do_not_create_digest_or_com_tags(self):
reshape = self.module.EntryContext(
name="reshape",
signature="reshape(data, shape)",
kind="function",
scope="builtin",
page="builtin/array.md",
summary="重构数组形状",
)
uncompress = self.module.EntryContext(
name="uniuncompress",
signature="uniuncompress(data, type)",
kind="function",
scope="builtin",
page="builtin/string.md",
summary="统一解压缩数据",
)
reshape_tags = self.module.derive_tags(
reshape,
self.module.PageContext("Builtin / 数组", reshape.page),
)
uncompress_tags = self.module.derive_tags(
uncompress,
self.module.PageContext("Builtin / 字符串", uncompress.page),
)
self.assertNotIn("摘要", reshape_tags)
self.assertNotIn("hash", reshape_tags)
self.assertNotIn("COM", uncompress_tags)
self.assertNotIn("OLE", uncompress_tags)
def test_runtime_and_character_names_do_not_create_time_or_graph_tags(self):
random_entry = self.module.EntryContext(
name="randomfrom",
signature="randomfrom(values)",
kind="function",
scope="builtin",
page="builtin/runtime.md",
summary="返回随机数组元素",
)
char_entry = self.module.EntryContext(
name="charToByteLen",
signature="charToByteLen(s, max_len)",
kind="function",
scope="builtin",
page="builtin/string.md",
summary="计算字符串占用的字节数",
)
random_tags = self.module.derive_tags(
random_entry,
self.module.PageContext("Builtin / 运行时", random_entry.page),
)
char_tags = self.module.derive_tags(
char_entry,
self.module.PageContext("Builtin / 字符串", char_entry.page),
)
self.assertNotIn("日期时间", random_tags)
self.assertNotIn("图形", char_tags)
def test_parameter_name_does_not_create_unrelated_time_domain(self):
entry = self.module.EntryContext(
name="futureValue",
signature=(
"futureValue(rate, n_periods, payment, present_value, payment_time)"
),
kind="function",
scope="builtin",
page="builtin/numeric.md",
summary="返回一项投资的未来值",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext("Builtin / 数值计算", entry.page),
)
self.assertNotIn("日期时间", tags)
def test_hash_index_is_not_tagged_as_cryptographic_digest(self):
entry = self.module.EntryContext(
name="THashedStringList",
signature="THashedStringList",
kind="class",
scope="builtin",
page="builtin/object/thashedstringlist.md",
summary="使用哈希索引加速查找的字符串列表内置对象",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext(
"Object / THashedStringList 字符串列表", entry.page
),
)
self.assertIn("哈希索引", tags)
self.assertIn("hash", tags)
self.assertNotIn("摘要", tags)
self.assertNotIn("digest", tags)
def test_business_statistics_verb_does_not_add_probability_domain(self):
entry = self.module.EntryContext(
name="fundCount",
signature="fundCount(report_date)",
kind="function",
scope="dotnet",
page="dotnet/financial/fund-holdings.md",
summary="统计指定报告期的基金数量",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext("金融 / 基金 / 持仓", entry.page),
)
self.assertNotIn("概率", tags)
self.assertNotIn("statistics", tags)
self.assertNotIn("统计指定报告期的基金数量", tags)
self.assertNotIn("证券", tags)
def test_taxonomy_tags_remove_generic_suffixes(self):
entry = self.module.EntryContext(
name="weightedMedian",
signature="weightedMedian(data, weights)",
kind="function",
scope="dotnet",
page="dotnet/financial/weighted-statistics.md",
summary="计算加权中位数",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext(
"基础算法常见加权统计量及其实现 / 时间相关函数",
entry.page,
),
)
self.assertIn("基础算法常见加权统计量", tags)
self.assertIn("时间", tags)
self.assertNotIn("基础算法常见加权统计量及其实现", tags)
self.assertNotIn("时间相关函数", tags)
def test_taxonomy_tags_drop_parenthetical_explanations_and_punctuation(self):
entry = self.module.EntryContext(
name="close",
signature="close()",
kind="function",
scope="dotnet",
page="dotnet/datawarehouse/after_market.md",
summary="收盘价,与系统参数股票、时间、周期和复权相关",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext(
"数据仓库 / 盘后相关(在当日可用于盘中)", entry.page
),
)
self.assertIn("数据仓库", tags)
self.assertIn("盘后", tags)
self.assertNotIn("(", tags)
self.assertNotIn(")", tags)
self.assertNotIn("盘后相关在当日可用于盘中", tags)
self.assertNotIn("关闭", tags)
self.assertNotIn("close", tags)
def test_name_intent_does_not_override_an_explicit_action_clause(self):
entry = self.module.EntryContext(
name="openEndFundFilterByFundIndex",
signature="openEndFundFilterByFundIndex(fund_inds)",
kind="function",
scope="dotnet",
page="dotnet/financial/fund-pool.md",
summary="根据基金指数获取成分基金,并对基金进行筛选",
)
tags = self.module.derive_tags(
entry,
self.module.PageContext("金融 / 基金 / 基金池", entry.page),
)
self.assertNotIn("打开", tags)
self.assertNotIn("open", tags)
class MarkdownEnrichmentTest(unittest.TestCase):
def setUp(self):
self.module = load_script()
def test_inserts_tags_after_description_without_touching_code(self):
text = (
"# Builtin / 数组\n\n"
"## `arrDropDuplicate(data)`\n\n"
"声明:function\n\n"
"删除数组中的重复元素。\n\n"
"| 参数 | 类型 | 说明 |\n"
"| --- | --- | --- |\n"
"| `data` | array | 输入数组 |\n\n"
"返回:array\n\n"
"### 示例\n\n"
"```tsl\nreturn arrDropDuplicate(array(1, 1));\n```\n"
)
result, audit = self.module.enrich_markdown(
text, "builtin/array.md", self.module.SourceCatalog.empty()
)
repeated, repeated_audit = self.module.enrich_markdown(
result, "builtin/array.md", self.module.SourceCatalog.empty()
)
self.assertIn("删除数组中的重复元素\n\n<!-- tags:", result)
self.assertIn("return arrDropDuplicate(array(1, 1));", result)
self.assertEqual(result, repeated)
self.assertEqual(1, audit.changed_descriptions)
self.assertEqual(1, audit.changed_tags)
self.assertEqual(0, repeated_audit.changed_descriptions)
self.assertEqual(0, repeated_audit.changed_tags)
if __name__ == "__main__":
unittest.main()