diff --git a/skills/tsl-api-reference/SKILL.md b/skills/tsl-api-reference/SKILL.md index ac4370b7..cb3a9e2d 100644 --- a/skills/tsl-api-reference/SKILL.md +++ b/skills/tsl-api-reference/SKILL.md @@ -1,6 +1,6 @@ --- name: tsl-api-reference -description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员或框架回调是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。" +description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员、框架回调或 API 参数可用值是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「getbk 的板块名」「申万煤炭的分类代码」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。" --- # TSL API 参考 @@ -16,13 +16,8 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class [`workflows/api-lookup.md`](workflows/api-lookup.md)。 - 查询 class 摘要、实现子类、重写回调或处理生命周期:读取 [`workflows/class-and-framework.md`](workflows/class-and-framework.md)。 -- 按中文业务含义查表、数据源或字段:读取 +- 按中文业务含义查表、数据源、字段或 API 参数可用值:读取 [`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` 的目录。 @@ -34,6 +29,8 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class - 不得凭记忆或参照相似语言推断 API 名称、签名、参数、返回值或回调字段。 - 同名结果不得默认取第一条;必须按 `scope`、`module`、owner 或完全限定名消歧。 - `lookup.py` 的 rc=1 表示 skill 数据缺失、损坏或正文不可达,不表示 API 不存在。 +- 参数取值域标为 `runtime_catalog` 或“静态记录不完整”时,样例只用于定位 API;完整名称 + 必须按结果给出的运行时解析器核验,不能把样例当成完整清单。 ## 🔴 CHECKPOINT · 🛑 STOP @@ -45,6 +42,7 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class - `class_lookup.py` 返回 `status: not_found`; - 请求依赖生命周期、配置或回调,但 `profile_status=not_profiled`; - Framework Profile 的 `contract_status` 不是 `resolved`。 +- 参数取值需要运行时核验但当前环境无法核验,且代码会依赖该具体值。 rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。 diff --git a/skills/tsl-api-reference/data/function_index.tsv b/skills/tsl-api-reference/data/function_index.tsv index e6dde1cc..f886f6b9 100644 --- a/skills/tsl-api-reference/data/function_index.tsv +++ b/skills/tsl-api-reference/data/function_index.tsv @@ -5390,6 +5390,7 @@ GetBenchmarkWeight module ts-multi-factor GetBenchmarkWeight(v_end_t) module/ts- GetBenchmarkWeightandReturn module ts-factor-attribution GetBenchmarkWeightandReturn(v_end_t) module/ts-factor-attribution.md getbenchmarkweightandreturnv_end_t 查询 组合权重 资产配置 比较基准 业绩基准 表数据 二维表 基准权重表,数据表结构如下。注意,当FindexId 有值时,则不需要重载此方法 method instance public tsrm_FactorAttribution tsrm_FactorAttribution.GetBenchmarkWeightandReturn GetBenchRiskForecasting module tsrm-exante-risk-attribution GetBenchRiskForecasting() module/tsrm-exante-risk-attribution.md getbenchriskforecasting 查询 预测 风险管理 风控 比较基准 业绩基准 获取基准风险预估值 method instance public tsrm_ExAnte_RiskAttribution tsrm_ExAnte_RiskAttribution.GetBenchRiskForecasting GetBitsPerComponent module pdf-file GetBitsPerComponent() module/pdf-file.md getbitspercomponent 查询 图片 色彩 获取图像颜色分量值位数 method instance public pdfImage pdfImage.GetBitsPerComponent +getBk dotnet datawarehouse getBk(marketlist) dotnet/datawarehouse/market_sector.md getbk 数据仓库 市场 板块 股票 成分 分类属性代码 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 最新 获取 得到指定市场板块列表包含的最新成分。普通市场板块返回股票代码;行业或分类目录可能返回 function getBk getBkAll dotnet financial getBkAll(bkname) dotnet/financial/sector-sector_constituents.md getbkall 金融 板块 板块成分 返回 获取 返回板块所有样本(包括退市) 功能特别说明: 不适用以下板块或目录下的板块: 不适用板块:A股,上证A股,深证A股,中小企业板,创业板,科创板,(可用getabkbydate获取指定日) B股,上证B股,深证B股, 全球指数(目前4个不存在退市) 期货主力,期货连续,期货指数线,期货品种代码(这4个板块不区分上市退市) 存款利率,贷款利率,Shibor,贷款基础利率 不适用以下目录下的板块:股票\概念板块(可用getbkbydate获取指定日) 股票\股票地域 股票\指数成份(可用getbkbydate获取指定日) 股票\融资融券(可用getbkbydate获取指定日) 股票\互通股(可用getbkbydate获取指定日) 股票\上市状态(已包含所有直接用getbk) 基金\基金托管人 指数\指数提供商 回购\上市状态(已包含所有直接用getbk) 基金\上市状态(已包含所有直接用getbk) 期货\上市状态(已包含所有直接用getbk) 期权\上市状态(已包含所有直接用getbk) 债券\上市状态(已包含所有直接用getbk) 指数\上市状态(已包含所有直接用getbk) function getBkAll getbkall_arr dotnet financial getbkall_arr() dotnet/financial/sector-sector_constituents-intermediate.md getbkall_arr 金融 板块 板块成分 中间函数 获取 查询 获取所有板块样本配置表,此表配置只能单独列出规则的板块 function getbkall_arr getbkall_arr2 dotnet financial getbkall_arr2() dotnet/financial/sector-sector_constituents-intermediate.md getbkall_arr2 金融 板块 板块成分 中间函数 获取 查询 获取所有板块样本配置表,此表配置通用规则及板块名,开发用 function getbkall_arr2 @@ -5397,10 +5398,12 @@ getBkAllbyEndT dotnet financial getBkAllbyEndT(bkname, end_t) dotnet/financial/s getBkBacktrackByDate dotnet financial getBkBacktrackByDate(index_id, end_t, ex_type) dotnet/financial/index-index_info.md getbkbacktrackbydate 金融 指数 指数情况 获取 查询 获取指数历史上指定日样本(可回溯)。当ExType=1时,如指数成分中取不到成分,则从权重表中获取。此函数可以简单回溯历史数据,当Endt早于指数开始日,则选取指数开始日成分股在endt在市交易的成分股列表 function getBkBacktrackByDate getbkbybegtendt dotnet financial getbkbybegtendt(indexid, begt, endt) dotnet/financial/index-index_info.md getbkbybegtendt 金融 指数 指数情况 返回 获取 返回区间曾经是成分股的股票,区间范围内是指数成份股的股票 function getbkbybegtendt getBkByBegTEndT_ii dotnet financial getBkByBegTEndT_ii(bk_name, beg_t, end_t) dotnet/financial/sector-basic_info.md getbkbybegtendt_ii 金融 板块 基本情况 返回 获取 返回区间范围内曾经在板块上市过的所有股票(含已暂停已退市)。目前只能使用以下板块:'A股','上证A股','科创板','深证A股','创业板','北证A股','B股','上证B股','深证B股' function getBkByBegTEndT_ii -getBkByDate dotnet financial getBkByDate(index_id, end_t, extype) dotnet/financial/index-index_info.md getbkbydate 金融 指数 指数情况 获取 查询 获取指数历史指定日的成份股列表。与GetBK不同,该函数需要的第一个参数是指数代码,获取该指数的指定日的成分股,比如沪深300定期调整,在不同时间获取的成分股将不同。 新增功能,当ExType=1时指数成分中取不到成分时,则从权重表中获取 function getBkByDate +getBkByDate dotnet financial getBkByDate(index_id, end_t, extype) dotnet/financial/index-index_info.md getbkbydate 金融 指数 指数情况 历史成份股 分类属性代码 下级分类 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 TSI000001 TSI000002 TSI000003 TSI000004 TSI000005 TSI000006 TSI000007 TSI000008 TSI000009 获取 查询 获取指数或分类属性在指定日的成份股列表。与getBk不同,该函数需要日期参数;普通指数使用 function getBkByDate getBkByIndexName dotnet financial getBkByIndexName(index_name, end_t) dotnet/financial/index-index_info.md getbkbyindexname 金融 指数 指数情况 返回 获取 返回使用指数名称获取指定日指数成分,只支持申万行业、A股 function getBkByIndexName getBkGoMarketByDate dotnet financial getBkGoMarketByDate(index_id, end_t, ex_type) dotnet/financial/index-index_info.md getbkgomarketbydate 金融 指数 指数情况 获取 查询 获取指数历史上指定日样本(已上市)。当ExType=1时指数成分中取不到成分时,则从权重表中获取。存在申万行业分类的成份股中,它的选入日期可能早于上市日,现剔除这部分标的 function getBkGoMarketByDate getbkIndexIDs dotnet financial getbkIndexIDs() dotnet/financial/sector-basic_info-intermediate.md getbkindexids 金融 板块 基本情况 中间函数 获取 查询 AB 获取AB股板块及编码对照表 function getbkIndexIDs +getBkList dotnet datawarehouse getBkList(Type) dotnet/datawarehouse/market_sector.md getbklist 数据仓库 市场 板块 系统板块 行业分类 分类目录 列表 获取 得到所有的或指定系统板块类别下的板块名称列表。`Type` 是板块类别或具体板块目录名称; function getBkList +getBkList2 dotnet datawarehouse getBkList2([bktype]) dotnet/datawarehouse/market_sector.md getbklist2 数据仓库 市场 板块 系统板块 路径 列表 获取 得到所有的或指定系统板块类别下的板块名称及其所在路径。与 `getBkList` 不同,该函数会 function getBkList2 getBkNameToId dotnet financial getBkNameToId(index_name, end_t) dotnet/financial/index-index_info.md getbknametoid 金融 指数 指数情况 返回 获取 返回使用指数名称获取指定日指数成分 function getBkNameToId getbktypelist dotnet financial getbktypelist(bktype) dotnet/financial/stock-basic_info-basic_info-intermediate.md getbktypelist 金融 股票 基本情况 中间函数 获取 查询 获取指定板块分类下的板块名称列表 function getbktypelist getBkWeightByDate2 dotnet financial getBkWeightByDate2(index_id, benchmark_id, end_t) dotnet/financial/index-basic_information.md getbkweightbydate2 金融 指数 基本信息 返回 获取 SH000985 返回本模型从基准指数BenchmarkID的成分股权重数据中获取目标指数IndexID的成份股,并取其权重进行归一处理,计算目标指数IndexID的成分股权重。 使用时需注意: 1.基准指数BenchmarkID的选取比较重要,比如申万二级和三级行业,可以选择对应的申万一级行业指数作为估算的基准指数,经测算是较为接近的; 2.目标指数IndexID的成分股必须可获取; 3.基准指数BenchmarkID的成分股应包含目标指数IndexID的成分股; 4.如果BenchmarkID为nil,则取默认基准指数--SH000985 中证全指 function getBkWeightByDate2 @@ -5594,8 +5597,8 @@ getHttpContent builtin network getHttpContent(session[, from_pos]) builtin/netwo getHttpCookies builtin network getHttpCookies(session) builtin/network.md gethttpcookies 网络 获取 查询 HTTP 请求 network 获取指定 HTTP 会话保存的服务器 Cookie function getHttpCookies getHttpResponseHeader builtin network getHttpResponseHeader([session]) builtin/network.md gethttpresponseheader 网络 获取 查询 HTTP 请求 network 获取 HTTP 响应头;session 可省略 function getHttpResponseHeader getHyDyFlDetails dotnet financial getHyDyFlDetails(type) dotnet/financial/stock-industry.md gethydyfldetails 金融 股票 行业 获取 查询 获取分行业/地域/分类的股票的详细情况 function getHyDyFlDetails -getHyDyFlList dotnet financial getHyDyFlList(type) dotnet/financial/stock-industry.md gethydyfllist 金融 股票 行业 获取 查询 获取行业/地域/分类的列表,用于替代GetBKList function getHyDyFlList -getHyDyFlStocks dotnet financial getHyDyFlStocks(type, name) dotnet/financial/stock-industry.md gethydyflstocks 金融 股票 行业 获取 查询 获取行业的股票。取代GetBK,比其更精确 function getHyDyFlStocks +getHyDyFlList dotnet financial getHyDyFlList(type) dotnet/financial/stock-industry.md gethydyfllist 金融 股票 行业 获取 查询 获取行业/地域/分类的列表,用于替代getBkList function getHyDyFlList +getHyDyFlStocks dotnet financial getHyDyFlStocks(type, name) dotnet/financial/stock-industry.md gethydyflstocks 金融 股票 行业 获取 查询 获取行业的股票。取代getBk,比其更精确 function getHyDyFlStocks GetIC module tsf-factor-ext-indicator-base GetIC(begt, endt, ic_type) module/tsf-factor-ext-indicator-base.md geticbegtendtic_type 查询 信息系数 列表 集合 获得 IC 数据,需要用户重载,基类未实现,返回空数组,并提示报错:getIC 方法必须重写! method instance public tsf_factorExtindiCator_base tsf_factorExtindiCator_base.GetIC GetImpactCostEstimatedDetail module impact-cost GetImpactCostEstimatedDetail() module/impact-cost.md getimpactcostestimateddetail 查询 预估 交易成本 获取冲击成本预测详情,包含"永久性冲击成本(BP)"、"暂时性冲击成本(BP)"、 method instance public ts_ImpactCost ts_ImpactCost.GetImpactCostEstimatedDetail GetImpactCostEstimatedValue module impact-cost GetImpactCostEstimatedValue() module/impact-cost.md getimpactcostestimatedvalue 查询 已实现冲击 冲击成本 BP 获取已实现冲击成本(BP),单位 BP method instance public ts_ImpactCost ts_ImpactCost.GetImpactCostEstimatedValue @@ -5953,6 +5956,8 @@ GetUnicode module pdf-file GetUnicode(code) module/pdf-file.md getunicodecode GetUnicodeWidth module pdf-file GetUnicodeWidth(code) module/pdf-file.md getunicodewidthcode 查询 尺寸 宽高 字型 获取当前字体下指定 Unicode 字符的宽度 method instance public pdfFont pdfFont.GetUnicodeWidth getUnilateralValue dotnet financial getUnilateralValue(exp, endt) dotnet/financial/market-intermediate.md getunilateralvalue 金融 行情 中间函数 返回 获取 返回期货单边指标,与系统参数(证券代码)相关 function getUnilateralValue getUrl builtin network getUrl(src_url, parse_url) builtin/network.md geturl 网络 解析 parse HTTP 请求 network HTML URL 解析在HTML中出现的URL,返回全路径URL串 function getUrl +getUserBkList dotnet datawarehouse getUserBkList([bktype]) dotnet/datawarehouse/market_sector.md getuserbklist 数据仓库 市场 板块 用户板块 列表 获取 得到所有的或指定板块类别下的用户板块名称。`bktype` 省略时返回全部用户板块;指定时可 function getUserBkList +getUserBkList2 dotnet datawarehouse getUserBkList2([bktype]) dotnet/datawarehouse/market_sector.md getuserbklist2 数据仓库 市场 板块 用户板块 路径 列表 获取 得到所有的或指定板块类别下用户板块的名称及其所在路径。与 `getUserBkList` 不同, function getUserBkList2 GetValuationDetail module bond-research GetValuationDetail() module/bond-research/convertible-bond-ccb.md getvaluationdetail 返回各估值路径的明细 method instance public cb_CompleteDecompose cb_CompleteDecompose.GetValuationDetail GetValueByEndt module ts-persistence-of-fund GetValueByEndt(funds, endt) module/ts-persistence-of-fund.md getvaluebyendtfundsendt 查询 绩效 业绩表现 基金产品 获取指定日基金池基金业绩指标 method instance public ts_PersistenceOfFund ts_PersistenceOfFund.GetValueByEndt getValuesByFieldValue dotnet base getValuesByFieldValue(t, f_name, field_value) dotnet/base/array.md getvaluesbyfieldvalue 返回 获取 返回指定字段值等于指定值的所有记录 function getValuesByFieldValue @@ -7116,7 +7121,7 @@ marketAndTotalValueStructure dotnet financial_report marketAndTotalValueStructur marketLastTradeDay dotnet financial marketLastTradeDay(end_t) dotnet/financial/market-n_day.md marketlasttradeday 金融 行情 N 日 返回 获取 返回指定日前一个市场交易日 function marketLastTradeDay marketlasttradeday2 dotnet financial marketlasttradeday2() dotnet/financial/market-current_day.md marketlasttradeday2 金融 行情 当日 返回 获取 SH000001 HKHSI001 HSG000001 HSG000002 返回市场最近交易日 系统参数:股票,日期获取市场指定日前最近交易日。与系统参数(股票,日期)相关。 其中各市场代表指数为: A股市场:SH000001; 港股市场:HKHSI001 港股通:HSG000001,包括港股通(沪)?和港股通(深)两个通道 陆股通:HSG000002,包括沪股通和深股通两个通道 南北向开始日:2014-11-17 如果返回nil,则指定日通道未开通 function marketlasttradeday2 marketlasttradedayn dotnet financial marketlasttradedayn(n) dotnet/financial/market-n_day.md marketlasttradedayn 金融 行情 N 日 返回 获取 返回A股市场、港股市场、沪深港通板块获取市场指定日前N个交易日。系统参数:市场指数代码 function marketlasttradedayn -marketList dotnet financial marketList() dotnet/financial/sector-basic_info-miscellaneous.md marketlist 金融 板块 基本情况 其它 返回 获取 返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数GetBKList,使用方法参见该函数的函数说明 function marketList +marketList dotnet financial marketList() dotnet/financial/sector-basic_info-miscellaneous.md marketlist 金融 板块 基本情况 其它 返回 获取 返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数getBkList,使用方法参见该函数的函数说明 function marketList marketNextTradeDay dotnet financial marketNextTradeDay(end_t) dotnet/financial/market-n_day.md marketnexttradeday 金融 行情 N 日 返回 获取 返回市场后一个交易日 function marketNextTradeDay marketnexttradedayn dotnet financial marketnexttradedayn(n) dotnet/financial/market-n_day.md marketnexttradedayn 金融 行情 N 日 返回 获取 返回A股市场、港股市场、沪深港通板块获取市场指定日后N个交易日。系统参数:市场指数代码 function marketnexttradedayn marketPRInitTradeAmountEndT dotnet financial marketPRInitTradeAmountEndT(market_type, end_t) dotnet/financial/stock-stock_pledge_repo_transaction_summary-specified_date.md marketprinittradeamountendt 金融 股票 质押回购 交易汇总 初始交易金额 市场 指定日 返回指定日指定市场的初始交易金额 function marketPRInitTradeAmountEndT diff --git a/skills/tsl-api-reference/data/value_domains.json b/skills/tsl-api-reference/data/value_domains.json new file mode 100644 index 00000000..9a78e6c1 --- /dev/null +++ b/skills/tsl-api-reference/data/value_domains.json @@ -0,0 +1,461 @@ +{ + "version": 1, + "domains": [ + { + "id": "market_board", + "label": "市场和板块名称", + "mode": "runtime_catalog", + "complete": false, + "bindings": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBk", + "parameter": "marketlist", + "role": "current_components" + } + ], + "resolvers": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBkList2", + "parameter": "bktype", + "catalog": "system", + "source": "net_function:28964" + }, + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getUserBkList2", + "parameter": "bktype", + "catalog": "user", + "source": "net_function:28963" + } + ], + "values": [], + "related_tables": [], + "sources": ["net_function:28960", "faq:37996"], + "as_of": "2026-08-20" + }, + { + "id": "classification_code", + "label": "分类属性代码", + "mode": "versioned_catalog", + "complete": false, + "bindings": [ + { + "scope": "dotnet", + "module": "financial", + "api": "getBkByDate", + "parameter": "index_id", + "role": "historical_components" + } + ], + "resolvers": [], + "related_tables": [ + { + "id": "138", + "name": "股票.股票行业分类信息", + "scope": "stock", + "page": "references/data_dictionary/股票__股票行业分类信息.md" + }, + { + "id": "355", + "name": "基金.基金分类信息", + "scope": "fund", + "page": "references/data_dictionary/基金__基金分类信息.md" + } + ], + "values": [ + { + "value": "SWHY", + "label": "申万行业", + "aliases": ["申万行业分类"], + "level": 0, + "related_table": "138", + "sources": ["faq:31492", "faq:31494"], + "relations": [ + { + "kind": "runtime_parameter", + "domain": "classification_catalog_selector", + "value": "申万行业", + "verification": { + "status": "documented", + "scope": "dotnet", + "module": "datawarehouse", + "resolver": "getBkList2", + "parameter": "bktype", + "argument": "申万行业" + }, + "sources": [ + "faq:31494", + "net_function:28962", + "net_function:28964" + ] + } + ] + }, + { + "value": "SWHY740000", + "label": "申万煤炭", + "aliases": ["申万煤炭一级行业"], + "parent": "SWHY", + "parent_label": "申万行业", + "level": 1, + "valid_from": "20211213", + "related_table": "138", + "as_of": "2023-10-13", + "sources": ["faq:31494", "faq:35791"], + "relations": [ + { + "kind": "runtime_parameter", + "domain": "market_board", + "value": "申万煤炭", + "verification": { + "status": "runtime_required", + "scope": "dotnet", + "module": "datawarehouse", + "resolver": "getBkList2", + "parameter": "bktype", + "argument": "申万行业" + }, + "sources": ["faq:31494", "net_function:28964"] + } + ] + }, + { + "value": "CAPCHY", + "label": "中上协行业", + "aliases": ["中国上市公司协会行业"], + "level": 0, + "related_table": "138", + "sources": ["faq:31492"] + }, + { + "value": "CSRCHY", + "label": "证监会行业", + "aliases": [], + "level": 0, + "note": "2024-02-19 起不再维护", + "related_table": "138", + "sources": ["faq:31492"] + }, + { + "value": "YHFL", + "label": "银河基金分类", + "aliases": [], + "level": 0, + "related_table": "355", + "sources": ["faq:31492"] + }, + { + "value": "TSJJ02", + "label": "投资风格", + "aliases": ["基金投资风格"], + "level": 0, + "related_table": "355", + "sources": ["faq:31492"] + }, + { + "value": "TSJJ03", + "label": "净值增长率计算方法", + "aliases": [], + "level": 0, + "related_table": "355", + "sources": ["faq:31492"] + } + ], + "sources": ["faq:31492"], + "as_of": "2026-08-20" + }, + { + "id": "classification_catalog_selector", + "label": "分类属性目录选择器", + "mode": "catalog", + "complete": false, + "bindings": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBk", + "parameter": "marketlist", + "role": "catalog_selector" + } + ], + "resolvers": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBkList2", + "parameter": "bktype", + "catalog": "system", + "source": "net_function:28964" + } + ], + "related_tables": [ + { + "id": "138", + "name": "股票.股票行业分类信息", + "scope": "stock", + "page": "references/data_dictionary/股票__股票行业分类信息.md" + }, + { + "id": "355", + "name": "基金.基金分类信息", + "scope": "fund", + "page": "references/data_dictionary/基金__基金分类信息.md" + } + ], + "values": [ + { + "value": "属性代码", + "label": "全部股票与基金分类属性代码", + "aliases": ["分类属性代码目录"], + "sources": ["faq:31492"] + } + ], + "sources": ["faq:31492"], + "as_of": "2026-08-20" + }, + { + "id": "classification_child_code", + "label": "下级分类属性代码模式", + "mode": "pattern", + "complete": true, + "bindings": [ + { + "scope": "dotnet", + "module": "financial", + "api": "getBkByDate", + "parameter": "index_id", + "role": "child_codes" + } + ], + "resolvers": [], + "related_tables": [ + { + "id": "138", + "name": "股票.股票行业分类信息", + "scope": "stock", + "page": "references/data_dictionary/股票__股票行业分类信息.md" + }, + { + "id": "355", + "name": "基金.基金分类信息", + "scope": "fund", + "page": "references/data_dictionary/基金__基金分类信息.md" + } + ], + "values": [ + { + "value": "<分类属性代码>.N", + "label": "指定分类节点的下级分类属性代码", + "aliases": [".N", "下级分类代码"], + "sources": ["faq:31492"] + } + ], + "sources": ["faq:31492"], + "as_of": "2026-08-20" + }, + { + "id": "market_board_type", + "label": "板块类别", + "mode": "enum", + "complete": true, + "bindings": [ + { + "scope": "dotnet", + "module": "financial", + "api": "getbktypelist", + "parameter": "bktype", + "role": "category" + }, + { + "scope": "dotnet", + "module": "financial", + "api": "stocksbklist", + "parameter": "bktype", + "role": "category" + } + ], + "resolvers": [], + "related_tables": [], + "values": [ + { + "value": "-1", + "label": "自定义板块序列", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "0", + "label": "概念板块", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "1", + "label": "中证行业", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "2", + "label": "中证证监会行业", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "3", + "label": "股票地域", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "4", + "label": "指数成份", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "5", + "label": "申万行业(一级)", + "aliases": ["申万一级行业"], + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "6", + "label": "申万二级行业", + "sources": ["net_function:22421", "net_function:22416"] + }, + { + "value": "7", + "label": "申万三级行业", + "sources": ["net_function:22421", "net_function:22416"] + } + ], + "sources": ["net_function:22421", "net_function:22416"], + "as_of": "2026-08-21" + }, + { + "id": "historical_market_board_code", + "label": "常用股票市场板块历史代码", + "mode": "enum", + "complete": true, + "bindings": [ + { + "scope": "dotnet", + "module": "financial", + "api": "getBkByDate", + "parameter": "index_id", + "role": "historical_market_components" + } + ], + "resolvers": [], + "related_tables": [], + "values": [ + { + "value": "TSI000001", + "label": "A股板块", + "aliases": ["A股历史成份股"], + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000002", + "label": "上证A股板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000003", + "label": "科创板板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000004", + "label": "深证A股板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000005", + "label": "创业板板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000006", + "label": "北证A股板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000007", + "label": "B股板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000008", + "label": "上证B股板块", + "sources": ["faq:34194", "faq:35788"] + }, + { + "value": "TSI000009", + "label": "深证B股板块", + "sources": ["faq:34194", "faq:35788"] + } + ], + "sources": ["faq:34194", "faq:35788"], + "as_of": "2023-10-19" + }, + { + "id": "historical_a_share_board_name", + "label": "按名称查询的历史 A 股板块", + "mode": "enum", + "complete": true, + "bindings": [ + { + "scope": "dotnet", + "module": "financial", + "api": "getAbkbyDate", + "parameter": "bk_name", + "role": "historical_named_components" + } + ], + "resolvers": [], + "related_tables": [], + "values": [ + { + "value": "A股", + "label": "全部 A 股", + "note": "包括上证A股、科创板、深证A股、创业板和北证A股,不含新三板", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "上证A股", + "label": "上证 A 股", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "深证A股", + "label": "深证 A 股", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "北证A股", + "label": "北证 A 股", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "创业板", + "label": "创业板", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "科创板", + "label": "科创板", + "sources": ["net_function:24767", "faq:35788"] + }, + { + "value": "中小企业板", + "label": "中小企业板(历史兼容)", + "note": "仅用于历史兼容;该板块已于 2021-04-06 并入主板", + "sources": ["net_function:24767", "faq:35788"] + } + ], + "sources": ["net_function:24767", "faq:35788"], + "as_of": "2025-08-13" + } + ] +} diff --git a/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/market_sector.md b/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/market_sector.md new file mode 100644 index 00000000..c19f4218 --- /dev/null +++ b/skills/tsl-api-reference/references/codegen/dotnet/datawarehouse/market_sector.md @@ -0,0 +1,160 @@ +# 数据仓库 / 市场和板块函数 + +## `getBk(marketlist)` + +声明:function + +得到指定市场板块列表包含的最新成分。普通市场板块返回股票代码;行业或分类目录可能返回 +行业名称、板块名称或分类属性代码,不是指数代码。板块或行业名称必须与板块管理中的名称 +一致;需要指定历史日期时使用 `getBkByDate` + + + +| 参数 | 类型 | 说明 | +| ------------ | ------ | ---------------------------------------- | +| `marketlist` | string | 市场板块列表,多个市场板块之间用分号分隔 | + +返回:array of string + +### 示例 + +范例01:获取多个市场板块的当前成分 + +```tsl +stocks := getBk('上证A股;创业板'); +return stocks; +``` + +范例02:获取申万行业分类属性代码目录 + +```tsl +codes := getBk("申万行业"); +return select * from infotable 138 of codes end; +``` + +`getBk("申万行业")` 返回的分类属性代码可作为股票行业分类信息表(表 138)的访问代码。 +具体板块名称应先从板块管理或 `getBkList` 返回结果确认 + +范例03:获取股票和基金的全部分类属性代码 + +```tsl +codes := getBk('属性代码'); +stock_classes := select * from infotable 138 of codes end; +fund_classes := select * from infotable 355 of codes end; +return stock_classes union fund_classes; +``` + +## `getUserBkList([bktype])` + +声明:function + +得到所有的或指定板块类别下的用户板块名称。`bktype` 省略时返回全部用户板块;指定时可 +使用地域或行业目录,也可以用反斜杠分隔多级目录 + + + +| 参数 | 类型 | 说明 | +| -------- | ------ | -------------------------------------------------------- | +| `bktype` | string | 可选。板块类别或目录路径,例如 `地域`、`行业` 或多级目录 | + +返回:array of string + +### 示例 + +范例01:获取全部用户板块名称 + +```tsl +my_sectors := getUserBkList(); +return my_sectors; +``` + +范例02:获取用户板块的指定子目录 + +```tsl +my_sectors := getUserBkList("申万行业\\申万金融业"); +return my_sectors; +``` + +## `getBkList(Type)` + +声明:function + +得到所有的或指定系统板块类别下的板块名称列表。`Type` 是板块类别或具体板块目录名称; +官方定义同时说明省略该参数时返回全部板块 + + + +| 参数 | 类型 | 说明 | +| ------ | ------ | ----------------------------------- | +| `Type` | string | 板块类别或目录名称,例如 `申万行业` | + +返回:array of string + +### 示例 + +范例01:获取申万行业列表 + +```tsl +bk_list := getBkList('申万行业'); +return bk_list; +``` + +范例02:获取申万行业的子目录列表 + +```tsl +bk_list2 := getBkList('申万行业\\申万采掘'); +return bk_list2; +``` + +## `getUserBkList2([bktype])` + +声明:function + +得到所有的或指定板块类别下用户板块的名称及其所在路径。与 `getUserBkList` 不同, +该函数会包含指定目录及其子目录下的用户板块 + + + +| 参数 | 类型 | 说明 | +| -------- | ------ | ---------------------------------------------- | +| `bktype` | string | 可选。板块类别或目录路径,多级目录用反斜杠分隔 | + +返回:table_array + +### 示例 + +范例01:获取申万行业目录下的用户板块及所属组别 + +```tsl +my_sectors := getUserBkList2("申万行业"); +return my_sectors; +``` + +## `getBkList2([bktype])` + +声明:function + +得到所有的或指定系统板块类别下的板块名称及其所在路径。与 `getBkList` 不同,该函数会 +包含指定目录及其子目录下的系统板块 + + + +| 参数 | 类型 | 说明 | +| -------- | ------ | ------------------------------------- | +| `bktype` | string | 可选。板块类别或目录名称,例如 `行业` | + +返回:table_array + +### 示例 + +范例01:获取全部系统板块及其路径 + +```tsl +return getBkList2(); +``` + +范例02:获取系统申万行业板块及其路径 + +```tsl +return getBkList2('申万行业'); +``` diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/index-index_info.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/index-index_info.md index ad9e3d52..cfc9c4d1 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/index-index_info.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/index-index_info.md @@ -67,13 +67,15 @@ return GetBKBacktrackByDate("SWHY740000", 20140805T); 声明:function -获取指数历史指定日的成份股列表。与GetBK不同,该函数需要的第一个参数是指数代码,获取该指数的指定日的成分股,比如沪深300定期调整,在不同时间获取的成分股将不同。 新增功能,当ExType=1时指数成分中取不到成分时,则从权重表中获取 +获取指数或分类属性在指定日的成份股列表。与getBk不同,该函数需要日期参数;普通指数使用 +指数代码,行业或基金分类使用分类属性代码。新增功能,当ExType=1时指数成分中取不到成分时, +则从权重表中获取 - + | 参数 | 类型 | 说明 | | ---------- | -------- | ------------------------------------------------------------------------------------- | -| `index_id` | string | 指数代码 | +| `index_id` | string | 指数代码、分类属性代码或 TSI 市场板块代码 | | `end_t` | datetime | 截止日 | | `extype` | int | 整型,是否扩展,默认不扩展;0:不扩展;1:扩展取数(指数成分表无数据时,从权重表中获取) | @@ -95,6 +97,37 @@ Return GetBkByDate('SH000300', inttodate(20120908)); return GetBKByDate("SZ399102", 20201201T, 1); // 取权重中的成份股替代 ``` +范例03:取指定日申万分类的下级属性代码 + +```tsl +return GetBKByDate("SWHY.N", 20210423T, 0); +``` + +范例04:取指定日申万分类的成份股 + +```tsl +return GetBKByDate("SWHY740000", 20220101T, 0); +``` + +范例05:取指定日 A 股市场板块的成份股 + +```tsl +return GetBKByDate("TSI000001", 20230810T, 0); +``` + +范例06:取指定日投资风格分类下的基金 + +```tsl +return GetBKByDate("TSJJ0201", 20220101T, 0); +``` + +`index_id` 使用分类属性代码时,代码末尾的 `.N` 表示返回该分类节点在指定日的下级 +分类属性代码;不带 `.N` 时返回该分类节点的成份券。常用根分类包括股票的 `SWHY`、 +`CAPCHY`、`CSRCHY`,以及基金的 `YHFL`、`TSJJ02`、`TSJJ03`。分类目录信息和分类归属 +信息分别见股票表 138、139 及基金表 355、356;不能把分类属性代码当作个券代码传给归属表。 +级数为 0 的根代码本身没有成份券;要先用根代码加 `.N` 取得一级分类代码。`CSRCHY` +自 2024-02-19 起不再维护,此后使用 `CAPCHY`。 + ## `getBkByIndexName(index_name, end_t)` 声明:function diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/sector-basic_info-miscellaneous.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/sector-basic_info-miscellaneous.md index 4455d565..215b991b 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/sector-basic_info-miscellaneous.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/sector-basic_info-miscellaneous.md @@ -100,7 +100,7 @@ return isValueBetweeMaxAndMin(t, 'close', 0, 5); 声明:function -返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数GetBKList,使用方法参见该函数的函数说明 +返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数getBkList,使用方法参见该函数的函数说明 diff --git a/skills/tsl-api-reference/references/codegen/dotnet/financial/stock-industry.md b/skills/tsl-api-reference/references/codegen/dotnet/financial/stock-industry.md index d4cbe983..e3aca812 100644 --- a/skills/tsl-api-reference/references/codegen/dotnet/financial/stock-industry.md +++ b/skills/tsl-api-reference/references/codegen/dotnet/financial/stock-industry.md @@ -146,7 +146,7 @@ Return GetHyDyFlDetails('行业'); 声明:function -获取行业/地域/分类的列表,用于替代GetBKList +获取行业/地域/分类的列表,用于替代getBkList @@ -168,7 +168,7 @@ Return GetHyDyFlList('行业'); 声明:function -获取行业的股票。取代GetBK,比其更精确 +获取行业的股票。取代getBk,比其更精确 diff --git a/skills/tsl-api-reference/scripts/dictionary_lookup.py b/skills/tsl-api-reference/scripts/dictionary_lookup.py index 1e181d31..d003d8f5 100644 --- a/skills/tsl-api-reference/scripts/dictionary_lookup.py +++ b/skills/tsl-api-reference/scripts/dictionary_lookup.py @@ -6,16 +6,29 @@ from __future__ import annotations import argparse import csv import json +import re import sys import unicodedata from dataclasses import dataclass from pathlib import Path from typing import Iterable, Mapping, Sequence +SCRIPT_DIR = Path(__file__).resolve().parent +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +from value_domains import ( # noqa: E402 + RelatedTable, + ValueDomainCatalog, + ValueMatch, + domains_path_for_index, +) + SKILL_ROOT = Path(__file__).resolve().parents[1] DEFAULT_TSV = SKILL_ROOT / "data" / "dictionary_index.tsv" DEFAULT_LEXICON = SKILL_ROOT / "data" / "dictionary_lexicon.json" +DEFAULT_VALUE_DOMAINS = SKILL_ROOT / "data" / "value_domains.json" REQUIRED_COLUMNS = ( "kind", "scope", @@ -36,20 +49,24 @@ REQUIRED_COLUMNS = ( ) HELP_EPILOG = """\ -按中文业务含义查询表、数据源或字段: +按中文业务含义查询表、数据源、字段或参数取值域: dictionary_lookup.py --query "股票现金流指标销售现金比率" dictionary_lookup.py --query "市值" --scope fund dictionary_lookup.py --query "买一量" --table TradeTable dictionary_lookup.py --query "9900700" --field 销售现金比率 + dictionary_lookup.py --query "申万煤炭" 输出第一行是 in-band 状态: status: ok 最高分候选唯一 status: ambiguous 多个同分候选;必须补充 scope、table 或 field 范围 - status: no_match 数据字典无匹配,不等于 API not found + status: no_match 表/字段/参数取值域均无匹配,不等于 API not found + +参数取值域结果会标明值、名称、绑定 API、关联表和来源;动态目录只提供已知样例, +完整名称必须使用结果中的运行时解析器重新确认。 退出码: 0 查询完成;ok、ambiguous、no_match 均由 status 表达 - 1 dictionary_index.tsv 或词表缺失、损坏、格式错误 + 1 dictionary_index.tsv、词表或 value_domains.json 缺失、损坏、格式错误 2 参数不合法 """ @@ -73,10 +90,47 @@ class QueryItem: score: int +@dataclass(frozen=True) +class ValueDomainItem: + kind: str + domain_id: str + domain_label: str + value: str + label: str + parent: str + parent_label: str + level: int | None + valid_from: str + valid_to: str + note: str + related_tables: tuple[str, ...] + bindings: tuple[str, ...] + mode: str + complete: bool + as_of: str + sources: tuple[str, ...] + resolver_text: str + + +@dataclass(frozen=True) +class ValueDomainSummaryItem: + kind: str + domain_id: str + label: str + related_tables: tuple[str, ...] + bindings: tuple[str, ...] + mode: str + complete: bool + as_of: str + sources: tuple[str, ...] + resolvers: tuple[str, ...] + candidates: tuple[str, ...] + + @dataclass(frozen=True) class QueryResult: status: str - items: tuple[QueryItem, ...] + items: tuple[QueryItem | ValueDomainItem | ValueDomainSummaryItem, ...] @dataclass(frozen=True) @@ -229,6 +283,205 @@ def _item(scored: _ScoredRow) -> QueryItem: ) +def _value_item( + catalog: ValueDomainCatalog, + match: ValueMatch, + related_tables: Sequence[RelatedTable] | None = None, +) -> ValueDomainItem: + value = match.value + hints = catalog.api_hints((match,)) + bindings = tuple(dict.fromkeys(hint.target for hint in hints)) + verification = [] + for hint in hints: + if not hint.verification: + continue + if hint.verification.status == "runtime_required": + call = hint.verification.resolver + if hint.verification.argument: + call += f"('{hint.verification.argument}')" + verification.append(f"{call}(运行时)") + elif hint.verification.status == "documented": + verification.append("文档已确认") + resolver_text = "; ".join(dict.fromkeys(verification)) + table = match.domain.related_table(value.related_table) + selected_tables = tuple(related_tables) if related_tables is not None else () + if not selected_tables: + selected_tables = (table,) if table else match.domain.related_tables + rendered_tables = tuple( + f"{related.table_id} {related.name}" + for related in selected_tables + if related is not None + ) + return ValueDomainItem( + kind="parameter_value", + domain_id=match.domain.domain_id, + domain_label=match.domain.label, + value=value.value, + label=value.label, + parent=value.parent, + parent_label=value.parent_label, + level=value.level, + valid_from=value.valid_from, + valid_to=value.valid_to, + note=value.note, + related_tables=rendered_tables, + bindings=bindings, + mode=match.domain.mode, + complete=match.domain.complete, + as_of=value.as_of or match.domain.as_of, + sources=value.sources, + resolver_text=resolver_text, + ) + + +def _domain_summary_items( + catalog: ValueDomainCatalog, + query: str, + *, + scope: str | None, + table: str | None, + limit: int, +) -> tuple[ValueDomainSummaryItem, ...]: + scope_key = _compact(scope or "") + table_key = _compact(table or "") + items = [] + for domain in catalog.exact_domains(query): + related_tables = domain.related_tables + if scope_key: + related_tables = tuple( + related + for related in related_tables + if _compact(related.scope) == scope_key + ) + if table_key: + related_tables = tuple( + related + for related in related_tables + if table_key in _compact(related.name) + or table_key in _compact(related.table_id) + ) + if (scope_key or table_key) and not related_tables: + continue + bindings = tuple( + f"{binding.api}.{binding.parameter}" for binding in domain.bindings + ) + resolvers = [] + for resolver in domain.resolvers: + catalog_label = { + "system": "系统", + "user": "用户", + }.get(resolver.catalog, resolver.catalog) + resolvers.append( + f"{resolver.api}({resolver.parameter})({catalog_label}目录)" + ) + candidates = tuple( + value if not label or label == value else f"{value}({label})" + for value, label in catalog.recorded_candidates(domain) + ) + items.append( + ValueDomainSummaryItem( + kind="parameter_domain", + domain_id=domain.domain_id, + label=domain.label, + related_tables=tuple( + f"{related.table_id} {related.name}" for related in related_tables + ), + bindings=bindings, + mode=domain.mode, + complete=domain.complete, + as_of=domain.as_of, + sources=domain.sources, + resolvers=tuple(resolvers), + candidates=candidates, + ) + ) + return tuple(items[:limit]) + + +def _value_matches( + catalog: ValueDomainCatalog, + query: str, + *, + scope: str | None, + table: str | None, + field: str | None, + limit: int, + strong_only: bool, +) -> tuple[ValueDomainItem, ...]: + if field: + return () + query_terms = tuple(query.split()) + matches = catalog.search(query_terms) if len(query_terms) > 1 else () + if not matches: + matches = catalog.search(query) + if re.fullmatch(r"\s*-?\d{1,2}\s*", query): + matches = tuple( + match + for match in matches + if not re.fullmatch(r"-?\d{1,2}", match.value.value) + ) + if strong_only: + query_key = _compact(query) + intent_markers = ("代码", "分类", "属性", "取值", "枚举") + matches = tuple( + match + for match in matches + if match.exact + or ( + len(_compact(match.value.value)) >= 4 + and any( + character.isascii() and character.isalpha() + for character in match.value.value + ) + and any(character.isdigit() for character in match.value.value) + and _compact(match.value.value) in query_key + ) + or ( + match.domain.mode == "versioned_catalog" + and _compact(match.value.label) in query_key + and any(marker in query for marker in intent_markers) + ) + or ( + match.domain.mode == "enum" + and _compact(match.domain.label) in query_key + and _compact(match.value.value) in query_key + ) + ) + filtered = [] + scope_key = _compact(scope or "") + table_key = _compact(table or "") + for match in matches: + related_tables = match.domain.related_tables + if match.value.related_table: + related = match.domain.related_table(match.value.related_table) + related_tables = (related,) if related else () + if scope_key: + related_tables = tuple( + related + for related in related_tables + if _compact(related.scope) == scope_key + ) + if table_key: + related_tables = tuple( + related + for related in related_tables + if table_key in _compact(related.name) + or table_key in _compact(related.table_id) + ) + if (scope_key or table_key) and not related_tables: + continue + filtered.append((match, related_tables)) + matches_with_tables = tuple(filtered) + if not matches_with_tables: + return () + top_score = matches_with_tables[0][0].score + top = [item for item in matches_with_tables if item[0].score == top_score] + return tuple( + _value_item(catalog, match, related_tables) + for match, related_tables in top[:limit] + ) + + def query_index( rows: Iterable[Mapping[str, str]], query: str, @@ -243,6 +496,7 @@ def query_index( raise ValueError("query must not be empty") if limit < 1: raise ValueError("limit must be at least 1") + rows = list(rows) variants = _query_variants(query, lexicon or {}) scope_key = _compact(scope or "") scored = [] @@ -294,6 +548,26 @@ def query_index( return QueryResult(status, items) +def _direct_dictionary_items(result: QueryResult, query: str) -> tuple[QueryItem, ...]: + query_key = _compact(query) + if not query_key: + return () + direct = [] + for item in result.items: + if not isinstance(item, QueryItem): + continue + names = (_compact(item.table_name), _compact(item.field_name)) + identifiers = (_compact(item.table_id), _compact(item.field_id)) + access_code = _compact(item.access_code) + if ( + any(name and (query_key == name or name in query_key) for name in names) + or any(identifier and query_key == identifier for identifier in identifiers) + or (len(query_key) >= 3 and query_key in access_code) + ): + direct.append(item) + return tuple(direct) + + def _render_item(index: int, item: QueryItem) -> list[str]: return [ f"[{index}]", @@ -314,14 +588,66 @@ def _render_item(index: int, item: QueryItem) -> list[str]: ] +def _render_value_item(index: int, item: ValueDomainItem) -> list[str]: + validity = "" + if item.valid_from or item.valid_to: + validity = f"{item.valid_from or '不限'} 至 {item.valid_to or '今'}" + parent = item.parent or "无" + if item.parent_label: + parent = f"{parent}({item.parent_label})" + return [ + f"[{index}]", + "类型:参数取值", + f"参数域:{item.domain_id}({item.domain_label})", + f"取值:{item.value}", + f"名称:{item.label}", + f"父级:{parent}", + f"层级:{item.level if item.level is not None else ''}", + f"有效期:{validity}", + f"备注:{item.note}", + f"关联表:{'; '.join(item.related_tables)}", + f"绑定 API:{'; '.join(item.bindings)}", + f"模式:{item.mode}", + f"完整性:{'完整' if item.complete else '静态记录不完整'}", + f"快照日期:{item.as_of}", + f"核验:{item.resolver_text}", + f"来源:{', '.join(item.sources)}", + ] + + +def _render_domain_summary_item( + index: int, item: ValueDomainSummaryItem +) -> list[str]: + return [ + f"[{index}]", + "类型:参数取值域", + f"参数域:{item.domain_id}", + f"名称:{item.label}", + f"关联表:{'; '.join(item.related_tables)}", + f"绑定 API:{'; '.join(item.bindings)}", + f"模式:{item.mode}", + f"完整性:{'完整' if item.complete else '静态记录不完整'}", + f"快照日期:{item.as_of}", + f"运行时解析器:{'; '.join(item.resolvers)}", + f"已记录值或候选:{'; '.join(item.candidates)}", + f"来源:{', '.join(item.sources)}", + ] + + def render_result(result: QueryResult) -> str: lines = [f"status: {result.status}"] if result.status == "no_match": lines.append("未找到匹配的数据字典条目") elif result.status == "ambiguous": - lines.append("存在多个同分候选,请补充表或数据源范围") + lines.append("存在多个同分候选,请补充 scope、表、数据源或字段范围") for index, item in enumerate(result.items, start=1): - lines.extend(["", *_render_item(index, item)]) + if isinstance(item, ValueDomainSummaryItem): + renderer = _render_domain_summary_item + elif isinstance(item, ValueDomainItem): + renderer = _render_value_item + else: + renderer = _render_item + lines.extend(["", *renderer(index, item)]) return "\n".join(lines) + "\n" @@ -337,7 +663,7 @@ def _positive_int(value: str) -> int: def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="查询独立的天软表、数据源与字段字典索引。", + description="查询天软表、数据源、字段及 API 参数取值域。", epilog=HELP_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False, @@ -351,15 +677,31 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument( "--lexicon", type=Path, default=DEFAULT_LEXICON, help="受控同义词词表路径" ) + parser.add_argument( + "--value-domains", + type=Path, + help="显式指定 value_domains.json;默认使用所选索引同目录的数据," + "自定义索引旁没有该文件时不加载参数取值域", + ) return parser.parse_args(argv) def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) + domain_path = ( + args.value_domains + if args.value_domains + else domains_path_for_index( + args.tsv, + default_index=DEFAULT_TSV, + default_domains=DEFAULT_VALUE_DOMAINS, + ) + ) try: rows = load_rows(args.tsv) lexicon = load_lexicon(args.lexicon) - result = query_index( + catalog = ValueDomainCatalog.load(domain_path) if domain_path else None + dictionary_result = query_index( rows, args.query, scope=args.scope, @@ -368,6 +710,59 @@ def main(argv: Sequence[str] | None = None) -> int: limit=args.limit, lexicon=lexicon, ) + domain_items = ( + _domain_summary_items( + catalog, + args.query, + scope=args.scope, + table=args.table, + limit=args.limit, + ) + if catalog and not args.field + else () + ) + value_items = ( + _value_matches( + catalog, + args.query, + scope=args.scope, + table=args.table, + field=args.field, + limit=args.limit, + strong_only=True, + ) + if catalog + else () + ) + if domain_items: + result = QueryResult( + "ambiguous" if len(domain_items) > 1 else "ok", + domain_items, + ) + elif value_items: + dictionary_items = _direct_dictionary_items(dictionary_result, args.query) + combined = (*value_items, *dictionary_items)[: args.limit] + result = QueryResult( + "ambiguous" if len(value_items) > 1 or dictionary_items else "ok", + combined, + ) + else: + result = dictionary_result + if result.status == "no_match" and catalog: + fallback_items = _value_matches( + catalog, + args.query, + scope=args.scope, + table=args.table, + field=args.field, + limit=args.limit, + strong_only=False, + ) + if fallback_items: + result = QueryResult( + "ambiguous" if len(fallback_items) > 1 else "ok", + fallback_items, + ) except (OSError, ValueError, json.JSONDecodeError) as error: print(f"ERROR: {error}", file=sys.stderr) return 1 diff --git a/skills/tsl-api-reference/scripts/lookup.py b/skills/tsl-api-reference/scripts/lookup.py index 85a0f4ba..6f498f76 100644 --- a/skills/tsl-api-reference/scripts/lookup.py +++ b/skills/tsl-api-reference/scripts/lookup.py @@ -6,6 +6,16 @@ import sys import unicodedata from pathlib import Path +SCRIPT_DIR = Path(__file__).resolve().parent +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +from value_domains import ( # noqa: E402 + ApiHint, + ValueDomainCatalog, + domains_path_for_index, +) + TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$") PLAIN_H2_RE = re.compile(r"^##(?!#)\s+") BARE_ENTRY_RE = re.compile(r"^(#{3,4})(?!#)\s+`(.+?)`\s*$") @@ -26,6 +36,7 @@ CLASS_MEMBER_LABELS = { UNIT_DIRECT_LABELS = {"function", "var", "const", "class"} SKILL_ROOT = Path(__file__).resolve().parents[1] DEFAULT_TSV = SKILL_ROOT / "data" / "function_index.tsv" +DEFAULT_VALUE_DOMAINS = SKILL_ROOT / "data" / "value_domains.json" DEFAULT_CODEGEN_ROOT = SKILL_ROOT / "references" / "codegen" REQUIRED_COLUMNS = ( "name", @@ -77,13 +88,14 @@ HELP_EPILOG = """\ 不存在的 scope 会列出当前索引实际提供的值并返回 2。 结果: - --kw 输出候选摘要表;选定后仍须运行 --name + --kw 输出候选摘要表;参数取值域可补充召回相关 API,选定后仍须运行 --name --name 输出完整条目正文和 scope/module、page#anchor 来源标记; - 简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回 + 简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回; + 若该 API 有参数取值域,同时显示动态目录解析器和已核实值边界 退出码: 0 查询完成;--name 无匹配和 --kw 零候选也通过正文提示表达 - 1 function_index.tsv 缺失、格式错误,或候选指向的条目正文找不到 + 1 function_index.tsv/value_domains.json 缺失、格式错误,或候选指向的条目正文找不到 2 参数不合法(缺少动作、空值、--limit 小于 1、使用短选项或缩写) """ @@ -366,6 +378,91 @@ def format_row(row): ) +def row_identity(row): + return ( + normalize(row.get("qualified_name", "") or row.get("name", "")), + normalize(row.get("signature", "")), + normalize(row.get("scope", "")), + row.get("page", ""), + row.get("anchor", ""), + ) + + +def signature_parameters(signature): + match = re.search(r"\((.*)\)", signature) + if not match: + return set() + parameters = set() + for raw_parameter in match.group(1).split(","): + parameter = raw_parameter.strip().strip("[]").strip() + parameter = re.sub(r"^(?:const|out|var)\s+", "", parameter, flags=re.I) + parameter = parameter.split("=", maxsplit=1)[0].strip() + parameter = parameter.split(":", maxsplit=1)[0].strip() + if parameter: + parameters.add(normalize(parameter)) + return parameters + + +def rows_for_hint(rows, hint): + api = normalize(hint.api) + parameter = normalize(hint.parameter) + return [ + row + for row in rows + if api + in { + normalize(row.get("name", "")), + normalize(row.get("qualified_name", "")), + } + and normalize(row.get("scope", "")) == normalize(hint.scope) + and normalize(row.get("module", "")) == normalize(hint.module) + and parameter in signature_parameters(row.get("signature", "")) + ] + + +def merge_keyword_matches(rows, normal_matches, hints): + merged = [] + annotations = {} + seen = set() + for hint in hints: + for row in rows_for_hint(rows, hint): + key = row_identity(row) + annotations.setdefault(key, []).append(hint) + if key not in seen: + seen.add(key) + merged.append(row) + for row in normal_matches: + key = row_identity(row) + if key not in seen: + seen.add(key) + merged.append(row) + return merged, annotations + + +def format_api_hint(hint: ApiHint) -> str: + parts = [f" 参数取值域:{hint.target}", hint.summary()] + verification = hint.verification_text() + if verification: + parts.append(verification) + if hint.sources: + parts.append(f"来源:{', '.join(hint.sources)}") + return " · ".join(parts) + + +def top_value_domain_matches(matches, *, score_window=50): + matches = tuple(matches) + if not matches: + return () + minimum_score = matches[0].score - score_window + return tuple(match for match in matches if match.score >= minimum_score) + + +def value_domain_keyword_matches(catalog, terms): + if len(terms) == 1 and re.fullmatch(r"\s*-?\d{1,2}\s*", terms[0]): + return () + return top_value_domain_matches(catalog.search(terms)) + + def codegen_root_for_tsv(tsv_path): if tsv_path.parent.name == "data": candidate = tsv_path.parent.parent / "references" / "codegen" @@ -423,6 +520,12 @@ def main(argv=None): help="显式指定 function_index.tsv;默认只使用本 skill 内置的 " "data/function_index.tsv,不从工作目录回退查找旧索引", ) + parser.add_argument( + "--value-domains", + metavar="PATH", + help="显式指定 value_domains.json;默认使用所选索引同目录的数据," + "自定义索引旁没有该文件时不加载参数取值域", + ) parser.add_argument( "--scope", type=non_empty, @@ -451,11 +554,25 @@ def main(argv=None): ) return 1 + domain_path = ( + Path(args.value_domains) + if args.value_domains + else domains_path_for_index( + tsv_path, + default_index=DEFAULT_TSV, + default_domains=DEFAULT_VALUE_DOMAINS, + ) + ) try: rows = load_rows(tsv_path) except (OSError, UnicodeError, ValueError) as error: print(f"ERROR: failed to load {tsv_path}: {error}", file=sys.stderr) return 1 + try: + catalog = ValueDomainCatalog.load(domain_path) if domain_path else None + except (OSError, UnicodeError, ValueError) as error: + print(f"ERROR: failed to load {domain_path}: {error}", file=sys.stderr) + return 1 if not rows: print( f"ERROR: {tsv_path} has no entries; reinstall tsl-api-reference " @@ -499,17 +616,34 @@ def main(argv=None): entries.append((row, body)) for row, body in entries: print(body) + if catalog: + domain_text = catalog.describe_api( + row.get("qualified_name", "") or row.get("name", ""), + scope=row.get("scope", ""), + module=row.get("module", ""), + parameters=signature_parameters(row.get("signature", "")), + ) + if domain_text: + print(f"\n{domain_text}", end="") print( f"\n\n" ) return 0 - matches = search_keyword(rows, args.kw) + normal_matches = search_keyword(rows, args.kw) + hints = ( + catalog.api_hints(value_domain_keyword_matches(catalog, args.kw)) + if catalog + else () + ) + matches, annotations = merge_keyword_matches(rows, normal_matches, hints) print(f"# {len(matches)} matches for: {' '.join(args.kw)}") print("qualified_name\tdeclaration\tsignature\tpage#anchor\tsummary") for row in matches[: args.limit]: print(format_row(row)) + for hint in annotations.get(row_identity(row), ()): + print(format_api_hint(hint)) if len(matches) > args.limit: print(f"... {len(matches) - args.limit} more; refine keywords or raise --limit") if not matches: diff --git a/skills/tsl-api-reference/scripts/value_domains.py b/skills/tsl-api-reference/scripts/value_domains.py new file mode 100644 index 00000000..f4f10e60 --- /dev/null +++ b/skills/tsl-api-reference/scripts/value_domains.py @@ -0,0 +1,737 @@ +#!/usr/bin/env python3 +"""Load and query bundled TSL API parameter value domains.""" + +from __future__ import annotations + +import json +import re +import unicodedata +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, Sequence + + +DOMAIN_MODES = { + "catalog", + "enum", + "pattern", + "runtime_catalog", + "versioned_catalog", +} +DOMAIN_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") + + +def _fold(value: str) -> str: + normalized = unicodedata.normalize("NFKC", value).casefold() + return "".join( + character + for character in normalized + if character in {"_", "."} + or unicodedata.category(character)[0] in {"L", "N"} + ) + + +def _fold_value(value: str) -> str: + """Normalize a domain value without conflating negative and positive numbers.""" + normalized = unicodedata.normalize("NFKC", value).casefold() + return "".join( + character + for character in normalized + if character in {"-", "_", "."} + or unicodedata.category(character)[0] in {"L", "N"} + ) + + +def _required_string(value: object, where: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{where} must be a non-empty string") + return value.strip() + + +def _optional_string(value: object, where: str) -> str: + if value is None: + return "" + if not isinstance(value, str): + raise ValueError(f"{where} must be a string") + return value.strip() + + +def _string_list(value: object, where: str, *, required: bool = False) -> tuple[str, ...]: + if not isinstance(value, list): + raise ValueError(f"{where} must be a list") + items = tuple(_required_string(item, f"{where}[]") for item in value) + if required and not items: + raise ValueError(f"{where} must not be empty") + if len({_fold(item) for item in items}) != len(items): + raise ValueError(f"{where} contains duplicate values") + return items + + +def _mapping(value: object, where: str) -> dict: + if not isinstance(value, dict): + raise ValueError(f"{where} must be an object") + return value + + +def _list(value: object, where: str) -> list: + if not isinstance(value, list): + raise ValueError(f"{where} must be a list") + return value + + +def _reject_unknown(value: dict, allowed: set[str], where: str) -> None: + unknown = sorted(set(value) - allowed) + if unknown: + raise ValueError(f"{where} contains unknown fields: {', '.join(unknown)}") + + +@dataclass(frozen=True) +class Binding: + scope: str + module: str + api: str + parameter: str + role: str + + +@dataclass(frozen=True) +class Resolver: + scope: str + module: str + api: str + parameter: str + catalog: str + source: str + + +@dataclass(frozen=True) +class RelatedTable: + table_id: str + name: str + scope: str + page: str + + +@dataclass(frozen=True) +class Verification: + status: str + scope: str + module: str + resolver: str + parameter: str + argument: str + + +@dataclass(frozen=True) +class Relation: + kind: str + domain_id: str + value: str + verification: Verification + sources: tuple[str, ...] + + +@dataclass(frozen=True) +class DomainValue: + value: str + label: str + aliases: tuple[str, ...] + parent: str + parent_label: str + level: int | None + valid_from: str + valid_to: str + note: str + related_table: str + as_of: str + sources: tuple[str, ...] + relations: tuple[Relation, ...] + + +@dataclass(frozen=True) +class ValueDomain: + domain_id: str + label: str + mode: str + complete: bool + bindings: tuple[Binding, ...] + resolvers: tuple[Resolver, ...] + related_tables: tuple[RelatedTable, ...] + values: tuple[DomainValue, ...] + sources: tuple[str, ...] + as_of: str + + def related_table(self, table_id: str) -> RelatedTable | None: + return next( + (table for table in self.related_tables if table.table_id == table_id), + None, + ) + + +@dataclass(frozen=True) +class ValueMatch: + domain: ValueDomain + value: DomainValue + score: int + exact: bool + + +@dataclass(frozen=True) +class ApiHint: + scope: str + module: str + api: str + parameter: str + role: str + domain: ValueDomain + value: str + label: str + verification: Verification | None + sources: tuple[str, ...] + + @property + def target(self) -> str: + return f"{self.api}.{self.parameter}" + + def summary(self) -> str: + if self.role == "historical_components": + text = f"历史分类代码:{self.value}" + if self.label and self.label != self.value: + text += f"({self.label})" + return text + if self.role == "historical_market_components": + text = f"历史市场板块代码:{self.value}" + if self.label and self.label != self.value: + text += f"({self.label})" + return text + if self.role == "historical_named_components": + return f"历史市场板块名:{self.value}" + if self.role == "current_components": + prefix = ( + "当前板块候选" + if self.verification + and self.verification.status == "runtime_required" + else "当前板块名" + ) + return f"{prefix}:{self.value}" + if self.role == "catalog_selector": + return f"目录选择器:{self.value}" + if self.role == "child_codes": + return f"下级分类代码模式:{self.value}" + text = f"参数值:{self.value}" + if self.label and self.label != self.value: + text += f"({self.label})" + return text + + def verification_text(self) -> str: + if not self.verification: + return "" + call = self.verification.resolver + if self.verification.argument: + call += f"('{self.verification.argument}')" + if self.verification.status == "runtime_required": + return f"运行时核验:{call}" + if self.verification.status == "documented": + return "文档已确认" + return self.verification.status + + +class ValueDomainCatalog: + def __init__(self, domains: Sequence[ValueDomain]): + self.domains = tuple(domains) + self._by_id = {domain.domain_id: domain for domain in self.domains} + + @classmethod + def load(cls, path: Path) -> "ValueDomainCatalog": + return cls(_parse_document(json.loads(path.read_text(encoding="utf-8")))) + + def search(self, terms: str | Iterable[str]) -> tuple[ValueMatch, ...]: + raw_terms = [terms] if isinstance(terms, str) else list(terms) + normalized_terms = tuple( + _fold_value(term) for term in raw_terms if _fold_value(term) + ) + if not normalized_terms: + return () + + matches = [] + for domain in self.domains: + for value in domain.values: + scored = _score_value(domain, value, normalized_terms) + if scored: + score, exact = scored + matches.append(ValueMatch(domain, value, score, exact)) + matches.sort( + key=lambda match: ( + -match.score, + _fold(match.domain.domain_id), + _fold(match.value.value), + ) + ) + return tuple(matches) + + def exact_domains(self, query: str) -> tuple[ValueDomain, ...]: + query_key = _fold_value(query) + if not query_key: + return () + return tuple( + domain + for domain in self.domains + if query_key + in { + _fold_value(domain.domain_id), + _fold_value(domain.label), + } + ) + + def recorded_candidates( + self, domain: ValueDomain + ) -> tuple[tuple[str, str], ...]: + candidates = [(value.value, value.label) for value in domain.values] + candidates.extend( + (relation.value, value.label) + for source_domain in self.domains + for value in source_domain.values + for relation in value.relations + if relation.domain_id == domain.domain_id + ) + unique = [] + seen = set() + for value, label in candidates: + key = _fold_value(value) + if key in seen: + continue + seen.add(key) + unique.append((value, label)) + return tuple(unique) + + def api_hints(self, matches: Iterable[ValueMatch]) -> tuple[ApiHint, ...]: + hints = [] + seen = set() + for match in matches: + for binding in match.domain.bindings: + hint = ApiHint( + scope=binding.scope, + module=binding.module, + api=binding.api, + parameter=binding.parameter, + role=binding.role, + domain=match.domain, + value=match.value.value, + label=match.value.label, + verification=None, + sources=match.value.sources, + ) + key = _hint_key(hint) + if key not in seen: + seen.add(key) + hints.append(hint) + for relation in match.value.relations: + target = self._by_id[relation.domain_id] + for binding in target.bindings: + hint = ApiHint( + scope=binding.scope, + module=binding.module, + api=binding.api, + parameter=binding.parameter, + role=binding.role, + domain=target, + value=relation.value, + label=match.value.label, + verification=relation.verification, + sources=relation.sources, + ) + key = _hint_key(hint) + if key not in seen: + seen.add(key) + hints.append(hint) + hints.sort( + key=lambda hint: ( + _fold(hint.api), + _fold(hint.parameter), + hint.summary(), + ) + ) + return tuple(hints) + + def describe_api( + self, + api: str, + *, + scope: str = "", + module: str = "", + parameters: Iterable[str] | None = None, + ) -> str: + api_key = _fold(api) + scope_key = _fold(scope) + module_key = _fold(module) + parameter_keys = ( + {_fold(parameter) for parameter in parameters} + if parameters is not None + else None + ) + bound = [ + (domain, binding) + for domain in self.domains + for binding in domain.bindings + if _fold(binding.api) == api_key + and (not scope_key or _fold(binding.scope) == scope_key) + and (not module_key or _fold(binding.module) == module_key) + and ( + parameter_keys is None + or _fold(binding.parameter) in parameter_keys + ) + ] + if not bound: + return "" + + lines = ["### 参数取值域", ""] + for domain, binding in bound: + completeness = "完整" if domain.complete else "静态记录不完整" + lines.append( + f"- `{binding.parameter}` -> `{domain.domain_id}`({domain.label};" + f"`{domain.mode}`,{completeness})" + ) + lines.append(f" - 快照日期:`{domain.as_of}`") + sources = ", ".join(f"`{source}`" for source in domain.sources) + lines.append(f" - 来源:{sources}") + for resolver in domain.resolvers: + catalog = {"system": "系统", "user": "用户"}.get( + resolver.catalog, resolver.catalog + ) + lines.append( + f" - 运行时解析器:`{resolver.api}`({catalog}目录;" + f"来源 `{resolver.source}`)" + ) + recorded_count = len(self.recorded_candidates(domain)) + if domain.complete and domain.mode in {"enum", "pattern"}: + for value in domain.values: + label = f" — {value.label}" if value.label != value.value else "" + note = f";{value.note}" if value.note else "" + lines.append(f" - `{value.value}`{label}{note}") + else: + lines.append(f" - 已记录值或候选:{recorded_count} 条(此处不展开)") + lines.append( + " - 按值查询:`dictionary_lookup.py --query " + "\"<域 ID、名称、代码或模式>\"`" + ) + return "\n".join(lines) + "\n" + + +def domains_path_for_index( + index_path: Path, *, default_index: Path, default_domains: Path +) -> Path | None: + if index_path.parent.name == "data": + sibling = index_path.parent / default_domains.name + if sibling.is_file(): + return sibling + try: + is_default = index_path.resolve() == default_index.resolve() + except OSError: + is_default = index_path == default_index + return default_domains if is_default else None + + +def _parse_document(document: object) -> tuple[ValueDomain, ...]: + root = _mapping(document, "value domain document") + _reject_unknown(root, {"version", "domains"}, "value domain document") + if root.get("version") != 1: + raise ValueError("value domain document version must be 1") + domain_items = _list(root.get("domains"), "domains") + if not domain_items: + raise ValueError("domains must not be empty") + domains = tuple( + _parse_domain(item, f"domains[{index}]") + for index, item in enumerate(domain_items) + ) + ids = [domain.domain_id for domain in domains] + if len(set(ids)) != len(ids): + raise ValueError("domains contains duplicate ids") + known = set(ids) + by_id = {domain.domain_id: domain for domain in domains} + for domain in domains: + table_ids = {table.table_id for table in domain.related_tables} + for value in domain.values: + if value.related_table and value.related_table not in table_ids: + raise ValueError( + f"domain {domain.domain_id!r} value {value.value!r} references " + f"unknown related table {value.related_table!r}" + ) + for relation in value.relations: + if relation.domain_id not in known: + raise ValueError( + f"domain {domain.domain_id!r} value {value.value!r} references " + f"unknown domain {relation.domain_id!r}" + ) + target = by_id[relation.domain_id] + verification_key = ( + _fold(relation.verification.scope), + _fold(relation.verification.module), + _fold(relation.verification.resolver), + _fold(relation.verification.parameter), + ) + resolver_keys = { + ( + _fold(resolver.scope), + _fold(resolver.module), + _fold(resolver.api), + _fold(resolver.parameter), + ) + for resolver in target.resolvers + } + if verification_key not in resolver_keys: + raise ValueError( + f"domain {domain.domain_id!r} value {value.value!r} " + f"references a resolver not owned by {relation.domain_id!r}" + ) + return domains + + +def _parse_domain(value: object, where: str) -> ValueDomain: + item = _mapping(value, where) + _reject_unknown( + item, + { + "id", + "label", + "mode", + "complete", + "bindings", + "resolvers", + "related_tables", + "values", + "sources", + "as_of", + }, + where, + ) + domain_id = _required_string(item.get("id"), f"{where}.id") + if not DOMAIN_ID_RE.fullmatch(domain_id): + raise ValueError(f"{where}.id must use lowercase snake_case") + mode = _required_string(item.get("mode"), f"{where}.mode") + if mode not in DOMAIN_MODES: + raise ValueError(f"{where}.mode must be one of {', '.join(sorted(DOMAIN_MODES))}") + complete = item.get("complete") + if not isinstance(complete, bool): + raise ValueError(f"{where}.complete must be a boolean") + if mode == "enum" and not complete: + raise ValueError(f"{where}.complete must be true for mode enum") + if mode == "runtime_catalog" and complete: + raise ValueError(f"{where}.complete must be false for mode runtime_catalog") + bindings = tuple( + _parse_binding(binding, f"{where}.bindings[{index}]") + for index, binding in enumerate(_list(item.get("bindings"), f"{where}.bindings")) + ) + if not bindings: + raise ValueError(f"{where}.bindings must not be empty") + resolvers = tuple( + _parse_resolver(resolver, f"{where}.resolvers[{index}]") + for index, resolver in enumerate(_list(item.get("resolvers"), f"{where}.resolvers")) + ) + if mode == "runtime_catalog" and not resolvers: + raise ValueError(f"{where}.resolvers must not be empty for mode runtime_catalog") + tables = tuple( + _parse_table(table, f"{where}.related_tables[{index}]") + for index, table in enumerate( + _list(item.get("related_tables"), f"{where}.related_tables") + ) + ) + values = tuple( + _parse_value(domain_value, f"{where}.values[{index}]") + for index, domain_value in enumerate(_list(item.get("values"), f"{where}.values")) + ) + value_keys = [_fold_value(domain_value.value) for domain_value in values] + if len(set(value_keys)) != len(value_keys): + raise ValueError(f"{where}.values contains duplicate values") + if mode != "runtime_catalog" and not values: + raise ValueError(f"{where}.values must not be empty for mode {mode}") + return ValueDomain( + domain_id=domain_id, + label=_required_string(item.get("label"), f"{where}.label"), + mode=mode, + complete=complete, + bindings=bindings, + resolvers=resolvers, + related_tables=tables, + values=values, + sources=_string_list(item.get("sources"), f"{where}.sources", required=True), + as_of=_required_string(item.get("as_of"), f"{where}.as_of"), + ) + + +def _parse_binding(value: object, where: str) -> Binding: + item = _mapping(value, where) + _reject_unknown(item, {"scope", "module", "api", "parameter", "role"}, where) + return Binding( + scope=_required_string(item.get("scope"), f"{where}.scope"), + module=_required_string(item.get("module"), f"{where}.module"), + api=_required_string(item.get("api"), f"{where}.api"), + parameter=_required_string(item.get("parameter"), f"{where}.parameter"), + role=_required_string(item.get("role"), f"{where}.role"), + ) + + +def _parse_resolver(value: object, where: str) -> Resolver: + item = _mapping(value, where) + _reject_unknown( + item, + {"scope", "module", "api", "parameter", "catalog", "source"}, + where, + ) + return Resolver( + scope=_required_string(item.get("scope"), f"{where}.scope"), + module=_required_string(item.get("module"), f"{where}.module"), + api=_required_string(item.get("api"), f"{where}.api"), + parameter=_required_string(item.get("parameter"), f"{where}.parameter"), + catalog=_required_string(item.get("catalog"), f"{where}.catalog"), + source=_required_string(item.get("source"), f"{where}.source"), + ) + + +def _parse_table(value: object, where: str) -> RelatedTable: + item = _mapping(value, where) + _reject_unknown(item, {"id", "name", "scope", "page"}, where) + return RelatedTable( + table_id=_required_string(item.get("id"), f"{where}.id"), + name=_required_string(item.get("name"), f"{where}.name"), + scope=_required_string(item.get("scope"), f"{where}.scope"), + page=_required_string(item.get("page"), f"{where}.page"), + ) + + +def _parse_value(value: object, where: str) -> DomainValue: + item = _mapping(value, where) + _reject_unknown( + item, + { + "value", + "label", + "aliases", + "parent", + "parent_label", + "level", + "valid_from", + "valid_to", + "note", + "related_table", + "as_of", + "sources", + "relations", + }, + where, + ) + level = item.get("level") + if level is not None and (not isinstance(level, int) or level < 0): + raise ValueError(f"{where}.level must be a non-negative integer") + return DomainValue( + value=_required_string(item.get("value"), f"{where}.value"), + label=_required_string(item.get("label"), f"{where}.label"), + aliases=_string_list(item.get("aliases", []), f"{where}.aliases"), + parent=_optional_string(item.get("parent"), f"{where}.parent"), + parent_label=_optional_string(item.get("parent_label"), f"{where}.parent_label"), + level=level, + valid_from=_optional_string(item.get("valid_from"), f"{where}.valid_from"), + valid_to=_optional_string(item.get("valid_to"), f"{where}.valid_to"), + note=_optional_string(item.get("note"), f"{where}.note"), + related_table=_optional_string( + item.get("related_table"), f"{where}.related_table" + ), + as_of=_optional_string(item.get("as_of"), f"{where}.as_of"), + sources=_string_list(item.get("sources"), f"{where}.sources", required=True), + relations=tuple( + _parse_relation(relation, f"{where}.relations[{index}]") + for index, relation in enumerate( + _list(item.get("relations", []), f"{where}.relations") + ) + ), + ) + + +def _parse_relation(value: object, where: str) -> Relation: + item = _mapping(value, where) + _reject_unknown(item, {"kind", "domain", "value", "verification", "sources"}, where) + verification_item = _mapping(item.get("verification"), f"{where}.verification") + _reject_unknown( + verification_item, + {"status", "scope", "module", "resolver", "parameter", "argument"}, + f"{where}.verification", + ) + status = _required_string( + verification_item.get("status"), f"{where}.verification.status" + ) + if status not in {"documented", "runtime_required"}: + raise ValueError( + f"{where}.verification.status must be documented or runtime_required" + ) + return Relation( + kind=_required_string(item.get("kind"), f"{where}.kind"), + domain_id=_required_string(item.get("domain"), f"{where}.domain"), + value=_required_string(item.get("value"), f"{where}.value"), + verification=Verification( + status=status, + scope=_required_string( + verification_item.get("scope"), f"{where}.verification.scope" + ), + module=_required_string( + verification_item.get("module"), f"{where}.verification.module" + ), + resolver=_required_string( + verification_item.get("resolver"), f"{where}.verification.resolver" + ), + parameter=_required_string( + verification_item.get("parameter"), + f"{where}.verification.parameter", + ), + argument=_optional_string( + verification_item.get("argument"), f"{where}.verification.argument" + ), + ), + sources=_string_list(item.get("sources"), f"{where}.sources", required=True), + ) + + +def _score_value( + domain: ValueDomain, value: DomainValue, terms: Sequence[str] +) -> tuple[int, bool] | None: + fields = ( + (value.value, 600), + (value.label, 560), + *((alias, 520) for alias in value.aliases), + (value.parent, 260), + (value.parent_label, 300), + (domain.label, 180), + (domain.domain_id, 160), + ) + folded_fields = tuple( + (_fold_value(text), weight) for text, weight in fields if _fold_value(text) + ) + scores = [] + exact = False + for term in terms: + best = 0 + for field, weight in folded_fields: + if term == field: + best = max(best, weight + 500) + exact = exact or len(terms) == 1 + elif len(term) < 2: + continue + elif term in field: + best = max(best, weight) + elif len(field) >= 2 and field in term: + best = max(best, weight - 80) + if not best: + return None + scores.append(best) + same_field_bonus = 300 if any(all(term in field for term in terms) for field, _ in folded_fields) else 0 + return sum(scores) + same_field_bonus, exact + + +def _hint_key(hint: ApiHint) -> tuple[str, ...]: + return ( + _fold(hint.scope), + _fold(hint.module), + _fold(hint.api), + _fold(hint.parameter), + _fold(hint.value), + hint.role, + hint.verification.status if hint.verification else "", + ) diff --git a/skills/tsl-api-reference/workflows/api-lookup.md b/skills/tsl-api-reference/workflows/api-lookup.md index 5e864674..f1d1fc9d 100644 --- a/skills/tsl-api-reference/workflows/api-lookup.md +++ b/skills/tsl-api-reference/workflows/api-lookup.md @@ -1,6 +1,6 @@ # API 检索 -用于普通函数、具体成员或“有没有某种行为的 API”这类请求。 +用于普通函数、具体成员、“有没有某种行为的 API”或“某个 API 的参数可以填什么”这类请求。 先查看脚本当前契约: @@ -25,8 +25,9 @@ python /scripts/lookup.py --name arrDropDuplicate ``` `--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、 -`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后, -必须再用 `--name` 取正文;不得把 summary 当作签名。 +`qualified_name`、owner、kind、binding 和 visibility;如果关键词命中参数取值域,结果会 +在对应 API 候选下附上参数名、值/名称、绑定关系和来源。它仍只输出候选摘要,选定候选后 +必须再用 `--name` 取正文;不得把 summary 或参数域样例当作签名。 已知范围时用当前索引实际提供的 scope 过滤,例如: @@ -55,4 +56,7 @@ binding 和 visibility。使用自定义 `--tsv` 时,只有索引实际包含 - rc=1:索引缺失、格式错误、为空,或候选指向的正文不可达;这是数据/部署错误。 - rc=2:参数不合法;按 stderr 和 `--help` 修正后重跑。 -`--name` 成功输出完整条目正文和来源标记;最终报告按 `SKILL.md` 的输出约定组织。 +`--name` 成功输出完整条目正文和来源标记;若 API 绑定参数取值域,还会显示域模式、 +完整性、快照和系统/用户运行时解析器。完整的封闭枚举或值模式会直接列出;不完整的版本 +目录和运行时目录只显示已记录数量,用 `dictionary_lookup.py --query` 按域 ID、名称、代码 +或模式取回。最终报告按 `SKILL.md` 的输出约定组织。 diff --git a/skills/tsl-api-reference/workflows/classification-codes.md b/skills/tsl-api-reference/workflows/classification-codes.md deleted file mode 100644 index 075ae3ef..00000000 --- a/skills/tsl-api-reference/workflows/classification-codes.md +++ /dev/null @@ -1,167 +0,0 @@ -# 分类属性代码(行业与基金分类) - -天软把「个券归属会随时间变更」的分类维护成**分类属性**,用一套属性代码 -(如 `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 deleted file mode 100644 index 25452d21..00000000 --- a/skills/tsl-api-reference/workflows/code-sequences.md +++ /dev/null @@ -1,194 +0,0 @@ -# 证券代码序列取数 - -「先拿到一批证券代码,再逐个取指标」是天软里最常见的起手动作。代码序列有三条互不 -等价的取数路径,选错路径会静默拿到错误的样本集: - -| 路径 | 函数 | 时点语义 | 入参 | -| ----------------- | -------------- | ------------------ | -------------------------- | -| 板块最新成分 | `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-api-reference/workflows/dictionary-lookup.md b/skills/tsl-api-reference/workflows/dictionary-lookup.md index 89b6f008..7aae9ff6 100644 --- a/skills/tsl-api-reference/workflows/dictionary-lookup.md +++ b/skills/tsl-api-reference/workflows/dictionary-lookup.md @@ -1,7 +1,7 @@ # 数据字典查询 -用于按中文业务含义查询天软数据字典中的表、数据源或字段。它们不是可调用 API, -不进入 `function_index.tsv`。 +用于按中文业务含义查询天软数据字典中的表、数据源、字段或 API 参数取值域。表、数据源 +和字段不是可调用 API,不进入 `function_index.tsv`;参数取值域是独立的随 skill 分发数据。 先查看脚本当前契约: @@ -17,17 +17,24 @@ python /scripts/dictionary_lookup.py \ python /scripts/dictionary_lookup.py --query "市值" --scope fund python /scripts/dictionary_lookup.py --query "买一量" --table TradeTable python /scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率 +python /scripts/dictionary_lookup.py --query "申万煤炭" ``` -结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问代码、 -对应 API 和字典页面。 +表/字段结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问 +代码、对应 API 和字典页面。参数取值域结果包含原始值、名称、父级/层级、有效期、绑定 +API 参数、关联表、模式和来源;`runtime_catalog` 还会给出系统或用户目录解析器。 ## 状态判读 - `status: ok`:最高分候选唯一。 - `status: ambiguous`:存在多个同分候选;列出候选并请用户补充 scope、表或数据源范围, 不能默认选第一条。 -- `status: no_match`:数据字典无匹配,不能改述为 API 的 `not found`。 +- `status: no_match`:表、字段和参数取值域均无匹配,不能改述为 API 的 `not found`。 + +参数取值域的“静态记录不完整”表示只记录了已核实样例。需要使用未列出的板块名、用户板块 +或其他动态名称时,先调用结果给出的运行时解析器确认;不能凭名称相似性推断可用值。 +用精确的参数域 ID 或域名称查询时,结果返回该域的绑定 API、完整性、已记录候选和运行时 +解析器;用具体名称或代码查询时,结果返回对应取值及其父级、有效期和关联表。 标识符匹配保留下划线;同义词只来自 `dictionary_lexicon.json` 的受控映射。 `MarketTable` 和 `TradeTable` 是 TS-SQL 数据源,不是函数。 @@ -35,5 +42,5 @@ python /scripts/dictionary_lookup.py --query "9900700" --field ## 退出码 - rc=0:查询完成,实际结果由 `status` 判定,包括 `ambiguous` 和 `no_match`。 -- rc=1:字典索引或词表缺失、损坏、格式错误。 +- rc=1:字典索引、词表或参数取值域数据缺失、损坏、格式错误。 - rc=2:参数不合法。 diff --git a/test/test_playbook.py b/test/test_playbook.py index 537ea8bc..4fe2d47a 100644 --- a/test/test_playbook.py +++ b/test/test_playbook.py @@ -382,6 +382,8 @@ no_backup = true / "tsl-api-reference" ) self.assertTrue((installed / "scripts" / "class_lookup.py").is_file()) + self.assertTrue((installed / "scripts" / "value_domains.py").is_file()) + self.assertTrue((installed / "data" / "value_domains.json").is_file()) self.assertTrue((installed / "workflows" / "api-lookup.md").is_file()) self.assertTrue( (installed / "workflows" / "class-and-framework.md").is_file() diff --git a/tools/tsl-codegen/MAINTENANCE.md b/tools/tsl-codegen/MAINTENANCE.md index a5a90fcc..3bba4e11 100644 --- a/tools/tsl-codegen/MAINTENANCE.md +++ b/tools/tsl-codegen/MAINTENANCE.md @@ -73,11 +73,45 @@ python tools/tsl-codegen/scripts/build_dictionary.py \ 生成完成后,用 `dictionary_lookup.py --help` 中的真实示例做查询检查。 +## 维护参数取值域 + +`skills/tsl-api-reference/data/value_domains.json` 是 API 参数取值域的检索数据,供 +`lookup.py` 和 `dictionary_lookup.py` 共同读取。它不改变 `function_index.tsv` 的 API 元数据 +职责,也不向 `dictionary_index.tsv` 增加 table/source/field 之外的 kind。 + +以下内容进入参数取值域: + +- 由外部目录维护、需要跨 API 查询的版本化代码,如分类属性代码; +- 多个 API 参数共享的封闭枚举;只属于单个 API 的枚举仍留在该 API 页面; +- 已有正式文档但不构成封闭枚举的特殊字面值,使用 `catalog`; +- 运行时才完整的系统或用户目录,如市场板块名称; +- 多个 API 共享的特殊选择器或值模式,如分类节点的 `.N` 后缀。 + +单个 API 自带的封闭枚举仍写在 codegen 录入数据的 `params[].values` 中,由生成器渲染为 +参数取值段,不在两个位置重复维护。 + +维护规则: + +1. FAQ 或帮助页逐条人工核对,不用脚本从语料批量筛选后直接入库。 +2. 每个值记录原样值、名称、来源和适用的 API 参数;版本化目录按需记录父级、层级、 + 有效期及关联表。 +3. `runtime_catalog` 必须为 `complete: false`,并提供系统/用户运行时解析器;静态值只能 + 作为已核实样例,不能宣称是完整目录。 +4. 分类名称映射为当前板块名但本地证据不足时,关系标记为 `runtime_required`,由 + `getBkList2` 或 `getUserBkList2` 核验后再用于代码。 +5. 修改后运行参数域 CLI 回归和安装测试: + + ```bash + python -m unittest tools.tsl-codegen.tests.test_value_domains + python -m unittest test.test_playbook + ``` + ## 完成检查 ```bash python skills/tsl-api-reference/scripts/class_lookup.py --check python tools/tsl-codegen/scripts/build_index.py \ --skill-dir skills/tsl-api-reference --check +python -m unittest tools.tsl-codegen.tests.test_value_domains python test/integration/check_doc_links.py ``` diff --git a/tools/tsl-codegen/tests/test_value_domains.py b/tools/tsl-codegen/tests/test_value_domains.py new file mode 100644 index 00000000..6b178896 --- /dev/null +++ b/tools/tsl-codegen/tests/test_value_domains.py @@ -0,0 +1,542 @@ +import csv +import json +import re +import subprocess +import sys +import tempfile +import unittest +from collections import defaultdict +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +SKILL_ROOT = ROOT / "skills" / "tsl-api-reference" +LOOKUP = SKILL_ROOT / "scripts" / "lookup.py" +DICTIONARY_LOOKUP = SKILL_ROOT / "scripts" / "dictionary_lookup.py" + + +class ValueDomainCliTest(unittest.TestCase): + def run_cli(self, script, *args): + return subprocess.run( + [sys.executable, str(script), *args], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + + def test_dictionary_query_resolves_classification_by_label_and_code(self): + for query in ("申万煤炭", "SWHY740000"): + with self.subTest(query=query): + result = self.run_cli(DICTIONARY_LOOKUP, "--query", query) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("status: ok", result.stdout) + self.assertIn("类型:参数取值", result.stdout) + self.assertIn( + "参数域:classification_code(分类属性代码)", + result.stdout, + ) + self.assertIn("取值:SWHY740000", result.stdout) + self.assertIn("名称:申万煤炭", result.stdout) + self.assertIn("父级:SWHY(申万行业)", result.stdout) + self.assertIn("层级:1", result.stdout) + self.assertIn("关联表:138 股票.股票行业分类信息", result.stdout) + self.assertIn("快照日期:2023-10-13", result.stdout) + self.assertIn("来源:faq:31494", result.stdout) + + def test_dictionary_query_uses_strong_values_without_hijacking_table_names(self): + for query in ("SWHY740000 是什么", "申万煤炭分类代码"): + with self.subTest(query=query): + result = self.run_cli(DICTIONARY_LOOKUP, "--query", query) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("类型:参数取值", result.stdout) + self.assertIn("取值:SWHY740000", result.stdout) + + table = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万行业配置") + + self.assertEqual(0, table.returncode, table.stderr) + self.assertIn("类型:table", table.stdout) + self.assertIn("表 ID:629", table.stdout) + self.assertNotIn("类型:参数取值", table.stdout) + + def test_pattern_punctuation_is_significant(self): + pattern = self.run_cli(DICTIONARY_LOOKUP, "--query", ".N") + single_letter = self.run_cli(DICTIONARY_LOOKUP, "--query", "N") + + self.assertEqual(0, pattern.returncode, pattern.stderr) + self.assertIn("类型:参数取值", pattern.stdout) + self.assertIn("取值:<分类属性代码>.N", pattern.stdout) + self.assertEqual(0, single_letter.returncode, single_letter.stderr) + self.assertNotIn("类型:参数取值", single_letter.stdout) + + def test_shared_board_type_enum_maps_multiple_api_parameters(self): + label = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万三级行业") + numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "板块类别 7") + bare_numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "7") + api = self.run_cli(LOOKUP, "--name", "getbktypelist") + bare_lookup = self.run_cli(LOOKUP, "--kw", "7") + + self.assertEqual(0, label.returncode, label.stderr) + self.assertIn("status: ambiguous", label.stdout) + self.assertIn("取值:7", label.stdout) + self.assertIn("名称:申万三级行业", label.stdout) + self.assertIn("绑定 API:getbktypelist.bktype; stocksbklist.bktype", label.stdout) + self.assertIn("字段:申万三级行业", label.stdout) + self.assertEqual(0, numeric.returncode, numeric.stderr) + self.assertIn("名称:申万三级行业", numeric.stdout) + self.assertEqual(0, bare_numeric.returncode, bare_numeric.stderr) + self.assertNotIn("类型:参数取值", bare_numeric.stdout) + self.assertEqual(0, api.returncode, api.stderr) + self.assertIn("market_board_type", api.stdout) + self.assertIn("完整", api.stdout) + self.assertIn("`7` — 申万三级行业", api.stdout) + self.assertEqual(0, bare_lookup.returncode, bare_lookup.stderr) + self.assertNotIn("参数取值域:getbktypelist.bktype", bare_lookup.stdout) + + def test_historical_market_board_codes_use_the_shared_domain(self): + dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "TSI000001") + lookup = self.run_cli(LOOKUP, "--kw", "TSI000001") + + self.assertEqual(0, dictionary.returncode, dictionary.stderr) + self.assertIn("取值:TSI000001", dictionary.stdout) + self.assertIn("名称:A股板块", dictionary.stdout) + self.assertIn("绑定 API:getBkByDate.index_id", dictionary.stdout) + self.assertEqual(0, lookup.returncode, lookup.stderr) + self.assertIn("历史市场板块代码:TSI000001(A股板块)", lookup.stdout) + self.assertNotIn("参数取值域:getbktypelist.bktype", lookup.stdout) + + def test_exact_code_value_ignores_incidental_dictionary_id_substrings(self): + result = self.run_cli(DICTIONARY_LOOKUP, "--query", "SWHY740000") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("status: ok", result.stdout) + self.assertIn("取值:SWHY740000", result.stdout) + self.assertNotIn("字段:截止日", result.stdout) + + def test_named_historical_board_domain_is_complete_and_api_specific(self): + dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "中小企业板") + lookup = self.run_cli(LOOKUP, "--kw", "中小企业板") + exact = self.run_cli(LOOKUP, "--name", "getAbkbyDate") + + self.assertEqual(0, dictionary.returncode, dictionary.stderr) + self.assertIn("绑定 API:getAbkbyDate.bk_name", dictionary.stdout) + self.assertIn("2021-04-06 并入主板", dictionary.stdout) + self.assertEqual(0, lookup.returncode, lookup.stderr) + self.assertIn("历史市场板块名:中小企业板", lookup.stdout) + self.assertEqual(0, exact.returncode, exact.stderr) + self.assertIn("historical_a_share_board_name", exact.stdout) + self.assertIn("`中小企业板` — 中小企业板(历史兼容)", exact.stdout) + + def test_scope_filters_multi_table_value_domains(self): + stock = self.run_cli( + DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "stock" + ) + fund = self.run_cli( + DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "fund" + ) + + self.assertEqual(0, stock.returncode, stock.stderr) + self.assertIn("关联表:138 股票.股票行业分类信息", stock.stdout) + self.assertNotIn("355 基金.基金分类信息", stock.stdout) + self.assertEqual(0, fund.returncode, fund.stderr) + self.assertIn("关联表:355 基金.基金分类信息", fund.stdout) + self.assertNotIn("138 股票.股票行业分类信息", fund.stdout) + + def test_keyword_query_maps_value_to_current_and_historical_apis(self): + result = self.run_cli(LOOKUP, "--kw", "申万", "煤炭") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("getBk\tfunction\tgetBk(marketlist)", result.stdout) + self.assertIn( + "getBkByDate\tfunction\tgetBkByDate(index_id, end_t, extype)", + result.stdout, + ) + self.assertIn("getBk.marketlist", result.stdout) + self.assertIn("当前板块候选:申万煤炭", result.stdout) + self.assertIn("运行时核验", result.stdout) + self.assertIn("getBkByDate.index_id", result.stdout) + self.assertIn("历史分类代码:SWHY740000", result.stdout) + + def test_keyword_annotations_only_include_the_top_value_group(self): + exact_code = self.run_cli(LOOKUP, "--kw", "SWHY740000") + domain_query = self.run_cli(LOOKUP, "--kw", "属性", "代码") + + self.assertEqual(0, exact_code.returncode, exact_code.stderr) + self.assertIn("历史分类代码:SWHY740000", exact_code.stdout) + self.assertNotIn("历史分类代码:SWHY(申万行业)", exact_code.stdout) + self.assertEqual(0, domain_query.returncode, domain_query.stderr) + self.assertIn("目录选择器:属性代码", domain_query.stdout) + self.assertIn("下级分类代码模式:<分类属性代码>.N", domain_query.stdout) + self.assertNotIn("历史分类代码:CAPCHY", domain_query.stdout) + + def test_exact_getbk_query_explains_runtime_catalog_boundary(self): + result = self.run_cli(LOOKUP, "--name", "getBk") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("参数取值域", result.stdout) + self.assertIn("marketlist", result.stdout) + self.assertIn("runtime_catalog", result.stdout) + self.assertIn("静态记录不完整", result.stdout) + self.assertIn("getBkList2", result.stdout) + self.assertIn("getUserBkList2", result.stdout) + self.assertIn("按值查询", result.stdout) + self.assertNotIn("`申万煤炭`", result.stdout) + + def test_runtime_domain_query_returns_resolvers_not_unrelated_market_codes(self): + result = self.run_cli(DICTIONARY_LOOKUP, "--query", "market_board") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("status: ok", result.stdout) + self.assertIn("类型:参数取值域", result.stdout) + self.assertIn("参数域:market_board", result.stdout) + self.assertIn("绑定 API:getBk.marketlist", result.stdout) + self.assertIn("getBkList2(bktype)(系统目录)", result.stdout) + self.assertIn("getUserBkList2(bktype)(用户目录)", result.stdout) + self.assertIn("申万煤炭", result.stdout) + self.assertNotIn("TSI000001", result.stdout) + + def test_domain_label_query_returns_one_domain_summary(self): + result = self.run_cli(DICTIONARY_LOOKUP, "--query", "分类属性代码") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("status: ok", result.stdout) + self.assertIn("参数域:classification_code", result.stdout) + self.assertIn("绑定 API:getBkByDate.index_id", result.stdout) + self.assertIn("SWHY740000(申万煤炭)", result.stdout) + self.assertNotIn("类型:field", result.stdout) + + def test_exact_api_query_does_not_expand_versioned_catalog(self): + result = self.run_cli(LOOKUP, "--name", "getBkByDate") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("分类属性代码", result.stdout) + self.assertIn("已记录值或候选:7 条", result.stdout) + self.assertIn("dictionary_lookup.py --query", result.stdout) + parameter_domain = result.stdout.split("### 参数取值域", maxsplit=1)[1] + self.assertNotIn("`SWHY740000`", parameter_domain) + + def test_invalid_value_domain_data_is_a_deployment_error(self): + with tempfile.TemporaryDirectory() as temp_dir: + invalid = Path(temp_dir) / "value_domains.json" + invalid.write_text('{"version": 1, "domains": []}\n', encoding="utf-8") + + for script, action in ( + (LOOKUP, ("--kw", "申万")), + (DICTIONARY_LOOKUP, ("--query", "申万")), + ): + with self.subTest(script=script.name): + result = self.run_cli( + script, + *action, + "--value-domains", + str(invalid), + ) + + self.assertEqual(1, result.returncode) + self.assertEqual("", result.stdout) + self.assertIn("domains must not be empty", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_parameter_domain_binding_does_not_cross_scope_or_module(self): + with tempfile.TemporaryDirectory() as temp_dir: + data_dir = Path(temp_dir) / "data" + data_dir.mkdir() + tsv = data_dir / "function_index.tsv" + tsv.write_text( + "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" + "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" + "dotnet/market.md\tgetbk\t\t系统板块\n" + "getBk\tdotnet\tdatawarehouse\tgetBk()\t" + "dotnet/market.md\tgetbk-empty\t\t无参数重载\n" + "getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t" + "getbk\t\t项目函数\n", + encoding="utf-8", + ) + domains = data_dir / "value_domains.json" + domains.write_text( + json.dumps( + { + "version": 1, + "domains": [ + { + "id": "market_board", + "label": "市场板块", + "mode": "runtime_catalog", + "complete": False, + "bindings": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBk", + "parameter": "marketlist", + "role": "current_components", + } + ], + "resolvers": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBkList2", + "parameter": "bktype", + "catalog": "system", + "source": "net_function:28964", + } + ], + "related_tables": [], + "values": [ + { + "value": "申万煤炭", + "label": "申万煤炭", + "sources": ["faq:31494"], + } + ], + "sources": ["faq:31494"], + "as_of": "2026-08-20", + } + ], + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = self.run_cli( + LOOKUP, + "--kw", + "申万煤炭", + "--tsv", + str(tsv), + "--value-domains", + str(domains), + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("dotnet/market.md#getbk", result.stdout) + self.assertNotIn("dotnet/market.md#getbk-empty", result.stdout) + self.assertNotIn("project/demo.md#getbk", result.stdout) + + def test_exact_query_attaches_domain_only_to_overload_with_bound_parameter(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + data_dir = root / "data" + codegen = root / "references" / "codegen" / "dotnet" + data_dir.mkdir() + codegen.mkdir(parents=True) + tsv = data_dir / "function_index.tsv" + tsv.write_text( + "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" + "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" + "dotnet/market.md\tgetbk\t\t系统板块\n" + "getBk\tdotnet\tdatawarehouse\tgetBk()\t" + "dotnet/market.md\tgetbk-1\t\t无参数重载\n", + encoding="utf-8", + ) + (codegen / "market.md").write_text( + "# Dotnet\n\n" + "## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n\n" + "## `getBk()`\n\n声明:function\n\n无参数重载\n", + encoding="utf-8", + ) + domains = data_dir / "value_domains.json" + domains.write_text( + json.dumps( + { + "version": 1, + "domains": [ + { + "id": "market_board", + "label": "市场板块", + "mode": "runtime_catalog", + "complete": False, + "bindings": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBk", + "parameter": "marketlist", + "role": "current_components", + } + ], + "resolvers": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBkList2", + "parameter": "bktype", + "catalog": "system", + "source": "net_function:28964", + } + ], + "related_tables": [], + "values": [], + "sources": ["net_function:28960"], + "as_of": "2026-08-20", + } + ], + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = self.run_cli( + LOOKUP, + "--name", + "getBk", + "--tsv", + str(tsv), + "--value-domains", + str(domains), + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("## `getBk(marketlist)`", result.stdout) + self.assertIn("## `getBk()`", result.stdout) + self.assertEqual(1, result.stdout.count("### 参数取值域")) + + def test_exact_query_does_not_attach_domain_to_same_name_in_other_scope(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + data_dir = root / "data" + codegen = root / "references" / "codegen" + (codegen / "dotnet").mkdir(parents=True) + (codegen / "project").mkdir(parents=True) + data_dir.mkdir() + tsv = data_dir / "function_index.tsv" + tsv.write_text( + "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" + "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" + "dotnet/market.md\tgetbk\t\t系统板块\n" + "getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t" + "getbk\t\t项目函数\n", + encoding="utf-8", + ) + (codegen / "dotnet" / "market.md").write_text( + "# Dotnet\n\n## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n", + encoding="utf-8", + ) + (codegen / "project" / "demo.md").write_text( + "# Project\n\n## `getBk(name)`\n\n声明:function\n\n项目函数\n", + encoding="utf-8", + ) + domains = data_dir / "value_domains.json" + domains.write_text( + json.dumps( + { + "version": 1, + "domains": [ + { + "id": "market_board", + "label": "市场板块", + "mode": "runtime_catalog", + "complete": False, + "bindings": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBk", + "parameter": "marketlist", + "role": "current_components", + } + ], + "resolvers": [ + { + "scope": "dotnet", + "module": "datawarehouse", + "api": "getBkList2", + "parameter": "bktype", + "catalog": "system", + "source": "net_function:28964", + } + ], + "related_tables": [], + "values": [], + "sources": ["net_function:28960"], + "as_of": "2026-08-20", + } + ], + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + result = self.run_cli( + LOOKUP, + "--scope", + "project", + "--name", + "getBk", + "--tsv", + str(tsv), + "--value-domains", + str(domains), + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("项目函数", result.stdout) + self.assertNotIn("参数取值域", result.stdout) + + def test_bundled_value_domains_reference_existing_apis_parameters_and_tables(self): + document = json.loads( + (SKILL_ROOT / "data" / "value_domains.json").read_text(encoding="utf-8") + ) + with (SKILL_ROOT / "data" / "function_index.tsv").open( + encoding="utf-8", newline="" + ) as handle: + rows = list(csv.DictReader(handle, delimiter="\t")) + with (SKILL_ROOT / "data" / "dictionary_index.tsv").open( + encoding="utf-8", newline="" + ) as handle: + dictionary_rows = list(csv.DictReader(handle, delimiter="\t")) + + apis = defaultdict(set) + for row in rows: + match = re.search(r"\((.*)\)", row["signature"]) + if not match: + continue + key = ( + row["scope"].casefold(), + row["module"].casefold(), + row["name"].casefold(), + ) + for parameter in match.group(1).split(","): + parameter = parameter.strip().strip("[]") + if parameter: + apis[key].add(parameter.casefold()) + table_pages = { + row["table_id"]: row["page"] + for row in dictionary_rows + if row["kind"] in {"table", "source"} and row["table_id"] + } + + for domain in document["domains"]: + for binding in domain["bindings"]: + self.assert_api_parameter_exists(apis, binding) + for resolver in domain["resolvers"]: + self.assert_api_parameter_exists(apis, resolver) + for table in domain["related_tables"]: + self.assertEqual(table["page"], table_pages.get(table["id"])) + self.assertTrue((SKILL_ROOT / table["page"]).is_file()) + for value in domain["values"]: + for relation in value.get("relations", []): + self.assert_api_parameter_exists(apis, relation["verification"]) + + def assert_api_parameter_exists(self, apis, reference): + key = ( + reference["scope"].casefold(), + reference["module"].casefold(), + (reference.get("api") or reference.get("resolver")).casefold(), + ) + parameter = reference.get("parameter") + + self.assertIn(key, apis) + if parameter: + self.assertIn(parameter.casefold(), apis[key]) + + +if __name__ == "__main__": + unittest.main()