From 5484fc7519c4ddb9df9ca32cef20c66b72998374 Mon Sep 17 00:00:00 2001 From: csh Date: Fri, 21 Aug 2026 10:11:48 +0800 Subject: [PATCH] :recycle: refactor(tsl-api): tighten docs and retire stale tests --- .gitignore | 3 - .markdownlintignore | 1 + skills/tsl-api-reference/SKILL.md | 5 + .../dotnet/datawarehouse/wizard-n_day.md | 50 +- .../bond-bond_sensitivity_indicators.md | 12 +- .../dotnet/financial/fund-fund_return.md | 6 +- .../option-option_basic_functions.md | 22 +- ...futures_indicators-bond_valuation_based.md | 12 +- .../shrinkage-estimation.md | 6 +- .../宏观__农产品生产者价格.md | 10 +- .../宏观__国内生产总值.md | 6 +- .../宏观__居民消费价格.md | 8 +- .../workflows/classification-codes.md | 167 ++++ .../workflows/code-sequences.md | 194 ++++ skills/tsl-syntax-reference/data/README.md | 11 +- test/README.md | 38 +- test/agent/README.md | 296 ------ test/agent/prompts_zh.md | 879 ------------------ test/test_commit_message_policy.py | 29 - test/test_template_contracts.py | 745 --------------- test/test_thirdparty_skills_pipeline.py | 8 - test/test_tsl_syntax_reference.py | 647 ------------- tools/tsl-codegen/scripts/build_dictionary.py | 7 +- tools/tsl-codegen/tests/test_pipeline.py | 43 + 24 files changed, 505 insertions(+), 2700 deletions(-) create mode 100644 skills/tsl-api-reference/workflows/classification-codes.md create mode 100644 skills/tsl-api-reference/workflows/code-sequences.md delete mode 100644 test/agent/README.md delete mode 100644 test/agent/prompts_zh.md delete mode 100644 test/test_template_contracts.py delete mode 100644 test/test_tsl_syntax_reference.py diff --git a/.gitignore b/.gitignore index 908b4c75..3223297f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,6 @@ docs/superpowers/ # Local agent instructions /AGENTS.md -# Agent 测试结果 -test/agent/result/ - __pycache__/ *.pyc diff --git a/.markdownlintignore b/.markdownlintignore index 9e1bc266..04838b12 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -3,3 +3,4 @@ node_modules tmp reports skills/thirdparty +/AGENTS.md diff --git a/skills/tsl-api-reference/SKILL.md b/skills/tsl-api-reference/SKILL.md index aff52b96..ac4370b7 100644 --- a/skills/tsl-api-reference/SKILL.md +++ b/skills/tsl-api-reference/SKILL.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` 为权威。 将 `` 替换为包含本 `SKILL.md` 的目录。 diff --git a/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/wizard-n_day.md b/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/wizard-n_day.md index d6017065..eb32ead5 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/wizard-n_day.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/wizard-n_day.md @@ -58,11 +58,11 @@ return ContinuedMatchOfNDay(@close(), 3); -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------- | -| `exp_1` | string | 字符串。统计表达式1 | -| `exp_2` | string | 字符串。统计表达式2 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp_1` | string | 字符串。统计表达式1 | +| `exp_2` | string | 字符串。统计表达式2 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:float @@ -164,10 +164,10 @@ N日线性衰减加权平均,与系统参数(股票,日期,周期,复权 -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------ | -| `exp` | string | 字符串。统计表达式 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp` | string | 字符串。统计表达式 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:float @@ -293,10 +293,10 @@ N日最高点到当前的周期数。N日表示市场交易日,当日最低为 -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------ | -| `exp` | string | 字符串。统计表达式 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp` | string | 字符串。统计表达式 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:int @@ -422,10 +422,10 @@ N日最低点到当前的周期数。N日表示市场交易日,当日最低为 -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------ | -| `exp` | string | 字符串。统计表达式 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp` | string | 字符串。统计表达式 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:int @@ -524,10 +524,10 @@ return OccuredOfNDay(@close(), 10); -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------ | -| `exp` | string | 字符串。统计表达式 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp` | string | 字符串。统计表达式 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:float @@ -550,10 +550,10 @@ return PercentRankOfNDay(@close(), 10, ""); -| 参数 | 类型 | 说明 | -| ----------- | ------ | ------------------ | -| `exp` | string | 字符串。统计表达式 | -| `n` | int | 整数。天数 | +| 参数 | 类型 | 说明 | +| ----------- | ------ | -------------------- | +| `exp` | string | 字符串。统计表达式 | +| `n` | int | 整数。天数 | | `cache_str` | string | 可选。字符串,缓存串 | 返回:float diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/bond-bond_sensitivity_indicators.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/bond-bond_sensitivity_indicators.md index 205c05c0..2ba718eb 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/bond-bond_sensitivity_indicators.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/bond-bond_sensitivity_indicators.md @@ -130,9 +130,9 @@ Return BondBasisPointValue(Endt, MV); -| 参数 | 类型 | 说明 | -| ----------------- | -------- | -------- | -| `settlement_date` | datetime | 结算日期 | +| 参数 | 类型 | 说明 | +| ----------------- | -------- | ---------------------------------- | +| `settlement_date` | datetime | 结算日期 | | `option` | int | 可选。是否行权,默认 0(持有到期) | 返回:float @@ -233,9 +233,9 @@ return bondMacaulayDuration(endt); // 麦考利久期,返回3.12 -| 参数 | 类型 | 说明 | -| ---------------- | -------- | -------- | -| `settlementdate` | datetime | 结算日 | +| 参数 | 类型 | 说明 | +| ---------------- | -------- | ---------------------------------- | +| `settlementdate` | datetime | 结算日 | | `option` | int | 可选。是否行权,默认 0(持有到期) | 返回:float diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/fund-fund_return.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/fund-fund_return.md index 0621ff9f..2d6eba49 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/fund-fund_return.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/fund-fund_return.md @@ -993,9 +993,9 @@ return FundZjRatio(endt); -| 参数 | 类型 | 说明 | -| ----------- | -------- | ------------------------------------------- | -| `end_t` | datetime | 截止日期 | +| 参数 | 类型 | 说明 | +| ----------- | -------- | ------------------------------------------ | +| `end_t` | datetime | 截止日期 | | `fund_type` | integer | 基金类别;原始文档示例中 `2` 表示分级 B 类 | 返回:float diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/option-option_basic_functions.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/option-option_basic_functions.md index 54fc0a0c..04787597 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/option-option_basic_functions.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/option-option_basic_functions.md @@ -195,17 +195,17 @@ return op_hv("SZ000002", 20210101T, -6); -| 参数 | 类型 | 说明 | -| --------------- | ------- | ---------------------------------------------------- | -| `option_type` | integer | 期权类型,0 为看涨期权,1 为看跌期权 | -| `s` | real | 现货价格 | -| `x` | real | 行权价 | -| `close` | real | 期权交易价格 | -| `r` | real | 无风险收益率百分比 | -| `t` | real | 剩余期限月数 | -| `option_style` | string | 可选。默认行权方式,支持 `欧式` 和 `美式` | -| `method` | integer | 可选。默认定价方式,0 为默认,1 为 B_S,2 为二叉树 | -| `contract_type` | integer | 可选。默认 0;0 股票、1 指数、2 外汇、3 期货期权 | +| 参数 | 类型 | 说明 | +| --------------- | ------- | --------------------------------------------------- | +| `option_type` | integer | 期权类型,0 为看涨期权,1 为看跌期权 | +| `s` | real | 现货价格 | +| `x` | real | 行权价 | +| `close` | real | 期权交易价格 | +| `r` | real | 无风险收益率百分比 | +| `t` | real | 剩余期限月数 | +| `option_style` | string | 可选。默认行权方式,支持 `欧式` 和 `美式` | +| `method` | integer | 可选。默认定价方式,0 为默认,1 为 B_S,2 为二叉树 | +| `contract_type` | integer | 可选。默认 0;0 股票、1 指数、2 外汇、3 期货期权 | | `rf` | real | 可选。默认 0;标的资产年股息率或外币无风险年利率(%) | 返回:array diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/special_topics-derivatives_analysis-futures_special-common_treasury_futures_indicators-bond_valuation_based.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/special_topics-derivatives_analysis-futures_special-common_treasury_futures_indicators-bond_valuation_based.md index 0341c2a4..34b4e9e8 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/special_topics-derivatives_analysis-futures_special-common_treasury_futures_indicators-bond_valuation_based.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/special_topics-derivatives_analysis-futures_special-common_treasury_futures_indicators-bond_valuation_based.md @@ -91,12 +91,12 @@ return bondFutureBasicindicators2(endt, -1, 1, 0); -| 参数 | 类型 | 说明 | -| --------------- | ----------- | ----------------------------- | -| `end_t` | date | 日期,截止日 | -| `bond_id` | string | 字符串,债券代码 | -| `return_option` | user_define | 用户自定义, 返回结果选项 | -| `market_id` | user_define | 用户自定义, 国债市场选项 | +| 参数 | 类型 | 说明 | +| --------------- | ----------- | ---------------------------------- | +| `end_t` | date | 日期,截止日 | +| `bond_id` | string | 字符串,债券代码 | +| `return_option` | user_define | 用户自定义, 返回结果选项 | +| `market_id` | user_define | 用户自定义, 国债市场选项 | | `dbtype` | user_define | 可选。用户自定义,国债估值数据来源 | 返回:real diff --git a/skills/tsl-api-reference/references/codegen/module/mathematical-algorithms/shrinkage-estimation.md b/skills/tsl-api-reference/references/codegen/module/mathematical-algorithms/shrinkage-estimation.md index ec461f21..156a6097 100644 --- a/skills/tsl-api-reference/references/codegen/module/mathematical-algorithms/shrinkage-estimation.md +++ b/skills/tsl-api-reference/references/codegen/module/mathematical-algorithms/shrinkage-estimation.md @@ -8,9 +8,9 @@ -| 参数 | 类型 | 说明 | -| -------- | ------ | ------------------------------------------------------------------------------- | -| `data` | array | 二维数据表,T×N,T 期样本数量,N 个股票 | +| 参数 | 类型 | 说明 | +| -------- | ------ | -------------------------------------------------------------------------- | +| `data` | array | 二维数据表,T×N,T 期样本数量,N 个股票 | | `b_type` | string | 目标均值向量:b0 零向量、b1 均值、b2 波动率加权均值、b3 样本均值的均方误差 | 返回:array diff --git a/skills/tsl-api-reference/references/data_dictionary/宏观__农产品生产者价格.md b/skills/tsl-api-reference/references/data_dictionary/宏观__农产品生产者价格.md index b96d66bc..7e7b3152 100644 --- a/skills/tsl-api-reference/references/data_dictionary/宏观__农产品生产者价格.md +++ b/skills/tsl-api-reference/references/data_dictionary/宏观__农产品生产者价格.md @@ -56,11 +56,11 @@ - 农产品生产者价格表统计的是农产品价格的变化情况。 - 区别于农产品生产者价格(简表): -- 1. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度; -- 2. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据; -- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期; -- 4. 简表数据公布会早于此表; -- 5. 没有简表中的累计同比数据。 +- 1\. 本表获取历史数据更多,数据开始时间为 2003 年一季度,简表的数据开始时间为 2007 年一季度; +- 2\. 简表含有部分生活常见的农产品(与食用相关)价格数据,包含本季同比和累计同比两种类型数据,本表统计的是涵盖农林牧渔业的部分农产品的价格,范围更广,多包含林业等与工业材料相关产品的价格数据,只含有本季同比的数据; +- 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期; +- 4\. 简表数据公布会早于此表; +- 5\. 没有简表中的累计同比数据。 - 1、数据开始时间:2003年一季度 - 2、数据更新频率:季度 diff --git a/skills/tsl-api-reference/references/data_dictionary/宏观__国内生产总值.md b/skills/tsl-api-reference/references/data_dictionary/宏观__国内生产总值.md index e52b67b0..e6dac422 100644 --- a/skills/tsl-api-reference/references/data_dictionary/宏观__国内生产总值.md +++ b/skills/tsl-api-reference/references/data_dictionary/宏观__国内生产总值.md @@ -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、数据更新频率:季度 diff --git a/skills/tsl-api-reference/references/data_dictionary/宏观__居民消费价格.md b/skills/tsl-api-reference/references/data_dictionary/宏观__居民消费价格.md index e16ff016..86322843 100644 --- a/skills/tsl-api-reference/references/data_dictionary/宏观__居民消费价格.md +++ b/skills/tsl-api-reference/references/data_dictionary/宏观__居民消费价格.md @@ -211,10 +211,10 @@ - 居民消费价格主要介绍的是居民消费价格分类及其指数的变化情况。 - 区别于居民消费价格(简表); -- 1. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月; -- 2. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。 -- 3. 简表记录数据的公布时间,精确到秒,此表仅记录日期; -- 4. 简表数据公布会早于此表; +- 1\. 数据获取历史周期更长,本表数据开始时间 1987 年一月,简表数据开始为 2007 年一月; +- 2\. 简表重点于消费价格的细类划分,把居民消费分为八个大类再分别统计其子类的数据,本表主要表现城乡消费模式的差异,分别统计全国、城市、农村主要大类产品的价格数据。 +- 3\. 简表记录数据的公布时间,精确到秒,此表仅记录日期; +- 4\. 简表数据公布会早于此表; - 1、数据开始时间:1987年一月 - 2、数据更新频率:月度 diff --git a/skills/tsl-api-reference/workflows/classification-codes.md b/skills/tsl-api-reference/workflows/classification-codes.md new file mode 100644 index 00000000..075ae3ef --- /dev/null +++ b/skills/tsl-api-reference/workflows/classification-codes.md @@ -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` 组合在运行时取回当前列表。 diff --git a/skills/tsl-api-reference/workflows/code-sequences.md b/skills/tsl-api-reference/workflows/code-sequences.md new file mode 100644 index 00000000..25452d21 --- /dev/null +++ b/skills/tsl-api-reference/workflows/code-sequences.md @@ -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` 均已在索引中核对存在。 diff --git a/skills/tsl-syntax-reference/data/README.md b/skills/tsl-syntax-reference/data/README.md index 2d883f31..4f26d0a8 100644 --- a/skills/tsl-syntax-reference/data/README.md +++ b/skills/tsl-syntax-reference/data/README.md @@ -21,7 +21,8 @@ - 键必须与 `references/` 下的文件名完全一致;每个参考页至少一条短语。 键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言 - 入口。`lookup.py --check` 负责拦截页键漂移,行为回归测试负责拦截排序漂移。 + 入口。`lookup.py --check` 负责拦截页键漂移;查询排序以 lookup 实现和实际 + 查询结果为准。 ## Skill 维护与校验 @@ -36,14 +37,12 @@ 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、排序与退出码行为。 +本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。 diff --git a/test/README.md b/test/README.md index efee1bd1..c1c5184f 100644 --- a/test/README.md +++ b/test/README.md @@ -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 文件,确保本地文档引用的目标路径有效。 diff --git a/test/agent/README.md b/test/agent/README.md deleted file mode 100644 index 9d93c9f0..00000000 --- a/test/agent/README.md +++ /dev/null @@ -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 的产品名、命令名、SDK provider 名或当前运行器已知的 agent 身份。 - - 无法从运行器上下文、命令名或已知 provider 可靠识别时,向用户确认一次 agent 短名;不要猜测。 - - 目录名统一转成小写短名,只使用 ASCII 字母、数字、短横线、下划线或点。 -- 命名格式: - - `.tsl`:`tsl-001.tsl` - - `.tsf`:`tsl-037/.tsf` - - `.tsf` 调用验证脚本:`tsl-037/verify.tsl` -- 修复轮次按 case 计数:`repair-1/tsl-001.tsl`、`repair-1/tsl-037/.tsf` -- 示例: - - `result/claude/20260610/tsl-001.tsl` - - `result/claude/20260610/tsl-037/.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/ - │ ├── .tsf - │ └── verify.tsl - └── repair-1/ - ├── tsl-001.tsl - └── tsl-037/ - ├── .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="" -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///tsl-001.tsl`。 -- 只有题面明确要求输出 `.tsf` 时,才保存为 `.tsf`,并放入对应 case 目录, - 例如 `test/agent/result///tsl-037/.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// -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///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 diff --git a/test/agent/prompts_zh.md b/test/agent/prompts_zh.md deleted file mode 100644 index 5c227276..00000000 --- a/test/agent/prompts_zh.md +++ /dev/null @@ -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"。 -最后输出总金额。 -``` diff --git a/test/test_commit_message_policy.py b/test/test_commit_message_policy.py index 716f2713..88ead45c 100644 --- a/test/test_commit_message_policy.py +++ b/test/test_commit_message_policy.py @@ -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[^|]*)\|(?P[^|]*)\|") 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) diff --git a/test/test_template_contracts.py b/test/test_template_contracts.py deleted file mode 100644 index 53da1ed8..00000000 --- a/test/test_template_contracts.py +++ /dev/null @@ -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//spec.md", templates_readme) - self.assertIn("`.scratch//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 ( - "", - "", - "", - "", - ): - self.assertIn(marker, lines, msg=f"missing marker: {marker}") - - framework_start = lines.index("") - framework_end = lines.index("") - agents_start = lines.index("") - agents_end = lines.index("") - 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("/scripts/main_loop.py", bundle) - self.assertNotIn("", 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 --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", - "`` 作为 `code-review` 的 fixed point", - "仅运行 Standards axis", - "`diagnosing-bugs` 完整执行 Phase 1-6", - "improve-codebase-architecture", - ): - self.assertIn(required, workflow) - self.assertNotIn("`...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//spec.md`", - "`.scratch//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/.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 "/.scratch"', execution) - self.assertIn('--repo-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", - "`//spec.md`", - "`//issues/-*.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//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//`", - "`.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 --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() diff --git a/test/test_thirdparty_skills_pipeline.py b/test/test_thirdparty_skills_pipeline.py index a8620bbd..2c0affd9 100644 --- a/test/test_thirdparty_skills_pipeline.py +++ b/test/test_thirdparty_skills_pipeline.py @@ -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))) diff --git a/test/test_tsl_syntax_reference.py b/test/test_tsl_syntax_reference.py deleted file mode 100644 index 4d5da1ec..00000000 --- a/test/test_tsl_syntax_reference.py +++ /dev/null @@ -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 - - ## 本篇职责 - - - - 测试职责。 - - ## 原标题 - - - - 事实正文。 - """, - ) - 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 - - ## 本篇职责 - - - - 测试职责。 - - ## 重复 ID - - - - 事实正文。 - """, - ) - - 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 - - ## 本篇职责 - - - - 测试职责。 - - ## 示例 - - - - 代码块身份:可直接照写示例 - """, - ) - - 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 - - ## 本篇职责 - - - - 测试职责。 - - ##### 不会被索引的标题 - - 隐藏事实。 - """, - ) - - 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 - - ## 本篇职责 - - - - 派生摘要。 - - ## 语言核心事实速查 - - - - - - 普通赋值使用 `=`。 - """, - ) - write_reference( - references, - "02_topic.md", - """ - # Topic - - ## 本篇职责 - - - - 完整事实源。 - - ## 核心规则 - - - - - - 普通赋值使用 `:=`。 - """, - ) - - 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 - - ## 本篇职责 - - - - 派生摘要。 - - ## 语言核心事实速查 - - - - 没有任何规则。 - """, - ) - 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 - - ## 本篇职责 - - - - 派生摘要。 - - ## 语言核心事实速查 - - - - - - 普通赋值使用 `:=`。 - Owner Section:`syntax-02-999` - """, - ) - write_reference( - references, - "02_topic.md", - """ - # Topic - - ## 本篇职责 - - - - 完整事实源。 - - ## 核心规则 - - - - - - 普通赋值使用 `:=`。 - """, - ) - - 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" - "\n\n" - "测试职责。\n\n" - "## 过长事实段\n\n" - "\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("