♻️ refactor(tsl-api-reference): split lookup workflows and maintenance tooling
This commit is contained in:
+4
-1
@@ -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)。
|
||||
|
||||
@@ -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` 分支。
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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` 为权威。
|
||||
将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --list
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
|
||||
python <this-skill-dir>/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 <this-skill-dir>/scripts/class_lookup.py --check
|
||||
```
|
||||
rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。
|
||||
|
||||
天软数据字典中的表、数据源和字段不是可调用 API,不进入 `function_index.tsv`
|
||||
函数问题使用 `lookup.py`,表或字段问题使用 `dictionary_lookup.py`
|
||||
## 输出约定
|
||||
|
||||
## 数据字典查询
|
||||
|
||||
用户按中文业务含义描述表或字段时,运行独立的字典查询脚本
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "股票现金流指标销售现金比率"
|
||||
```
|
||||
|
||||
已知业务范围、表名、数据源名或字段名时使用过滤参数
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
|
||||
python <this-skill-dir>/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 时不要删除下划线,也不要把名称改写成驼峰式。
|
||||
|
||||
运行随附的查询脚本。将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
|
||||
|
||||
- 已知 API 名称:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name argmax
|
||||
```
|
||||
|
||||
- 已知 class/unit 成员的完全限定名称:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
|
||||
```
|
||||
|
||||
- 未知名称,但知道其行为或中文关键词:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --kw 最大值 位置
|
||||
```
|
||||
|
||||
- 只查询一个 scope:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
|
||||
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
|
||||
python <this-skill-dir>/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 只能用于选候选,不能作为签名事实。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# API 检索
|
||||
|
||||
用于普通函数、具体成员或“有没有某种行为的 API”这类请求。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --help
|
||||
```
|
||||
|
||||
## 检索顺序
|
||||
|
||||
已知名称时直接精确查询:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --name argmax
|
||||
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
|
||||
```
|
||||
|
||||
只知道行为时必须走两步:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --kw 数组 去重
|
||||
python <this-skill-dir>/scripts/lookup.py --name arrDropDuplicate
|
||||
```
|
||||
|
||||
`--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、
|
||||
`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后,
|
||||
必须再用 `--name` 取正文;不得把 summary 当作签名。
|
||||
|
||||
已知范围时用当前索引实际提供的 scope 过滤,例如:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
|
||||
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
|
||||
python <this-skill-dir>/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` 的输出约定组织。
|
||||
@@ -0,0 +1,57 @@
|
||||
# Class 与 Framework 查询
|
||||
|
||||
用于 class 摘要、成员分类、子类实现、回调重写和生命周期协议。公开入口只有
|
||||
`class_lookup.py`;Framework Profile 是 class 查询的附加层,不单独调用另一个 CLI。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --help
|
||||
```
|
||||
|
||||
## 查询
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/class_lookup.py --list
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
|
||||
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
|
||||
python <this-skill-dir>/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` 属维护动作,普通编码查询不需要运行。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 数据字典查询
|
||||
|
||||
用于按中文业务含义查询天软数据字典中的表、数据源或字段。它们不是可调用 API,
|
||||
不进入 `function_index.tsv`。
|
||||
|
||||
先查看脚本当前契约:
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --help
|
||||
```
|
||||
|
||||
## 查询
|
||||
|
||||
```bash
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py \
|
||||
--query "股票现金流指标销售现金比率"
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
|
||||
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
|
||||
python <this-skill-dir>/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:参数不合法。
|
||||
@@ -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"
|
||||
|
||||
+163
-32
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 <skill-dir>/scripts/class_lookup.py \
|
||||
--tsv <index-root>/data/function_index.tsv \
|
||||
--class MyClass --include-members --format json
|
||||
```
|
||||
|
||||
若确实维护了自定义 profile,显式传入 `--profiles <path>`,再运行 `--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 <exported-html-dir> \
|
||||
--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
|
||||
```
|
||||
@@ -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 可以由维护者在自己的
|
||||
版本库中管理
|
||||
版本库中管理。
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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 <exported-html-dir>",
|
||||
):
|
||||
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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user