From cd0405d64b1a67a79530e5fcbfa90c389d000f3e Mon Sep 17 00:00:00 2001 From: csh Date: Thu, 20 Aug 2026 17:47:48 +0800 Subject: [PATCH] :recycle: refactor(tsl-api-reference): split lookup workflows and maintenance tooling --- CONTRIBUTING.md | 5 +- README.md | 4 +- skills/README.md | 2 +- skills/tsl-api-reference/SKILL.md | 169 +++------------ .../tsl-api-reference/scripts/class_lookup.py | 73 +++++-- .../scripts/dictionary_lookup.py | 41 +++- .../scripts/framework_lookup.py | 166 +-------------- skills/tsl-api-reference/scripts/lookup.py | 13 +- .../tsl-api-reference/workflows/api-lookup.md | 58 ++++++ .../workflows/class-and-framework.md | 57 +++++ .../workflows/dictionary-lookup.md | 39 ++++ test/test_playbook.py | 54 +++++ test/test_tsl_api_framework.py | 195 +++++++++++++++--- tools/tsl-codegen/MAINTENANCE.md | 83 ++++++++ tools/tsl-codegen/README.md | 14 +- .../tsl-codegen}/scripts/build_dictionary.py | 2 +- tools/tsl-codegen/tests/test_lookup.py | 97 ++++++++- tools/tsl-codegen/tests/test_pipeline.py | 9 +- 18 files changed, 707 insertions(+), 374 deletions(-) create mode 100644 skills/tsl-api-reference/workflows/api-lookup.md create mode 100644 skills/tsl-api-reference/workflows/class-and-framework.md create mode 100644 skills/tsl-api-reference/workflows/dictionary-lookup.md create mode 100644 tools/tsl-codegen/MAINTENANCE.md rename {skills/tsl-api-reference => tools/tsl-codegen}/scripts/build_dictionary.py (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 685da7bb..531ed9a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ backwards compatible when possible. ## What to change - Templates: `templates/`, `rulesets/`, `docs/` -- Tooling: `scripts/` +- Tooling: `scripts/`, `tools/tsl-codegen/` - Tests: `test/` ## Commit messages @@ -28,3 +28,6 @@ python test/integration/check_doc_links.py - Keep placeholder definitions documented in `templates/README.md`. - Update template last-updated dates when changing template content. + +TSL API 页面、索引、数据字典和 Framework Profile 的仓库维护流程见 +[`tools/tsl-codegen/MAINTENANCE.md`](tools/tsl-codegen/MAINTENANCE.md)。 diff --git a/README.md b/README.md index fcfad9ab..19d17871 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Layer 2: skills/ (按需加载,$skill-name 触发) ├─ commit-message: 提交信息规范 ├─ cook-it-through: 工程任务路由、本地 ticket 生命周期及其执行引擎 ├─ tsl-syntax-reference: TSL 语法条目、写法验证和错误边界 - ├─ tsl-api-reference: TSL API 名称、签名、参数和返回值 + ├─ tsl-api-reference: TSL API、class/framework 契约和天软数据字典 └─ thirdparty/: 第三方同步 skills Layer 3: docs/ (仓库内静态文档) @@ -153,7 +153,7 @@ skills = ["tsl-syntax-reference", "tsl-api-reference"] ## tools/(维护工具) -- `tools/tsl-codegen/`:TSL 函数文档生成、校验与索引工具套件,供维护 `tsl-api-reference` 的函数文档树使用,详见 [tools/tsl-codegen/README.md](tools/tsl-codegen/README.md)。 +- `tools/tsl-codegen/`:TSL 函数文档生成、校验与索引工具套件,供维护 `tsl-api-reference` 的函数文档树使用,详见 [用户指南](tools/tsl-codegen/README.md) 和 [维护指南](tools/tsl-codegen/MAINTENANCE.md)。 套件与 skill 解耦:手动部署 skill 时套件不随行;仅公共同步(`scripts/build_tsl_playbook.py` 构建 + `.gitea/workflows/sync-tsl-playbook.yml` 发布)会把套件随 skill 一起带到 `tsl-playbook` 分支。 diff --git a/skills/README.md b/skills/README.md index 018cd0e8..ba537a17 100644 --- a/skills/README.md +++ b/skills/README.md @@ -15,7 +15,7 @@ | `gitea-fix-ci` | 基于 Gitea Actions run/job/log 诊断失败 CI,先形成修复计划再改代码;含 `fetch_ci_logs.py` 取证脚本 | Gitea PR checks 失败、远端 CI 红但本地需要定位 | | `cook-it-through` | 运行任务路由、Matt 工程主链和本地 Markdown ticket 生命周期 | 处理实质性工程任务,或操作 `.scratch` queue、claim、finish、integrate | | `tsl-syntax-reference` | 查询 TSL 语法条目,验证具体写法和错误边界;不负责 API、命名、风格、工具链或模块集成 | 写/改/审 TSL 时确认语言结构、表达式、控制流、对象模型和语法限制 | -| `tsl-api-reference` | 查询随 skill 分发的 TSL API 参考:按名精确查条目,或按中文关键词发现候选 | 写/审 TSL 时确认 builtin、dotnet、模块 API 的签名、参数、返回值和示例 | +| `tsl-api-reference` | 查询 TSL API、class/framework 契约和天软数据字典;按 workflow 路由并由脚本 help 定义调用契约 | 确认 API/成员/回调的签名和示例,或按中文业务含义查表、数据源与字段 | ## 第三方 Skills diff --git a/skills/tsl-api-reference/SKILL.md b/skills/tsl-api-reference/SKILL.md index 465516a2..aff52b96 100644 --- a/skills/tsl-api-reference/SKILL.md +++ b/skills/tsl-api-reference/SKILL.md @@ -1,157 +1,50 @@ --- name: tsl-api-reference -description: "当编写、修改或审查 TSL 代码时需要确认 builtin、dotnet 或 module API 是否存在及其精确签名、参数、返回值和示例时使用;也用于按自然语言查找天软数据字典中的表、数据源与字段。" +description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员或框架回调是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。" --- # TSL API 参考 -本 skill 仅用于 TSL API 事实:函数/模块名、精确签名、参数、返回值和示例。 -若需查询 TSL 语法、控制流、变量、对象模型或运行时语言规则,请改用 -`tsl-syntax-reference` skill。 +本 skill 只负责可检索的 TSL API、class/framework 契约和天软数据字典事实。 +语法、控制流、语言对象模型或运行方式问题不属于本 skill。 ## 查询路由 -先判断用户请求是普通 API、class,还是带生命周期的框架类工作流: +先选择一条路由,并完整读取对应文件后再执行: -- 普通函数、单个成员或数据字典问题走现有 `lookup.py` / `dictionary_lookup.py`。 -- 查询或使用任意 class 时,先用 `class_lookup.py` 取回紧凑的类摘要和成员分类; - 需要某个成员正文时,再运行 `lookup.py --name` 精确查询。 -- `class_lookup.py` 自动覆盖 `function_index.tsv` 中的全部 class,不要求每个普通对象类 - 都维护人工脚手架。 -- 用户要实现框架子类、重写回调或组合一组有生命周期的 class API 时,类级查询会叠加 - `framework_index.json` 中证据化的 Framework Profile,返回生命周期、必要回调、 - 状态、执行入口和结果查询。 -- 不得因为查询目标是 class 就推断它是框架;普通对象、服务类和数据类只使用自动生成的 - 类成员视图。只有 `kind=framework` 且具有证据化生命周期的类才加载人工 Profile。 +- 已知函数或成员名,或只知道 API 行为:读取 + [`workflows/api-lookup.md`](workflows/api-lookup.md)。 +- 查询 class 摘要、实现子类、重写回调或处理生命周期:读取 + [`workflows/class-and-framework.md`](workflows/class-and-framework.md)。 +- 按中文业务含义查表、数据源或字段:读取 + [`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。 -列出和查询 class: +具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。 +将 `` 替换为包含本 `SKILL.md` 的目录。 -```bash -python /scripts/class_lookup.py --list -python /scripts/class_lookup.py --class TStringList -python /scripts/class_lookup.py --class TSBackTesting -python /scripts/class_lookup.py \ - --class TSBackTesting --config fGroupType=1 --format json -``` +## 全局硬规则 -同名 class 出现在多个 scope 或模块时,状态为 `ambiguous`;使用 `--scope`、`--module` -或完全限定名消歧,不能选择第一项。 +- API 名称按索引原样查询和报告。TSL 名称大小写不敏感,但下划线有意义;不得删除 + 下划线或改写成驼峰式。 +- 不得凭记忆或参照相似语言推断 API 名称、签名、参数、返回值或回调字段。 +- 同名结果不得默认取第一条;必须按 `scope`、`module`、owner 或完全限定名消歧。 +- `lookup.py` 的 rc=1 表示 skill 数据缺失、损坏或正文不可达,不表示 API 不存在。 -普通 class 的 `profile_status=not_profiled` 不是错误,只表示它使用索引自动生成的成员 -视图。框架类的 Profile 只描述 API 协议,不直接生成 TSL 语法;将结果作为 -Framework Contract Packet 交给 `tsl-syntax-reference` 组合代码结构。 +## 🔴 CHECKPOINT · 🛑 STOP -`scaffold_status=resolved` 只表示类级生命周期和成员引用完整,不表示模式相关的回调 -记录结构已经确定。只要 `contract_status` 不是 `resolved`,就必须停止生成回调字段, -按 `diagnostics` 补齐模式和返回 schema 证据。 +出现任一情况时,停止生成包含该 API 或回调字段的代码,并明确报告缺口: -维护 Profile 后运行引用校验: +- `lookup.py --name` 无匹配; +- `lookup.py --kw` 换用更准确的中文关键词后仍无候选; +- 同名 API 或 class 无法按 `scope`、`module`、owner 或完全限定名消歧; +- `class_lookup.py` 返回 `status: not_found`; +- 请求依赖生命周期、配置或回调,但 `profile_status=not_profiled`; +- Framework Profile 的 `contract_status` 不是 `resolved`。 -```bash -python /scripts/class_lookup.py --check -``` +rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。 -天软数据字典中的表、数据源和字段不是可调用 API,不进入 `function_index.tsv` -函数问题使用 `lookup.py`,表或字段问题使用 `dictionary_lookup.py` +## 输出约定 -## 数据字典查询 - -用户按中文业务含义描述表或字段时,运行独立的字典查询脚本 - -```bash -python /scripts/dictionary_lookup.py --query "股票现金流指标销售现金比率" -``` - -已知业务范围、表名、数据源名或字段名时使用过滤参数 - -```bash -python /scripts/dictionary_lookup.py --query "市值" --scope fund -python /scripts/dictionary_lookup.py --query "买一量" --table TradeTable -python /scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率 -``` - -查询结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、 -访问代码、对应 API 和字典页面 - -- 标识符匹配不删除下划线 -- 同义词只从 `dictionary_lexicon.json` 中取受控映射 -- `status: ambiguous` 时必须向用户列出候选并要求补充表或数据源范围,不能默认选择第一项 -- `MarketTable` 和 `TradeTable` 是 TS-SQL 数据源,不是函数 -- `status: no_match` 表示字典无匹配,不能改述为 API 的 `not found` - -不要凭记忆或参照相似语言推断 API 签名。TSL 名称大小写不敏感,但下划线是有意义的: -查询或报告 API 时不要删除下划线,也不要把名称改写成驼峰式。 - -运行随附的查询脚本。将 `` 替换为包含本 `SKILL.md` 的目录。 - -- 已知 API 名称: - - ```bash - python /scripts/lookup.py --name argmax - ``` - -- 已知 class/unit 成员的完全限定名称: - - ```bash - python /scripts/lookup.py --name DemoUnit.Document.Save - ``` - -- 未知名称,但知道其行为或中文关键词: - - ```bash - python /scripts/lookup.py --kw 最大值 位置 - ``` - -- 只查询一个 scope: - - ```bash - python /scripts/lookup.py --scope builtin --name boolToStr - python /scripts/lookup.py --scope dotnet --kw argmax - python /scripts/lookup.py --scope module --name bondAccruedInterest - ``` - -- 关键词采用字面 AND 语义;增加关键词可缩小结果范围。常用同义词由 API 条目的 - Tags 提供,例如“位置/下标”“列表/数组”“打印/输出”。 -- 关键词会搜索 `name`、`signature`、`tags`、`summary`、`module`、`scope` 及 - `qualified_name`、`owner`、`kind`、`binding`、`visibility`,并按字段加权稳定排序; - 因此也可用 `--kw dotnet argmax` 或 `--kw protected method` 缩小范围。 -- 使用 `--limit N` 调整打印的候选行数量,N 必须 >= 1。 - -精确查询会打印完整的条目正文和来源标记。关键词查询打印候选行; -选定候选项后,再用 `--name` 重新执行精确查询。 - -`--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。查询简单成员名会 -返回所有 owner 下的同名 API;查询 `Unit.Class.Member` 等完全限定名称可限定所属 -class/unit。重载共享 `qualified_name`,精确查询会返回该名称的全部重载。 - -混合页面中的精确查询仍按唯一 `page#anchor` 定位,只截取目标 H2 或成员子树。 -每个 H2 的声明类型独立,不使用全局页面类型。 - -关键词候选优先显示 `qualified_name`,并根据 kind/binding 显示声明术语:实例 -method 显示 `function`,类方法显示 `class function`,静态字段与常量分别显示 -`static field`、`static const`。 - -同名 API 存在于多个 scope 或模块时(如 `host`、`login`、`port`),`--name` -会依次打印全部同名条目,每条带自己的来源标记。已知 scope 时先用 `--scope` -过滤;否则按当前代码所处的 scope 和模块选择条目,不要默认取第一条。 - -查询脚本兼容旧 8 列和新 13 列 `function_index.tsv`。旧 8 列索引仍可查询原有 -function;完全限定名称、owner、kind、binding、visibility 依赖新索引中的扩展列。 - -## 退出码 - -| 退出码 | 含义 | 处理方式 | -| ------ | -------------------------------------- | ------------------------------------------------ | -| 0 | 查询完成,含"查无此 API"和关键词零命中 | 读取输出;无匹配时换中文关键词重试 | -| 1 | 随附数据缺失或为空 | 重新安装本 skill,或用 `--tsv PATH` 指向可用索引 | -| 2 | 参数不合法 | 按提示修正参数后重跑 | - -rc=1 表示 skill 数据本身有问题,不表示该 API 不存在;此时不要据此断言 API 缺失。 - -## 常见错误 - -- 删掉下划线或改写成驼峰式后查询,导致误判 API 不存在。名称按原样查。 -- 把关键词查询的 summary 当作签名事实。summary 只用于挑候选,签名必须由 - `--name` 的条目正文提供。 -- 多条同名结果里随手取第一条,忽略 scope 与模块差异。 -- 查不到就凭记忆或相似语言推断签名。查不到时说明缺口并停止。 +报告 API 事实时必须包含:索引中的原样名称或 `qualified_name`、`scope`、`module`、 +由 `lookup.py --name` 正文取得的精确签名,以及对应的 `page#anchor` 来源。 +关键词候选的 summary 只能用于选候选,不能作为签名事实。 diff --git a/skills/tsl-api-reference/scripts/class_lookup.py b/skills/tsl-api-reference/scripts/class_lookup.py index 8a36584c..24a5569a 100644 --- a/skills/tsl-api-reference/scripts/class_lookup.py +++ b/skills/tsl-api-reference/scripts/class_lookup.py @@ -16,6 +16,43 @@ from framework_lookup import ( 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 Profile;not_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"] @@ -232,24 +269,32 @@ def main(argv=None): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser( - description="Query indexed TSL classes and curated framework profiles.", + 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="exact class name") - action.add_argument("--list", action="store_true", help="list indexed classes") - action.add_argument("--check", action="store_true", help="validate framework profiles") - parser.add_argument("--scope", help="filter class scope") - parser.add_argument("--module", help="filter class module") + 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="raw config" + "--config", + action="append", + default=[], + metavar="KEY=VALUE", + help="传入原始框架配置,可重复", ) parser.add_argument( - "--include-members", action="store_true", help="include the compact member list" + "--include-members", action="store_true", help="在类摘要中包含紧凑成员清单" ) - parser.add_argument("--format", choices=("text", "json"), default="text") - parser.add_argument("--profiles", metavar="PATH", help="framework profile index") - parser.add_argument("--tsv", metavar="PATH", help="function index") + 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: @@ -257,10 +302,12 @@ def main(argv=None): if args.include_members and not args.class_name: parser.error("--include-members requires --class") - profile_path = Path(args.profiles) if args.profiles else DEFAULT_INDEX tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV + profile_path = profile_path_for(args) try: - profiles = load_index(profile_path) + 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) diff --git a/skills/tsl-api-reference/scripts/dictionary_lookup.py b/skills/tsl-api-reference/scripts/dictionary_lookup.py index fbe2038f..1e181d31 100644 --- a/skills/tsl-api-reference/scripts/dictionary_lookup.py +++ b/skills/tsl-api-reference/scripts/dictionary_lookup.py @@ -35,6 +35,24 @@ REQUIRED_COLUMNS = ( "tags", ) +HELP_EPILOG = """\ +按中文业务含义查询表、数据源或字段: + dictionary_lookup.py --query "股票现金流指标销售现金比率" + dictionary_lookup.py --query "市值" --scope fund + dictionary_lookup.py --query "买一量" --table TradeTable + dictionary_lookup.py --query "9900700" --field 销售现金比率 + +输出第一行是 in-band 状态: + status: ok 最高分候选唯一 + status: ambiguous 多个同分候选;必须补充 scope、table 或 field 范围 + status: no_match 数据字典无匹配,不等于 API not found + +退出码: + 0 查询完成;ok、ambiguous、no_match 均由 status 表达 + 1 dictionary_index.tsv 或词表缺失、损坏、格式错误 + 2 参数不合法 +""" + @dataclass(frozen=True) class QueryItem: @@ -318,14 +336,21 @@ def _positive_int(value: str) -> int: def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False) - parser.add_argument("--query", required=True) - parser.add_argument("--scope") - parser.add_argument("--table") - parser.add_argument("--field") - parser.add_argument("--limit", type=_positive_int, default=10) - parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV) - parser.add_argument("--lexicon", type=Path, default=DEFAULT_LEXICON) + parser = argparse.ArgumentParser( + description="查询独立的天软表、数据源与字段字典索引。", + epilog=HELP_EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + allow_abbrev=False, + ) + parser.add_argument("--query", required=True, help="中文业务描述、名称或 ID") + parser.add_argument("--scope", help="按业务范围过滤") + parser.add_argument("--table", help="按表名或数据源名过滤") + parser.add_argument("--field", help="按字段名或字段 ID 过滤") + parser.add_argument("--limit", type=_positive_int, default=10, help="候选条数上限") + parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV, help="字典索引路径") + parser.add_argument( + "--lexicon", type=Path, default=DEFAULT_LEXICON, help="受控同义词词表路径" + ) return parser.parse_args(argv) diff --git a/skills/tsl-api-reference/scripts/framework_lookup.py b/skills/tsl-api-reference/scripts/framework_lookup.py index 5bce9e79..c491ea0b 100644 --- a/skills/tsl-api-reference/scripts/framework_lookup.py +++ b/skills/tsl-api-reference/scripts/framework_lookup.py @@ -1,15 +1,10 @@ #!/usr/bin/env python3 -"""Resolve framework-class profiles without replacing ordinary API lookup.""" +"""Library for validating and resolving evidence-backed framework profiles.""" -import argparse import json -import sys from pathlib import Path from lookup import ( - DEFAULT_TSV, - codegen_root_for_tsv, - load_rows, normalize, search_exact, slice_entry, @@ -230,33 +225,6 @@ def validate_index(data, rows, codegen_root): return errors -def find_framework(data, name): - requested = normalize(name) - return next( - ( - framework - for framework in data.get("frameworks", []) - if isinstance(framework, dict) - and normalize(framework.get("qualified_name", "")) == requested - ), - None, - ) - - -def parse_config(values): - config = {} - errors = [] - for value in values: - key, separator, raw_value = value.partition("=") - if not separator or not key.strip() or not raw_value.strip(): - errors.append( - f"invalid --config {value!r}; expected a non-empty key=value pair" - ) - continue - config[key.strip()] = raw_value.strip() - return config, errors - - def scaffold_packet(framework, config): configured_names = { entry["api"].rsplit(".", 1)[-1] @@ -320,135 +288,3 @@ def scaffold_packet(framework, config): "diagnostics": diagnostics, "evidence": framework.get("evidence", []), } - - -def print_text(packet): - print(f"framework: {packet['framework']}") - print(f"scope/module: {packet['scope']}/{packet['module']}") - print(f"scaffold_status: {packet['scaffold_status']}") - print(f"contract_status: {packet['contract_status']}") - print("lifecycle: " + " -> ".join(item["phase"] for item in packet["lifecycle"])) - print("configuration: " + ", ".join(item["api"] for item in packet["configuration"])) - required_hooks = [item["api"] for item in packet["hooks"] if item.get("required")] - print("required_hooks: " + (", ".join(required_hooks) or "none")) - print("state_apis: " + ", ".join(item["api"] for item in packet["state_apis"])) - print("execution: " + ", ".join(item["api"] for item in packet["execution"])) - print("result_apis: " + ", ".join(item["api"] for item in packet["result_apis"])) - if packet["configured_values"]: - print("configured_values: " + ", ".join( - f"{key}={value}" for key, value in packet["configured_values"].items() - )) - if packet["diagnostics"]: - print("diagnostics:") - for diagnostic in packet["diagnostics"]: - print(f"- {diagnostic['code']}: {diagnostic['message']}") - - -def main(argv=None): - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - parser = argparse.ArgumentParser( - description="Resolve framework-class scaffolds for TSL API consumers.", - allow_abbrev=False, - ) - action = parser.add_mutually_exclusive_group(required=True) - action.add_argument("--framework", help="exact framework qualified name") - action.add_argument( - "--check", action="store_true", help="validate all framework profile references" - ) - parser.add_argument( - "--config", - action="append", - default=[], - metavar="KEY=VALUE", - help="raw framework configuration; may be repeated", - ) - parser.add_argument("--format", choices=("text", "json"), default="text") - parser.add_argument("--index", metavar="PATH", help="framework_index.json path") - parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv path") - args = parser.parse_args(argv) - - index_path = Path(args.index) if args.index else DEFAULT_INDEX - tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV - try: - data = load_index(index_path) - 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 - codegen_root = codegen_root_for_tsv(tsv_path) - - errors = validate_index(data, rows, codegen_root) - if args.check: - if errors: - for error in errors: - print(f"ERROR: {error}", file=sys.stderr) - return 1 - print(f"OK: {len(data['frameworks'])} framework profile(s) validated") - return 0 - - if errors: - packet = { - "status": "data_error", - "framework": args.framework, - "diagnostics": [ - { - "code": "PROFILE_REFERENCE_ERROR", - "severity": "error", - "message": error, - "next_action": "Fix framework_index.json or rebuild the API index.", - } - for error in errors - ], - } - if args.format == "json": - print(json.dumps(packet, ensure_ascii=False, indent=2)) - else: - print(f"Framework profile data is invalid for {args.framework!r}.") - for diagnostic in packet["diagnostics"]: - print(f"- {diagnostic['code']}: {diagnostic['message']}") - return 1 - - framework = find_framework(data, args.framework) - if framework is None: - packet = { - "status": "not_found", - "framework": args.framework, - "diagnostics": [ - { - "code": "FRAMEWORK_NOT_FOUND", - "severity": "error", - "message": f"No framework profile named {args.framework!r}.", - "next_action": "Use --check or add a framework profile.", - } - ], - } - if args.format == "json": - print(json.dumps(packet, ensure_ascii=False, indent=2)) - else: - print(f"No framework profile named {args.framework!r}.") - return 0 - - config, config_errors = parse_config(args.config) - packet = scaffold_packet(framework, config) - for error in config_errors: - packet["diagnostics"].append( - { - "code": "INVALID_CONFIGURATION", - "severity": "error", - "message": error, - "next_action": "Use KEY=VALUE syntax with non-empty values.", - } - ) - if args.format == "json": - print(json.dumps(packet, ensure_ascii=False, indent=2)) - else: - print_text(packet) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/tsl-api-reference/scripts/lookup.py b/skills/tsl-api-reference/scripts/lookup.py index edf0dfe1..85a0f4ba 100644 --- a/skills/tsl-api-reference/scripts/lookup.py +++ b/skills/tsl-api-reference/scripts/lookup.py @@ -73,11 +73,16 @@ HELP_EPILOG = """\ lookup.py --name arrDropDuplicate 已知确切名称时可以直接用 --name,跳过第 1 步。 -只查询一个 API scope 时使用 --scope,例如 --scope builtin、 ---scope dotnet 或 --scope module。 +只查询一个 API scope 时使用 --scope;可用值从当前 function_index.tsv 读取, +不存在的 scope 会列出当前索引实际提供的值并返回 2。 + +结果: + --kw 输出候选摘要表;选定后仍须运行 --name + --name 输出完整条目正文和 scope/module、page#anchor 来源标记; + 简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回 退出码: - 0 取回成功;--name 无匹配也是 0(打印提示,不算错误) + 0 查询完成;--name 无匹配和 --kw 零候选也通过正文提示表达 1 function_index.tsv 缺失、格式错误,或候选指向的条目正文找不到 2 参数不合法(缺少动作、空值、--limit 小于 1、使用短选项或缩写) """ @@ -423,7 +428,7 @@ def main(argv=None): type=non_empty, metavar="SCOPE", help="只查询指定 scope,大小写不敏感;作用于 --name 和 --kw。" - "内置索引当前提供 builtin、dotnet 与 module", + "可用值从所选 function_index.tsv 读取并在加载后校验", ) parser.add_argument( "--limit", diff --git a/skills/tsl-api-reference/workflows/api-lookup.md b/skills/tsl-api-reference/workflows/api-lookup.md new file mode 100644 index 00000000..5e864674 --- /dev/null +++ b/skills/tsl-api-reference/workflows/api-lookup.md @@ -0,0 +1,58 @@ +# API 检索 + +用于普通函数、具体成员或“有没有某种行为的 API”这类请求。 + +先查看脚本当前契约: + +```bash +python /scripts/lookup.py --help +``` + +## 检索顺序 + +已知名称时直接精确查询: + +```bash +python /scripts/lookup.py --name argmax +python /scripts/lookup.py --name DemoUnit.Document.Save +``` + +只知道行为时必须走两步: + +```bash +python /scripts/lookup.py --kw 数组 去重 +python /scripts/lookup.py --name arrDropDuplicate +``` + +`--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、 +`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后, +必须再用 `--name` 取正文;不得把 summary 当作签名。 + +已知范围时用当前索引实际提供的 scope 过滤,例如: + +```bash +python /scripts/lookup.py --scope builtin --name boolToStr +python /scripts/lookup.py --scope dotnet --kw argmax +python /scripts/lookup.py --scope module --name bondAccruedInterest +``` + +## 精确结果判读 + +- `--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。 +- 简单成员名会返回所有 owner 下的同名 API;完全限定名用于限定 class 或 unit。 +- 重载共享 `qualified_name`,精确查询会返回该名称的全部重载。 +- 混合页面按唯一 `page#anchor` 截取目标 H2 或成员子树;每个 H2 的声明类型独立。 +- 同名结果跨 scope 或 module 时,结合当前代码上下文消歧,不能取第一条。 +- 候选中的实例方法显示 `function`,类方法显示 `class function`,静态字段和常量 + 分别显示 `static field`、`static const`。 + +脚本可读取必需的 8 列索引;13 列索引额外提供 `qualified_name`、owner、kind、 +binding 和 visibility。使用自定义 `--tsv` 时,只有索引实际包含的扩展列才能支持对应消歧。 + +## 结果与退出码 + +- rc=0:查询完成。`--name` 无匹配和 `--kw` 零候选也属于查询完成,必须读取正文状态。 +- rc=1:索引缺失、格式错误、为空,或候选指向的正文不可达;这是数据/部署错误。 +- rc=2:参数不合法;按 stderr 和 `--help` 修正后重跑。 + +`--name` 成功输出完整条目正文和来源标记;最终报告按 `SKILL.md` 的输出约定组织。 diff --git a/skills/tsl-api-reference/workflows/class-and-framework.md b/skills/tsl-api-reference/workflows/class-and-framework.md new file mode 100644 index 00000000..9327e96b --- /dev/null +++ b/skills/tsl-api-reference/workflows/class-and-framework.md @@ -0,0 +1,57 @@ +# Class 与 Framework 查询 + +用于 class 摘要、成员分类、子类实现、回调重写和生命周期协议。公开入口只有 +`class_lookup.py`;Framework Profile 是 class 查询的附加层,不单独调用另一个 CLI。 + +先查看脚本当前契约: + +```bash +python /scripts/class_lookup.py --help +``` + +## 查询 + +```bash +python /scripts/class_lookup.py --list +python /scripts/class_lookup.py --class TStringList +python /scripts/class_lookup.py --class TSBackTesting +python /scripts/class_lookup.py \ + --class TSBackTesting --config FGroupType=1 --format json +``` + +- 查询或使用任意 class 时,先取类摘要;需要具体成员正文时,再按返回的 + `qualified_name` 运行 `lookup.py --name`。 +- `class_lookup.py` 自动覆盖 `function_index.tsv` 中 `kind=class` 的条目,不要求普通 + 对象类维护人工 Profile。 +- 只有 `kind=framework` 且有证据化生命周期的类才叠加 Framework Profile。不得因目标 + 是 class 就推断它是框架。 +- 同名 class 的 `status=ambiguous` 时,使用 `--scope`、`--module` 或完全限定名消歧。 + +## 状态判读 + +- `status=resolved`:类查询已解析;`status=not_found`:索引中无该 class; + `status=ambiguous`:存在多个候选。 +- `profile_status=not_profiled`:普通 class,只使用自动成员视图,不是错误,也不表示数据 + 不完整;成员查询可以继续。若当前请求依赖生命周期、配置或回调,则表示没有已验证的 + framework 契约,必须停止该部分,不能假设空的 Framework Profile。 +- `profile_status=resolved`:返回了 Framework Profile。 +- `scaffold_status=resolved` 只表示生命周期和成员引用完整。 +- `contract_status` 不是 `resolved` 时,必须按 `diagnostics` 补齐模式与返回 schema 证据, + 并停止生成回调字段。 + +Framework Profile 只描述 API 协议,不定义 TSL 语法或代码结构。解析后的 Framework +Contract Packet 仅作为已验证的 API 事实输出。 + +只有返回的 Profile 本身带有生命周期、配置、回调、执行入口或结果查询证据时,才使用 +这些协议;`not_profiled` 时不能从 class 名称或成员列表推断 framework。 + +使用自定义 `--tsv` 时默认不加载 Framework Profile,普通 class 仍可查询;该索引确有 +curated Profile 时必须同时显式传入 `--profiles`。 + +## 退出码 + +- rc=0:查询或校验完成;`not_found` 和 `ambiguous` 通过输出状态表达。 +- rc=1:索引/Profile 数据缺失、损坏或引用校验失败。 +- rc=2:参数不合法。 + +`--check` 属维护动作,普通编码查询不需要运行。 diff --git a/skills/tsl-api-reference/workflows/dictionary-lookup.md b/skills/tsl-api-reference/workflows/dictionary-lookup.md new file mode 100644 index 00000000..89b6f008 --- /dev/null +++ b/skills/tsl-api-reference/workflows/dictionary-lookup.md @@ -0,0 +1,39 @@ +# 数据字典查询 + +用于按中文业务含义查询天软数据字典中的表、数据源或字段。它们不是可调用 API, +不进入 `function_index.tsv`。 + +先查看脚本当前契约: + +```bash +python /scripts/dictionary_lookup.py --help +``` + +## 查询 + +```bash +python /scripts/dictionary_lookup.py \ + --query "股票现金流指标销售现金比率" +python /scripts/dictionary_lookup.py --query "市值" --scope fund +python /scripts/dictionary_lookup.py --query "买一量" --table TradeTable +python /scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率 +``` + +结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问代码、 +对应 API 和字典页面。 + +## 状态判读 + +- `status: ok`:最高分候选唯一。 +- `status: ambiguous`:存在多个同分候选;列出候选并请用户补充 scope、表或数据源范围, + 不能默认选第一条。 +- `status: no_match`:数据字典无匹配,不能改述为 API 的 `not found`。 + +标识符匹配保留下划线;同义词只来自 `dictionary_lexicon.json` 的受控映射。 +`MarketTable` 和 `TradeTable` 是 TS-SQL 数据源,不是函数。 + +## 退出码 + +- rc=0:查询完成,实际结果由 `status` 判定,包括 `ambiguous` 和 `no_match`。 +- rc=1:字典索引或词表缺失、损坏、格式错误。 +- rc=2:参数不合法。 diff --git a/test/test_playbook.py b/test/test_playbook.py index 9c512a40..537ea8bc 100644 --- a/test/test_playbook.py +++ b/test/test_playbook.py @@ -338,6 +338,60 @@ no_backup = true ).is_file() ) + def test_tsl_api_install_excludes_codegen_dictionary_builder(self): + self.assertTrue( + (ROOT / "tools" / "tsl-codegen" / "scripts" / "build_dictionary.py").is_file() + ) + self.assertFalse( + ( + ROOT + / "skills" + / "tsl-api-reference" + / "scripts" + / "build_dictionary.py" + ).exists() + ) + with tempfile.TemporaryDirectory() as tmp_dir: + project_root = Path(tmp_dir) / "project" + project_root.mkdir() + config = project_root / "playbook.toml" + config.write_text( + """ +[playbook] +project_root = "." +playbook_root = "custom/playbook" +install_mode = "snapshot" + +[install_skills] +agents_home = ".test-agents" +mode = "list" +skills = ["tsl-api-reference"] +no_backup = true +""".lstrip(), + encoding="utf-8", + newline="\n", + ) + + result = run_playbook(SCRIPT, config, project_root) + + self.assertEqual(0, result.returncode, result.stderr) + installed = ( + project_root + / ".test-agents" + / "skills" + / "tsl-api-reference" + ) + self.assertTrue((installed / "scripts" / "class_lookup.py").is_file()) + self.assertTrue((installed / "workflows" / "api-lookup.md").is_file()) + self.assertTrue( + (installed / "workflows" / "class-and-framework.md").is_file() + ) + self.assertTrue( + (installed / "workflows" / "dictionary-lookup.md").is_file() + ) + self.assertFalse((installed / "scripts" / "build_dictionary.py").exists()) + self.assertFalse((installed / "references" / "maintenance.md").exists()) + def test_invalid_toml_is_reported_without_a_traceback(self): with tempfile.TemporaryDirectory() as tmp_dir: project_root = Path(tmp_dir) / "project" diff --git a/test/test_tsl_api_framework.py b/test/test_tsl_api_framework.py index 8f52b529..2112e340 100644 --- a/test/test_tsl_api_framework.py +++ b/test/test_tsl_api_framework.py @@ -1,3 +1,4 @@ +import ast import csv import json import re @@ -10,7 +11,6 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "skills" / "tsl-api-reference" -FRAMEWORK_LOOKUP = SKILL_ROOT / "scripts" / "framework_lookup.py" CLASS_LOOKUP = SKILL_ROOT / "scripts" / "class_lookup.py" API_LOOKUP = SKILL_ROOT / "scripts" / "lookup.py" FRAMEWORK_INDEX = SKILL_ROOT / "data" / "framework_index.json" @@ -357,6 +357,121 @@ class TslApiFrameworkTests(unittest.TestCase): self.assertIsNone(packet["profile"]) self.assertEqual({"method": 25, "property": 15}, packet["member_summary"]) + def test_custom_index_without_profiles_queries_generic_class(self): + fieldnames = [ + "name", + "scope", + "module", + "signature", + "page", + "anchor", + "tags", + "summary", + "kind", + "binding", + "visibility", + "owner", + "qualified_name", + ] + rows = [ + { + "name": "MyClass", + "scope": "project", + "module": "demo", + "signature": "MyClass", + "page": "project/demo.md", + "anchor": "myclass", + "tags": "示例 class", + "summary": "示例类。", + "kind": "class", + "binding": "", + "visibility": "", + "owner": "", + "qualified_name": "MyClass", + }, + { + "name": "Run", + "scope": "project", + "module": "demo", + "signature": "Run()", + "page": "project/demo.md", + "anchor": "run", + "tags": "运行", + "summary": "运行示例。", + "kind": "method", + "binding": "instance", + "visibility": "public", + "owner": "MyClass", + "qualified_name": "MyClass.Run", + }, + ] + + with tempfile.TemporaryDirectory() as temp_dir: + data_dir = Path(temp_dir) / "data" + data_dir.mkdir() + tsv_path = data_dir / "function_index.tsv" + with tsv_path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter( + handle, + fieldnames=fieldnames, + delimiter="\t", + lineterminator="\n", + ) + writer.writeheader() + writer.writerows(rows) + + result = run_script( + CLASS_LOOKUP, + "--tsv", + str(tsv_path), + "--class", + "MyClass", + "--include-members", + "--format", + "json", + ) + + self.assertEqual(0, result.returncode, result.stderr) + packet = json.loads(result.stdout) + self.assertEqual("resolved", packet["status"]) + self.assertEqual("not_profiled", packet["profile_status"]) + self.assertIsNone(packet["profile"]) + self.assertEqual({"method": 1}, packet["member_summary"]) + self.assertEqual("MyClass.Run", packet["members"][0]["qualified_name"]) + + def test_explicit_missing_framework_file_remains_a_data_error(self): + with tempfile.TemporaryDirectory() as temp_dir: + missing = Path(temp_dir) / "missing-framework-index.json" + result = run_script( + CLASS_LOOKUP, + "--profiles", + str(missing), + "--class", + "TStringList", + ) + + self.assertEqual(1, result.returncode) + self.assertIn("failed to load", result.stderr) + + def test_configuration_on_generic_class_requires_curated_profile(self): + result = run_script( + CLASS_LOOKUP, + "--class", + "TStringList", + "--config", + "Mode=1", + "--format", + "json", + ) + + self.assertEqual(0, result.returncode, result.stderr) + packet = json.loads(result.stdout) + self.assertEqual("not_profiled", packet["profile_status"]) + self.assertIn( + "CLASS_NOT_PROFILED", + {item["code"] for item in packet["diagnostics"]}, + ) + def test_profiled_class_adds_framework_lifecycle(self): result = run_script( CLASS_LOOKUP, @@ -475,16 +590,29 @@ class TslApiFrameworkTests(unittest.TestCase): self.assertIn(f"OK: {indexed_class_count()} classes indexed", result.stdout) self.assertIn("6 framework profile(s) validated", result.stdout) - def test_framework_index_references_existing_api_entries(self): - result = run_script(FRAMEWORK_LOOKUP, "--check") + def test_framework_module_has_no_independent_cli(self): + source = (SKILL_ROOT / "scripts" / "framework_lookup.py").read_text( + encoding="utf-8" + ) + tree = ast.parse(source) + functions = { + node.name for node in tree.body if isinstance(node, ast.FunctionDef) + } + imported_modules = { + alias.name + for node in tree.body + if isinstance(node, ast.Import) + for alias in node.names + } - self.assertEqual(0, result.returncode, result.stderr) - self.assertIn("OK: 6 framework profile(s) validated", result.stdout) + self.assertNotIn("main", functions) + self.assertNotIn("argparse", imported_modules) + self.assertNotIn('__name__ == "__main__"', source) def test_ts_backtesting_returns_class_first_scaffold(self): result = run_script( - FRAMEWORK_LOOKUP, - "--framework", + CLASS_LOOKUP, + "--class", "tsBackTesting", "--format", "json", @@ -492,25 +620,26 @@ class TslApiFrameworkTests(unittest.TestCase): self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) - self.assertEqual("resolved", packet["scaffold_status"]) - self.assertEqual("incomplete", packet["contract_status"]) + profile = packet["profile"] + self.assertEqual("resolved", profile["scaffold_status"]) + self.assertEqual("incomplete", profile["contract_status"]) self.assertEqual( ["construct", "configure", "schedule", "callback", "execute", "inspect"], - [phase["phase"] for phase in packet["lifecycle"]], + [phase["phase"] for phase in profile["lifecycle"]], ) self.assertEqual( ["tsBackTesting.GetTradeOrder"], - [hook["api"] for hook in packet["hooks"] if hook["required"]], + [hook["api"] for hook in profile["hooks"] if hook["required"]], ) self.assertEqual( "CONTRACT_INCOMPLETE", - packet["diagnostics"][0]["code"], + profile["diagnostics"][0]["code"], ) def test_candidate_discriminator_does_not_invent_a_mode(self): result = run_script( - FRAMEWORK_LOOKUP, - "--framework", + CLASS_LOOKUP, + "--class", "tsBackTesting", "--config", "FGroupType=1", @@ -520,17 +649,18 @@ class TslApiFrameworkTests(unittest.TestCase): self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) - self.assertEqual({"FGroupType": "1"}, packet["configured_values"]) - self.assertEqual("incomplete", packet["contract_status"]) + profile = packet["profile"] + self.assertEqual({"FGroupType": "1"}, profile["configured_values"]) + self.assertEqual("incomplete", profile["contract_status"]) self.assertIn( "CONTRACT_MODE_UNRESOLVED", - {item["code"] for item in packet["diagnostics"]}, + {item["code"] for item in profile["diagnostics"]}, ) def test_unknown_configuration_is_reported(self): result = run_script( - FRAMEWORK_LOOKUP, - "--framework", + CLASS_LOOKUP, + "--class", "tsBackTesting", "--config", "notAField=1", @@ -542,13 +672,13 @@ class TslApiFrameworkTests(unittest.TestCase): packet = json.loads(result.stdout) self.assertIn( "UNKNOWN_CONFIGURATION", - {item["code"] for item in packet["diagnostics"]}, + {item["code"] for item in packet["profile"]["diagnostics"]}, ) - def test_unknown_framework_uses_query_success_exit_code(self): + def test_unknown_class_uses_query_success_exit_code(self): result = run_script( - FRAMEWORK_LOOKUP, - "--framework", + CLASS_LOOKUP, + "--class", "MissingFramework", "--format", "json", @@ -566,16 +696,16 @@ class TslApiFrameworkTests(unittest.TestCase): index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( - FRAMEWORK_LOOKUP, + CLASS_LOOKUP, "--check", - "--index", + "--profiles", str(index_path), ) self.assertEqual(1, result.returncode) self.assertIn("does not identify an indexed overload", result.stderr) - def test_query_reports_invalid_profile_as_data_error(self): + def test_query_rejects_invalid_profile_data(self): data = json.loads(FRAMEWORK_INDEX.read_text(encoding="utf-8")) data["frameworks"][0]["execution"][0]["evidence"] = [ "module/ts-backtesting.md#missing" @@ -584,17 +714,18 @@ class TslApiFrameworkTests(unittest.TestCase): index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( - FRAMEWORK_LOOKUP, - "--framework", + CLASS_LOOKUP, + "--class", "tsBackTesting", "--format", "json", - "--index", + "--profiles", str(index_path), ) self.assertEqual(1, result.returncode) - self.assertEqual("data_error", json.loads(result.stdout)["status"]) + self.assertEqual("", result.stdout) + self.assertIn("does not identify an indexed overload", result.stderr) def test_check_rejects_profile_without_indexed_class(self): data = json.loads(FRAMEWORK_INDEX.read_text(encoding="utf-8")) @@ -603,9 +734,9 @@ class TslApiFrameworkTests(unittest.TestCase): index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( - FRAMEWORK_LOOKUP, + CLASS_LOOKUP, "--check", - "--index", + "--profiles", str(index_path), ) diff --git a/tools/tsl-codegen/MAINTENANCE.md b/tools/tsl-codegen/MAINTENANCE.md new file mode 100644 index 00000000..a5a90fcc --- /dev/null +++ b/tools/tsl-codegen/MAINTENANCE.md @@ -0,0 +1,83 @@ +# TSL API 数据维护 + +本文档面向本仓库维护者,不是安装后 API skill 的运行说明。命令均从仓库根目录执行。 + +## 维护 API 页面和索引 + +1. 按 [`STANDARD.md`](STANDARD.md) 修改或生成 + `skills/tsl-api-reference/references/codegen/` 下的页面。 +2. 对修改过的页面运行 lint;确认页面中的每个 `page#anchor` 都能定位。 +3. 重建并检查统一索引: + + ```bash + python tools/tsl-codegen/scripts/build_index.py \ + --skill-dir skills/tsl-api-reference + python tools/tsl-codegen/scripts/build_index.py \ + --skill-dir skills/tsl-api-reference --check + ``` + +4. 用 `lookup.py --name` 和 `class_lookup.py --class` 做一次精确查询,确认名称、归属、 + 成员和来源没有漂移。 + +## 维护 class:先判断是否真的需要 Framework Profile + +`function_index.tsv` 中 `kind=class` 的每个 class 都会自动获得成员视图。普通 class 没有 +Framework Profile 是正常且完整的状态,不要为了让它出现在列表里创建空 profile: + +```bash +python skills/tsl-api-reference/scripts/class_lookup.py \ + --class TStringList --include-members --format json +``` + +看到 `profile_status=not_profiled` 时,继续维护 class 页面、成员正文和索引即可。配置、 +生命周期、回调或执行阶段不能从普通 class 成员列表臆造出来。 + +只有在参考页面提供了可核验的生命周期协议,并且 class 确实需要配置字段、回调、状态、 +执行入口或结果查询时,才在 +`skills/tsl-api-reference/data/framework_index.json` 增加 `kind=framework` 的条目。 +Profile 必须为每个引用提供对应的 `page#anchor` 证据,并通过: + +```bash +python skills/tsl-api-reference/scripts/class_lookup.py --check +``` + +如果证据不足,保留 `profile_status=not_profiled`,不要用猜测补齐 framework 生命周期。 +Profile 的 `contract_status` 不是 `resolved` 时,也不能生成依赖未证实回调字段的代码。 + +### 用户或临时索引中的 class + +维护自定义 `function_index.tsv` 时,可直接查询普通 class;只传 `--tsv` 不加载任何 +curated Framework Profile,这不是错误: + +```bash +python /scripts/class_lookup.py \ + --tsv /data/function_index.tsv \ + --class MyClass --include-members --format json +``` + +若确实维护了自定义 profile,显式传入 `--profiles `,再运行 `--check` 验证引用。 + +## 维护数据字典快照 + +字典生成器属于本工具包,位于 `tools/tsl-codegen/scripts/build_dictionary.py`,不随 +`tsl-api-reference` skill 安装。它依赖 `beautifulsoup4`,并要求导出的原始 HTML 目录: + +```bash +python -m pip install beautifulsoup4 +python tools/tsl-codegen/scripts/build_dictionary.py \ + --source-root \ + --output-root skills/tsl-api-reference/references/data_dictionary \ + --index skills/tsl-api-reference/data/dictionary_index.tsv \ + --lexicon skills/tsl-api-reference/data/dictionary_lexicon.json +``` + +生成完成后,用 `dictionary_lookup.py --help` 中的真实示例做查询检查。 + +## 完成检查 + +```bash +python skills/tsl-api-reference/scripts/class_lookup.py --check +python tools/tsl-codegen/scripts/build_index.py \ + --skill-dir skills/tsl-api-reference --check +python test/integration/check_doc_links.py +``` diff --git a/tools/tsl-codegen/README.md b/tools/tsl-codegen/README.md index cce92288..ff738eed 100644 --- a/tools/tsl-codegen/README.md +++ b/tools/tsl-codegen/README.md @@ -18,7 +18,8 @@ tools/tsl-codegen/ │ ├─ generate.py yaml/json → markdown │ ├─ lint.py markdown 格式校验 │ ├─ api_markdown.py lint/index 共用标题模型 - │ └─ build_index.py 重建 13 列 function_index.tsv + │ ├─ build_index.py 重建 13 列 function_index.tsv + │ └─ build_dictionary.py HTML 数据字典 → markdown/TSV └─ tests/ 工具测试 ``` @@ -193,6 +194,15 @@ tmp/my-api.json 一个录入文件对应一个 markdown 叶子页。录入文件不放入 skill;是否长期保留由 维护者自行决定 +#### Class 与 Framework Profile + +生成 class 页面并更新 `function_index.tsv` 后,class 已可通过自动成员视图查询,不要求 +额外的 Framework Profile。`class_lookup.py` 返回 `profile_status=not_profiled` 表示普通 +class,属于正常且完整的结果;不要为它创建空 profile。 + +本工具不会从 class 名称或成员列表推断生命周期、配置、回调和执行阶段。只有另有可核验 +协议证据的框架类才需要 curated Framework Profile。 + ### 4. 选择 Skill 中的目标位置 TSL API skill 的相关目录如下: @@ -391,4 +401,4 @@ python skills/tsl-api-reference/scripts/lookup.py --name DemoUnit.Document.Save - `skills/tsl-api-reference/data/function_index.tsv` TSL API skill 只需要 markdown 和 TSV。录入用的 yaml/json 可以由维护者在自己的 -版本库中管理 +版本库中管理。 diff --git a/skills/tsl-api-reference/scripts/build_dictionary.py b/tools/tsl-codegen/scripts/build_dictionary.py similarity index 99% rename from skills/tsl-api-reference/scripts/build_dictionary.py rename to tools/tsl-codegen/scripts/build_dictionary.py index 5af6d5d4..6c3c5be1 100644 --- a/skills/tsl-api-reference/scripts/build_dictionary.py +++ b/tools/tsl-codegen/scripts/build_dictionary.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build an independent searchable snapshot of the Tinysoft data dictionary.""" +"""Build the repository's searchable Tinysoft data-dictionary snapshot.""" from __future__ import annotations diff --git a/tools/tsl-codegen/tests/test_lookup.py b/tools/tsl-codegen/tests/test_lookup.py index a160af2f..40b70273 100644 --- a/tools/tsl-codegen/tests/test_lookup.py +++ b/tools/tsl-codegen/tests/test_lookup.py @@ -16,6 +16,7 @@ SCRIPT = ( / "lookup.py" ) SKILL_MD = SCRIPT.parents[1] / "SKILL.md" +API_WORKFLOW = SCRIPT.parents[1] / "workflows" / "api-lookup.md" def load_script(): @@ -573,7 +574,7 @@ class LookupTest(unittest.TestCase): self.assertEqual(2, result.returncode) self.assertIn("--limit must be >= 1", result.stderr) - def test_help_lists_all_bundled_scopes(self): + def test_help_derives_scopes_from_selected_index(self): result = subprocess.run( [sys.executable, str(SCRIPT), "--help"], capture_output=True, @@ -582,7 +583,9 @@ class LookupTest(unittest.TestCase): ) self.assertEqual(0, result.returncode, result.stderr) - self.assertIn("builtin、dotnet 与 module", result.stdout) + self.assertIn("可用值从当前 function_index.tsv 读取", result.stdout) + self.assertNotIn("deprecated", result.stdout) + self.assertNotIn("--scope third", result.stdout) def test_exact_search_uses_anchor_for_duplicate_signatures(self): module = load_script() @@ -775,14 +778,98 @@ class LookupTest(unittest.TestCase): self.assertIn("DemoUnit.Document.Save", visibility.stdout) def test_skill_documents_qualified_queries_and_legacy_index_compatibility(self): - text = SKILL_MD.read_text(encoding="utf-8") + skill_text = SKILL_MD.read_text(encoding="utf-8") + text = API_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("workflows/api-lookup.md", skill_text) self.assertIn("完全限定名称", text) self.assertIn("--scope module", text) self.assertIn("qualified_name", text) - self.assertIn("owner、kind、binding、visibility", text) + self.assertIn("owner、kind、", text) + self.assertIn("binding 和 visibility", text) self.assertIn("class function", text) - self.assertIn("旧 8 列", text) + self.assertIn("8 列索引", text) + self.assertIn("13 列索引", text) + + def test_skill_routes_workflows_and_keeps_stop_resident(self): + skill_text = SKILL_MD.read_text(encoding="utf-8") + skill_root = SKILL_MD.parent + instruction_paths = sorted((skill_root / "workflows").glob("*.md")) + + for path in instruction_paths: + self.assertIn(path.relative_to(skill_root).as_posix(), skill_text) + self.assertIn("🔴 CHECKPOINT · 🛑 STOP", skill_text) + self.assertIn("rc=1 不进入上述“API 缺失”分支", skill_text) + self.assertIn("原样名称或 `qualified_name`", skill_text) + self.assertIn("`profile_status=not_profiled`", skill_text) + + def test_skill_instruction_package_has_no_reverse_dependencies(self): + skill_root = SKILL_MD.parent + package_text = "\n".join( + path.read_text(encoding="utf-8") + for path in [SKILL_MD, *sorted((skill_root / "workflows").glob("*.md"))] + ) + + for forbidden in ( + "tsl-syntax-reference", + "references/maintenance.md", + "tools/tsl-codegen", + "scripts/build_dictionary.py", + "AGENTS.md", + "CONTEXT.md", + "docs/adr/", + ): + self.assertNotIn(forbidden, package_text) + + def test_repository_maintenance_doc_owns_api_data_maintenance(self): + readme = ( + SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "MAINTENANCE.md" + ).read_text(encoding="utf-8") + for required in ( + "TSL API 数据维护", + "class_lookup.py --check", + "tools/tsl-codegen/scripts/build_index.py", + "--skill-dir skills/tsl-api-reference", + "tools/tsl-codegen/scripts/build_dictionary.py", + "beautifulsoup4", + "profile_status=not_profiled", + ): + self.assertIn(required, readme) + + user_readme = ( + SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "README.md" + ).read_text(encoding="utf-8") + for maintainer_only in ( + "class_lookup.py --check", + "beautifulsoup4", + "--source-root ", + ): + self.assertNotIn(maintainer_only, user_readme) + self.assertIn("profile_status=not_profiled", user_readme) + + def test_public_cli_help_owns_status_and_exit_contracts(self): + skill_root = SKILL_MD.parent + expected = { + "lookup.py": ("退出码:", "--name 无匹配", "page#anchor"), + "class_lookup.py": ("输出状态:", "contract_status", "退出码:"), + "dictionary_lookup.py": ("status: ambiguous", "status: no_match", "退出码:"), + } + + for script_name, fragments in expected.items(): + with self.subTest(script=script_name): + result = subprocess.run( + [ + sys.executable, + str(skill_root / "scripts" / script_name), + "--help", + ], + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(0, result.returncode, result.stderr) + for fragment in fragments: + self.assertIn(fragment, result.stdout) def test_keyword_search_includes_tags_and_summary(self): module = load_script() diff --git a/tools/tsl-codegen/tests/test_pipeline.py b/tools/tsl-codegen/tests/test_pipeline.py index 243e61c4..78d7e292 100644 --- a/tools/tsl-codegen/tests/test_pipeline.py +++ b/tools/tsl-codegen/tests/test_pipeline.py @@ -24,6 +24,9 @@ README = TOOL_ROOT / "README.md" EXAMPLE_JSON = TOOL_ROOT / "examples" / "example.json" EXAMPLE_YAML = TOOL_ROOT / "examples" / "example.yaml" SKILL = REPO_ROOT / "skills" / "tsl-api-reference" / "SKILL.md" +API_WORKFLOW = ( + REPO_ROOT / "skills" / "tsl-api-reference" / "workflows" / "api-lookup.md" +) class UnifiedPipelineTest(unittest.TestCase): @@ -212,6 +215,7 @@ class UnifiedPipelineTest(unittest.TestCase): standard = STANDARD.read_text(encoding="utf-8") readme = README.read_text(encoding="utf-8") skill = SKILL.read_text(encoding="utf-8") + api_workflow = API_WORKFLOW.read_text(encoding="utf-8") self.assertTrue(standard.startswith("# TSL API 文档标准\n")) self.assertNotIn("根字段 `functions`、`class`、`unit`", standard) @@ -273,8 +277,9 @@ class UnifiedPipelineTest(unittest.TestCase): self.assertNotIn("generate.py tmp/my-api.yaml", readme) self.assertNotIn("generate.py tmp/my-api.json", readme) self.assertNotIn("已废弃,请使用 --file", readme) - self.assertIn("混合页面", skill) - self.assertIn("page#anchor", skill) + self.assertIn("workflows/api-lookup.md", skill) + self.assertIn("混合页面", api_workflow) + self.assertIn("page#anchor", api_workflow) def test_standard_class_methods_reuse_top_level_function_structure(self): standard = STANDARD.read_text(encoding="utf-8")