♻️ refactor(tsl-api): tighten docs and retire stale tests
This commit is contained in:
@@ -28,9 +28,6 @@ docs/superpowers/
|
||||
# Local agent instructions
|
||||
/AGENTS.md
|
||||
|
||||
# Agent 测试结果
|
||||
test/agent/result/
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ node_modules
|
||||
tmp
|
||||
reports
|
||||
skills/thirdparty
|
||||
/AGENTS.md
|
||||
|
||||
@@ -18,6 +18,11 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class
|
||||
[`workflows/class-and-framework.md`](workflows/class-and-framework.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` 为权威。
|
||||
将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
|
||||
|
||||
@@ -59,7 +59,7 @@ return ContinuedMatchOfNDay(@close(), 3);
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------- |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp_1` | string | 字符串。统计表达式1 |
|
||||
| `exp_2` | string | 字符串。统计表达式2 |
|
||||
| `n` | int | 整数。天数 |
|
||||
@@ -165,7 +165,7 @@ N日线性衰减加权平均,与系统参数(股票,日期,周期,复权
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------ |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp` | string | 字符串。统计表达式 |
|
||||
| `n` | int | 整数。天数 |
|
||||
| `cache_str` | string | 可选。字符串,缓存串 |
|
||||
@@ -294,7 +294,7 @@ N日最高点到当前的周期数。N日表示市场交易日,当日最低为
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------ |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp` | string | 字符串。统计表达式 |
|
||||
| `n` | int | 整数。天数 |
|
||||
| `cache_str` | string | 可选。字符串,缓存串 |
|
||||
@@ -423,7 +423,7 @@ N日最低点到当前的周期数。N日表示市场交易日,当日最低为
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------ |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp` | string | 字符串。统计表达式 |
|
||||
| `n` | int | 整数。天数 |
|
||||
| `cache_str` | string | 可选。字符串,缓存串 |
|
||||
@@ -525,7 +525,7 @@ return OccuredOfNDay(@close(), 10);
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------ |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp` | string | 字符串。统计表达式 |
|
||||
| `n` | int | 整数。天数 |
|
||||
| `cache_str` | string | 可选。字符串,缓存串 |
|
||||
@@ -551,7 +551,7 @@ return PercentRankOfNDay(@close(), 10, "");
|
||||
<!-- tags: 数据仓库 向导函数 N 日 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------ |
|
||||
| ----------- | ------ | -------------------- |
|
||||
| `exp` | string | 字符串。统计表达式 |
|
||||
| `n` | int | 整数。天数 |
|
||||
| `cache_str` | string | 可选。字符串,缓存串 |
|
||||
|
||||
+2
-2
@@ -131,7 +131,7 @@ Return BondBasisPointValue(Endt, MV);
|
||||
<!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------------- | -------- | -------- |
|
||||
| ----------------- | -------- | ---------------------------------- |
|
||||
| `settlement_date` | datetime | 结算日期 |
|
||||
| `option` | int | 可选。是否行权,默认 0(持有到期) |
|
||||
|
||||
@@ -234,7 +234,7 @@ return bondMacaulayDuration(endt); // 麦考利久期,返回3.12
|
||||
<!-- tags: 金融 债券 债券敏感性指标 返回 获取 TS -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---------------- | -------- | -------- |
|
||||
| ---------------- | -------- | ---------------------------------- |
|
||||
| `settlementdate` | datetime | 结算日 |
|
||||
| `option` | int | 可选。是否行权,默认 0(持有到期) |
|
||||
|
||||
|
||||
@@ -994,7 +994,7 @@ return FundZjRatio(endt);
|
||||
<!-- tags: 金融 基金 基金收益率 分级基金 折价率 返回 获取 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ----------- | -------- | ------------------------------------------- |
|
||||
| ----------- | -------- | ------------------------------------------ |
|
||||
| `end_t` | datetime | 截止日期 |
|
||||
| `fund_type` | integer | 基金类别;原始文档示例中 `2` 表示分级 B 类 |
|
||||
|
||||
|
||||
+1
-1
@@ -196,7 +196,7 @@ return op_hv("SZ000002", 20210101T, -6);
|
||||
<!-- tags: 金融 期权 基本函数 隐含波动率 Greeks Delta Gamma Theta Vega Rho -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------------- | ------- | ---------------------------------------------------- |
|
||||
| --------------- | ------- | --------------------------------------------------- |
|
||||
| `option_type` | integer | 期权类型,0 为看涨期权,1 为看跌期权 |
|
||||
| `s` | real | 现货价格 |
|
||||
| `x` | real | 行权价 |
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ return bondFutureBasicindicators2(endt, -1, 1, 0);
|
||||
<!-- tags: 金融 专题函数 衍生品分析 期货专项 常用国债期货基本指标 基于债券估值 返回 获取 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------------- | ----------- | ----------------------------- |
|
||||
| --------------- | ----------- | ---------------------------------- |
|
||||
| `end_t` | date | 日期,截止日 |
|
||||
| `bond_id` | string | 字符串,债券代码 |
|
||||
| `return_option` | user_define | 用户自定义, 返回结果选项 |
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<!-- tags: 数学算法 压缩估计 均值 -->
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| -------- | ------ | ------------------------------------------------------------------------------- |
|
||||
| -------- | ------ | -------------------------------------------------------------------------- |
|
||||
| `data` | array | 二维数据表,T×N,T 期样本数量,N 个股票 |
|
||||
| `b_type` | string | 目标均值向量:b0 零向量、b1 均值、b2 波动率加权均值、b3 样本均值的均方误差 |
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
|
||||
- 农产品生产者价格表统计的是农产品价格的变化情况。
|
||||
- 区别于农产品生产者价格(简表):
|
||||
- 1. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度;
|
||||
- 2. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据;
|
||||
- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
|
||||
- 4. 简表数据公布会早于此表;
|
||||
- 5. 没有简表中的累计同比数据。
|
||||
- 1\. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度;
|
||||
- 2\. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据;
|
||||
- 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
|
||||
- 4\. 简表数据公布会早于此表;
|
||||
- 5\. 没有简表中的累计同比数据。
|
||||
- 1、数据开始时间:2003年一季度
|
||||
- 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数据,累计数据通过当季数据相加得到。
|
||||
- 此表为核算后数据。
|
||||
- 该表与国内生产总值(简表)区别:
|
||||
- 1. 数据获取历史周期更长,数据开始时间为1992年一季度;
|
||||
- 2.包含数据类别更多,统计项目更多;
|
||||
- 3.本表为核算调整后的数据;
|
||||
- 1\. 数据获取历史周期更长,数据开始时间为1992年一季度;
|
||||
- 2\.包含数据类别更多,统计项目更多;
|
||||
- 3\.本表为核算调整后的数据;
|
||||
- 1、数据开始时间:1992年一季度
|
||||
- 2、数据更新频率:季度
|
||||
|
||||
|
||||
@@ -211,10 +211,10 @@
|
||||
|
||||
- 居民消费价格主要介绍的是居民消费价格分类及其指数的变化情况。
|
||||
- 区别于居民消费价格(简表);
|
||||
- 1. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月;
|
||||
- 2. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。
|
||||
- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
|
||||
- 4. 简表数据公布会早于此表;
|
||||
- 1\. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月;
|
||||
- 2\. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。
|
||||
- 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期;
|
||||
- 4\. 简表数据公布会早于此表;
|
||||
- 1、数据开始时间:1987年一月
|
||||
- 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` 均已在索引中核对存在。
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
- 键必须与 `references/` 下的文件名完全一致;每个参考页至少一条短语。
|
||||
键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言
|
||||
入口。`lookup.py --check` 负责拦截页键漂移,行为回归测试负责拦截排序漂移。
|
||||
入口。`lookup.py --check` 负责拦截页键漂移;查询排序以 lookup 实现和实际
|
||||
查询结果为准。
|
||||
|
||||
## Skill 维护与校验
|
||||
|
||||
@@ -36,14 +37,12 @@ Section ID 来自标题下的显式 `<!-- section-id: ... -->` 元数据,不
|
||||
|
||||
```bash
|
||||
python skills/tsl-syntax-reference/scripts/lookup.py --check
|
||||
python -m unittest test.test_tsl_syntax_reference -v
|
||||
```
|
||||
|
||||
`--check` 会校验 `page_intent_aliases` 的键与 `references/` 参考页一一对应:
|
||||
键指向不存在的页、或某页没有自然语言入口都会报错。新增参考页时必须同时
|
||||
在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;原始 alias、
|
||||
助词变体、弱命中和已知真实问法由专属测试覆盖。
|
||||
在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;自然语言
|
||||
排序和真实问法应通过实际查询验证。
|
||||
|
||||
`--check` 还覆盖显式 ID、标题层级、quickstart 派生与 Owner Section、代码块身份、
|
||||
本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。仓库级 unittest
|
||||
负责 CLI、排序与退出码行为。
|
||||
本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。
|
||||
|
||||
+18
-20
@@ -1,21 +1,22 @@
|
||||
# 🧪 Playbook 测试套件
|
||||
|
||||
本目录包含 Playbook 项目的测试,用于验证 CLI、模板与文档链接。
|
||||
本目录包含 Playbook 项目的可执行行为测试。
|
||||
|
||||
## 📋 目录结构
|
||||
|
||||
```txt
|
||||
test/
|
||||
├── README.md # 本文件:测试文档
|
||||
├── test_playbook.py # snapshot/subtree 参数化完整部署测试
|
||||
├── test_template_contracts.py # 模板内容、占位符、文案契约测试
|
||||
├── test_commit_message_policy.py # 提交信息校验器和 CI 委托行为
|
||||
├── test_gitea_fetch_ci_logs.py # Gitea CI 日志采集行为
|
||||
├── test_main_loop_cli.py # ticket 调度、隔离、证据与集成测试
|
||||
├── agent/ # Agent 题面/运行时验证测试定义
|
||||
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
|
||||
├── test_tsl_playbook_sync.py # TSL Playbook 构建与同步边界测试
|
||||
├── test_tsl_syntax_reference.py # TSL 语法文档结构与检索测试
|
||||
└── integration/ # 集成测试
|
||||
└── check_doc_links.py # 文档链接有效性检查
|
||||
├── test_main_loop_scheduler.py # 全局调度器和依赖图行为
|
||||
├── test_playbook.py # snapshot/subtree 参数化完整部署测试
|
||||
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步行为
|
||||
├── test_tsl_api_framework.py # TSL API 索引和 Framework Profile 行为
|
||||
├── test_tsl_playbook_sync.py # TSL Playbook 构建与同步行为
|
||||
└── integration/ # 文档完整性检查
|
||||
└── check_doc_links.py # 本地 Markdown 链接目标检查
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
@@ -24,10 +25,10 @@ test/
|
||||
# 进入 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
|
||||
|
||||
# 2. 运行文档链接检查
|
||||
# 文档链接检查
|
||||
python test/integration/check_doc_links.py
|
||||
```
|
||||
|
||||
@@ -51,20 +52,17 @@ python test/integration/check_doc_links.py
|
||||
- rules、memory bank、prompts、standards 和 skills 同步
|
||||
- 重复执行的幂等性与自定义文件保留
|
||||
|
||||
### 2. 模板合同测试 (`test_template_contracts.py`)
|
||||
|
||||
随 Python 测试检查通用模板的关键 marker、占位符和流程合同。
|
||||
|
||||
### 3. 主循环测试 (`test_main_loop_cli.py`)
|
||||
### 2. 主循环测试 (`test_main_loop_cli.py`)
|
||||
|
||||
使用临时 Git 仓库验证 ticket 队列、串行与 worktree 隔离、并发 claim、heartbeat/reclaim、
|
||||
验证与 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_syntax_reference.py`:验证语法文档结构、section ID 与检索结果
|
||||
- `test_thirdparty_skills_pipeline.py`:验证 thirdparty manifest、workflow 和同步产物
|
||||
|
||||
### 5. 文档链接检查 (integration/)
|
||||
### 4. 文档链接检查 (`integration/`)
|
||||
|
||||
扫描 `docs/` 与模板文件中的本地链接,确保引用路径有效。
|
||||
扫描仓库中的 Markdown 文件,确保本地文档引用的目标路径有效。
|
||||
|
||||
@@ -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
|
||||
@@ -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。
|
||||
如果温度大于等于 30,level 就是 "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 是 12,missing_score 是 0,候补值是 99。
|
||||
请用省略真值的条件表达式分别得到两个最终分数,并输出。
|
||||
```
|
||||
|
||||
### TSL-060: 发货判断
|
||||
|
||||
```text
|
||||
请写一份 .tsl 代码:
|
||||
paid 是 true,blocked 是 false,amount 是 120。
|
||||
请用 and、or、not 判断是否允许发货,并输出结果。
|
||||
```
|
||||
|
||||
### TSL-061: 权限掩码
|
||||
|
||||
```text
|
||||
请写一份 .tsl 代码:
|
||||
read_write 是 6,audit_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"。
|
||||
最后输出总金额。
|
||||
```
|
||||
@@ -8,7 +8,6 @@ delegate rather than re-derive rules from that document.
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -18,11 +17,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILL_ROOT = ROOT / "skills" / "commit-message"
|
||||
VALIDATOR = SKILL_ROOT / "scripts" / "validate_commit_message.py"
|
||||
POLICY = SKILL_ROOT / "references" / "commit_policy.json"
|
||||
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):
|
||||
@@ -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):
|
||||
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):
|
||||
source = CI_ENTRY.read_text(encoding="utf-8")
|
||||
self.assertIn("validate_commit_message.py", source)
|
||||
|
||||
@@ -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()
|
||||
@@ -15,7 +15,6 @@ TSL_SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml"
|
||||
LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml"
|
||||
UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh"
|
||||
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
|
||||
SKILLS_MD = ROOT / "SKILLS.md"
|
||||
|
||||
|
||||
def load_manifest() -> dict:
|
||||
@@ -235,13 +234,6 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
self.assertNotIn("COMMIT_AUTHOR_EMAIL", 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):
|
||||
source_lists = [entry["source_list"] for entry in load_manifest()["sources"]]
|
||||
self.assertEqual(len(source_lists), len(set(source_lists)))
|
||||
|
||||
@@ -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>")
|
||||
|
||||
|
||||
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:
|
||||
path = " / ".join((*page.source_path, page.title))
|
||||
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:
|
||||
lines.extend(["", "## 数据说明", ""])
|
||||
for note in (*page.notes, *page.table.update_info):
|
||||
lines.append(f"- {note}")
|
||||
lines.append(f"- {_render_note(note)}")
|
||||
if page.examples:
|
||||
lines.extend(["", "## 取数示例", ""])
|
||||
for index, example in enumerate(page.examples, start=1):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -11,6 +12,8 @@ try:
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
BS4_AVAILABLE = importlib.util.find_spec("bs4") is not None
|
||||
|
||||
|
||||
TOOL_ROOT = Path(__file__).resolve().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"
|
||||
LINT = TOOL_ROOT / "scripts" / "lint.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"
|
||||
STANDARD = TOOL_ROOT / "STANDARD.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):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
@@ -38,6 +54,33 @@ class UnifiedPipelineTest(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
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):
|
||||
return subprocess.run(
|
||||
[sys.executable, *map(str, args)],
|
||||
|
||||
Reference in New Issue
Block a user