♻️ refactor(tsl-api-reference): split lookup workflows and maintenance tooling

This commit is contained in:
csh
2026-08-20 17:47:48 +08:00
parent 9b95bf2682
commit cd0405d64b
18 changed files with 707 additions and 374 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
| `gitea-fix-ci` | 基于 Gitea Actions run/job/log 诊断失败 CI,先形成修复计划再改代码;含 `fetch_ci_logs.py` 取证脚本 | Gitea PR checks 失败、远端 CI 红但本地需要定位 |
| `cook-it-through` | 运行任务路由、Matt 工程主链和本地 Markdown ticket 生命周期 | 处理实质性工程任务,或操作 `.scratch` queue、claim、finish、integrate |
| `tsl-syntax-reference` | 查询 TSL 语法条目,验证具体写法和错误边界;不负责 API、命名、风格、工具链或模块集成 | 写/改/审 TSL 时确认语言结构、表达式、控制流、对象模型和语法限制 |
| `tsl-api-reference` | 查询随 skill 分发的 TSL API 参考:按名精确查条目,或按中文关键词发现候选 | 写/审 TSL 时确认 builtin、dotnet、模块 API 的签名、参数、返回值和示例 |
| `tsl-api-reference` | 查询 TSL API、class/framework 契约和天软数据字典;按 workflow 路由并由脚本 help 定义调用契约 | 确认 API/成员/回调的签名和示例,或按中文业务含义查表、数据源与字段 |
## 第三方 Skills
+31 -138
View File
@@ -1,157 +1,50 @@
---
name: tsl-api-reference
description: "当编写、修改或审查 TSL 代码需要确认 builtin、dotnet 或 module API 是否存在及其精确签名、参数、返回值和示例时使用;也用于按自然语言查找天软数据字典中的表、数据源与字段。"
description: "当编写、修改或审查 TSL 代码需要确认 API、class 成员或框架回调是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。"
---
# TSL API 参考
本 skill 仅用于 TSL API 事实:函数/模块名、精确签名、参数、返回值和示例
若需查询 TSL 语法、控制流、变量、对象模型或运行时语言规则,请改用
`tsl-syntax-reference` skill。
本 skill 只负责可检索的 TSL API、class/framework 契约和天软数据字典事实
语法、控制流、语言对象模型或运行方式问题不属于本 skill。
## 查询路由
判断用户请求是普通 API、class,还是带生命周期的框架类工作流
选择一条路由,并完整读取对应文件后再执行
- 普通函数、单个成员或数据字典问题走现有 `lookup.py` / `dictionary_lookup.py`
- 查询或使用任意 class 时,先用 `class_lookup.py` 取回紧凑的类摘要和成员分类;
需要某个成员正文时,再运行 `lookup.py --name` 精确查询。
- `class_lookup.py` 自动覆盖 `function_index.tsv` 中的全部 class,不要求每个普通对象类
都维护人工脚手架。
- 用户要实现框架子类、重写回调或组合一组有生命周期的 class API 时,类级查询会叠加
`framework_index.json` 中证据化的 Framework Profile,返回生命周期、必要回调、
状态、执行入口和结果查询。
- 不得因为查询目标是 class 就推断它是框架;普通对象、服务类和数据类只使用自动生成的
类成员视图。只有 `kind=framework` 且具有证据化生命周期的类才加载人工 Profile。
- 已知函数或成员名,或只知道 API 行为:读取
[`workflows/api-lookup.md`](workflows/api-lookup.md)。
- 查询 class 摘要、实现子类、重写回调或处理生命周期:读取
[`workflows/class-and-framework.md`](workflows/class-and-framework.md)。
- 按中文业务含义查表、数据源或字段:读取
[`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。
列出和查询 class
具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。
`<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
```bash
python <this-skill-dir>/scripts/class_lookup.py --list
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
python <this-skill-dir>/scripts/class_lookup.py \
--class TSBackTesting --config fGroupType=1 --format json
```
## 全局硬规则
同名 class 出现在多个 scope 或模块时,状态为 `ambiguous`;使用 `--scope``--module`
或完全限定名消歧,不能选择第一项
- API 名称按索引原样查询和报告。TSL 名称大小写不敏感,但下划线有意义;不得删除
下划线或改写成驼峰式
- 不得凭记忆或参照相似语言推断 API 名称、签名、参数、返回值或回调字段。
- 同名结果不得默认取第一条;必须按 `scope``module`、owner 或完全限定名消歧。
- `lookup.py` 的 rc=1 表示 skill 数据缺失、损坏或正文不可达,不表示 API 不存在。
普通 class 的 `profile_status=not_profiled` 不是错误,只表示它使用索引自动生成的成员
视图。框架类的 Profile 只描述 API 协议,不直接生成 TSL 语法;将结果作为
Framework Contract Packet 交给 `tsl-syntax-reference` 组合代码结构。
## 🔴 CHECKPOINT · 🛑 STOP
`scaffold_status=resolved` 只表示类级生命周期和成员引用完整,不表示模式相关的回调
记录结构已经确定。只要 `contract_status` 不是 `resolved`,就必须停止生成回调字段,
`diagnostics` 补齐模式和返回 schema 证据。
出现任一情况时,停止生成包含该 API 或回调字段的代码,并明确报告缺口:
维护 Profile 后运行引用校验:
- `lookup.py --name` 无匹配;
- `lookup.py --kw` 换用更准确的中文关键词后仍无候选;
- 同名 API 或 class 无法按 `scope``module`、owner 或完全限定名消歧;
- `class_lookup.py` 返回 `status: not_found`
- 请求依赖生命周期、配置或回调,但 `profile_status=not_profiled`
- Framework Profile 的 `contract_status` 不是 `resolved`
```bash
python <this-skill-dir>/scripts/class_lookup.py --check
```
rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。
天软数据字典中的表、数据源和字段不是可调用 API,不进入 `function_index.tsv`
函数问题使用 `lookup.py`,表或字段问题使用 `dictionary_lookup.py`
## 输出约定
## 数据字典查询
用户按中文业务含义描述表或字段时,运行独立的字典查询脚本
```bash
python <this-skill-dir>/scripts/dictionary_lookup.py --query "股票现金流指标销售现金比率"
```
已知业务范围、表名、数据源名或字段名时使用过滤参数
```bash
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率
```
查询结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、
访问代码、对应 API 和字典页面
- 标识符匹配不删除下划线
- 同义词只从 `dictionary_lexicon.json` 中取受控映射
- `status: ambiguous` 时必须向用户列出候选并要求补充表或数据源范围,不能默认选择第一项
- `MarketTable``TradeTable` 是 TS-SQL 数据源,不是函数
- `status: no_match` 表示字典无匹配,不能改述为 API 的 `not found`
不要凭记忆或参照相似语言推断 API 签名。TSL 名称大小写不敏感,但下划线是有意义的:
查询或报告 API 时不要删除下划线,也不要把名称改写成驼峰式。
运行随附的查询脚本。将 `<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
- 已知 API 名称:
```bash
python <this-skill-dir>/scripts/lookup.py --name argmax
```
- 已知 class/unit 成员的完全限定名称:
```bash
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
```
- 未知名称,但知道其行为或中文关键词:
```bash
python <this-skill-dir>/scripts/lookup.py --kw 最大值 位置
```
- 只查询一个 scope
```bash
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
python <this-skill-dir>/scripts/lookup.py --scope module --name bondAccruedInterest
```
- 关键词采用字面 AND 语义;增加关键词可缩小结果范围。常用同义词由 API 条目的
Tags 提供,例如“位置/下标”“列表/数组”“打印/输出”。
- 关键词会搜索 `name`、`signature`、`tags`、`summary`、`module`、`scope` 及
`qualified_name`、`owner`、`kind`、`binding`、`visibility`,并按字段加权稳定排序;
因此也可用 `--kw dotnet argmax` 或 `--kw protected method` 缩小范围。
- 使用 `--limit N` 调整打印的候选行数量,N 必须 >= 1。
精确查询会打印完整的条目正文和来源标记。关键词查询打印候选行;
选定候选项后,再用 `--name` 重新执行精确查询。
`--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。查询简单成员名会
返回所有 owner 下的同名 API;查询 `Unit.Class.Member` 等完全限定名称可限定所属
class/unit。重载共享 `qualified_name`,精确查询会返回该名称的全部重载。
混合页面中的精确查询仍按唯一 `page#anchor` 定位,只截取目标 H2 或成员子树。
每个 H2 的声明类型独立,不使用全局页面类型。
关键词候选优先显示 `qualified_name`,并根据 kind/binding 显示声明术语:实例
method 显示 `function`,类方法显示 `class function`,静态字段与常量分别显示
`static field`、`static const`。
同名 API 存在于多个 scope 或模块时(如 `host`、`login`、`port`),`--name`
会依次打印全部同名条目,每条带自己的来源标记。已知 scope 时先用 `--scope`
过滤;否则按当前代码所处的 scope 和模块选择条目,不要默认取第一条。
查询脚本兼容旧 8 列和新 13 列 `function_index.tsv`。旧 8 列索引仍可查询原有
function;完全限定名称、owner、kind、binding、visibility 依赖新索引中的扩展列。
## 退出码
| 退出码 | 含义 | 处理方式 |
| ------ | -------------------------------------- | ------------------------------------------------ |
| 0 | 查询完成,含"查无此 API"和关键词零命中 | 读取输出;无匹配时换中文关键词重试 |
| 1 | 随附数据缺失或为空 | 重新安装本 skill,或用 `--tsv PATH` 指向可用索引 |
| 2 | 参数不合法 | 按提示修正参数后重跑 |
rc=1 表示 skill 数据本身有问题,不表示该 API 不存在;此时不要据此断言 API 缺失。
## 常见错误
- 删掉下划线或改写成驼峰式后查询,导致误判 API 不存在。名称按原样查。
- 把关键词查询的 summary 当作签名事实。summary 只用于挑候选,签名必须由
`--name` 的条目正文提供。
- 多条同名结果里随手取第一条,忽略 scope 与模块差异。
- 查不到就凭记忆或相似语言推断签名。查不到时说明缺口并停止。
报告 API 事实时必须包含:索引中的原样名称或 `qualified_name``scope``module`
`lookup.py --name` 正文取得的精确签名,以及对应的 `page#anchor` 来源。
关键词候选的 summary 只能用于选候选,不能作为签名事实。
@@ -1,832 +0,0 @@
#!/usr/bin/env python3
"""Build an independent searchable snapshot of the Tinysoft data dictionary."""
from __future__ import annotations
import argparse
import csv
import io
import json
import os
import re
import tempfile
import unicodedata
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Iterable, Sequence
from bs4 import BeautifulSoup, NavigableString, Tag
INDEX_COLUMNS = (
"kind",
"scope",
"table_id",
"table_name",
"table_alias",
"field_id",
"field_name",
"field_alias",
"data_type",
"unit",
"description",
"extract_method",
"access_code",
"api_name",
"page",
"tags",
)
SECTION_LABELS = {
"更新日志",
"数据说明",
"数据来源",
"数据更新情况",
"基本概况",
"字段说明",
"特别说明",
"数据范例",
"访问代码",
"访问方法",
"取数示例",
"参考",
}
SCOPE_NAMES = {
"股票": "stock",
"基金": "fund",
"基金扩展": "fund",
"债券": "bond",
"期货": "futures",
"期权": "options",
"新三板": "neeq",
"指数": "index",
"回购": "repo",
"现货": "spot",
"宏观": "macro",
"中证": "csindex",
"天软": "tinysoft",
"行情": "market",
"证券": "security",
}
FIELD_NAME_HEADERS = ("字段名", "函数名", "名称", "含义", "字段")
UNIT_HEADERS = ("单位", "单位【注1】", "单位【注4】")
DESCRIPTION_HEADERS = ("详细说明", "说明", "备注")
INVALID_SLUG_CHARS_RE = re.compile(r"[\\/:*?\"<>|]+")
SPACE_RE = re.compile(r"\s+")
class DictionaryParseError(ValueError):
"""A dictionary page advertises a schema that cannot be parsed."""
class NotDictionaryPage(DictionaryParseError):
"""The source page is outside the data-dictionary branch."""
@dataclass(frozen=True)
class DictionaryField:
field_id: str
field_name: str
data_type: str
display_name: str
unit: str
description: str
api_name: str
@dataclass(frozen=True)
class DictionaryTable:
name: str
table_id: str
extract_method: str
access_code: str
update_info: tuple[str, ...]
aliases: tuple[str, ...] = ()
@dataclass(frozen=True)
class DictionaryExample:
description: str
code: str
output: str | None
@dataclass(frozen=True)
class DictionaryPage:
kind: str
scope: str
title: str
source_path: tuple[str, ...]
source_file: str
table: DictionaryTable
fields: tuple[DictionaryField, ...]
notes: tuple[str, ...]
examples: tuple[DictionaryExample, ...]
output_name: str = ""
@dataclass(frozen=True)
class DictionaryDocument:
source_file: str
source_path: tuple[str, ...]
records: tuple[DictionaryPage, ...]
@dataclass(frozen=True)
class BuildReport:
scanned_page_count: int
dictionary_page_count: int
navigation_page_count: int
page_count: int
table_count: int
source_count: int
field_count: int
ignored_pages: tuple[str, ...]
malformed_dictionary_pages: tuple[str, ...]
def _clean_text(value: str) -> str:
value = value.replace("\xa0", " ")
return SPACE_RE.sub(" ", value).strip()
def _cell_text(cell: Tag) -> str:
return _clean_text(cell.get_text(" ", strip=True))
def _table_rows(table: Tag) -> tuple[list[str], list[list[str]]]:
rows = []
for row in table.find_all("tr"):
cells = [_cell_text(cell) for cell in row.find_all(["th", "td"], recursive=False)]
if cells:
rows.append(cells)
if not rows:
return [], []
return rows[0], rows[1:]
def _page_title(content: Tag) -> str:
heading = content.find(["h1", "h2", "h3"])
if heading is None:
return ""
return _clean_text(heading.get_text(" ", strip=True).replace("复制链接", ""))
def _breadcrumb(raw: str) -> tuple[str, ...]:
prefix = raw.split('<div id="help_content">', 1)[0]
soup = BeautifulSoup(prefix, "html.parser")
labels = [_clean_text(link.get_text(" ", strip=True)) for link in soup.find_all("a")]
starts = [index for index, label in enumerate(labels) if label == "知识库"]
if starts:
labels = labels[starts[-1] :]
return tuple(label for label in labels if label)
def _is_dictionary_path(source_path: Sequence[str], title: str) -> bool:
return "天软数据字典" in source_path or title == "天软数据字典"
def _scope(source_path: Sequence[str]) -> str:
if "天软数据字典" not in source_path:
return ""
start = source_path.index("天软数据字典") + 1
for label in source_path[start:]:
if label in SCOPE_NAMES:
return SCOPE_NAMES[label]
return ""
def _domain_path(source_path: Sequence[str]) -> tuple[str, ...]:
if "天软数据字典" not in source_path:
return ()
start = source_path.index("天软数据字典") + 1
return tuple(source_path[start:])
def _section_marker(content: Tag, label: str) -> Tag | None:
for tag in content.find_all(["span", "strong"]):
if _clean_text(tag.get_text(" ", strip=True)).rstrip(":") == label:
return tag
return None
def _is_section_marker(node: Tag) -> bool:
if node.name not in {"span", "strong"}:
return False
return _clean_text(node.get_text(" ", strip=True)).rstrip(":") in SECTION_LABELS
def _inside(node: NavigableString, tag_name: str, class_name: str | None = None) -> bool:
parent = node.parent
while isinstance(parent, Tag):
if parent.name == tag_name and (class_name is None or class_name in parent.get("class", [])):
return True
parent = parent.parent
return False
def _section_lines(content: Tag, label: str) -> tuple[str, ...]:
marker = _section_marker(content, label)
if marker is None:
return ()
lines = []
for node in marker.next_elements:
if isinstance(node, Tag) and node is not marker and _is_section_marker(node):
break
if not isinstance(node, NavigableString) or node.parent is marker:
continue
if _inside(node, "table") or _inside(node, "div", "text-container"):
continue
text = _clean_text(str(node))
if text and (not lines or lines[-1] != text):
lines.append(text)
return tuple(lines)
def _nearest_section(table: Tag) -> str:
for tag in table.find_all_previous(["span", "strong"]):
label = _clean_text(tag.get_text(" ", strip=True)).rstrip(":")
if label in SECTION_LABELS:
return label
return ""
def _find_basic_table(content: Tag) -> Tag | None:
for table in content.find_all("table"):
header, _ = _table_rows(table)
if "表ID" in header and "表名" in header and "提取方式" in header:
return table
return None
def _field_table_header(header: Sequence[str]) -> bool:
return "类型" in header and any(name in header for name in FIELD_NAME_HEADERS)
def _find_field_tables(content: Tag) -> tuple[Tag, ...]:
found = []
for table in content.find_all("table"):
header, _ = _table_rows(table)
if _field_table_header(header) and _nearest_section(table) == "字段说明":
found.append(table)
return tuple(found)
def _column_index(header: Sequence[str], names: Sequence[str]) -> int | None:
for name in names:
if name in header:
return header.index(name)
return None
def _value(row: Sequence[str], index: int | None) -> str:
if index is None or index >= len(row):
return ""
return row[index]
def _parse_fields(table: Tag) -> tuple[DictionaryField, ...]:
header, rows = _table_rows(table)
id_index = _column_index(header, ("ID",))
identifier_index = _column_index(header, ("字段名", "函数名", "字段"))
display_index = _column_index(header, ("中文名", "名称", "含义"))
type_index = _column_index(header, ("类型",))
unit_index = _column_index(header, UNIT_HEADERS)
description_indices = [header.index(name) for name in DESCRIPTION_HEADERS if name in header]
api_index = _column_index(header, ("对应函数名称",))
fields = []
for row in rows:
field_id = _value(row, id_index)
identifier = _value(row, identifier_index)
display_name = _value(row, display_index)
api_name = _value(row, api_index)
if "函数名" in header and identifier:
api_name = api_name or identifier
field_name = identifier or display_name or field_id
display_name = display_name or identifier or field_id
descriptions = []
for index in description_indices:
value = _value(row, index)
if value and value not in descriptions:
descriptions.append(value)
if not field_name:
continue
fields.append(
DictionaryField(
field_id=field_id,
field_name=field_name,
data_type=_value(row, type_index),
display_name=display_name,
unit=_value(row, unit_index),
description=" ".join(descriptions),
api_name=api_name,
)
)
return tuple(fields)
def _extract_examples(content: Tag) -> tuple[DictionaryExample, ...]:
marker = _section_marker(content, "取数示例")
if marker is None:
return ()
examples = []
for container in marker.find_all_next("div", class_="text-container"):
previous = container.find_previous(["span", "strong"])
if previous is not marker and _is_section_marker(previous):
break
raw_lines = [_clean_text(line) for line in container.get_text("\n").splitlines()]
chunks: list[list[str]] = []
current: list[str] = []
for line in (line for line in raw_lines if line):
empty_output = re.fullmatch(r"//\s*(?:返回|输出)\s*[:]?", line, re.IGNORECASE)
if empty_output:
if current:
chunks.append(current)
current = []
continue
output_match = re.match(r"//\s*(?:返回|输出)\s*[:]\s*(.+)", line, re.IGNORECASE)
if not output_match and line.startswith("//") and current and any(
re.match(r"(?i)^\s*return\b", existing) for existing in current
):
chunks.append(current)
current = []
if output_match:
line = f"// 输出:{output_match.group(1).strip()}"
elif line.startswith("//"):
line = f"// {line[2:].strip()}"
current.append(line)
if current:
chunks.append(current)
for lines in chunks:
description = lines[0][2:].strip() if lines[0].startswith("//") else ""
output = None
for line in lines:
match = re.match(r"//\s*输出:(.+)", line)
if match:
output = match.group(1).strip()
examples.append(DictionaryExample(description, "\n".join(lines), output))
return tuple(examples)
def _basic_metadata(table: Tag) -> tuple[str, str, str]:
header, rows = _table_rows(table)
if len(rows) != 1:
raise DictionaryParseError("basic table must contain exactly one data row")
row = rows[0]
return (
_value(row, header.index("表ID")),
_value(row, header.index("表名")),
_value(row, header.index("提取方式")),
)
def _resolve_extract_method(content: Tag, extract_method: str) -> str:
if not re.fullmatch(r"【注\d+】", extract_method):
return extract_method
page_text = _clean_text(content.get_text(" ", strip=True))
match = re.search(r"(?:取数接口|接口)\s*([A-Za-z][A-Za-z0-9_]*)", page_text)
return match.group(1) if match else extract_method
def _normal_aliases(table_name: str, title: str) -> tuple[str, ...]:
candidates = [title, table_name.replace(".", ""), table_name]
if "." in table_name:
candidates.append(table_name.rsplit(".", 1)[-1])
return tuple(dict.fromkeys(value for value in candidates if value and value != table_name))
def _field_available(field: DictionaryField, source_name: str) -> bool:
description = unicodedata.normalize("NFKC", field.description).casefold()
source = source_name.casefold()
pattern = rf"{re.escape(source)}\s*[:]\s*(?:没有|无)(?:该)?字段"
return re.search(pattern, description) is None
def _split_market_sources(page: DictionaryPage) -> tuple[DictionaryPage, ...]:
shared = page.table
trade_table = replace(
shared,
name="TradeTable",
extract_method="TradeTable",
aliases=("交易明细", "交易明细表", "tradetable"),
)
market_table = replace(
shared,
name="MarketTable",
extract_method="MarketTable",
aliases=("分时", "分时表", "markettable"),
)
trade_examples = tuple(example for example in page.examples if "tradetable" in example.code.casefold())
market_examples = tuple(example for example in page.examples if "markettable" in example.code.casefold())
return (
replace(
page,
kind="source",
title="交易明细",
table=trade_table,
fields=tuple(field for field in page.fields if _field_available(field, "tradetable")),
examples=trade_examples,
),
replace(
page,
kind="source",
title="分时",
table=market_table,
fields=tuple(field for field in page.fields if _field_available(field, "markettable")),
examples=market_examples,
),
)
def _special_field_records(
content: Tag,
title: str,
source_path: tuple[str, ...],
source_file: str,
fields_by_table: Sequence[tuple[Tag, tuple[DictionaryField, ...]]],
) -> tuple[DictionaryPage, ...]:
access_lines = _section_lines(content, "访问方法")
methods = []
for line in access_lines:
if "" in line or ":" in line:
_, method = re.split(r"[:]", line, maxsplit=1)
methods.append(_clean_text(method))
all_examples = _extract_examples(content)
records = []
for index, (table, fields) in enumerate(fields_by_table):
label = ""
for sibling in table.previous_siblings:
if isinstance(sibling, NavigableString):
label = _clean_text(str(sibling)).rstrip(":")
if label:
break
record_title = f"{title}.{label}" if label else title
extract_method = methods[index] if index < len(methods) else ""
matching_examples = tuple(
example
for example in all_examples
if extract_method and extract_method.casefold() in example.code.casefold()
)
records.append(
DictionaryPage(
kind="source",
scope=_scope(source_path),
title=record_title,
source_path=_domain_path(source_path),
source_file=source_file,
table=DictionaryTable(
name=record_title,
table_id="",
extract_method=extract_method,
access_code="".join(access_lines),
update_info=_section_lines(content, "数据更新情况"),
aliases=tuple(filter(None, (title, label))),
),
fields=fields,
notes=_section_lines(content, "数据说明"),
examples=matching_examples or (all_examples if len(fields_by_table) == 1 else ()),
)
)
return tuple(records)
def parse_dictionary_document(path: Path) -> DictionaryDocument:
raw = path.read_text(encoding="utf-8", errors="replace")
soup = BeautifulSoup(raw, "html.parser")
content = soup.select_one("#help_content")
if content is None:
raise DictionaryParseError("missing #help_content")
title = _page_title(content)
source_path = _breadcrumb(raw)
if not _is_dictionary_path(source_path, title):
raise NotDictionaryPage(path.name)
basic_table = _find_basic_table(content)
field_tables = _find_field_tables(content)
if basic_table is None and not field_tables:
return DictionaryDocument(path.name, source_path, ())
if not field_tables:
raise DictionaryParseError("dictionary record has no recognized field table")
parsed_tables = tuple((table, _parse_fields(table)) for table in field_tables)
if any(not fields for _, fields in parsed_tables):
raise DictionaryParseError("field table contains no data rows")
if basic_table is None:
records = _special_field_records(content, title, source_path, path.name, parsed_tables)
return DictionaryDocument(path.name, source_path, records)
table_id, table_name, extract_method = _basic_metadata(basic_table)
extract_method = _resolve_extract_method(content, extract_method)
if not table_name:
raise DictionaryParseError("basic table has no table name")
fields = tuple(field for _, group in parsed_tables for field in group)
page = DictionaryPage(
kind="table",
scope=_scope(source_path),
title=title,
source_path=_domain_path(source_path),
source_file=path.name,
table=DictionaryTable(
name=table_name,
table_id=table_id,
extract_method=extract_method,
access_code="".join(_section_lines(content, "访问代码")),
update_info=_section_lines(content, "数据更新情况"),
aliases=_normal_aliases(table_name, title),
),
fields=fields,
notes=_section_lines(content, "数据说明"),
examples=_extract_examples(content),
)
method_key = re.sub(r"[^a-z]", "", extract_method.casefold())
if "markettable" in method_key and "tradetable" in method_key:
records = _split_market_sources(page)
else:
records = (page,)
return DictionaryDocument(path.name, source_path, records)
def _slug_part(value: str) -> str:
value = unicodedata.normalize("NFKC", value).strip().casefold()
value = INVALID_SLUG_CHARS_RE.sub("_", value)
value = re.sub(r"[.。·,,、]+", "_", value)
value = SPACE_RE.sub("_", value)
value = re.sub(r"_+", "_", value).strip("_")
return value
def dictionary_slug(page: DictionaryPage) -> str:
if page.output_name:
return page.output_name
parts = [*page.source_path, page.title]
if page.kind == "source" and page.table.name.casefold() not in page.title.casefold():
parts.append(page.table.name)
compact = []
for part in parts:
slug = _slug_part(part)
if slug and (not compact or compact[-1] != slug):
compact.append(slug)
if not compact:
raise DictionaryParseError(f"cannot create slug for {page.source_file}")
return "__".join(compact) + ".md"
def _resolve_slug_collisions(pages: Sequence[DictionaryPage]) -> tuple[DictionaryPage, ...]:
grouped: dict[str, list[DictionaryPage]] = {}
for page in pages:
grouped.setdefault(dictionary_slug(page), []).append(page)
resolved = []
used = set()
for page in pages:
base = dictionary_slug(page)
group = grouped[base]
if len(group) == 1:
output_name = base
else:
discriminator = _slug_part(page.table.table_id or page.table.extract_method)
if not discriminator or "" in discriminator:
sources = ", ".join(item.source_file for item in group)
raise DictionaryParseError(f"slug collision needs a semantic discriminator: {base}: {sources}")
output_name = f"{Path(base).stem}__{discriminator}.md"
if output_name in used:
sources = ", ".join(item.source_file for item in group)
raise DictionaryParseError(f"slug collision: {output_name}: {sources}")
used.add(output_name)
resolved.append(replace(page, output_name=output_name))
return tuple(resolved)
def _markdown_cell(value: str) -> str:
return value.replace("|", "\\|").replace("\n", "<br>")
def render_dictionary_page(page: DictionaryPage) -> str:
path = " / ".join((*page.source_path, page.title))
lines = [f"# 天软数据字典 / {path}", "", f"类型:{page.kind}", "", "## 表信息", ""]
name_label = "数据源名称" if page.kind == "source" else "表名"
metadata = [(name_label, page.table.name)]
if page.table.table_id:
metadata.append(("表 ID", page.table.table_id))
if page.table.extract_method:
metadata.append(("提取方式", page.table.extract_method))
if page.table.access_code:
metadata.append(("访问代码", page.table.access_code))
if page.scope:
metadata.append(("范围", page.scope))
lines.extend(["| 项目 | 内容 |", "| --- | --- |"])
lines.extend(f"| {label} | {_markdown_cell(value)} |" for label, value in metadata)
lines.extend(["", "## 字段", "", "| 字段 ID | 字段名 | 类型 | 中文名 | 单位 | 说明 | 对应 API |", "| --- | --- | --- | --- | --- | --- | --- |"])
for field in page.fields:
values = (
field.field_id,
field.field_name,
field.data_type,
field.display_name,
field.unit,
field.description,
field.api_name,
)
lines.append("| " + " | ".join(_markdown_cell(value) for value in values) + " |")
if page.notes or page.table.update_info:
lines.extend(["", "## 数据说明", ""])
for note in (*page.notes, *page.table.update_info):
lines.append(f"- {note}")
if page.examples:
lines.extend(["", "## 取数示例", ""])
for index, example in enumerate(page.examples, start=1):
if example.description:
lines.extend([f"范例{index:02d}{example.description}", ""])
lines.extend(["```tsl", example.code, "```", ""])
while lines and not lines[-1]:
lines.pop()
return "\n".join(lines) + "\n"
def _page_reference(page: DictionaryPage) -> str:
return f"references/data_dictionary/{dictionary_slug(page)}"
def _field_alias(field: DictionaryField) -> str:
values = []
for value in (field.display_name, field.field_name):
if value and value not in values:
values.append(value)
return "|".join(values)
def build_index(pages: Iterable[DictionaryPage]) -> list[dict[str, str]]:
rows = []
keys = set()
for page in sorted(pages, key=lambda item: (dictionary_slug(item), item.table.name.casefold())):
common = {
"scope": page.scope,
"table_id": page.table.table_id,
"table_name": page.table.name,
"table_alias": "|".join(page.table.aliases),
"extract_method": page.table.extract_method,
"access_code": page.table.access_code,
"page": _page_reference(page),
"tags": "|".join((*page.source_path, page.title)),
}
entity = {
**{column: "" for column in INDEX_COLUMNS},
**common,
"kind": page.kind,
"description": " ".join(page.notes),
}
rows.append(entity)
for field in page.fields:
row = {
**{column: "" for column in INDEX_COLUMNS},
**common,
"kind": "field",
"field_id": field.field_id,
"field_name": field.field_name,
"field_alias": _field_alias(field),
"data_type": field.data_type,
"unit": field.unit,
"description": field.description,
"api_name": field.api_name,
}
key = (
row["kind"],
row["table_id"],
row["table_name"].casefold(),
row["field_id"],
row["field_name"].casefold(),
row["field_alias"].casefold(),
row["page"],
)
if key in keys:
raise DictionaryParseError(f"duplicate dictionary index key: {key}")
keys.add(key)
rows.append(row)
return rows
def _atomic_write_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
encoded = content.encode("utf-8")
descriptor, temporary_name = tempfile.mkstemp(
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(encoded)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
if path.read_bytes() != encoded:
raise OSError(f"generated file verification failed: {path}")
finally:
temporary.unlink(missing_ok=True)
def write_index(rows: Iterable[dict[str, str]], path: Path) -> None:
handle = io.StringIO(newline="")
writer = csv.DictWriter(handle, fieldnames=INDEX_COLUMNS, delimiter="\t", lineterminator="\n")
writer.writeheader()
for row in rows:
writer.writerow({column: row.get(column, "") for column in INDEX_COLUMNS})
_atomic_write_text(path, handle.getvalue())
def validate_lexicon(lexicon: object) -> dict[str, list[str]]:
if not isinstance(lexicon, dict):
raise ValueError("dictionary lexicon must be a JSON object")
validated = {}
for canonical, aliases in lexicon.items():
if not isinstance(canonical, str) or not canonical.strip():
raise ValueError("dictionary lexicon contains an empty canonical term")
if not isinstance(aliases, list) or not all(isinstance(item, str) and item.strip() for item in aliases):
raise ValueError(f"aliases for {canonical!r} must be non-empty strings")
validated[canonical] = list(dict.fromkeys(aliases))
return validated
def write_lexicon(lexicon: object, path: Path) -> None:
validated = validate_lexicon(lexicon)
_atomic_write_text(path, json.dumps(validated, ensure_ascii=False, indent=2) + "\n")
def load_lexicon(path: Path) -> dict[str, list[str]]:
return validate_lexicon(json.loads(path.read_text(encoding="utf-8")))
def build_snapshot(source_root: Path, output_root: Path, index_path: Path) -> BuildReport:
records = []
ignored = []
malformed = []
dictionary_count = 0
navigation_count = 0
source_paths = sorted(source_root.glob("*.html"), key=lambda path: int(path.stem) if path.stem.isdigit() else path.name)
for source_path in source_paths:
try:
document = parse_dictionary_document(source_path)
except NotDictionaryPage:
ignored.append(source_path.name)
continue
except DictionaryParseError as error:
dictionary_count += 1
malformed.append(f"{source_path.name}: {error}")
continue
dictionary_count += 1
if not document.records:
navigation_count += 1
continue
records.extend(document.records)
records = list(_resolve_slug_collisions(records))
slugs = {dictionary_slug(page): page.source_file for page in records}
output_root.mkdir(parents=True, exist_ok=True)
expected = set(slugs)
for stale in output_root.glob("*.md"):
if stale.name not in expected:
stale.unlink()
for page in records:
_atomic_write_text(output_root / dictionary_slug(page), render_dictionary_page(page))
rows = build_index(records)
write_index(rows, index_path)
return BuildReport(
scanned_page_count=len(source_paths),
dictionary_page_count=dictionary_count,
navigation_page_count=navigation_count,
page_count=len(records),
table_count=sum(page.kind == "table" for page in records),
source_count=sum(page.kind == "source" for page in records),
field_count=sum(len(page.fields) for page in records),
ignored_pages=tuple(ignored),
malformed_dictionary_pages=tuple(malformed),
)
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("--source-root", required=True, type=Path)
parser.add_argument("--output-root", required=True, type=Path)
parser.add_argument("--index", required=True, type=Path)
parser.add_argument("--lexicon", type=Path)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
if args.lexicon is not None:
load_lexicon(args.lexicon)
report = build_snapshot(args.source_root, args.output_root, args.index)
print(
f"dictionary_pages={report.dictionary_page_count} generated_pages={report.page_count} "
f"tables={report.table_count} sources={report.source_count} fields={report.field_count} "
f"navigation={report.navigation_page_count} malformed={len(report.malformed_dictionary_pages)}"
)
for item in report.malformed_dictionary_pages:
print(f"MALFORMED\t{item}")
return 1 if report.malformed_dictionary_pages else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -16,6 +16,43 @@ from framework_lookup import (
from lookup import DEFAULT_TSV, codegen_root_for_tsv, load_rows, normalize
HELP_EPILOG = """\
查询顺序:
1. 先按 class 取类摘要或 Framework Profile
class_lookup.py --class TStringList
class_lookup.py --class TSBackTesting --format json
2. 需要成员正文时,再按返回的 qualified_name 调用 lookup.py --name
输出状态:
status resolved、not_found 或 ambiguous
profile_status resolved 表示有 Framework Profilenot_profiled 表示普通 class
scaffold_status 生命周期和成员引用是否完整,仅在有 Profile 时输出
contract_status 回调字段契约是否完整;非 resolved 时不得生成回调字段
diagnostics 消歧、配置或契约缺口及下一步动作
--check 校验全部 Framework Profile 对 class、成员正文和证据的引用。
使用自定义 --tsv 时默认不加载 Framework Profile;普通 class 仍可正常查询。若该索引
另有 curated Profile,必须同时显式传入 --profiles。
退出码:
0 查询或校验完成;not_found、ambiguous 通过输出状态表达
1 function/framework 索引缺失、格式错误、为空或引用校验失败
2 参数不合法
"""
def empty_profile_index():
return {"schema_version": 1, "frameworks": []}
def profile_path_for(args):
if args.profiles:
return Path(args.profiles)
if args.tsv:
return None
return DEFAULT_INDEX
def class_rows(rows):
return [row for row in rows if row.get("kind") == "class"]
@@ -232,24 +269,32 @@ def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Query indexed TSL classes and curated framework profiles.",
description="查询 TSL class 摘要及其证据化的 Framework Profile",
epilog=HELP_EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False,
)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--class", dest="class_name", help="exact class name")
action.add_argument("--list", action="store_true", help="list indexed classes")
action.add_argument("--check", action="store_true", help="validate framework profiles")
parser.add_argument("--scope", help="filter class scope")
parser.add_argument("--module", help="filter class module")
action.add_argument("--class", dest="class_name", help="精确 class 名或完全限定名")
action.add_argument("--list", action="store_true", help="列出索引中的 class")
action.add_argument("--check", action="store_true", help="校验全部 Framework Profile 引用")
parser.add_argument("--scope", help=" class scope 过滤")
parser.add_argument("--module", help=" class module 过滤")
parser.add_argument(
"--config", action="append", default=[], metavar="KEY=VALUE", help="raw config"
"--config",
action="append",
default=[],
metavar="KEY=VALUE",
help="传入原始框架配置,可重复",
)
parser.add_argument(
"--include-members", action="store_true", help="include the compact member list"
"--include-members", action="store_true", help="在类摘要中包含紧凑成员清单"
)
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument("--profiles", metavar="PATH", help="framework profile index")
parser.add_argument("--tsv", metavar="PATH", help="function index")
parser.add_argument(
"--format", choices=("text", "json"), default="text", help="输出格式"
)
parser.add_argument("--profiles", metavar="PATH", help="Framework Profile 索引路径")
parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv 路径")
args = parser.parse_args(argv)
if args.config and not args.class_name:
@@ -257,10 +302,12 @@ def main(argv=None):
if args.include_members and not args.class_name:
parser.error("--include-members requires --class")
profile_path = Path(args.profiles) if args.profiles else DEFAULT_INDEX
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
profile_path = profile_path_for(args)
try:
profiles = load_index(profile_path)
profiles = (
load_index(profile_path) if profile_path else empty_profile_index()
)
rows = load_rows(tsv_path)
except (OSError, UnicodeError, ValueError) as error:
print(f"ERROR: {error}", file=sys.stderr)
@@ -35,6 +35,24 @@ REQUIRED_COLUMNS = (
"tags",
)
HELP_EPILOG = """\
按中文业务含义查询表、数据源或字段:
dictionary_lookup.py --query "股票现金流指标销售现金比率"
dictionary_lookup.py --query "市值" --scope fund
dictionary_lookup.py --query "买一量" --table TradeTable
dictionary_lookup.py --query "9900700" --field 销售现金比率
输出第一行是 in-band 状态:
status: ok 最高分候选唯一
status: ambiguous 多个同分候选;必须补充 scope、table 或 field 范围
status: no_match 数据字典无匹配,不等于 API not found
退出码:
0 查询完成;ok、ambiguous、no_match 均由 status 表达
1 dictionary_index.tsv 或词表缺失、损坏、格式错误
2 参数不合法
"""
@dataclass(frozen=True)
class QueryItem:
@@ -318,14 +336,21 @@ def _positive_int(value: str) -> int:
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("--query", required=True)
parser.add_argument("--scope")
parser.add_argument("--table")
parser.add_argument("--field")
parser.add_argument("--limit", type=_positive_int, default=10)
parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV)
parser.add_argument("--lexicon", type=Path, default=DEFAULT_LEXICON)
parser = argparse.ArgumentParser(
description="查询独立的天软表、数据源与字段字典索引。",
epilog=HELP_EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False,
)
parser.add_argument("--query", required=True, help="中文业务描述、名称或 ID")
parser.add_argument("--scope", help="按业务范围过滤")
parser.add_argument("--table", help="按表名或数据源名过滤")
parser.add_argument("--field", help="按字段名或字段 ID 过滤")
parser.add_argument("--limit", type=_positive_int, default=10, help="候选条数上限")
parser.add_argument("--tsv", type=Path, default=DEFAULT_TSV, help="字典索引路径")
parser.add_argument(
"--lexicon", type=Path, default=DEFAULT_LEXICON, help="受控同义词词表路径"
)
return parser.parse_args(argv)
@@ -1,15 +1,10 @@
#!/usr/bin/env python3
"""Resolve framework-class profiles without replacing ordinary API lookup."""
"""Library for validating and resolving evidence-backed framework profiles."""
import argparse
import json
import sys
from pathlib import Path
from lookup import (
DEFAULT_TSV,
codegen_root_for_tsv,
load_rows,
normalize,
search_exact,
slice_entry,
@@ -230,33 +225,6 @@ def validate_index(data, rows, codegen_root):
return errors
def find_framework(data, name):
requested = normalize(name)
return next(
(
framework
for framework in data.get("frameworks", [])
if isinstance(framework, dict)
and normalize(framework.get("qualified_name", "")) == requested
),
None,
)
def parse_config(values):
config = {}
errors = []
for value in values:
key, separator, raw_value = value.partition("=")
if not separator or not key.strip() or not raw_value.strip():
errors.append(
f"invalid --config {value!r}; expected a non-empty key=value pair"
)
continue
config[key.strip()] = raw_value.strip()
return config, errors
def scaffold_packet(framework, config):
configured_names = {
entry["api"].rsplit(".", 1)[-1]
@@ -320,135 +288,3 @@ def scaffold_packet(framework, config):
"diagnostics": diagnostics,
"evidence": framework.get("evidence", []),
}
def print_text(packet):
print(f"framework: {packet['framework']}")
print(f"scope/module: {packet['scope']}/{packet['module']}")
print(f"scaffold_status: {packet['scaffold_status']}")
print(f"contract_status: {packet['contract_status']}")
print("lifecycle: " + " -> ".join(item["phase"] for item in packet["lifecycle"]))
print("configuration: " + ", ".join(item["api"] for item in packet["configuration"]))
required_hooks = [item["api"] for item in packet["hooks"] if item.get("required")]
print("required_hooks: " + (", ".join(required_hooks) or "none"))
print("state_apis: " + ", ".join(item["api"] for item in packet["state_apis"]))
print("execution: " + ", ".join(item["api"] for item in packet["execution"]))
print("result_apis: " + ", ".join(item["api"] for item in packet["result_apis"]))
if packet["configured_values"]:
print("configured_values: " + ", ".join(
f"{key}={value}" for key, value in packet["configured_values"].items()
))
if packet["diagnostics"]:
print("diagnostics:")
for diagnostic in packet["diagnostics"]:
print(f"- {diagnostic['code']}: {diagnostic['message']}")
def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Resolve framework-class scaffolds for TSL API consumers.",
allow_abbrev=False,
)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--framework", help="exact framework qualified name")
action.add_argument(
"--check", action="store_true", help="validate all framework profile references"
)
parser.add_argument(
"--config",
action="append",
default=[],
metavar="KEY=VALUE",
help="raw framework configuration; may be repeated",
)
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument("--index", metavar="PATH", help="framework_index.json path")
parser.add_argument("--tsv", metavar="PATH", help="function_index.tsv path")
args = parser.parse_args(argv)
index_path = Path(args.index) if args.index else DEFAULT_INDEX
tsv_path = Path(args.tsv) if args.tsv else DEFAULT_TSV
try:
data = load_index(index_path)
rows = load_rows(tsv_path)
except (OSError, UnicodeError, ValueError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
if not rows:
print(f"ERROR: {tsv_path} has no entries", file=sys.stderr)
return 1
codegen_root = codegen_root_for_tsv(tsv_path)
errors = validate_index(data, rows, codegen_root)
if args.check:
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"OK: {len(data['frameworks'])} framework profile(s) validated")
return 0
if errors:
packet = {
"status": "data_error",
"framework": args.framework,
"diagnostics": [
{
"code": "PROFILE_REFERENCE_ERROR",
"severity": "error",
"message": error,
"next_action": "Fix framework_index.json or rebuild the API index.",
}
for error in errors
],
}
if args.format == "json":
print(json.dumps(packet, ensure_ascii=False, indent=2))
else:
print(f"Framework profile data is invalid for {args.framework!r}.")
for diagnostic in packet["diagnostics"]:
print(f"- {diagnostic['code']}: {diagnostic['message']}")
return 1
framework = find_framework(data, args.framework)
if framework is None:
packet = {
"status": "not_found",
"framework": args.framework,
"diagnostics": [
{
"code": "FRAMEWORK_NOT_FOUND",
"severity": "error",
"message": f"No framework profile named {args.framework!r}.",
"next_action": "Use --check or add a framework profile.",
}
],
}
if args.format == "json":
print(json.dumps(packet, ensure_ascii=False, indent=2))
else:
print(f"No framework profile named {args.framework!r}.")
return 0
config, config_errors = parse_config(args.config)
packet = scaffold_packet(framework, config)
for error in config_errors:
packet["diagnostics"].append(
{
"code": "INVALID_CONFIGURATION",
"severity": "error",
"message": error,
"next_action": "Use KEY=VALUE syntax with non-empty values.",
}
)
if args.format == "json":
print(json.dumps(packet, ensure_ascii=False, indent=2))
else:
print_text(packet)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+9 -4
View File
@@ -73,11 +73,16 @@ HELP_EPILOG = """\
lookup.py --name arrDropDuplicate
已知确切名称时可以直接用 --name,跳过第 1 步。
只查询一个 API scope 时使用 --scope,例如 --scope builtin、
--scope dotnet 或 --scope module
只查询一个 API scope 时使用 --scope;可用值从当前 function_index.tsv 读取,
不存在的 scope 会列出当前索引实际提供的值并返回 2
结果:
--kw 输出候选摘要表;选定后仍须运行 --name
--name 输出完整条目正文和 scope/module、page#anchor 来源标记;
简单成员名可能返回多个 ownerqualified_name 可消歧;重载会全部返回
退出码:
0 取回成功--name 无匹配也是 0(打印提示,不算错误)
0 查询完成--name 无匹配和 --kw 零候选也通过正文提示表达
1 function_index.tsv 缺失、格式错误,或候选指向的条目正文找不到
2 参数不合法(缺少动作、空值、--limit 小于 1、使用短选项或缩写)
"""
@@ -423,7 +428,7 @@ def main(argv=None):
type=non_empty,
metavar="SCOPE",
help="只查询指定 scope,大小写不敏感;作用于 --name 和 --kw。"
"内置索引当前提供 builtin、dotnet 与 module",
"可用值从所选 function_index.tsv 读取并在加载后校验",
)
parser.add_argument(
"--limit",
@@ -0,0 +1,58 @@
# API 检索
用于普通函数、具体成员或“有没有某种行为的 API”这类请求。
先查看脚本当前契约:
```bash
python <this-skill-dir>/scripts/lookup.py --help
```
## 检索顺序
已知名称时直接精确查询:
```bash
python <this-skill-dir>/scripts/lookup.py --name argmax
python <this-skill-dir>/scripts/lookup.py --name DemoUnit.Document.Save
```
只知道行为时必须走两步:
```bash
python <this-skill-dir>/scripts/lookup.py --kw 数组 去重
python <this-skill-dir>/scripts/lookup.py --name arrDropDuplicate
```
`--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、
`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后,
必须再用 `--name` 取正文;不得把 summary 当作签名。
已知范围时用当前索引实际提供的 scope 过滤,例如:
```bash
python <this-skill-dir>/scripts/lookup.py --scope builtin --name boolToStr
python <this-skill-dir>/scripts/lookup.py --scope dotnet --kw argmax
python <this-skill-dir>/scripts/lookup.py --scope module --name bondAccruedInterest
```
## 精确结果判读
- `--name` 对简单名称和完全限定名称做大小写不敏感的精确匹配。
- 简单成员名会返回所有 owner 下的同名 API;完全限定名用于限定 class 或 unit。
- 重载共享 `qualified_name`,精确查询会返回该名称的全部重载。
- 混合页面按唯一 `page#anchor` 截取目标 H2 或成员子树;每个 H2 的声明类型独立。
- 同名结果跨 scope 或 module 时,结合当前代码上下文消歧,不能取第一条。
- 候选中的实例方法显示 `function`,类方法显示 `class function`,静态字段和常量
分别显示 `static field``static const`
脚本可读取必需的 8 列索引;13 列索引额外提供 `qualified_name`、owner、kind、
binding 和 visibility。使用自定义 `--tsv` 时,只有索引实际包含的扩展列才能支持对应消歧。
## 结果与退出码
- rc=0:查询完成。`--name` 无匹配和 `--kw` 零候选也属于查询完成,必须读取正文状态。
- rc=1:索引缺失、格式错误、为空,或候选指向的正文不可达;这是数据/部署错误。
- rc=2:参数不合法;按 stderr 和 `--help` 修正后重跑。
`--name` 成功输出完整条目正文和来源标记;最终报告按 `SKILL.md` 的输出约定组织。
@@ -0,0 +1,57 @@
# Class 与 Framework 查询
用于 class 摘要、成员分类、子类实现、回调重写和生命周期协议。公开入口只有
`class_lookup.py`Framework Profile 是 class 查询的附加层,不单独调用另一个 CLI。
先查看脚本当前契约:
```bash
python <this-skill-dir>/scripts/class_lookup.py --help
```
## 查询
```bash
python <this-skill-dir>/scripts/class_lookup.py --list
python <this-skill-dir>/scripts/class_lookup.py --class TStringList
python <this-skill-dir>/scripts/class_lookup.py --class TSBackTesting
python <this-skill-dir>/scripts/class_lookup.py \
--class TSBackTesting --config FGroupType=1 --format json
```
- 查询或使用任意 class 时,先取类摘要;需要具体成员正文时,再按返回的
`qualified_name` 运行 `lookup.py --name`
- `class_lookup.py` 自动覆盖 `function_index.tsv``kind=class` 的条目,不要求普通
对象类维护人工 Profile。
- 只有 `kind=framework` 且有证据化生命周期的类才叠加 Framework Profile。不得因目标
是 class 就推断它是框架。
- 同名 class 的 `status=ambiguous` 时,使用 `--scope``--module` 或完全限定名消歧。
## 状态判读
- `status=resolved`:类查询已解析;`status=not_found`:索引中无该 class
`status=ambiguous`:存在多个候选。
- `profile_status=not_profiled`:普通 class,只使用自动成员视图,不是错误,也不表示数据
不完整;成员查询可以继续。若当前请求依赖生命周期、配置或回调,则表示没有已验证的
framework 契约,必须停止该部分,不能假设空的 Framework Profile。
- `profile_status=resolved`:返回了 Framework Profile。
- `scaffold_status=resolved` 只表示生命周期和成员引用完整。
- `contract_status` 不是 `resolved` 时,必须按 `diagnostics` 补齐模式与返回 schema 证据,
并停止生成回调字段。
Framework Profile 只描述 API 协议,不定义 TSL 语法或代码结构。解析后的 Framework
Contract Packet 仅作为已验证的 API 事实输出。
只有返回的 Profile 本身带有生命周期、配置、回调、执行入口或结果查询证据时,才使用
这些协议;`not_profiled` 时不能从 class 名称或成员列表推断 framework。
使用自定义 `--tsv` 时默认不加载 Framework Profile,普通 class 仍可查询;该索引确有
curated Profile 时必须同时显式传入 `--profiles`
## 退出码
- rc=0:查询或校验完成;`not_found``ambiguous` 通过输出状态表达。
- rc=1:索引/Profile 数据缺失、损坏或引用校验失败。
- rc=2:参数不合法。
`--check` 属维护动作,普通编码查询不需要运行。
@@ -0,0 +1,39 @@
# 数据字典查询
用于按中文业务含义查询天软数据字典中的表、数据源或字段。它们不是可调用 API,
不进入 `function_index.tsv`
先查看脚本当前契约:
```bash
python <this-skill-dir>/scripts/dictionary_lookup.py --help
```
## 查询
```bash
python <this-skill-dir>/scripts/dictionary_lookup.py \
--query "股票现金流指标销售现金比率"
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率
```
结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问代码、
对应 API 和字典页面。
## 状态判读
- `status: ok`:最高分候选唯一。
- `status: ambiguous`:存在多个同分候选;列出候选并请用户补充 scope、表或数据源范围,
不能默认选第一条。
- `status: no_match`:数据字典无匹配,不能改述为 API 的 `not found`
标识符匹配保留下划线;同义词只来自 `dictionary_lexicon.json` 的受控映射。
`MarketTable``TradeTable` 是 TS-SQL 数据源,不是函数。
## 退出码
- rc=0:查询完成,实际结果由 `status` 判定,包括 `ambiguous``no_match`
- rc=1:字典索引或词表缺失、损坏、格式错误。
- rc=2:参数不合法。