♻️ refactor(tsl-api): tighten docs and retire stale tests

This commit is contained in:
csh
2026-08-21 10:11:48 +08:00
parent 2e8c46e152
commit 5484fc7519
24 changed files with 505 additions and 2700 deletions
-3
View File
@@ -28,9 +28,6 @@ docs/superpowers/
# Local agent instructions # Local agent instructions
/AGENTS.md /AGENTS.md
# Agent 测试结果
test/agent/result/
__pycache__/ __pycache__/
*.pyc *.pyc
+1
View File
@@ -3,3 +3,4 @@ node_modules
tmp tmp
reports reports
skills/thirdparty skills/thirdparty
/AGENTS.md
+5
View File
@@ -18,6 +18,11 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class
[`workflows/class-and-framework.md`](workflows/class-and-framework.md)。 [`workflows/class-and-framework.md`](workflows/class-and-framework.md)。
- 按中文业务含义查表、数据源或字段:读取 - 按中文业务含义查表、数据源或字段:读取
[`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。 [`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。
- 涉及行业分类、基金分类或分类属性代码(`SWHY` / `CAPCHY` / `CSRCHY` / `YHFL` /
`TSJJ02` / `TSJJ03`)的代码体系与取数路径:读取
[`workflows/classification-codes.md`](workflows/classification-codes.md)。
- 需要先取一批证券代码(板块成分、指数成分、指定日在市列表、沪深港通名单):读取
[`workflows/code-sequences.md`](workflows/code-sequences.md)。
具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。 具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。
`<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
@@ -58,11 +58,11 @@ return ContinuedMatchOfNDay(@close(), 3);
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------- | | ----------- | ------ | -------------------- |
| `exp_1` | string | 字符串。统计表达式1 | | `exp_1` | string | 字符串。统计表达式1 |
| `exp_2` | string | 字符串。统计表达式2 | | `exp_2` | string | 字符串。统计表达式2 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:float 返回:float
@@ -164,10 +164,10 @@ N日线性衰减加权平均,与系统参数(股票,日期,周期,复权
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------ | | ----------- | ------ | -------------------- |
| `exp` | string | 字符串。统计表达式 | | `exp` | string | 字符串。统计表达式 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:float 返回:float
@@ -293,10 +293,10 @@ N日最高点到当前的周期数。N日表示市场交易日,当日最低为
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------ | | ----------- | ------ | -------------------- |
| `exp` | string | 字符串。统计表达式 | | `exp` | string | 字符串。统计表达式 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:int 返回:int
@@ -422,10 +422,10 @@ N日最低点到当前的周期数。N日表示市场交易日,当日最低为
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------ | | ----------- | ------ | -------------------- |
| `exp` | string | 字符串。统计表达式 | | `exp` | string | 字符串。统计表达式 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:int 返回:int
@@ -524,10 +524,10 @@ return OccuredOfNDay(@close(), 10);
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------ | | ----------- | ------ | -------------------- |
| `exp` | string | 字符串。统计表达式 | | `exp` | string | 字符串。统计表达式 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:float 返回:float
@@ -550,10 +550,10 @@ return PercentRankOfNDay(@close(), 10, "");
<!-- tags: 数据仓库 向导函数 N 日 --> <!-- tags: 数据仓库 向导函数 N 日 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | ------ | ------------------ | | ----------- | ------ | -------------------- |
| `exp` | string | 字符串。统计表达式 | | `exp` | string | 字符串。统计表达式 |
| `n` | int | 整数。天数 | | `n` | int | 整数。天数 |
| `cache_str` | string | 可选。字符串,缓存串 | | `cache_str` | string | 可选。字符串,缓存串 |
返回:float 返回:float
@@ -130,9 +130,9 @@ Return BondBasisPointValue(Endt, MV);
<!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS --> <!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------------- | -------- | -------- | | ----------------- | -------- | ---------------------------------- |
| `settlement_date` | datetime | 结算日期 | | `settlement_date` | datetime | 结算日期 |
| `option` | int | 可选。是否行权,默认 0(持有到期) | | `option` | int | 可选。是否行权,默认 0(持有到期) |
返回:float 返回:float
@@ -233,9 +233,9 @@ return bondMacaulayDuration(endt); // 麦考利久期,返回3.12
<!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS --> <!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ---------------- | -------- | -------- | | ---------------- | -------- | ---------------------------------- |
| `settlementdate` | datetime | 结算日 | | `settlementdate` | datetime | 结算日 |
| `option` | int | 可选。是否行权,默认 0(持有到期) | | `option` | int | 可选。是否行权,默认 0(持有到期) |
返回:float 返回:float
@@ -993,9 +993,9 @@ return FundZjRatio(endt);
<!-- tags: 金融 基金 基金收益率 分级基金 折价率 返回 获取 --> <!-- tags: 金融 基金 基金收益率 分级基金 折价率 返回 获取 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| ----------- | -------- | ------------------------------------------- | | ----------- | -------- | ------------------------------------------ |
| `end_t` | datetime | 截止日期 | | `end_t` | datetime | 截止日期 |
| `fund_type` | integer | 基金类别;原始文档示例中 `2` 表示分级 B 类 | | `fund_type` | integer | 基金类别;原始文档示例中 `2` 表示分级 B 类 |
返回:float 返回:float
@@ -195,17 +195,17 @@ return op_hv("SZ000002", 20210101T, -6);
<!-- tags: 金融 期权 基本函数 隐含波动率 Greeks Delta Gamma Theta Vega Rho --> <!-- tags: 金融 期权 基本函数 隐含波动率 Greeks Delta Gamma Theta Vega Rho -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| --------------- | ------- | ---------------------------------------------------- | | --------------- | ------- | --------------------------------------------------- |
| `option_type` | integer | 期权类型,0 为看涨期权,1 为看跌期权 | | `option_type` | integer | 期权类型,0 为看涨期权,1 为看跌期权 |
| `s` | real | 现货价格 | | `s` | real | 现货价格 |
| `x` | real | 行权价 | | `x` | real | 行权价 |
| `close` | real | 期权交易价格 | | `close` | real | 期权交易价格 |
| `r` | real | 无风险收益率百分比 | | `r` | real | 无风险收益率百分比 |
| `t` | real | 剩余期限月数 | | `t` | real | 剩余期限月数 |
| `option_style` | string | 可选。默认行权方式,支持 `欧式``美式` | | `option_style` | string | 可选。默认行权方式,支持 `欧式``美式` |
| `method` | integer | 可选。默认定价方式,0 为默认,1 为 B_S,2 为二叉树 | | `method` | integer | 可选。默认定价方式,0 为默认,1 为 B_S,2 为二叉树 |
| `contract_type` | integer | 可选。默认 0;0 股票、1 指数、2 外汇、3 期货期权 | | `contract_type` | integer | 可选。默认 0;0 股票、1 指数、2 外汇、3 期货期权 |
| `rf` | real | 可选。默认 0;标的资产年股息率或外币无风险年利率(%) | | `rf` | real | 可选。默认 0;标的资产年股息率或外币无风险年利率(%) |
返回:array 返回:array
@@ -91,12 +91,12 @@ return bondFutureBasicindicators2(endt, -1, 1, 0);
<!-- tags: 金融 专题函数 衍生品分析 期货专项 常用国债期货基本指标 基于债券估值 返回 获取 --> <!-- tags: 金融 专题函数 衍生品分析 期货专项 常用国债期货基本指标 基于债券估值 返回 获取 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| --------------- | ----------- | ----------------------------- | | --------------- | ----------- | ---------------------------------- |
| `end_t` | date | 日期,截止日 | | `end_t` | date | 日期,截止日 |
| `bond_id` | string | 字符串,债券代码 | | `bond_id` | string | 字符串,债券代码 |
| `return_option` | user_define | 用户自定义, 返回结果选项 | | `return_option` | user_define | 用户自定义, 返回结果选项 |
| `market_id` | user_define | 用户自定义, 国债市场选项 | | `market_id` | user_define | 用户自定义, 国债市场选项 |
| `dbtype` | user_define | 可选。用户自定义,国债估值数据来源 | | `dbtype` | user_define | 可选。用户自定义,国债估值数据来源 |
返回:real 返回:real
@@ -8,9 +8,9 @@
<!-- tags: 数学算法 压缩估计 均值 --> <!-- tags: 数学算法 压缩估计 均值 -->
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
| -------- | ------ | ------------------------------------------------------------------------------- | | -------- | ------ | -------------------------------------------------------------------------- |
| `data` | array | 二维数据表,T×N,T 期样本数量,N 个股票 | | `data` | array | 二维数据表,T×N,T 期样本数量,N 个股票 |
| `b_type` | string | 目标均值向量:b0 零向量、b1 均值、b2 波动率加权均值、b3 样本均值的均方误差 | | `b_type` | string | 目标均值向量:b0 零向量、b1 均值、b2 波动率加权均值、b3 样本均值的均方误差 |
返回:array 返回:array
@@ -56,11 +56,11 @@
- 农产品生产者价格表统计的是农产品价格的变化情况。 - 农产品生产者价格表统计的是农产品价格的变化情况。
- 区别于农产品生产者价格(简表): - 区别于农产品生产者价格(简表):
- 1. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度; - 1\. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度;
- 2. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据; - 2\. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据;
- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期; - 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
- 4. 简表数据公布会早于此表; - 4\. 简表数据公布会早于此表;
- 5. 没有简表中的累计同比数据。 - 5\. 没有简表中的累计同比数据。
- 1、数据开始时间:2003年一季度 - 1、数据开始时间:2003年一季度
- 2、数据更新频率:季度 - 2、数据更新频率:季度
@@ -111,9 +111,9 @@
- 中国从1992年1季度开始到2015年2季度,采用累计核算方式核算季度GDP,即分别计算各年1季度,1-2季度,1-3季度和1-4季度的GDP数据,1-4季度 GDP初步核算即为年度GDP初步核算。从2015年3季度开始改为分季核算方式,即分别计算各年1季度,2季度,3季度和4季度的GDP数据,累计数据通过当季数据相加得到。 - 中国从1992年1季度开始到2015年2季度,采用累计核算方式核算季度GDP,即分别计算各年1季度,1-2季度,1-3季度和1-4季度的GDP数据,1-4季度 GDP初步核算即为年度GDP初步核算。从2015年3季度开始改为分季核算方式,即分别计算各年1季度,2季度,3季度和4季度的GDP数据,累计数据通过当季数据相加得到。
- 此表为核算后数据。 - 此表为核算后数据。
- 该表与国内生产总值(简表)区别: - 该表与国内生产总值(简表)区别:
- 1. 数据获取历史周期更长,数据开始时间为1992年一季度; - 1\. 数据获取历史周期更长,数据开始时间为1992年一季度;
- 2.包含数据类别更多,统计项目更多; - 2\.包含数据类别更多,统计项目更多;
- 3.本表为核算调整后的数据; - 3\.本表为核算调整后的数据;
- 1、数据开始时间:1992年一季度 - 1、数据开始时间:1992年一季度
- 2、数据更新频率:季度 - 2、数据更新频率:季度
@@ -211,10 +211,10 @@
- 居民消费价格主要介绍的是居民消费价格分类及其指数的变化情况。 - 居民消费价格主要介绍的是居民消费价格分类及其指数的变化情况。
- 区别于居民消费价格(简表) - 区别于居民消费价格(简表)
- 1. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月; - 1\. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月;
- 2. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。 - 2\. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。
- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期; - 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
- 4. 简表数据公布会早于此表; - 4\. 简表数据公布会早于此表;
- 1、数据开始时间:1987年一月 - 1、数据开始时间:1987年一月
- 2、数据更新频率:月度 - 2、数据更新频率:月度
@@ -0,0 +1,167 @@
# 分类属性代码(行业与基金分类)
天软把「个券归属会随时间变更」的分类维护成**分类属性**,用一套属性代码
(如 `SWHY210000`)标识。分类目录本身和个券归属分别放在不同的字典表里,
取数入口也不同。本页收拢这类分类的代码体系与取数路径。
本页是手写整理页,不由字典生成器产出;`references/data_dictionary/` 下的页面
是生成产物,不要把这里的内容写回那些页面。
## 目前维护的分类属性
| 资产 | 所属属性代码 | 分类名称 | 级数 | 备注 |
| ---- | ------------ | ------------------ | ---- | --------------------------- |
| 股票 | `SWHY` | 申万行业 | 三级 | |
| 股票 | `CAPCHY` | 中上协行业 | 二级 | 2024-02-19 起接替证监会行业 |
| 股票 | `CSRCHY` | 证监会行业 | 二级 | **2024-02-19 起不再维护** |
| 基金 | `YHFL` | 银河基金分类 | 二级 | 数据起始 2021-01-04 |
| 基金 | `TSJJ02` | 投资风格 | 二级 | |
| 基金 | `TSJJ03` | 净值增长率计算方法 | 二级 | |
属性代码的构成是「所属属性代码 + 层级编号」,例如 `SWHY``SWHY210000`(申万采掘,
一级)→ 其下二级、三级继续细分;`CSRCHY``CSRCHYA`(农、林、牧、渔业)→
`CSRCHYA01`(农业)。
## 两类表:分类目录 vs 个券归属
分类目录(有哪些分类、上下级关系、启用与剔除日期)和个券归属(某只券在某段时间属于
哪个分类)是两张不同的表,访问代码不同:
| 表 ID | 表名 | 访问代码 | 用途 |
| ----- | --------------------- | ------------------------------------- | ------------------ |
| 138 | 股票.股票行业分类信息 | **属性代码**,如 `SWHY``SWHY110000` | 股票分类目录 |
| 355 | 基金.基金分类信息 | **属性代码**,如 `YHFL``YHFL010000` | 基金分类目录 |
| 356 | 基金.基金分类 | **基金代码**,如 `OF000316` | 单只基金的分类归属 |
138 与 355 的字段结构一致:`属性代码``属性名称``级数``上级属性代码`
`上级属性名称``入选日期``剔除日期``最新标识``所属属性代码`
传错访问代码是这里最常见的错误:把基金代码传给 355、或把属性代码传给 356,都不会报错,
只会返回空表。字段清单与取数示例见字典页,用
`dictionary_lookup.py --query 股票行业分类信息` / `--query 基金分类信息` 取回。
## 取分类目录
按属性代码取某个分类节点及其记录:
```tsl
// 申万行业分类目录(全部层级)
return select * from infotable 138 of "SWHY" end;
```
`getbk(板块名)` 可以一次取回某个分类下的全部属性代码,再喂给 `infotable`
```tsl
// 申万行业的全部分类属性代码,按级数分组展示
codes := getbk("申万行业");
t := select * from infotable 138 of codes end;
return select ['级数'], select * from thisgroup end as '分类列表'
from t group by ['级数'] order by ['级数'] end;
```
`getbk("属性代码")` 是跨资产的总入口,同时覆盖股票与基金两侧:
```tsl
codes := getbk("属性代码");
t1 := select "股票" as '资产', * from infotable 138 of codes end;
t2 := select "基金" as '资产', * from infotable 355 of codes end;
return t1 union t2;
```
`getbk` 取的是**最新**成分,且入参是板块名称而非代码;签名与板块名约束见
`lookup.py --name getbk`
## 沿层级向下走
分类属性代码后缀 `.N` 表示「该属性下的所有下级分类属性代码」:
```tsl
// 指定日所有申万一级行业代码
return getbkbydate("SWHY.N", 20210423T);
// 指定一级行业下的二级行业代码
return getbkbydate("SWHY210000" $ ".N", 20210423T);
```
三级同理,用二级代码加 `.N`。也有直接返回下级列表的函数,签名用
`lookup.py --name stockIndustryLowerArr` / `--name fundYHIndustryArr` 取回。
## 取分类成分
不带 `.N` 时,`getBkByDate` 把属性代码当分类节点,返回该分类在指定日的成分券:
```tsl
return getbkbydate("SWHY740000", 20220101T); // 申万煤炭成分股
return getbkbydate("CAPCHYD", 20240219T); // 中上协:电力、热力、燃气及水
return getbkbydate("YHFL030000", 20220101T); // 银河债券型成分基金
return getbkbydate("TSJJ0201", 20220101T); // 投资风格:股票型
```
`getBkByDate` 的字典条目只描述了指数代码入参,分类属性代码入参同样成立;两种入参
共用一个函数。签名与 `extype` 参数见 `lookup.py --name getBkByDate`
## 行业分类代码 ≠ 行业指数代码
同一个申万行业有两套代码,用途不同,混用会取到空结果:
| 用途 | 代码形态 | 示例 |
| -------------------- | ------------------------ | ----------------------------- |
| 行业**分类**属性代码 | `SWHY` + 层级编号 | `SWHY210000` 申万采掘 |
| 行业**指数**代码 | `SWHY00000N`N 为级数) | `SWHY000001` 申万一级行业指数 |
取行业指数代码序列走指数侧:
```tsl
// 指定日所有申万一级行业指数代码
return getbkbydate("SWHY000001", 20190201T);
// 最新的行业指数序列也可按板块名取
return getbk("申万一级行业指数");
```
二级、三级分别对应 `SWHY000002` / `SWHY000003`,板块名分别是
`申万二级行业指数` / `申万三级行业指数`。代码转名称用
`lookup.py --name stockName`
## 反查:个券在指定日属于哪个分类
申万各级有成对的名称/编码查询函数,与系统参数「证券」「日期」相关:
```tsl
setSysParam(pn_stock(), "SZ000002");
setSysParam(pn_date(), 20200101T);
return array("一级名称": stockSWIndustryName1(),
"一级编码": stockSWIndustryID1(),
"二级名称": stockSWIndustryName2(),
"二级编码": stockSWIndustryID2(),
"三级名称": stockSWIndustryName3(),
"三级编码": stockSWIndustryID3());
```
批量反查配合 `spec(...)` 逐券切换系统参数:
```tsl
endt := 20240103T;
stocks := getbkbydate("SH000016", endt);
setSysParam(pn_date(), endt);
return select thisRow as "sid",
spec(stockSWIndustryName1(), thisRow) as "一级行业名称",
spec(stockSWIndustryID1(), thisRow) as "一级行业编码"
from stocks end;
```
这六个函数无参数,签名逐个用 `lookup.py --name stockSWIndustryName1` 等确认。
按板块分类批量反查另有 `stocksbklist`,其 `bktype` 是整数板块类别,取值不在本页
事实范围内,用前先确认。
## 其它取数代码
少量非个券维度的数据也用代码位取:`HG000001` 宏观数据表格,`HG000001``HG000004`
分别对应港股通(沪)、沪股通、港股通(深)、深股通方向。其余特别数据(如国债收益率
曲线)按字典页或专题文档确认,不在本页枚举。
## 事实边界
本页只给代码体系与取数路径。函数签名、参数类型、返回形态一律以
`lookup.py --name` 的条目正文为准,本页示例不作为签名依据。具体分类下有哪些属性代码
会随上游调整,不在本页硬编码;用上面的 `getbk` + `infotable` 组合在运行时取回当前列表。
@@ -0,0 +1,194 @@
# 证券代码序列取数
「先拿到一批证券代码,再逐个取指标」是天软里最常见的起手动作。代码序列有三条互不
等价的取数路径,选错路径会静默拿到错误的样本集:
| 路径 | 函数 | 时点语义 | 入参 |
| ----------------- | -------------- | ------------------ | -------------------------- |
| 板块最新成分 | `getbk` | **最新**,不带日期 | 板块中文名 |
| 板块历史成分 | `getAbkbyDate` | 指定日 | 板块中文名(仅限部分板块) |
| 指数/分类历史成分 | `getBkByDate` | 指定日 | 指数代码或分类属性代码 |
`getbk` 没有日期入参,拿到的永远是当前成分;做历史回测时用它会引入前视偏差。
签名与各自的适用范围用 `lookup.py --name getbk` / `--name getAbkbyDate` /
`--name getBkByDate` 取回。
本页是手写整理页,不由字典生成器产出。
## 板块名是复合的
多个板块名用英文分号拼接,一次传入:
```tsl
return getbk("深证A股;上证A股"); // 只要主板
return getbk("A股;暂停上市;终止上市"); // 含退市与暂停上市
return getbk("港股;终止上市港股"); // 全部港股
return getbk("上市指数;退市指数"); // 全部指数
return getbk("上证基金;深证基金"); // 在市基金交易代码
```
`A股` 本身就是复合板块,等于 `深证A股;上证A股;创业板;科创板;北证A股`。需要单独一个
板块时直接写该板块名,不要在 `A股` 结果上二次过滤。
板块划分会随上游调整:港股在 2026-06-25 之前全部在「港股」板块内,之后拆出
「终止上市港股」。依赖板块名的代码在跨越这类调整时要复核。
具体有哪些板块名不在本页事实范围内;`getbk` 要求名称与板块管理中的名称完全一致,
先在客户端板块管理里确认。
## 指定日的市场板块成分
`getAbkbyDate(bk_name, endt)` 按板块中文名取指定日成分,并剔除暂停上市:
```tsl
return getAbkbyDate("A股", 20200210T);
return getAbkbyDate("深证A股;上证A股;北证A股", 20200210T);
```
它只支持市场板块(`A股``上证A股``深证A股``北证A股``创业板``科创板`
`中小企业板`),其它板块名不支持。
另一条等价路径是用天软自编的市场板块**指数**代码走 `getBkByDate`
| 代码 | 板块 | 代码 | 板块 |
| ----------- | ------- | ----------- | ------- |
| `TSI000001` | A股 | `TSI000006` | 北证A股 |
| `TSI000002` | 上证A股 | `TSI000007` | B股 |
| `TSI000003` | 科创板 | `TSI000008` | 上证B股 |
| `TSI000004` | 深证A股 | `TSI000009` | 深证B股 |
| `TSI000005` | 创业板 | | |
```tsl
return getbkbydate("TSI000001", 20230810T); // A股板块 20230810 成分
```
## 指数成分必须用指数代码
指数成分来源于指数成份变更表(`infotable 752` 指数.指数成份),与板块来源不同,
只能用指数代码取:
```tsl
return getbkbydate("SH000300", 20210908T); // 沪深300 指定日成分
```
板块名传给 `getBkByDate` 不会报错,只会返回空表。字段清单用
`dictionary_lookup.py --query 指数成份` 取回。
## 行业分类与行业指数是两套代码
`getbkbydate("SWHY740000", ...)` 取的是**行业分类**成分(来源:个股所属行业),
`getbkbydate("SW801150", ...)` 取的是**行业指数**成分(来源:指数成份表)。两者
样本可能不同。先用 `SWHY00000N` 拿到该级全部行业指数代码,再逐个取成分:
```tsl
idx := getbkbydate("SWHY000001", 20190201T); // 所有申万一级行业指数代码
return getbkbydate(idx[0], 20190201T); // 其中一个的成分股
```
分类属性代码体系见 [`classification-codes.md`](classification-codes.md)。
## 沪深港通
沪深港通合资格名单也记在 `infotable 752`,用四个方向虚拟代码取:
| 代码 | 方向 | 代码 | 方向 |
| ---------- | ------------ | ---------- | ------------ |
| `HG000001` | 港股通(沪) | `HG000003` | 港股通(深) |
| `HG000002` | 沪股通 | `HG000004` | 深股通 |
```tsl
// 指定日北向(A股)合资格名单
return getbkbydate("HG000002", 20240701T) union2 getbkbydate("HG000004", 20240701T);
// 指定日南向(港股)合资格名单
return getbkbydate("HG000001", 20240701T) union2 getbkbydate("HG000003", 20240701T);
// 曾经进过港股通的全部港股代码
return sselect distinct ["证券代码"]
from infotable 752 of array("HG000001", "HG000003") end;
```
## 基金:在市与运作状态是两回事
板块决定「是否上市」,运作状态要另外筛:
```tsl
return getbk("上市基金"); // 当前在市
return getbk("已退市基金"); // 已退市
return getbkall("普通股票型"); // 某分类全部,含已停止运作
return fundOperateFundsbyBkname("上市基金;已退市基金", 20200924T); // 指定日仍在运作
return fundsOperateFunds(getbkall("股票型"), 20200101T); // 对给定列表筛运作中
```
`getBkAll` 返回板块全部样本含退市,但对一批板块不适用(A股类、指数成份类、概念板块
等),限制清单见 `lookup.py --name getBkAll` 的条目正文。
按基金分类取指定日成分优先走分类属性代码:
```tsl
return getbkbydate("TSJJ0201", 20220101T); // 投资风格:股票型
```
一级市场代码与二级市场(交易)代码不同:`getbk("上证基金;深证基金")` 取的是交易代码,
两者可通过基金基本信息表里的交易代码字段互转。
## 债券
```tsl
return getbk("深交所债券;上交所债券;银行间债券"); // 当前在市全部债券
return getbk("可转债"); // 当前在市可转债
return getbk("可转债;退市可转债"); // 含退市
return getbkall("可转债"); // 同上,走 getBkAll
return bondConvertibleBonds2(); // 专用函数:所有可转债
return cb_getTradeMarkByDate(20210813T); // 指定日有交易的转债
```
券种板块名(`国债``企业债` 等)走同一套 `getbk` 路径。两个专用函数的签名见
`lookup.py --name bondConvertibleBonds2` / `--name cb_getTradeMarkByDate`
## 期货
期货有「合约」和「品种」两层,代码序列要分清取哪一层:
```tsl
return getbk("上市期货"); // 当前在市合约
return getbk("上市期货;退市期货"); // 含退市合约
return getbk("期货品种代码"); // 品种代码(CU、IF 这类)
return getbk("期货品种代码;退市期货品种代码");
return getbk("期货主力"); // 主力线;期货连续/次主力/指数线同理
return getFuturesBkbydate("上市期货;退市期货", 20251218T); // 指定日在市合约
return getFuturesBkbydate("郑州商品交易所;中国金融期货交易所", 20240508T);
```
`getFuturesBkbydate` 的板块名可以是交易所名、期货类别(`股指期货``商品期货`
`农产品期货`)或品种名,签名见 `lookup.py --name getFuturesBkbydate`
品种代码与主力/次主力/指数线/连续代码的对照关系在 `infotable 708`
(期货.期货品种代码对照表),任一形态的代码都能查到其余形态;访问代码与字段用
`dictionary_lookup.py --query 期货品种` 取回。按品种取全部合约要先把品种代码换成
品种名称再走 `getBkAll`
## 期权
```tsl
return getbk("上市期权"); // 当前在市合约
return getbk("ETF期权"); // 按标的类型
return getbk("华泰柏瑞沪深300ETF期权"); // 按标的名称
```
## 其它代码位
`HG000001` 也用作宏观数据表格的取数代码位(如国内生产总值 816 表、居民消费价格
822 表)。其余特别数据(国债收益率曲线等)按字典页确认。
## 事实边界
本页只给取数路径与代码位体系。函数签名、参数类型、返回形态一律以
`lookup.py --name` 的条目正文为准。板块名清单、分类代码清单会随上游调整,不在本页
硬编码,用运行时取回。
FAQ 中出现但当前 API 索引查不到的名称(`GetETFListByDate``GetFuturesID`)不作为
可用 API;需要时先用 `lookup.py --kw` 确认存在再使用,查不到就按 SKILL.md 的
CHECKPOINT 停止。上表中 `getbk` 系列、`getAbkbyDate``getBkByDate``getBkAll`
`getFuturesBkbydate``fundOperateFundsbyBkname``fundsOperateFunds`
`bondConvertibleBonds2``cb_getTradeMarkByDate` 均已在索引中核对存在。
+5 -6
View File
@@ -21,7 +21,8 @@
- 键必须与 `references/` 下的文件名完全一致;每个参考页至少一条短语。 - 键必须与 `references/` 下的文件名完全一致;每个参考页至少一条短语。
键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言 键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言
入口。`lookup.py --check` 负责拦截页键漂移,行为回归测试负责拦截排序漂移。 入口。`lookup.py --check` 负责拦截页键漂移;查询排序以 lookup 实现和实际
查询结果为准。
## Skill 维护与校验 ## Skill 维护与校验
@@ -36,14 +37,12 @@ Section ID 来自标题下的显式 `<!-- section-id: ... -->` 元数据,不
```bash ```bash
python skills/tsl-syntax-reference/scripts/lookup.py --check python skills/tsl-syntax-reference/scripts/lookup.py --check
python -m unittest test.test_tsl_syntax_reference -v
``` ```
`--check` 会校验 `page_intent_aliases` 的键与 `references/` 参考页一一对应: `--check` 会校验 `page_intent_aliases` 的键与 `references/` 参考页一一对应:
键指向不存在的页、或某页没有自然语言入口都会报错。新增参考页时必须同时 键指向不存在的页、或某页没有自然语言入口都会报错。新增参考页时必须同时
在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;原始 alias、 在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;自然语言
助词变体、弱命中和已知真实问法由专属测试覆盖 排序和真实问法应通过实际查询验证
`--check` 还覆盖显式 ID、标题层级、quickstart 派生与 Owner Section、代码块身份、 `--check` 还覆盖显式 ID、标题层级、quickstart 派生与 Owner Section、代码块身份、
本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。仓库级 unittest 本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。
负责 CLI、排序与退出码行为。
+18 -20
View File
@@ -1,21 +1,22 @@
# 🧪 Playbook 测试套件 # 🧪 Playbook 测试套件
本目录包含 Playbook 项目的测试,用于验证 CLI、模板与文档链接 本目录包含 Playbook 项目的可执行行为测试。
## 📋 目录结构 ## 📋 目录结构
```txt ```txt
test/ test/
├── README.md # 本文件:测试文档 ├── README.md # 本文件:测试文档
├── test_playbook.py # snapshot/subtree 参数化完整部署测试 ├── test_commit_message_policy.py # 提交信息校验器和 CI 委托行为
├── test_template_contracts.py # 模板内容、占位符、文案契约测试 ├── test_gitea_fetch_ci_logs.py # Gitea CI 日志采集行为
├── test_main_loop_cli.py # ticket 调度、隔离、证据与集成测试 ├── test_main_loop_cli.py # ticket 调度、隔离、证据与集成测试
├── agent/ # Agent 题面/运行时验证测试定义 ├── test_main_loop_scheduler.py # 全局调度器和依赖图行为
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试 ├── test_playbook.py # snapshot/subtree 参数化完整部署测试
├── test_tsl_playbook_sync.py # TSL Playbook 构建与同步边界测试 ├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步行为
├── test_tsl_syntax_reference.py # TSL 语法文档结构与检索测试 ├── test_tsl_api_framework.py # TSL API 索引和 Framework Profile 行为
── integration/ # 集成测试 ── test_tsl_playbook_sync.py # TSL Playbook 构建与同步行为
└── check_doc_links.py # 文档链接有效性检查 └── integration/ # 文档完整性检查
└── check_doc_links.py # 本地 Markdown 链接目标检查
``` ```
## 🚀 快速开始 ## 🚀 快速开始
@@ -24,10 +25,10 @@ test/
# 进入 playbook 根目录 # 进入 playbook 根目录
cd /path/to/playbook cd /path/to/playbook
# 1. 运行 Python 测试(test/ 下的 test_*.py # 运行 Python 测试(test/ 下的 test_*.py
python -X utf8 -B -m unittest discover -s test -p "test_*.py" -v python -X utf8 -B -m unittest discover -s test -p "test_*.py" -v
# 2. 运行文档链接检查 # 文档链接检查
python test/integration/check_doc_links.py python test/integration/check_doc_links.py
``` ```
@@ -51,20 +52,17 @@ python test/integration/check_doc_links.py
- rules、memory bank、prompts、standards 和 skills 同步 - rules、memory bank、prompts、standards 和 skills 同步
- 重复执行的幂等性与自定义文件保留 - 重复执行的幂等性与自定义文件保留
### 2. 模板合同测试 (`test_template_contracts.py`) ### 2. 主循环测试 (`test_main_loop_cli.py`)
随 Python 测试检查通用模板的关键 marker、占位符和流程合同。
### 3. 主循环测试 (`test_main_loop_cli.py`)
使用临时 Git 仓库验证 ticket 队列、串行与 worktree 隔离、并发 claim、heartbeat/reclaim、 使用临时 Git 仓库验证 ticket 队列、串行与 worktree 隔离、并发 claim、heartbeat/reclaim、
验证与 review 证据绑定,以及 ticket/feature 两级集成。 验证与 review 证据绑定,以及 ticket/feature 两级集成。
### 4. TSL 契约测试 ### 3. TSL 和同步行为测试
- `test_tsl_api_framework.py`:验证 API 索引、class 查询和 Framework Profile 行为
- `test_tsl_playbook_sync.py`:验证 TSL Playbook 构建和同步文件边界 - `test_tsl_playbook_sync.py`:验证 TSL Playbook 构建和同步文件边界
- `test_tsl_syntax_reference.py`:验证语法文档结构、section ID 与检索结果 - `test_thirdparty_skills_pipeline.py`:验证 thirdparty manifest、workflow 和同步产物
### 5. 文档链接检查 (integration/) ### 4. 文档链接检查 (`integration/`)
扫描 `docs/` 与模板文件中的本地链接,确保引用路径有效。 扫描仓库中的 Markdown 文件,确保本地文档引用的目标路径有效。
-296
View File
@@ -1,296 +0,0 @@
# TSL Agent 测试定义
你是 TSL agent 测试执行方。
阅读本 README 后,先向用户询问本机 `tsl` 可执行文件路径。
用户提供路径后,优先自动识别被测 agent 名称;无法可靠识别时,向用户确认一次
agent 短名。随后严格按本文的标准测试流程执行。
轮次边界必须严格执行:
- 第一轮只允许生成首答、落盘、运行验证和记录评分;禁止修复,禁止把运行错误回传给被测 agent,禁止修改第一轮结果文件,也禁止测试执行方在命令失败后自动编辑文件。
- 本轮计划测试的所有 case 完成第一轮评分后,才允许进入第二轮修复。
- 第二轮只修复第一轮已经记录为 `fail` 的 case;每修复某个 case 一次,只增加该 case 的 `repair_count`
这个目录包含 TSL 语法的 agent 测试用例定义。当前评分以运行时验证为准:
生成的 `.tsl``.tsf` 能按标准流程执行通过即为通过,执行报错、超时或文件类型错误即为失败。
## 测试定义
- `prompts_zh.md`:给被测 agent 的题面文件。
- 包含 100 个唯一测试用例。
- 只包含题面,不包含答案、评分细则或事实来源映射。
- 除非题面明确要求 `.tsf`,默认都要求生成 `.tsl`
不再维护单独的 rubric 文件。不要用人工答案表给被测 agent 提供额外线索。
## 测试产物
所有测试结果都落盘为 `.tsl``.tsf` 文件,不保存 agent 回答 markdown。
- 存放目录:`test/agent/result/<agent>/<YYYYMMDD>/`
- `<agent>` 由测试执行方优先自动识别。
- 识别来源优先使用被测 agent 的产品名、命令名、SDK provider 名或当前运行器已知的 agent 身份。
- 无法从运行器上下文、命令名或已知 provider 可靠识别时,向用户确认一次 agent 短名;不要猜测。
- 目录名统一转成小写短名,只使用 ASCII 字母、数字、短横线、下划线或点。
- 命名格式:
- `.tsl``tsl-001.tsl`
- `.tsf``tsl-037/<TopLevelName>.tsf`
- `.tsf` 调用验证脚本:`tsl-037/verify.tsl`
- 修复轮次按 case 计数:`repair-1/tsl-001.tsl``repair-1/tsl-037/<TopLevelName>.tsf`
- 示例:
- `result/claude/20260610/tsl-001.tsl`
- `result/claude/20260610/tsl-037/<TopLevelName>.tsf`
- `result/claude/20260610/tsl-037/verify.tsl`
被测 agent 禁止读取、请求、搜索或引用 `result/`
## 目录结构
```text
test/agent/
├── README.md
├── prompts_zh.md
└── result/
└── claude/
└── 20260610/
├── tsl-001.tsl
├── tsl-037/
│ ├── <TopLevelName>.tsf
│ └── verify.tsl
└── repair-1/
├── tsl-001.tsl
└── tsl-037/
├── <TopLevelName>.tsf
└── verify.tsl
```
## 标准测试流程
### 1. 准备测试
阅读本 README 后,测试执行方必须在开始执行用例前询问并记录本机 `tsl`
可执行文件路径。该路径不写入配置文件,只在本轮运行时验证中使用。
`TSL_HOME` 记为 `tsl` 可执行文件所在目录;如果用户提供的是 TSL 根目录,
则以用户提供的目录作为 `TSL_HOME`
```text
TSL executable path: /path/to/tsl
```
优先自动识别被测 agent 名称并记为 `AGENT``AGENT` 不是普通配置项;如果无法从
当前运行器上下文、命令名或已知 provider 可靠识别,则在开始执行用例前向用户确认一次
agent 短名;不要猜测。
```bash
AGENT="<auto-detected-agent>"
DATE=$(date +%Y%m%d)
mkdir -p "test/agent/result/$AGENT/$DATE"
```
### 2. 逐题生成文件
阅读 `prompts_zh.md`,然后按 case 逐题执行测试。
对于每个测试用例:
1. 为当前 case 启动全新的被测 agent 会话。测试执行方可以保持同一个会话,
但被测 agent 不能在同一个会话里连续回答多个 case。
2. 只发送 `prompts_zh.md` 中的 `Runner Setup``Answer Rules` 加单个题面。
3. 记录 agent 生成的第一次完整代码并立即落盘。
4. 落盘后冻结该 case 的第一轮结果文件;第一轮不允许 agent 根据运行结果或评分反馈修订答案。
5. 被测 agent 不得主动读取、请求、搜索或引用任何评分材料。
Codex 作为被测 agent 时:
- CLI 自动化:每个 case 单独执行一次 `codex exec`,不要使用 `codex exec resume`
- CLI 自动化如果不想保留会话记录,可使用 `codex exec --ephemeral`
- Codex App 或 IDE:每个 case 新建一个 thread,不要在同一个 thread 里继续下一个 case。
- Codex SDK/MCP:每个 case 调用一次新的 thread/start;不要用同一个 thread 连续 run 多个 case。
任何被测 agent 都一样:第一轮运行时验证输出不能回传给被测 agent。第一轮发现错误时,
只记录 `fail`,不得修复当前文件,不得让被测 agent 重新回答,不得把修改后的文件计入
第一轮成绩。测试执行方自己的自动修复/纠错行为在第一轮也必须关闭。如果工具在看到
第一轮运行错误后自动修改结果文件,必须按首次落盘文件评分;无法恢复首次落盘文件时,
该 case 标记为 `invalid`。第二轮修复阶段是例外,见“自动修复轮次”。
评分材料包括:
- `result/`
- 运行时验证日志
- 其他 agent 的输出文件
- 对比报告或人工备注
若被测 agent 主动接触评分材料,该 case 或本轮测试应标记为 `invalid`
不计入有效成绩。
推荐的运行器设置:
```text
You are in this repository. When asked to write TSL, write valid TSL/TSF; do not
infer syntax from Pascal, TypeScript, JavaScript, Python, or SQL.
```
文件落盘规则:
- 默认每个 case 保存为 `.tsl`,例如 `test/agent/result/<agent>/<YYYYMMDD>/tsl-001.tsl`
- 只有题面明确要求输出 `.tsf` 时,才保存为 `.tsf`,并放入对应 case 目录,
例如 `test/agent/result/<agent>/<YYYYMMDD>/tsl-037/<TopLevelName>.tsf`
- `.tsf` 文件名不能使用 case 编号;文件基名必须与第一个顶层声明同名。
测试执行方应按被测 agent 代码中的第一个顶层声明命名文件,不为了凑文件名改写代码。
- 任何 `unit ... end.` 文件只能保存为 `.tsf`,不能保存为 `.tsl`
- 如果 agent 输出 markdown code fence,测试执行方只提取代码内容保存到文件,不保存回答 markdown。
### 3. 运行时验证
运行时验证是标准测试流程的一部分。保存 `.tsl``.tsf` 文件后,由测试执行方
逐个验证。第一轮验证必须在被测 agent 会话之外执行,不要把 stdout/stderr 或错误日志
发送回被测 agent;即使第一轮发现错误,也只记录评分,不修复文件。第二轮自动修复阶段
按“自动修复轮次”规则处理。
`.tsl` 验证规则:
```bash
cd test/agent/result/<agent>/<YYYYMMDD>
tsl tsl-001.tsl
tsl tsl-002.tsl
```
`.tsf` 验证规则:
1. 将 case 目录中的 `.tsf` 文件复制到 `TSL_HOME/funcext/tmp/`
2. 在同一个 case 目录中编写 `verify.tsl` 调用脚本。
调用脚本应按被测 agent 实际生成的函数名、unit 名或类型名来调用。
验证脚本必须实际调用 `.tsf` 暴露的功能,不能只输出成功标记。
3. 调用脚本在所有验证调用成功后,用 TSL 输出语句最后输出一行 `__TSL__AGENT__OK__`
4. 执行 `tsl verify.tsl`
5. 验证完成后删除 `TSL_HOME/funcext/tmp/`
```bash
cd test/agent/result/<agent>/<YYYYMMDD>/tsl-037
mkdir -p "$TSL_HOME/funcext/tmp"
cp ./*.tsf "$TSL_HOME/funcext/tmp/"
tsl verify.tsl
rm -rf "$TSL_HOME/funcext/tmp"
```
`funcext/tmp` 由本流程独占。开始验证前不要在该目录保留人工文件;结束后必须清理。
### 4. 第一轮评分规则
- `pass`:按标准流程执行,最后一个非空输出行是 `__TSL__AGENT__OK__`
- `fail`:超时、缺少文件、文件类型错误、`.tsf` 调用脚本失败,
`.tsf` 文件名与第一个顶层声明不一致,`.tsf` 无法被 TSL 加载/编译,
或最后一个非空输出行不是 `__TSL__AGENT__OK__`
- `invalid`:被测 agent 主动读取、请求、搜索或引用评分材料。
TSL 可能在打印错误后仍返回退出码 0,因此不能只看 `$LASTEXITCODE`
运行时评分以成功标记为准:脚本没有正常执行到最后,就不会输出最后的
`__TSL__AGENT__OK__`
最终成绩只统计有效 case
```text
pass_rate = pass / (pass + fail)
```
不由被测 agent 自评。
### 5. 自动修复轮次
本轮计划测试的所有 case 完成第一轮评分后,可以对 `fail` 的 case 启动第二轮自动修复。
修复规则:
1. 第一轮期间禁止修复;只有进入第二轮后,第一轮已经记录为 `fail` 的 case 才可以修复。
2. 修复阶段可以把当前文件内容和运行时验证输出发给被测 agent。
3. 每次 agent 产出修复版本,都保存到新的修复目录,不覆盖第一轮文件。
4. 第一次修复保存到 `repair-1/`,第二次修复保存到 `repair-2/`,依此类推。
5. `repair_count` 按 case/file 单独记录;修复 `tsl-019.tsl` 只增加 `tsl-019` 的次数,
不影响其他失败 case。
6. 修复版本仍按同样的运行时验证规则执行,最后一个非空输出行必须是 `__TSL__AGENT__OK__`
示例:
```text
test/agent/result/opencode/20260623/
├── tsl-019.tsl # 第一轮原始输出
├── repair-1/
│ └── tsl-019.tsl # 第 1 次修复
└── repair-2/
└── tsl-019.tsl # 第 2 次修复
```
修复统计:
```text
case_id = tsl-019
first_pass = 第一轮是否 pass
repair_pass = 修复后是否 pass
repair_count = 该 case 的修复版本数量
```
如果第一轮已经 `pass``repair_count = 0`。如果多次修复仍失败,
该 case 的 `repair_count` 记录实际修复次数,`repair_pass = false`
### 6. 对比分析
不同 agent 的输出直接按目录对比:
```bash
diff -r test/agent/result/claude/20260617 \
test/agent/result/gpt4/20260617
```
建议人工记录汇总表,但不要把汇总表放入被测 agent 可读上下文。
```markdown
| Agent | 测试日期 | 有效用例 | Pass | Fail | Invalid | Pass rate |
|-------|---------|---------:|-----:|-----:|--------:|----------:|
| Claude | 2026-06-17 | 100 | 96 | 4 | 0 | 96% |
| GPT-4 | 2026-06-17 | 100 | 88 | 12 | 0 | 88% |
```
## 版本控制策略
提交到 git
```text
test/agent/README.md
test/agent/prompts_zh.md
```
不提交:
```text
test/agent/result/
```
## 测试用例覆盖
`prompts_zh.md` 包含 100 个测试用例,覆盖:
| 语法点 | 测试用例数 |
| ----------- | ---------- |
| 基础脚本 | 10+ |
| 控制流 | 10+ |
| 函数调用 | 10+ |
| `.tsf`/Unit | 4+ |
| 集合与查询 | 10+ |
| 表达式 | 10+ |
| 类与对象 | 15+ |
| 运行时特性 | 10+ |
| 高级边界 | 10+ |
## 最佳实践
- 测试用例应该对所有 agent 都适用。
- 不要添加特定 agent 的专属测试。
- 关注 TSL 语法本身,而非 agent 特性。
- 每轮测试使用独立日期目录。
- 保留第一次输出作为基线,不根据运行反馈让同一个 agent 修订答案。
- 定期对比不同 agent 的通过率和常见运行错误。
**维护者**TSL Team
**支持的 Agent**:所有支持 TSL 的 agent
-879
View File
@@ -1,879 +0,0 @@
# TSL Agent Prompt Set (ZH)
本文件只包含发送给被测 agent 的题面。
## Runner Setup
```text
你正在编写 TSL 代码。
不要从 Pascal、TypeScript、JavaScript、Python 或 SQL 推断 TSL 语法或函数。
```
## Answer Rules
```text
每个 case 独立作答。
除非题面明确要求输出 .tsf,否则默认输出可执行 .tsl 文件内容。
题面模拟普通用户的需求,不要复制文档中的样例代码。
请按题面里的名称、数据和业务动作重新编写。
如果输出 .tsl,请在题面要求的业务输出全部完成后,用 TSL 输出语句最后输出一行 __TSL__AGENT__OK__
如果输出 .tsf,必须是 TSL 能从 funcext 加载的有效函数扩展或 unit 文件;
不要在 .tsf 中加入成功标记,成功标记由验证脚本输出。
只输出代码,不要解释。
```
## Prompts
### TSL-001: 订单折后金额
```text
请写一份 .tsl 代码:
一个订单里,单价是 19,数量是 6,优惠金额是 14。
请算出这行订单最后要收多少钱,并输出结果。
```
### TSL-002: 分页信息
```text
请写一份 .tsl 代码:
一共有 47 条记录,每页放 10 条。
请输出能装满的页数,以及最后一页还剩多少条。
```
### TSL-003: 订单编号
```text
请写一份 .tsl 代码:
把文字 "Order" 和编号 "42" 合成一个订单编号,然后输出它。
```
### TSL-004: 简单扣费
```text
请写一份 .tsl 代码:
账户余额是 12,本次扣费是 3。
请输出扣费后的余额。不要提前列出额外的变量清单。
```
### TSL-005: 固定单价
```text
请写一份 .tsl 代码:
某个商品固定单价是 9,本次买了 4 个。
请把固定单价作为常量保存,再输出总价。
```
### TSL-006: 严格模式库存
```text
请写一份 .tsl 代码:
这份脚本要开启显式变量模式。
请准备一个库存数 stock,把它设为 18,然后输出它。
```
### TSL-007: 读取分数
```text
请写一份 .tsl 代码:
有一组分数:15、30、45。
请输出第一项和最后一项。
```
### TSL-008: 读取编码字符
```text
请写一份 .tsl 代码:
编码是 "ZX9"。
请输出第一个字符和第三个字符。
```
### TSL-009: 空列表检查
```text
请写一份 .tsl 代码:
准备一个空列表 bucket。
请检查 bucket 的第一个位置是不是 nil,并输出检查结果。
```
### TSL-010: 文本数字参与计算
```text
请写一份 .tsl 代码:
有一个文本形式的数字 "58"。
请先把它变成整数,再减去 8,并输出结果。
```
### TSL-011: 温度标签
```text
请写一份 .tsl 代码:
temperature 是 32。
如果温度大于等于 30level 就是 "hot";否则 level 是 "normal"。
请最后输出 level。
```
### TSL-012: 余额审核
```text
请写一份 .tsl 代码:
balance 是 80。
如果 balance 大于 50,请先输出 "allow",再扣掉 10
否则输出 "deny"。最后输出 balance。
```
### TSL-013: 偶数累加
```text
请写一份 .tsl 代码:
请把 2、4、6、8 加起来并输出。
要求用带步长的循环完成。
```
### TSL-014: 权重检查
```text
请写一份 .tsl 代码:
weights 里有 3、5、9。
请逐个处理,每行输出“当前位置”和“这个位置的值”的乘积。
```
### TSL-015: 累加到上限
```text
请写一份 .tsl 代码:
从 n = 1 开始累加。
当 n 大于 4 时停止,最后输出累计结果。
```
### TSL-016: 跳过指定数字
```text
请写一份 .tsl 代码:
处理 1 到 5。
遇到 3 时跳过它,只累加其他数字,最后输出结果。
```
### TSL-017: 倒计时
```text
请写一份 .tsl 代码:
从 countdown = 4 开始倒数,一直到 0 停止。
请使用 repeat until,并输出最后的 countdown。
```
### TSL-018: 状态码
```text
请写一份 .tsl 代码:
status_code 是 2。
如果是 1,输出 "new";如果是 2 或 3,输出 "active"
其他情况输出 "closed"。
```
### TSL-019: 等级文字
```text
请写一份 .tsl 代码:
grade 是 3。
请用 case 表达式得到一段等级文字:1 是 "low"2 到 4 是 "mid",其他是 "high"。
最后输出这段等级文字。
```
### TSL-020: 捕获错误信息
```text
请写一份 .tsl 代码:
故意抛出 "network" 这个错误。
捕获它后,请输出错误对象里的错误信息。
```
### TSL-021: 清理动作
```text
请写一份 .tsl 代码:
先输出 "open"。
无论前面是否正常结束,最后都要输出 "close"。
请用 try/finally 完成。
```
### TSL-022: 计算数量乘积
```text
请写一份 .tsl 代码:
请做一个“计算两个整数乘积”的功能,名字由你自己取。
脚本里请用 6 和 8 调用它,并输出结果。
```
### TSL-023: 调整额度
```text
请写一份 .tsl 代码:
请做一个“把额度增加 5”的动作,名字由你自己取。
脚本里 quota 一开始是 10,调用后输出 quota。
```
### TSL-024: 默认手续费
```text
请写一份 .tsl 代码:
请做一个“计算手续费后金额”的功能,名字由你自己取。
如果没有传金额,就按 100 处理;最后结果是金额加 6。
请分别输出不传金额和传 20 时的结果。
```
### TSL-025: 下一批编号
```text
请写一份 .tsl 代码:
请做一个“取得下一批编号”的功能,名字由你自己取。
起始编号是整数,默认是 10,结果也是整数。
它要返回起始编号加 1。请输出默认情况下的结果。
```
### TSL-026: 区间组装
```text
请写一份 .tsl 代码:
请做一个“组装左右边界”的功能,名字由你自己取,把两个边界值组成一个两项列表。
调用时请用命名方式传入:right 是 90,left 是 40。
最后输出这两项。
```
### TSL-027: 跳过中间值
```text
请写一份 .tsl 代码:
请做一个“检查中间位置”的功能,名字由你自己取,里面要输出第二个位置 b 是不是 nil。
调用时只给第一个和第三个位置传值,故意跳过 b。
```
### TSL-028: 按名字调用功能
```text
请写一份 .tsl 代码:
请做一个“三位数字编码”的功能,名字由你自己取。
它把三个输入算成 a * 100 + b * 10 + c。
脚本里请通过 call 按这个名字调用它,并输出结果。
```
### TSL-029: 多个数字相乘
```text
请写一份 .tsl 代码:
请做一个“多个数字相乘”的功能,名字由你自己取。
它可以接收任意多个数字,并把它们全部相乘。
请用 2、3、4 调用,并输出结果。
```
### TSL-030: 看到多少输入
```text
请写一份 .tsl 代码:
请做一个“统计收到多少输入”的功能,名字由你自己取。
前两个位置是固定的,后面还能继续接收更多值。
它要输出 ParamCount * 100 + RealParamCount 的结果。
请用 5、6、7 调用。
```
### TSL-031: 保存一个小计算
```text
请写一份 .tsl 代码:
把一个“小计算”保存到你自己命名的变量里。这个小计算接收两个值,结果是两者相乘。
请通过 call 调用这个变量,并用 4 和 9 输出结果。
```
### TSL-032: 把计算交给别人执行
```text
请写一份 .tsl 代码:
请做一个“执行后再翻倍”的功能,名字由你自己取,它接收另一个计算。
这个功能要先用 3 调用收到的计算,再把得到的结果乘以 2。
调用它时,直接给它一个“把输入加 5”的匿名计算,并输出结果。
```
### TSL-033: 查找已有功能
```text
请写一份 .tsl 代码:
请做一个“把输入乘以 3”的功能,名字由你自己取。
脚本里找到这个名字对应的函数值,再用支持的函数值调用方式处理 7,并输出结果。
```
### TSL-034: 传递当前功能
```text
请写一份 .tsl 代码:
请做一个“把输入平方”的功能,名字由你自己取。
再做一个“调用传入计算”的功能,名字也由你自己取,它接收一个计算和一个值,并通过 call 执行。
脚本里取得平方功能的函数值,并输出调用结果。
```
### TSL-035: 函数里使用规则包
```text
请写一份 .tsl 代码:
请做一个“读取远程费率”的功能,名字由你自己取。
这个功能一进入函数体就要使用一个费率相关的 unit,然后返回该 unit 中读取费率动作的结果。
```
### TSL-036: 脚本开头使用规则包
```text
请写一份 .tsl 代码:
脚本需要先使用两个你自己命名的规则包:一个处理价格,一个处理税费。
之后调用一个“计算最终金额”的动作,并输出结果。
```
### TSL-037: 手续费扩展
```text
请写一份 .tsf 代码:
提供一个“计算服务费”的功能,名字由你自己取,接收金额。
金额大于 1000 时,费用是 amount div 20;否则费用是 30。
不要写可直接运行的脚本入口。
```
### TSL-038: 发票规则包
```text
请写一份 .tsf 代码:
请做一个“发票规则”unit,unit 名由你自己取。
外部只需要能调用一个“默认额度”功能,并得到 500。
请写成完整的 unit 文件。
```
### TSL-039: 运费规则包
```text
请写一份 .tsf 代码:
请做一个“运费规则”unit,unit 名由你自己取。
外部只能调用一个“计算可计费重量”的功能。
内部可以有一个不对外公开的小工具,用来把负数修正为 0。
可计费重量要返回修正后的毛重减去皮重。
```
### TSL-040: 会员积分规则包
```text
请写一份 .tsf 代码:
请做一个“会员积分规则”unit,unit 名由你自己取。
外部可以调用“基础分”和“最终积分”两个功能。
level 大于等于 3 时基础分是 20,否则是 5。
最终积分的结果是金额 div 10 加上基础分。
```
### TSL-041: 商品信息表
```text
请写一份 .tsl 代码:
准备一条商品信息,里面有 sku = "B17"qty = 12。
请输出 sku 和 qty。
```
### TSL-042: 只取第一个成绩
```text
请写一份 .tsl 代码:
有一组成绩 88、99、100。
请只取第一个值放到 first_mark,然后输出 first_mark。
```
### TSL-043: 读取两行数据
```text
请写一份 .tsl 代码:
有两行数据:(8, 1) 和 (9, 2)。
请把两行分别取出来,再输出每行的两个值。
```
### TSL-044: 检查是否存在
```text
请写一份 .tsl 代码:
先检查 5 是否在 2、5、8 这一组数字里,并输出结果。
再检查整行 (2, 5) 是否在两行数据 (1, 1)、(2, 5) 里面,并输出结果。
```
### TSL-045: 两组行数据对比
```text
请写一份 .tsl 代码:
left_rows 是 (1, "a")、(2, "b")。
right_rows 是 (2, "b")、(3, "c")。
请分别算出合并、交集、左边独有、两边不重叠的结果,并输出每个结果的大小。
```
### TSL-046: 只保留允许状态
```text
请写一份 .tsl 代码:
有一批订单,每行包含订单号和状态。
再准备一份允许状态列表。
请只保留状态命中的订单,并输出保留下来的行数。
```
### TSL-047: 排除禁止状态
```text
请写一份 .tsl 代码:
有一批任务,每行包含任务号和状态。
再准备一份禁止状态列表。
请排除命中的任务,并输出剩余行数。
```
### TSL-048: 查询商品数量
```text
请写一份 .tsl 代码:
有一批商品数据,每行包含商品编号和数量。
请用 TS-SQL 只取数量大于 3 的行,并输出结果行数。
```
### TSL-049: 筛选并排序成绩
```text
请写一份 .tsl 代码:
有一批姓名和分数。
请用 TS-SQL 只保留分数大于等于 60 的人,并按分数从高到低排序。
最后输出第一行的姓名。
```
### TSL-050: 部门金额汇总
```text
请写一份 .tsl 代码:
有一批部门和金额数据。
请用 TS-SQL 按部门汇总金额,并输出汇总后的行数。
```
### TSL-051: 客户订单关联
```text
请写一份 .tsl 代码:
有一组客户数据和一组订单数据,它们通过客户编号关联。
请用 TS-SQL 得到客户名称和订单金额,并输出关联后的行数。
```
### TSL-052: 多种数字写法
```text
请写一份 .tsl 代码:
有几个配置值:0x2A、0b1010、0o12、200L、6E2。
请分别输出它们对应的类型判断结果。
```
### TSL-053: 日期和时间
```text
请写一份 .tsl 代码:
有一个日期 20240618T,还有一个带时间的值 20240618.1530T。
请分别输出日期字符串和时间字符串。
```
### TSL-054: 数字真假判断
```text
请写一份 .tsl 代码:
先输出 true 和 false。
再分别判断 4、0、-2 在 if 里会走哪个分支,并输出分支标记。
```
### TSL-055: Nil 参与计算
```text
请写一份 .tsl 代码:
请输出 ifNil(nil) 的结果。
再输出 nil + 5、5 + nil,并检查 nil + nil 是不是 nil。
```
### TSL-056: 复数信息
```text
请写一份 .tsl 代码:
准备两个复数:8 + 6j 和 complex(3, -4)。
请输出第一个复数的实部、虚部,并输出第二个值是不是复数。
```
### TSL-057: 批次报告
```text
请写一份 .tsl 代码:
一批货有 17 件,每箱装 5 件;优先级是 3;距离是 9,基准是 4;ratio_base 是 7。
请输出完整箱数、剩余件数、优先级的四次方、距离与基准的按位关系结果,
以及 ratio_base 与 3 的左除计算结果。
```
### TSL-058: 补货数量
```text
请写一份 .tsl 代码:
最低库存是 20,当前库存是 14。
如果库存不足,reorder_count 就是最低库存减当前库存;否则是 0。
请用条件表达式得到 reorder_count,并输出它。
```
### TSL-059: 候补分数
```text
请写一份 .tsl 代码:
primary_score 是 12missing_score 是 0,候补值是 99。
请用省略真值的条件表达式分别得到两个最终分数,并输出。
```
### TSL-060: 发货判断
```text
请写一份 .tsl 代码:
paid 是 trueblocked 是 falseamount 是 120。
请用 and、or、not 判断是否允许发货,并输出结果。
```
### TSL-061: 权限掩码
```text
请写一份 .tsl 代码:
read_write 是 6audit_exec 是 3。
请输出共同权限、合并权限、差异权限,以及 read_write 取反后的结果。
```
### TSL-062: 计数调整
```text
请写一份 .tsl 代码:
counter 一开始是 5。
先加 4,再自增一次,最后输出 counter。
```
### TSL-063: 安全读取订单金额
```text
请写一份 .tsl 代码:
maybe_order 是 nil。
请安全读取 maybe_order.total,并输出读取结果是不是 nil。
```
### TSL-064: 阈值范围
```text
请写一份 .tsl 代码:
请判断 3、8、20 是否满足从小到大的连续比较,并输出结果。
```
### TSL-065: 三组指标逐项比较
```text
请写一份 .tsl 代码:
有三组指标:2、4、6;3、5、7;4、6、8。
请逐项判断第一组是否小于第二组、第二组是否小于第三组,
并输出得到的三个结果。
```
### TSL-066: 发票号格式
```text
请写一份 .tsl 代码:
发票号是 "INV-2024-07"。
请判断它是不是以 "INV-" 开头,并且后面包含数字,然后输出结果。
```
### TSL-067: 带注释的小脚本
```text
请写一份 .tsl 代码:
请包含两种 TSL 支持的注释写法。
然后把 daily_total 设为 31,并输出它。
```
### TSL-068: 编译时模式选择
```text
请写一份 .tsl 代码:
如果定义了 FAST_MODE,就输出 "fast";否则输出 "safe"。
请用条件编译完成。
```
### TSL-069: 临时上下文
```text
请写一份 .tsl 代码:
请使用运行时 with 单星块。
在临时上下文里设置一个字段,并在块内输出这个字段。
```
### TSL-070: 嵌套上下文
```text
请写一份 .tsl 代码:
请使用运行时 with 双星块。
在嵌套上下文里读取外层字段,并输出组合结果。
```
### TSL-071: 带超时的远程式调用
```text
请写一份 .tsl 代码:
准备一个“慢速评分”的功能,名字由你自己取,它返回 12。
请用网格调用方式调用它,并设置超时时间,然后输出得到的结果。
```
### TSL-072: 全局缓存
```text
请写一份 .tsl 代码:
把 "session_limit" 这个全局缓存设为 300。
再读取它并输出,同时输出这个缓存是否存在。
```
### TSL-073: 调试结果
```text
请写一份 .tsl 代码:
请做一个“检查金额”的功能,名字由你自己取。
如果输入小于 0,请用 debugReturn 返回调试值;否则返回原值。
请用 -1 调用它并输出结果。
```
### TSL-074: 简单计时
```text
请写一份 .tsl 代码:
请用内置计时开始和结束功能包住一个小循环,并输出计时结果。
```
### TSL-075: 兜底跳转
```text
请写一份 .tsl 代码:
flag 是 false。
如果 flag 为 false,就跳到 fallback 标签。
fallback 里输出 "fallback"。
```
### TSL-076: 账户卡片
```text
请写一份 .tsl 代码:
我需要一种“账户卡片”对象,类型名由你自己取。
它要有一个公开字段,用来保存持有人姓名,字段名也由你自己取。
脚本里创建一张卡,把持有人设为 "Lina",然后输出这个字段。
```
### TSL-077: 仪表盒
```text
请写一份 .tsl 代码:
我需要一种“仪表盒”对象,类型名由你自己取。
创建它时传入初始读数,保存到 reading。
它有一个“前进一步”的动作,动作名由你自己取,每次让读数增加 2,并给出新的读数。
脚本里创建一个初始读数为 10 的对象,输出调用这个动作后的结果。
```
### TSL-078: 设备总数
```text
请写一份 .tsl 代码:
我需要一种“设备计数器”类型,类型名由你自己取,它有一个所有对象共享的 total_count。
脚本里把 total_count 设为 77,然后输出它。
```
### TSL-079: 汇率工具
```text
请写一份 .tsl 代码:
我需要一种“汇率计算”类型,类型名由你自己取,上面直接提供一个“倍率相乘”的功能。
它返回 a * b。
脚本里通过类型本身调用这个功能处理 4 和 5,并输出结果。
```
### TSL-080: 货架编码
```text
请写一份 .tsl 代码:
我需要一种“货架盒”对象,类型名由你自己取。
它内部有一个保存货架编码的字段,字段名由你自己取。
对外通过一个属性读写这个编码,属性名也由你自己取。
脚本里创建对象,把编码设为 "S-8",然后通过属性输出编码。
```
### TSL-081: 客户显示名
```text
请写一份 .tsl 代码:
我需要一种“客户名称”对象,类型名由你自己取。
它内部有一个 string 类型字段,用来保存原始姓名,字段名由你自己取。
对外通过一个带类型的属性访问显示姓名,属性名也由你自己取。
脚本里设置并输出这个显示姓名属性。
```
### TSL-082: 两种渲染方式
```text
请写一份 .tsl 代码:
我需要一种“格式化工具”对象,类型名由你自己取。
它有两个同名动作,动作名由你自己取:一个接收一个值,一个接收两个值。
脚本里分别调用这两种动作,并输出结果。
```
### TSL-083: 自行车对象
```text
请写一份 .tsl 代码:
我需要一种“交通工具基础对象”,类型名由你自己取。
它有一个说明类别的动作,动作名由你自己取,结果是 "vehicle"。
再做一种“自行车对象”,类型名也由你自己取,继承前面的基础对象,
并增加一个说明轮子数量的动作,动作名由你自己取,结果是 2。
脚本里创建自行车对象,输出类别和轮子数量。
```
### TSL-084: 折扣价格标签
```text
请写一份 .tsl 代码:
我需要一种“价格基础对象”,类型名由你自己取。
它有一个可被子类型改写的标签动作,动作名由你自己取,默认结果是 "base"。
再做一种“折扣价格对象”,类型名也由你自己取,把这个标签动作的结果改成 "discount"。
脚本里创建折扣价格对象并输出标签。
```
### TSL-085: 追加消息
```text
请写一份 .tsl 代码:
我需要一种“基础消息对象”,类型名由你自己取。
它有一个给出文字的动作,动作名由你自己取,结果是 "base"。
再做一种“提醒消息对象”,类型名也由你自己取。
它的文字动作要先调用父级同名动作,再追加 "-alert"。
脚本里输出提醒消息对象的文字。
```
### TSL-086: 类外补充方法
```text
请写一份 .tsl 代码:
我需要一种“票据打印器”,类型名由你自己取。
类型里面只先写出一个“打印编号”动作的名字,动作名由你自己取。
请在类型外面补上这个动作的内容,让它给出 "T-100"。
脚本里创建对象并输出结果。
```
### TSL-087: 按名称创建对象
```text
请写一份 .tsl 代码:
我需要一种“动态盒子”对象,类型名由你自己取。
它有一个给出数值的动作,动作名由你自己取,结果是 64。
脚本里请通过字符串里的类型名创建这个对象,并输出这个数值。
```
### TSL-088: 临时句柄清理
```text
请写一份 .tsl 代码:
我需要一种“临时句柄”对象,类型名由你自己取。
它被销毁时要输出 "released"。
脚本里创建它,然后把引用设为 nil。
```
### TSL-089: 外部评分函数
```text
请写一份 .tsl 代码:
有一个外部提供的“评分”功能,TSL 中使用的名字由你自己取,它接收一个值并给出结果。
这里只需要写出它和外部功能的连接写法,不需要调用。
```
### TSL-090: 外部日志动作
```text
请写一份 .tsl 代码:
有一个外部提供的“写日志”动作,TSL 中使用的名字由你自己取,它接收一段文本。
这里只需要写出它和外部动作的连接写法,不需要调用。
```
### TSL-091: 包装原生函数指针
```text
请写一份 .tsl 代码:
假设 native_ptr 已经保存了一个原生函数指针。
请按 TSL 支持的方式把它包装成可调用对象,并演示传入两个数字调用。
```
### TSL-092: 后台任务
```text
请写一份 .tsl 代码:
准备一个后台任务,名字由你自己取,里面输出 "work"。
请用线程相关写法启动它,并保持代码尽量小。
```
### TSL-093: FMArray 尺寸
```text
请写一份 .tsl 代码:
创建一个 2 行 3 列的 FMArray。
请输出它是不是 FMArray,并输出行数、列数和总尺寸。
```
### TSL-094: 矩阵大小
```text
请写一份 .tsl 代码:
准备一个两行三列的数字矩阵。
请输出它的行数、列数和总尺寸。
```
### TSL-095: 金额对象相加
```text
请写一份 .tsl 代码:
我需要一种“金额盒子”对象,类型名由你自己取,里面有 value。
请让两个这种对象可以直接相加,相加后得到新的同类对象。
脚本里创建两个对象,并输出相加后的 value。
```
### TSL-096: 区分局部和全局功能
```text
请写一份 .tsl 代码:
写一个局部功能,名字故意和某个系统功能一样。
脚本里请分别调用局部功能和全局/系统功能,并输出两次结果。
```
### TSL-097: 关闭默认写回
```text
请写一份 .tsl 代码:
关闭默认的输入写回行为。
请做一个“双值触碰”动作,名字由你自己取:普通输入不要写回,明确标记为 var 的输入要写回。
脚本调用后输出两个变量,证明只有 var 那个改变了。
```
### TSL-098: In 和 Out
```text
请写一份 .tsl 代码:
关闭默认的输入写回行为。
请做一个“同步几个值”的动作,名字由你自己取,接收三个值,并只让 out 标记的值写回。
脚本调用时使用 in 和 out 前缀,然后输出三个变量。
```
### TSL-099: 提前退出
```text
请写一份 .tsl 代码:
请做一个“保护性计算”的功能,名字由你自己取。
如果输入小于 0,就直接退出;否则给出输入乘以 2 的结果。
脚本里分别用 -1 和 6 调用它,并输出两次结果。
```
### TSL-100: 订单汇总
```text
请写一份 .tsl 代码:
有一批订单行,每行包含商品名、数量、单价。
请做一个“计算单行金额”的功能,名字由你自己取。
遍历所有订单行,数量为 0 的行跳过,其他行累计金额。
如果总金额大于 100,输出 "bulk",否则输出 "normal"。
最后输出总金额。
```
-29
View File
@@ -8,7 +8,6 @@ delegate rather than re-derive rules from that document.
import ast import ast
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@@ -18,11 +17,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = ROOT / "skills" / "commit-message" SKILL_ROOT = ROOT / "skills" / "commit-message"
VALIDATOR = SKILL_ROOT / "scripts" / "validate_commit_message.py" VALIDATOR = SKILL_ROOT / "scripts" / "validate_commit_message.py"
POLICY = SKILL_ROOT / "references" / "commit_policy.json"
CI_ENTRY = ROOT / ".gitea" / "ci" / "commit_message_lint.py" CI_ENTRY = ROOT / ".gitea" / "ci" / "commit_message_lint.py"
SPEC = ROOT / "docs" / "common" / "commit_message.md"
TABLE_ROW_RE = re.compile(r"^\|(?P<type>[^|]*)\|(?P<emoji>[^|]*)\|")
def run(script, *args, env=None): def run(script, *args, env=None):
@@ -37,31 +32,7 @@ def run(script, *args, env=None):
) )
def spec_type_emoji_mapping():
mapping = {}
for line in SPEC.read_text(encoding="utf-8").splitlines():
match = TABLE_ROW_RE.match(line.strip())
if not match:
continue
type_cell = re.search(r"`([a-z][a-z0-9-]*)`", match.group("type"))
emoji_cell = re.search(r"`(:[a-z0-9_+-]+:)`", match.group("emoji"))
if type_cell and emoji_cell:
mapping[type_cell.group(1)] = emoji_cell.group(1)
return mapping
class CommitPolicySingleOwnerTests(unittest.TestCase): class CommitPolicySingleOwnerTests(unittest.TestCase):
def test_policy_and_spec_table_agree(self):
policy_types = json.loads(POLICY.read_text(encoding="utf-8"))["types"]
spec_types = spec_type_emoji_mapping()
self.assertTrue(spec_types, f"no type/emoji table parsed from {SPEC}")
self.assertEqual(
policy_types,
spec_types,
"commit_policy.json and docs/common/commit_message.md disagree; "
"update both when adding or renaming a type",
)
def test_ci_entry_point_does_not_reimplement_rules(self): def test_ci_entry_point_does_not_reimplement_rules(self):
source = CI_ENTRY.read_text(encoding="utf-8") source = CI_ENTRY.read_text(encoding="utf-8")
self.assertIn("validate_commit_message.py", source) self.assertIn("validate_commit_message.py", source)
-745
View File
@@ -1,745 +0,0 @@
import argparse
import importlib.util
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATES = ROOT / "templates"
MAIN_LOOP_ROOT = ROOT / "skills" / "cook-it-through"
MAIN_LOOP_SKILL = MAIN_LOOP_ROOT / "SKILL.md"
MAIN_LOOP_SESSION_BOUNDARY = MAIN_LOOP_ROOT / "rules" / "session-boundary.md"
MAIN_LOOP_WORKFLOWS = {
name: MAIN_LOOP_ROOT / "workflows" / f"{name}.md"
for name in (
"single-session",
"feature-planning",
"ticket-execution",
"feature-integration",
)
}
MAIN_LOOP_SCRIPTS = MAIN_LOOP_ROOT / "scripts"
MAIN_LOOP_SCRIPT = MAIN_LOOP_SCRIPTS / "main_loop.py"
_MAIN_LOOP_SPEC = importlib.util.spec_from_file_location(
"playbook_main_loop_contracts", MAIN_LOOP_SCRIPT
)
assert _MAIN_LOOP_SPEC and _MAIN_LOOP_SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(_MAIN_LOOP_SPEC)
sys.modules[_MAIN_LOOP_SPEC.name] = MAIN_LOOP
_MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
def subcommand_parsers() -> dict[str, argparse.ArgumentParser]:
parser = MAIN_LOOP.build_parser()
for action in parser._subparsers._group_actions: # noqa: SLF001
if isinstance(action, argparse._SubParsersAction): # noqa: SLF001
return dict(action.choices)
raise AssertionError("main_loop.py exposes no subcommands")
def required_flags(parser: argparse.ArgumentParser) -> set[str]:
return {
option
for action in parser._actions # noqa: SLF001
if action.required
for option in action.option_strings
if option.startswith("--")
}
def option_action(
parser: argparse.ArgumentParser, option: str
) -> argparse.Action:
for action in parser._actions: # noqa: SLF001
if option in action.option_strings:
return action
raise AssertionError(f"{parser.prog} exposes no {option}")
def normalized_prose(text: str) -> str:
return " ".join(text.split())
def isolation_choices() -> set[str]:
claim = subcommand_parsers()["claim"]
for action in claim._actions: # noqa: SLF001
if "--isolation" in action.option_strings:
return set(action.choices or ())
raise AssertionError("claim exposes no --isolation choices")
def installed_skills() -> set[str]:
names: set[str] = set()
for base in (ROOT / "skills", ROOT / "skills" / "thirdparty"):
if not base.is_dir():
continue
for path in base.iterdir():
if path.is_dir() and (path / "SKILL.md").is_file():
names.add(path.name)
return names
def rules_text() -> str:
return (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
def main_loop_skill_text() -> str:
return MAIN_LOOP_SKILL.read_text(encoding="utf-8")
def main_loop_session_boundary_text() -> str:
return MAIN_LOOP_SESSION_BOUNDARY.read_text(encoding="utf-8")
def main_loop_workflow_text(name: str) -> str:
return MAIN_LOOP_WORKFLOWS[name].read_text(encoding="utf-8")
def main_loop_instruction_paths() -> tuple[Path, ...]:
return (
MAIN_LOOP_SKILL,
MAIN_LOOP_SESSION_BOUNDARY,
*MAIN_LOOP_WORKFLOWS.values(),
)
def main_loop_bundle_text() -> str:
return "\n".join(
path.read_text(encoding="utf-8") for path in main_loop_instruction_paths()
)
def section(text: str, heading: str, until: str) -> str:
return text.split(heading, 1)[1].split(until, 1)[0]
def headings(text: str) -> list[str]:
return re.findall(r"^#{2,3} (.+)$", text, re.MULTILINE)
# Tokens written in backticks that name something other than a skill. Machine
# state, CLI vocabulary and harness commands are derived from the implementation
# where possible so the allowlist stays small and reviewable.
NON_SKILL_BACKTICK_TOKENS = frozenset(
{
"base",
"claude",
"clear",
"codex",
"command",
"commit",
"compact",
"main",
"pass",
"pass-with-notes",
"passed",
"output",
"result",
"released",
"report",
"spec",
"standards",
}
)
LEGACY_FLOW_TERMS = (
"using-superpowers",
"brainstorming",
"writing-plans",
"executing-plans",
"docs/superpowers/",
"plan-status",
"旧开发流程目录",
"## 禁止旧流程",
"旧开发队列",
"单文件开发计划主链",
"active-context.md",
"progress.md",
"decisions.md",
)
class TemplateContractsTests(unittest.TestCase):
def test_templates_define_only_the_matt_ticket_workflow(self):
combined = "\n".join(
[
*(path.read_text(encoding="utf-8") for path in sorted(TEMPLATES.rglob("*.md"))),
main_loop_bundle_text(),
]
)
for required in (
"setup-matt-pocock-skills",
"grill-with-docs",
"to-spec",
"to-tickets",
".scratch/",
"main_loop.py claim",
"main_loop.py integrate",
):
self.assertIn(required, combined)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, combined)
def test_project_templates_drop_legacy_language_placeholders(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", templates_readme)
self.assertNotIn("{{LANGUAGE_1}}", templates_readme)
self.assertNotIn("docs/workflows/", templates_readme)
self.assertNotIn("templates/workflows/", templates_readme)
self.assertNotIn("docs/superpowers/", templates_readme)
self.assertNotIn(".scratch/<feature>/spec.md", templates_readme)
self.assertIn("`.scratch/<feature>/spec.md`", main_loop_bundle_text())
self.assertNotIn("docs/prompts/", templates_readme)
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
self.assertIn("AGENT_RULES.md", agents_template)
self.assertIn("memory-bank/project-brief.md", agents_template)
self.assertIn("memory-bank/tech-context.md", agents_template)
self.assertIn("memory-bank/system-patterns.md", agents_template)
self.assertIn(".scratch/queue.md", agents_template)
self.assertNotIn("docs/prompts/", agents_template)
tech_context_template = (
TEMPLATES / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_context_template)
self.assertIn("## 不可假设项", tech_context_template)
project_brief_template = (
TEMPLATES / "memory-bank" / "project-brief.template.md"
).read_text(encoding="utf-8")
self.assertIn("## 成功定义", project_brief_template)
def test_agents_template_nests_agents_block_inside_framework_block(self):
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
lines = [line.strip() for line in agents_template.splitlines()]
for marker in (
"<!-- playbook:framework:start -->",
"<!-- playbook:framework:end -->",
"<!-- playbook:agents:start -->",
"<!-- playbook:agents:end -->",
):
self.assertIn(marker, lines, msg=f"missing marker: {marker}")
framework_start = lines.index("<!-- playbook:framework:start -->")
framework_end = lines.index("<!-- playbook:framework:end -->")
agents_start = lines.index("<!-- playbook:agents:start -->")
agents_end = lines.index("<!-- playbook:agents:end -->")
nesting_reason = (
"sync_agents_template replaces the whole framework block; the agents "
"sub-block must stay inside it so preserve_agents_subblock can carry "
"the deployed language rules across the replacement"
)
self.assertLess(framework_start, agents_start, msg=nesting_reason)
self.assertLess(agents_start, agents_end, msg=nesting_reason)
self.assertLess(agents_end, framework_end, msg=nesting_reason)
def test_templates_readme_separates_ownership_from_deployment_config(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
classification = section(templates_readme, "## 模板分类", "## 模板说明")
categories = [h for h in headings(classification) if h[:2] in ("1.", "2.", "3.")]
self.assertEqual(len(categories), 3, msg=f"unexpected categories: {categories}")
self.assertIn("项目新增的 `memory-bank/*`", classification)
self.assertIn(
"playbook:rules:start/end",
classification,
msg="ownership of AGENT_RULES.md is split by the marker block; the "
"classification has to say which side the project owns",
)
self.assertNotIn("[sync_", classification)
self.assertNotIn("force", classification)
self.assertNotIn("no_backup", classification)
def test_templates_readme_separates_state_source_and_protocol_authority(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
normalized = normalized_prose(templates_readme)
self.assertIn("`.scratch/` 是唯一机器状态源", normalized)
self.assertIn(
"由第一方 `cook-it-through` Skill 权威定义",
normalized,
)
self.assertNotIn("preserve_agents_subblock()", templates_readme)
self.assertNotIn("四个入口按成本递增", templates_readme)
self.assertIsNone(
re.search(r"\*\*最后更新\*\*\d{4}-\d{2}-\d{2}", templates_readme),
msg="templates README must not carry a hand-maintained update date",
)
def test_templates_readme_documents_skill_exclusion_boundary(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
deployment = normalized_prose(
section(templates_readme, "## 部署", "## 正式开发流程")
)
layout = normalized_prose(
section(
templates_readme,
"## `playbook.py` 部署后结构",
"## 正式流程运行后按需产生的结构",
)
)
self.assertIn("启用了 `[sync_rules]`", deployment)
self.assertIn("安装集合必须包含 `cook-it-through`", deployment)
self.assertIn('`mode = "all"` 时不得通过 `exclude` 排除', deployment)
self.assertIn(
"只有不部署官方 `AGENT_RULES.md` 且不使用正式工程主链的安装场景,才可以排除该 skill",
deployment,
)
self.assertIn("同时启用 `[sync_rules]` 时,必须遵守上文", layout)
def test_memory_bank_contains_only_stable_project_knowledge(self):
memory_templates = {
path.name for path in (TEMPLATES / "memory-bank").glob("*.template.md")
}
self.assertEqual(
memory_templates,
{
"project-brief.template.md",
"tech-context.template.md",
"system-patterns.template.md",
},
)
boundary = normalized_prose(main_loop_session_boundary_text())
planning = main_loop_workflow_text("feature-planning")
execution = main_loop_workflow_text("ticket-execution")
for path in (
"memory-bank/project-brief.md",
"memory-bank/tech-context.md",
"memory-bank/system-patterns.md",
):
self.assertIn(path, boundary)
for required in (
"已经验证且可复现",
"重新发现成本高",
"不能从代码直接看出",
"下一 session 仍需要",
"关键取舍及理由写入 `docs/adr/`",
"`handoff` 产物写入 OS 临时目录",
):
self.assertIn(required, boundary)
self.assertIn("进入 `grill-with-docs` 前", planning)
self.assertIn("领取后实现前", execution)
def test_prompt_templates_are_not_part_of_the_workflow(self):
self.assertFalse(TEMPLATES.joinpath("prompts").exists())
def test_agent_rules_routes_main_loop_work_to_the_firstparty_skill(self):
rules = rules_text()
normalized = normalized_prose(rules)
self.assertIn("{{PLAYBOOK_ROOT}}", rules)
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules)
self.assertEqual(
headings(rules),
["优先级", "沟通", "项目边界", "工作流入口"],
msg="always-loaded rules must remain a thin workflow bootstrap",
)
self.assertIn("必须加载 `cook-it-through`", rules)
self.assertIn(
"或读取/修改 `.scratch` 中的 queue、ticket、heartbeat、integration 状态前",
normalized,
)
self.assertIn("该 skill 独占", rules)
self.assertIn("主循环执行引擎随该 skill 安装", normalized)
self.assertIn("`.agents/index.md`", rules)
self.assertNotIn("**Blocked by:**", rules)
self.assertNotIn("## 主循环命令", rules)
self.assertLessEqual(len(rules.splitlines()), 50)
self.assertLessEqual(len(rules.encode("utf-8")), 5_000)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, rules)
def test_cook_it_through_skill_owns_the_ticket_contract(self):
rules = rules_text()
bundle = main_loop_bundle_text()
planning = main_loop_workflow_text("feature-planning")
readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
skills_readme = (ROOT / "skills/README.md").read_text(encoding="utf-8")
self.assertFalse((ROOT / "docs/common/main-loop-ticket-contract.md").exists())
self.assertEqual(
{
path.relative_to(MAIN_LOOP_ROOT).as_posix()
for path in MAIN_LOOP_ROOT.rglob("*")
if path.is_file() and "__pycache__" not in path.parts
},
{
"SKILL.md",
"rules/session-boundary.md",
"workflows/single-session.md",
"workflows/feature-planning.md",
"workflows/ticket-execution.md",
"workflows/feature-integration.md",
"scripts/main_loop.py",
"scripts/main_loop_scheduler.py",
},
)
self.assertFalse((ROOT / "scripts/main_loop.py").exists())
self.assertFalse((ROOT / "scripts/main_loop_scheduler.py").exists())
for required in (
"`TicketId`qualified `feature-slug/NN`",
"`FeatureIntegrationId``feature-slug@integrated`",
"**Blocked by:** None",
"**Blocked by:** feature-a/01; feature-b@integrated",
"同批重复 `--feature`",
"hard cut",
):
self.assertIn(required, planning)
self.assertIn("第三方 `to-tickets` 只定义通用 tracker 行为", planning)
self.assertIn("最终机器校验边界", planning)
self.assertIn("手工修改 ticket `Status`", bundle)
self.assertIn("<COOK_IT_THROUGH_ROOT>/scripts/main_loop.py", bundle)
self.assertNotIn("<PLAYBOOK_SCRIPTS>", bundle)
self.assertIn("`cook-it-through`", rules)
self.assertNotIn("**Blocked by:**", rules)
self.assertIn("只在第一方 `skills/cook-it-through/` 定义", readme)
self.assertNotIn("main-loop-ticket-contract.md", readme)
self.assertNotIn("**Blocked by:** feature-a/01; feature-b@integrated", readme)
for public_readme in (readme, skills_readme):
self.assertNotIn("main_loop.py", public_readme)
self.assertNotIn("--isolation", readme)
def test_cook_it_through_uses_routed_progressive_disclosure(self):
skill = main_loop_skill_text()
bundle = main_loop_bundle_text()
description = next(
line for line in skill.splitlines() if line.startswith("description:")
)
self.assertLessEqual(len(skill.splitlines()), 85)
self.assertLessEqual(len(skill.encode("utf-8")), 8_000)
self.assertLessEqual(sum(len(p.read_text().splitlines()) for p in main_loop_instruction_paths()), 330)
self.assertIn("main_loop.py <command> --help", skill)
self.assertNotIn("```bash", bundle)
self.assertNotIn("入口 1", description)
for command in subcommand_parsers():
self.assertNotIn(command, description)
for negative_boundary in ("纯 TSL 语法/API 查询", "commit message", "远端 Gitea CI"):
self.assertIn(negative_boundary, description)
for path in main_loop_instruction_paths()[1:]:
relative = path.relative_to(MAIN_LOOP_ROOT).as_posix()
other_text = "\n".join(
candidate.read_text(encoding="utf-8")
for candidate in main_loop_instruction_paths()
if candidate != path
)
self.assertIn(relative, other_text, msg=f"unrouted instruction file: {relative}")
self.assertNotIn("FILL:", bundle)
def test_cook_it_through_keeps_irrecoverable_red_lines_resident(self):
skill = normalized_prose(main_loop_skill_text())
rules = rules_text()
for required in (
"禁止手工修改 ticket `Status`",
"禁止伪造或复用证据 artifact",
"integration dependency 不可见时禁止继续",
"禁止 stash、reset 或覆盖其他 session 改动",
"远程 tracker、独立 clone 或跨机器状态",
):
self.assertIn(required, skill)
for migrated_rule in (
"main-loop:ticket-state",
"integration frontier",
"远程 tracker、独立 clone、跨机器状态",
):
self.assertNotIn(migrated_rule, rules)
def test_gitignore_template_tracks_scratch_and_ignores_only_runtime(self):
template = (TEMPLATES / "gitignore.template").read_text(encoding="utf-8")
for durable_rule in ("!/.scratch/", "!/.scratch/**"):
self.assertIn(durable_rule, template)
for runtime_rule in (
"/.scratch/*.lock",
"/.scratch/worktrees/",
"/.scratch/**/*.tmp",
):
self.assertIn(runtime_rule, template)
self.assertNotIn("是否纳入版本控制由项目决定", rules_text())
def test_cook_it_through_routes_entries_and_blast_radius_floor(self):
skill = main_loop_skill_text()
entries = section(skill, "## 任务路由", "## 常驻红线")
entry_headings = [h for h in headings(entries) if h.startswith("入口 ")]
self.assertEqual(
entry_headings,
[
"入口 1:直接执行",
"入口 2:单切片改动",
"入口 3:已明确预期行为的 bug",
"入口 4:新 feature 或设计变更",
],
)
self.assertEqual(entries.count("**升级条件**"), 2)
self.assertIn("AGENT_RULES.local.md", entries)
self.assertIn("高爆炸半径路径", entries)
self.assertIn("构建、CI 或分发配置", entries)
self.assertIn("最低入口 2", entries)
self.assertIn("入口 1 不加载按需文件", entries)
entry_two = normalized_prose(section(entries, "### 入口 2", "### 入口 3"))
entry_four = normalized_prose(entries.split("### 入口 4", 1)[1])
self.assertIn("不属于入口 3", entry_two)
self.assertIn("入口 4", entry_two)
self.assertIn("边界不清时先按入口 2 起步", entry_four)
def test_cook_it_through_routes_single_session_work(self):
workflow = main_loop_workflow_text("single-session")
for required in (
"当前 `HEAD` 为 review fixed point",
"`<fixed-point>` 作为 `code-review` 的 fixed point",
"仅运行 Standards axis",
"`diagnosing-bugs` 完整执行 Phase 1-6",
"improve-codebase-architecture",
):
self.assertIn(required, workflow)
self.assertNotIn("`<fixed-point>...HEAD`", workflow)
bug = section(workflow, "## 入口 3", "## 完成与升级")
self.assertNotIn("grill-with-docs", bug)
def test_cook_it_through_routes_feature_planning_and_onramps(self):
planning = main_loop_workflow_text("feature-planning")
for required in (
"`wayfinder`",
"`to-spec -> to-tickets`",
"`research`",
"先进入 `grill-with-docs`",
"首次运行 `setup-matt-pocock-skills`",
"seam confirmation 在 `to-spec` 与 `tdd`",
"`tdd` 不得在未经确认的 seam 上开始",
):
self.assertIn(required, planning)
self.assertNotIn("`prototype`", planning)
def test_cook_it_through_commits_planning_baseline_before_claim(self):
planning = main_loop_workflow_text("feature-planning")
ordered_steps = (
"-> to-spec",
"-> to-tickets",
"-> main_loop.py enqueue",
"-> 提交 planning baseline",
"-> main_loop.py claim",
)
positions = [planning.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
for durable_input in (
"`.scratch/<feature>/spec.md`",
"`.scratch/<feature>/issues/*.md`",
"`.scratch/queue.md`",
):
self.assertIn(durable_input, planning)
self.assertIn("任何 claim 前", planning)
self.assertIn("不隐式提交", planning)
def test_cook_it_through_defines_unattended_fallback(self):
planning = main_loop_workflow_text("feature-planning")
execution = main_loop_workflow_text("ticket-execution")
self.assertIn("尚未 claim ticket 时", planning)
self.assertIn("to-questionnaire", planning)
self.assertIn(".scratch/questions/<slug>.md", planning)
self.assertIn("从 `grill-with-docs` 恢复", normalized_prose(planning))
self.assertIn("finish --result blocked", execution)
def test_cook_it_through_binds_state_and_evidence_to_claim(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
for key in (
"FEATURE",
"TICKET",
"CONTROL_ROOT",
"STATE_ROOT",
"WORKSPACE",
"BRANCH",
"BASE",
"ISOLATION",
):
self.assertIn(f"`{key}`", execution)
self.assertIn('--state-root "<PROJECT_ROOT>/.scratch"', execution)
self.assertIn('--repo-root "<PROJECT_ROOT>"', execution)
self.assertNotIn("--repo-root .", execution)
self.assertIn("把该 `FEATURE_HEAD` 合入 ticket branch", execution)
def test_cook_it_through_defines_ticket_execution_adapter(self):
execution = main_loop_workflow_text("ticket-execution")
ordered_steps = (
"读取已领取 ticket 的 spec",
"按 `tdd`",
"提交全部实现",
"运行 `code-review` 的 Standards/Spec",
"结构化证据调用 main_loop.py finish",
)
positions = [execution.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
for required in (
"fixed point",
"`<STATE_ROOT>/<feature>/spec.md`",
"`<STATE_ROOT>/<feature>/issues/<NN>-*.md`",
"零个未解决的硬 finding",
"不给 pass/fail 判定",
"不得填写 pass",
):
self.assertIn(required, execution)
def test_cook_it_through_defines_lease_and_stuck_ticket_recovery(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
for required in (
"固定为 30 分钟",
"每 10 分钟",
"`reclaim` 只接管 stale 的 `claimed`",
"release-ticket",
"claim 环境准备失败",
"会占住该 ticket",
"原 `BASE`",
"blocked/skipped 必须给 reason",
):
self.assertIn(required, execution)
def test_cook_it_through_stops_on_integration_visibility_retry(self):
execution = main_loop_workflow_text("ticket-execution")
for key in (
"TICKET",
"DEPENDENCY",
"INTEGRATION_COMMIT",
"WORKSPACE",
"BRANCH",
"BRANCH_HEAD",
"TICKET_BRANCH",
"TICKET_BRANCH_HEAD",
"SYNC_BRANCH",
"MAIN_BRANCH",
"MAIN_HEAD",
"SYNC_COMMAND",
):
self.assertIn(f"`{key}`", execution)
self.assertIn("任一字段缺失", execution)
self.assertIn("取得正式 assignment 前不得继续", execution)
def test_cook_it_through_requires_fresh_evidence_artifacts(self):
bundle = main_loop_bundle_text()
execution = main_loop_workflow_text("ticket-execution")
integration = main_loop_workflow_text("feature-integration")
for required in (
"fresh UTF-8 JSON artifact",
"finish --help",
".scratch/<feature>/evidence/",
"无法证明命令真的执行过",
):
self.assertIn(required, execution)
for required in ("三个独立门禁,不能互相替代", "integrate --help"):
self.assertIn(required, integration)
self.assertIn("禁止伪造或复用证据 artifact", bundle)
for required in ("output_sha256", "report_sha256"):
self.assertIn(required, MAIN_LOOP.EVIDENCE_HELP)
self.assertIn("--main-verified", required_flags(subcommand_parsers()["integrate"]))
def test_cook_it_through_commits_final_state_after_integration(self):
integration = main_loop_workflow_text("feature-integration")
self.assertLess(
integration.index("main_loop.py integrate"),
integration.index("提交 final workflow state"),
)
for durable_path in (
"`.scratch/<feature>/`",
"`.scratch/queue.md`",
"`.main-loop.json`",
):
self.assertIn(durable_path, integration)
self.assertIn("不要运行 `git add .scratch`", integration)
self.assertIn("不得 amend/squash", integration)
self.assertIn("`MAIN_INTEGRATION_COMMIT`", integration)
def test_cook_it_through_orders_phase_boundary_options_and_reload(self):
boundary = normalized_prose(main_loop_session_boundary_text())
ordered_options = (
"继续当前 session",
"使用 `clear`",
"使用 `handoff`",
"交给 subagent",
"使用 `compact`",
)
positions = [boundary.index(option) for option in ordered_options]
self.assertEqual(positions, sorted(positions))
for required in (
"下一阶段需要当前 session 作为 primary source",
"约 150k tokens",
"`handoff` 解决的是可移植性",
"重新加载 `SKILL.md` 与当前路由文件",
"不得只依据 `status` 输出继续",
"`domain-modeling`",
"`codebase-design` 只作词汇来源",
):
self.assertIn(required, boundary)
def test_cook_it_through_delegates_command_semantics_to_help(self):
skill = main_loop_skill_text()
bundle = main_loop_bundle_text()
parsers = subcommand_parsers()
self.assertIn("main_loop.py <command> --help", skill)
self.assertIsNone(
re.search(r"\|\s*`?main_loop\.py (?:enqueue|status|claim|finish)", bundle),
msg="command responsibility tables duplicate argparse help",
)
for command, parser in parsers.items():
self.assertIn(f"main_loop.py {command}", bundle)
self.assertTrue(parser.description, msg=f"thin help for {command}")
for flag in required_flags(parser):
action = option_action(parser, flag)
self.assertNotIn(action.help, (None, argparse.SUPPRESS))
documented = set(re.findall(r"main_loop\.py ([a-z][a-z-]*)", bundle))
self.assertEqual(documented - set(parsers), set())
def test_cook_it_through_documents_executable_state_transitions(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
finish = subcommand_parsers()["finish"]
result = option_action(finish, "--result")
self.assertEqual(
set(result.choices or ()),
{"resolved", "blocked", "released", "skipped"},
)
self.assertIn("blocked/skipped 必须给 reason", execution)
release_ticket = subcommand_parsers()["release-ticket"]
release_options = {
option
for action in release_ticket._actions # noqa: SLF001
for option in action.option_strings
}
self.assertNotIn("--repo-root", release_options)
self.assertNotIn("--owner", release_options)
def test_workflow_instructions_only_reference_installed_skills(self):
instructions = "\n".join((rules_text(), main_loop_bundle_text()))
skills = installed_skills()
machine_vocabulary = (
set(subcommand_parsers())
| set(MAIN_LOOP.ALLOWED_STATUSES)
| isolation_choices()
| NON_SKILL_BACKTICK_TOKENS
)
referenced = {
token
for token in re.findall(r"`([a-z][a-z0-9-]+)`", instructions)
if token not in machine_vocabulary
}
self.assertTrue(referenced)
self.assertEqual(sorted(referenced - skills), [])
def test_cook_it_through_limits_state_to_shared_local_markdown(self):
skill = normalized_prose(main_loop_skill_text())
self.assertIn("local Markdown tracker", skill)
self.assertIn("远程 tracker、独立 clone 或跨机器状态", skill)
self.assertNotIn("必须改用具备远程", skill)
if __name__ == "__main__":
unittest.main()
-8
View File
@@ -15,7 +15,6 @@ TSL_SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml"
LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml" LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml"
UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh" UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh"
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh" SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
SKILLS_MD = ROOT / "SKILLS.md"
def load_manifest() -> dict: def load_manifest() -> dict:
@@ -235,13 +234,6 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
self.assertNotIn("COMMIT_AUTHOR_EMAIL", text) self.assertNotIn("COMMIT_AUTHOR_EMAIL", text)
self.assertNotIn("@local", text) self.assertNotIn("@local", text)
def test_skills_doc_points_to_generic_thirdparty_sources(self):
text = SKILLS_MD.read_text(encoding="utf-8")
# Check that third-party skills section exists (without enforcing exact heading format)
self.assertIn("thirdparty", text.lower())
self.assertIn("skills/thirdparty/", text)
self.assertNotIn("Third-party Skills (superpowers)", text)
def test_manifest_declares_unique_thirdparty_source_lists(self): def test_manifest_declares_unique_thirdparty_source_lists(self):
source_lists = [entry["source_list"] for entry in load_manifest()["sources"]] source_lists = [entry["source_list"] for entry in load_manifest()["sources"]]
self.assertEqual(len(source_lists), len(set(source_lists))) self.assertEqual(len(source_lists), len(set(source_lists)))
-647
View File
@@ -1,647 +0,0 @@
import importlib.util
import re
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = ROOT / "skills" / "tsl-syntax-reference"
LOOKUP_PATH = SKILL_ROOT / "scripts" / "lookup.py"
SKILL_PATH = SKILL_ROOT / "SKILL.md"
CI_PATH = ROOT / ".gitea" / "workflows" / "checks.yml"
PREPARE_PATH = ROOT / ".gitea" / "workflows" / "prepare.yml"
NODE_VERSION_PATH = ROOT / ".node-version"
def load_lookup_module():
spec = importlib.util.spec_from_file_location("tsl_syntax_lookup", LOOKUP_PATH)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {LOOKUP_PATH}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
lookup = load_lookup_module()
def write_reference(directory: Path, name: str, content: str) -> Path:
page = directory / name
page.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8", newline="\n")
return page
def run_lookup(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(LOOKUP_PATH), *args],
cwd=ROOT,
capture_output=True,
text=True,
)
class TslSyntaxReferenceTests(unittest.TestCase):
def test_bundled_references_pass_structural_check(self):
self.assertEqual([], lookup.validate_references())
def test_bundled_sections_use_unique_explicit_ids(self):
sections = lookup.load_sections()
ids = [section.id for section in sections]
self.assertEqual(len(ids), len(set(ids)))
self.assertTrue(ids)
for section_id in ids:
with self.subTest(section_id=section_id):
self.assertRegex(section_id, r"^syntax-\d{2}-\d{3}$")
def test_explicit_section_id_survives_heading_rename(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
page = write_reference(
references,
"99_fixture.md",
"""
# Fixture
## 本篇职责
<!-- section-id: syntax-99-001 -->
测试职责
## 原标题
<!-- section-id: syntax-99-002 -->
事实正文
""",
)
before = lookup.load_sections(references)[1].id
page.write_text(
page.read_text(encoding="utf-8").replace("## 原标题", "## 新标题"),
encoding="utf-8",
newline="\n",
)
after = lookup.load_sections(references)[1].id
self.assertEqual("syntax-99-002", before)
self.assertEqual(before, after)
def test_missing_explicit_section_id_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"99_fixture.md",
"""
# Fixture
## 本篇职责
测试职责
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(any("缺少显式 section ID" in message for message in messages))
def test_duplicate_explicit_section_id_fails_check_and_loading(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"99_fixture.md",
"""
# Fixture
## 本篇职责
<!-- section-id: syntax-99-001 -->
测试职责
## 重复 ID
<!-- section-id: syntax-99-001 -->
事实正文
""",
)
messages = [item.message for item in lookup.validate_references(references)]
with self.assertRaisesRegex(lookup.ReferenceStructureError, "重复 section ID"):
lookup.load_sections(references)
self.assertTrue(any("重复 section ID" in message for message in messages))
def test_orphan_identity_line_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"99_fixture.md",
"""
# Fixture
## 本篇职责
<!-- section-id: syntax-99-001 -->
测试职责
## 示例
<!-- section-id: syntax-99-002 -->
代码块身份可直接照写示例
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(any("孤立的代码块身份" in message for message in messages))
def test_h5_and_h6_headings_fail_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"99_fixture.md",
"""
# Fixture
## 本篇职责
<!-- section-id: syntax-99-001 -->
测试职责
##### 不会被索引的标题
隐藏事实
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(any("H5/H6" in message for message in messages))
def test_quickstart_rule_drift_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"01_quickstart.md",
"""
# Quickstart
## 本篇职责
<!-- section-id: syntax-01-001 -->
派生摘要
## 语言核心事实速查
<!-- section-id: syntax-01-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `=`
""",
)
write_reference(
references,
"02_topic.md",
"""
# Topic
## 本篇职责
<!-- section-id: syntax-02-001 -->
完整事实源
## 核心规则
<!-- section-id: syntax-02-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `:=`
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(
any("派生摘要规则与专题事实不一致" in message for message in messages)
)
def test_complete_quickstart_contract_detects_rule_deleted_from_both_pages(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
quickstart = write_reference(
references,
"01_quickstart.md",
"""
# Quickstart
## 本篇职责
<!-- section-id: syntax-01-001 -->
派生摘要
## 语言核心事实速查
<!-- section-id: syntax-01-002 -->
没有任何规则
""",
)
pages = {quickstart: quickstart.read_text(encoding="utf-8").splitlines()}
messages = [
item.message
for item in lookup._quickstart_rule_problems(
pages,
require_complete=True,
)
]
self.assertTrue(any("assignment" in message for message in messages))
def test_quickstart_owner_drift_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"01_quickstart.md",
"""
# Quickstart
## 本篇职责
<!-- section-id: syntax-01-001 -->
派生摘要
## 语言核心事实速查
<!-- section-id: syntax-01-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `:=`
Owner Section`syntax-02-999`
""",
)
write_reference(
references,
"02_topic.md",
"""
# Topic
## 本篇职责
<!-- section-id: syntax-02-001 -->
完整事实源
## 核心规则
<!-- section-id: syntax-02-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `:=`
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(any("Owner Section 不存在" in message for message in messages))
def test_quickstart_routes_every_derived_rule_to_its_owner_section(self):
quickstart = run_lookup("--section", "syntax-01-002")
self.assertEqual(0, quickstart.returncode, quickstart.stderr)
owner_ids = set(
re.findall(r"Owner Section`(syntax-\d{2}-\d{3})`", quickstart.stdout)
)
self.assertEqual(
{
"syntax-02-002",
"syntax-03-002",
"syntax-03-004",
"syntax-05-002",
"syntax-06-004",
"syntax-08-002",
"syntax-09-002",
},
owner_ids,
)
for owner_id in owner_ids:
with self.subTest(owner_id=owner_id):
owner = run_lookup("--section", owner_id)
self.assertEqual(0, owner.returncode, owner.stderr)
def test_oversized_leaf_section_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
long_body = "\n".join(f"事实行 {index}" for index in range(181))
write_reference(
references,
"99_fixture.md",
"# Fixture\n\n"
"## 本篇职责\n\n"
"<!-- section-id: syntax-99-001 -->\n\n"
"测试职责。\n\n"
"## 过长事实段\n\n"
"<!-- section-id: syntax-99-002 -->\n\n"
f"{long_body}\n",
)
result = run_lookup(
"--check",
"--references-dir",
str(references),
)
self.assertEqual(1, result.returncode)
self.assertIn("叶子 Section 正文超过 180 行", result.stderr)
def test_missing_references_are_installation_errors_for_every_action(self):
with tempfile.TemporaryDirectory() as temp_dir:
missing = Path(temp_dir) / "missing"
actions = (
("--map",),
("--query", "数组下标", "--mode", "explain"),
("--section", "syntax-03-002"),
("--check",),
)
for action in actions:
with self.subTest(action=action):
result = run_lookup(
*action,
"--references-dir",
str(missing),
)
self.assertEqual(1, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("参考目录不可用", result.stderr)
def test_documented_array_index_query_ranks_basic_array_section_first(self):
for query in ("数组下标", "请帮我解释数组的下标"):
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertEqual("03_values_and_literals.md", result.matches[0].section.page.name)
self.assertEqual("核心规则", result.matches[0].section.heading_path[-1])
def test_documented_write_and_diagnose_queries_rank_expected_sections(self):
write_result = lookup.query_sections("命名参数", "write", limit=1)
diagnose_result = lookup.query_sections(
"invalid statement 声明区",
"diagnose",
limit=1,
)
self.assertEqual(
["syntax-02-002", "syntax-01-002"],
[section.id for section in write_result.prelude],
)
self.assertEqual("syntax-05-008", write_result.matches[0].section.id)
self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id)
def test_object_and_class_queries_return_focused_sections(self):
cases = {
"成员访问可见性": "syntax-08-013",
"类外实现": "syntax-08-014",
"固定索引 property": "syntax-08-015",
"参数化 property": "syntax-08-016",
"方法隐藏 hide": "syntax-08-017",
"调用父类 inherited": "syntax-08-018",
"析构 destroy": "syntax-08-019",
}
for query, expected_id in cases.items():
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertTrue(result.matches, query)
self.assertEqual(expected_id, result.matches[0].section.id)
section = run_lookup("--section", expected_id)
self.assertEqual(0, section.returncode, section.stderr)
self.assertLessEqual(len(section.stdout.splitlines()), 186)
def test_external_call_queries_retrieve_platform_and_abi_boundaries(self):
cases = {
"动态库常驻": "syntax-17-012",
"外部出参": "syntax-17-013",
"Linux 动态加载": "syntax-17-014",
"函数指针释放": "syntax-17-008",
}
for query, expected_id in cases.items():
with self.subTest(query=query):
result = lookup.query_sections(query, "write", limit=1)
self.assertEqual(expected_id, result.matches[0].section.id)
def test_index_origin_variants_rank_value_rules_first(self):
for query in ("下标从几开始", "下标是从几开始", "请问下标是从几开始"):
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertEqual("03_values_and_literals.md", result.matches[0].section.page.name)
self.assertEqual("核心规则", result.matches[0].section.heading_path[-1])
def test_string_literal_queries_rank_generation_policy_first(self):
for query in ("普通中文字符串", "原始字符串 %%", "字符串 U L 前缀"):
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertEqual("syntax-03-004", result.matches[0].section.id)
def test_string_literal_generation_policy_is_in_quickstart_and_detail(self):
quickstart = run_lookup("--section", "syntax-01-002")
detail = run_lookup("--section", "syntax-03-004")
self.assertEqual(0, quickstart.returncode, msg=quickstart.stderr)
self.assertEqual(0, detail.returncode, msg=detail.stderr)
for output in (quickstart.stdout, detail.stdout):
self.assertIn("普通单行文本默认使用", output)
self.assertIn("不得因为内容是中文、非 ASCII 或较长", output)
self.assertIn("不能因为内容是中文就自动添加", output)
self.assertIn('普通中文内容优先直接写成 `"中文内容"`', output)
def test_every_curated_page_alias_still_ranks_its_page_first(self):
for page_name, aliases in lookup.PAGE_INTENT_ALIASES.items():
for alias in aliases:
with self.subTest(page=page_name, alias=alias):
result = lookup.query_sections(alias, "explain", limit=1)
self.assertTrue(result.matches, alias)
self.assertEqual(page_name, result.matches[0].section.page.name)
def test_weak_only_query_remains_a_no_match(self):
result = run_lookup("--query", "只能确认", "--mode", "explain")
self.assertEqual(2, result.returncode)
self.assertIn("only weak candidates", result.stderr)
def test_strong_candidates_are_ranked_before_weak_diagnose_boosts(self):
result = lookup.query_sections("变量赋值", "diagnose", limit=1)
self.assertTrue(result.matches)
self.assertFalse(result.matches[0].weak)
self.assertNotEqual("syntax-04-008", result.matches[0].section.id)
def test_page_intent_aliases_score_once_per_page(self):
result = lookup.query_sections("高性能矩阵 fmarray", "explain", limit=3)
self.assertTrue(result.matches)
self.assertEqual("syntax-22-004", result.matches[0].section.id)
for match in result.matches:
self.assertNotIn("intent=160", match.reasons)
self.assertIn("intent=80", result.matches[0].reasons)
def test_program_is_a_searchable_language_keyword(self):
result = lookup.query_sections("PROGRAM", "explain", limit=1)
self.assertTrue(result.matches)
self.assertEqual("syntax-02-009", result.matches[0].section.id)
def test_maintenance_html_comments_do_not_leak_into_lookup_output(self):
result = run_lookup("--query", "PROGRAM", "--mode", "explain")
self.assertEqual(0, result.returncode)
self.assertNotIn("prettier-ignore", result.stdout)
def test_new_language_gaps_have_stable_retrieval_entries(self):
cases = {
"全局变量": "syntax-04-009",
"运行时常量": "syntax-04-010",
"静态计算": "syntax-06-016",
"只计算一次": "syntax-06-016",
"指定系统函数": "syntax-05-013",
"运算符优先级": "syntax-06-015",
"内存上限": "syntax-14-011",
"嵌套注释": "syntax-15-005",
"JOIN 第二张表当前行": "syntax-13-010",
"按行广播": "syntax-11-011",
}
for query, expected_id in cases.items():
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertTrue(result.matches, query)
self.assertEqual(expected_id, result.matches[0].section.id)
def test_batch_section_retrieval_is_atomic(self):
ids = [section.id for section in lookup.load_sections()[:2]]
success = run_lookup("--section", *ids)
failure = run_lookup("--section", ids[0], "syntax-99-999")
self.assertEqual(0, success.returncode)
self.assertTrue(all(section_id in success.stdout for section_id in ids))
self.assertEqual(2, failure.returncode)
self.assertEqual("", failure.stdout)
self.assertIn("section not found: syntax-99-999", failure.stderr)
def test_structural_metadata_is_not_exposed_in_lookup_output(self):
sections = {section.id: section for section in lookup.load_sections()}
candidates = lookup.render_candidates(
lookup.query_sections("命名参数", "write", limit=1)
)
quickstart = lookup.render_section(sections["syntax-01-002"])
for output in (candidates, quickstart):
with self.subTest(output=output[:40]):
self.assertNotIn("<!-- section-id:", output)
self.assertNotIn("<!-- quickstart-rule:", output)
def test_skill_contract_uses_extracted_queries_and_scope_checks(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
help_result = run_lookup("--help")
self.assertEqual(0, help_result.returncode, help_result.stderr)
help_text = help_result.stdout
self.assertNotIn("## 构造查询词", skill)
self.assertNotIn("用户怎么说就怎么传", skill)
self.assertIn("从用户原话提取", help_text)
self.assertIn("保留原写法但不传完整用户句", help_text)
self.assertIn("不传完整用户句", help_text)
self.assertIn("代码交付只要依赖这些未验证事实", skill)
self.assertIn("不声称签名、行为、可用性或输出", skill)
self.assertIn("纯语法说明", skill)
self.assertIn("按「缺口时停止」处理", skill)
def test_help_owns_cli_details_and_skill_defers_to_it(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
result = run_lookup("--help")
self.assertEqual(0, result.returncode, result.stderr)
for text in (
"一次 --query 只覆盖一个语法要素",
"Required: yes",
"Owner Section",
"混合候选",
"180 行粒度上限",
"退出码",
):
with self.subTest(text=text):
self.assertIn(text, result.stdout)
self.assertIn("构造任一命令前先运行", skill)
self.assertIn("scripts/lookup.py --help", skill)
self.assertIn("不用于查询 API 签名", skill)
self.assertIn("不用于判断解释器、平台或运行方式", skill)
def test_skill_is_self_contained_and_does_not_reference_host_rules(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
for forbidden in (
"AGENTS.md",
"AGENT_RULES",
".agents/",
"CONTEXT.md",
"memory-bank",
"docs/",
"tsl-api-reference",
"data/README.md",
"项目脚本",
"项目文档",
"CI",
):
with self.subTest(forbidden=forbidden):
self.assertNotIn(forbidden, skill)
self.assertNotRegex(skill, r"\[[^\]]+\]\([^)]*\.md[^)]*\)")
self.assertEqual(
{"SKILL.md"},
set(re.findall(r"[A-Za-z0-9_./-]+\.md", skill)),
)
def test_ci_runs_syntax_structure_and_format_gates(self):
workflow = CI_PATH.read_text(encoding="utf-8")
prepare = PREPARE_PATH.read_text(encoding="utf-8")
self.assertIn(
"npm ci --ignore-scripts --no-audit --no-fund --no-bin-links",
workflow,
)
self.assertIn("skills/tsl-syntax-reference/scripts/lookup.py --check", workflow)
self.assertIn(
"node_modules/prettier/bin/prettier.cjs --check skills/tsl-syntax-reference",
workflow,
)
self.assertIn("[unzip]=\"unzip\"", prepare)
self.assertIn('FNM_BIN: "/data/bin/fnm"', prepare)
self.assertIn('FNM_DIR: "/data/fnm"', prepare)
self.assertIn('"$FNM_BIN" install "$node_version"', prepare)
self.assertNotIn('[node]="nodejs"', prepare)
self.assertNotIn('[npm]="npm"', prepare)
self.assertIn('"$FNM_BIN" use --install-if-missing "$node_version"', workflow)
self.assertIn("node --version", workflow)
self.assertRegex(
NODE_VERSION_PATH.read_text(encoding="utf-8").strip(),
r"^\d+\.\d+\.\d+$",
)
if __name__ == "__main__":
unittest.main()
@@ -602,6 +602,11 @@ def _markdown_cell(value: str) -> str:
return value.replace("|", "\\|").replace("\n", "<br>") return value.replace("|", "\\|").replace("\n", "<br>")
def _render_note(note: str) -> str:
"""Keep source numbering as text instead of creating a nested list."""
return re.sub(r"^(\s*\d+)[.]", r"\1\\.", note)
def render_dictionary_page(page: DictionaryPage) -> str: def render_dictionary_page(page: DictionaryPage) -> str:
path = " / ".join((*page.source_path, page.title)) path = " / ".join((*page.source_path, page.title))
lines = [f"# 天软数据字典 / {path}", "", f"类型:{page.kind}", "", "## 表信息", ""] lines = [f"# 天软数据字典 / {path}", "", f"类型:{page.kind}", "", "## 表信息", ""]
@@ -632,7 +637,7 @@ def render_dictionary_page(page: DictionaryPage) -> str:
if page.notes or page.table.update_info: if page.notes or page.table.update_info:
lines.extend(["", "## 数据说明", ""]) lines.extend(["", "## 数据说明", ""])
for note in (*page.notes, *page.table.update_info): for note in (*page.notes, *page.table.update_info):
lines.append(f"- {note}") lines.append(f"- {_render_note(note)}")
if page.examples: if page.examples:
lines.extend(["", "## 取数示例", ""]) lines.extend(["", "## 取数示例", ""])
for index, example in enumerate(page.examples, start=1): for index, example in enumerate(page.examples, start=1):
+43
View File
@@ -1,3 +1,4 @@
import importlib.util
import json import json
import subprocess import subprocess
import sys import sys
@@ -11,6 +12,8 @@ try:
except ImportError: except ImportError:
yaml = None yaml = None
BS4_AVAILABLE = importlib.util.find_spec("bs4") is not None
TOOL_ROOT = Path(__file__).resolve().parents[1] TOOL_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = TOOL_ROOT.parents[1] REPO_ROOT = TOOL_ROOT.parents[1]
@@ -18,6 +21,7 @@ CONVERT = TOOL_ROOT / "scripts" / "convert_tsf.py"
GENERATE = TOOL_ROOT / "scripts" / "generate.py" GENERATE = TOOL_ROOT / "scripts" / "generate.py"
LINT = TOOL_ROOT / "scripts" / "lint.py" LINT = TOOL_ROOT / "scripts" / "lint.py"
BUILD_INDEX = TOOL_ROOT / "scripts" / "build_index.py" BUILD_INDEX = TOOL_ROOT / "scripts" / "build_index.py"
BUILD_DICTIONARY = TOOL_ROOT / "scripts" / "build_dictionary.py"
LOOKUP = REPO_ROOT / "skills" / "tsl-api-reference" / "scripts" / "lookup.py" LOOKUP = REPO_ROOT / "skills" / "tsl-api-reference" / "scripts" / "lookup.py"
STANDARD = TOOL_ROOT / "STANDARD.md" STANDARD = TOOL_ROOT / "STANDARD.md"
README = TOOL_ROOT / "README.md" README = TOOL_ROOT / "README.md"
@@ -29,6 +33,18 @@ API_WORKFLOW = (
) )
def load_build_dictionary():
spec = importlib.util.spec_from_file_location(
"tsl_codegen_build_dictionary", BUILD_DICTIONARY
)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {BUILD_DICTIONARY}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class UnifiedPipelineTest(unittest.TestCase): class UnifiedPipelineTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory() self.temp_dir = tempfile.TemporaryDirectory()
@@ -38,6 +54,33 @@ class UnifiedPipelineTest(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.temp_dir.cleanup() self.temp_dir.cleanup()
@unittest.skipUnless(BS4_AVAILABLE, "beautifulsoup4 is not installed")
def test_dictionary_notes_escape_source_numbering(self):
module = load_build_dictionary()
page = module.DictionaryPage(
kind="table",
scope="macro",
title="示例",
source_path=("宏观",),
source_file="example.html",
table=module.DictionaryTable(
name="示例",
table_id="1",
extract_method="",
access_code="",
update_info=("1、更新频率:月度",),
),
fields=(),
notes=("1. 第一条说明", "2. 第二条说明"),
examples=(),
)
rendered = module.render_dictionary_page(page)
self.assertIn("- 1\\. 第一条说明", rendered)
self.assertIn("- 2\\. 第二条说明", rendered)
self.assertIn("- 1、更新频率:月度", rendered)
def run_command(self, *args, cwd=None): def run_command(self, *args, cwd=None):
return subprocess.run( return subprocess.run(
[sys.executable, *map(str, args)], [sys.executable, *map(str, args)],