diff --git a/.gitea/workflows/checks.yml b/.gitea/workflows/checks.yml index 7c46cc69..7245341b 100644 --- a/.gitea/workflows/checks.yml +++ b/.gitea/workflows/checks.yml @@ -130,10 +130,27 @@ jobs: echo "========================================" cd "$REPO_DIR" + echo "📦 安装锁定的 Node.js 校验工具" + npm ci --ignore-scripts --no-audit --no-fund --no-bin-links + echo "📋 覆盖:CLI、subtree/snapshot 部署路线、模板同步、文档一致性" python3 -m unittest discover -s test -p "test_*.py" -v echo "✅ Python 测试通过" + echo "========================================" + echo "🧭 TSL Syntax Reference 结构检查" + echo "========================================" + + python3 skills/tsl-syntax-reference/scripts/lookup.py --check + echo "✅ TSL Syntax Reference 结构检查通过" + + echo "========================================" + echo "🎨 TSL Syntax Reference 格式检查" + echo "========================================" + + node node_modules/prettier/bin/prettier.cjs --check skills/tsl-syntax-reference + echo "✅ TSL Syntax Reference 格式检查通过" + echo "========================================" echo "🔗 文档链接检查" echo "========================================" diff --git a/.gitea/workflows/prepare.yml b/.gitea/workflows/prepare.yml index 01faa435..2cd63adc 100644 --- a/.gitea/workflows/prepare.yml +++ b/.gitea/workflows/prepare.yml @@ -44,8 +44,10 @@ jobs: [curl]="curl" [flock]="util-linux" [realpath]="coreutils" + [node]="nodejs" + [npm]="npm" ) - required_commands=(git python3 curl flock realpath) + required_commands=(git python3 curl flock realpath node npm) missing_commands=() missing_packages=() declare -A seen_packages=() diff --git a/skills/tsl-syntax-reference/SKILL.md b/skills/tsl-syntax-reference/SKILL.md index 59f52360..dd7ff89c 100644 --- a/skills/tsl-syntax-reference/SKILL.md +++ b/skills/tsl-syntax-reference/SKILL.md @@ -24,7 +24,7 @@ description: 当用户需要编写、修改、审查或解释 TSL/TSF、TS-SQL 根据任务意图选择模式: ```bash -python /scripts/lookup.py --query "命名参数 默认参数" --mode write +python /scripts/lookup.py --query "命名参数" --mode write python /scripts/lookup.py --query "invalid statement 声明区" --mode diagnose python /scripts/lookup.py --query "数组下标" --mode explain ``` @@ -39,23 +39,25 @@ python /scripts/lookup.py --query "数组下标" --mode explain 语言核心事实速查)。它们不占 `--limit` 预算,也不是本次查询的命中结果:先按它们 核对文件模型和硬规则,再从 `Required: no` 的候选里挑选支持当前结论的章节。前置 章节在同一任务内取回一次即可;后续 `--query` 重复列出它们时,不必再次 `--section`。 +其中 `syntax-01-002` 是从专题页逐条派生的强制速查,不是另一份完整事实源;生成结论 +仍要取回拥有该规则的专题 Section。 `--query` 输出候选后,必须执行下面这种精确取回。多个已选定的 Section ID 可以 一次传入: ```bash -python /scripts/lookup.py --section "05_functions_and_calls--可直接照写示例--基础函数-过程骨架" "02_core_model--文件模型核心规则" +python /scripts/lookup.py --section "syntax-05-004" "syntax-02-002" ``` 只有 `--section` 返回的章节正文可作为本次任务的语法事实来源;候选摘要和概念地图都不能直接支持代码结论。需要补充时改进查询词再次检索。 ## 构造查询词 -查询词由用户原话里的术语、报错原文和目标语法要素名组成。脚本会自动处理两类词, -不必手工调整: +从用户原话中提取错误原文、TSL 标识符和单一语法要素名;保留这些词在用户原话中的 +写法,不传完整句,也不自行翻译成猜测术语。脚本会自动处理两类词,不必手工调整: - 中文口语词自动扩展到 TSL 术语(如"打印"→输出/writeLn,"列表"→数组, - "程序慢"→性能分析)。用户怎么说就怎么传。 + "程序慢"→性能分析)。保留提取词的原写法即可。 - `tsl`、`tsf`、`tinysoft`、`program`、`debug`、`please` 这几个词不参与逐词匹配, 但可能整体把查询导向某个专题页。无结果时靠加这类词补救没有用,改为换更具体的 语法要素名。 @@ -77,7 +79,7 @@ Summary 判断后再取回正文核对。 给出的每个 TSL/TSF 代码块,紧随其后写一行来源标注,列出所依据的全部 Section ID: ```txt -来源:05_functions_and_calls--可直接照写示例--基础函数-过程骨架(可直接照写示例),02_core_model--文件模型核心规则 +来源:syntax-05-004(可直接照写示例),syntax-02-002 ``` Section ID 抄自 `--section` 输出首部的 `Section ID:` 行。多个要素合成一个代码块时, @@ -86,7 +88,7 @@ Section ID 抄自 `--section` 输出首部的 `Section ID:` 行。多个要素 - 该 Section ID 提供了照写的代码围栏:在 ID 后用括号写出紧邻该围栏的 `代码块身份:` 值。 - 该 Section ID 只提供散文规则、正文里没有代码围栏(如 - `02_core_model--文件模型核心规则`):只写 ID,不加括号。规则段落没有身份行, + `syntax-02-002`):只写 ID,不加括号。规则段落没有身份行, 这不是缺口。 写不出某个要素的 Section ID,说明该要素还没有取回:先补齐检索,再给代码。 @@ -160,16 +162,28 @@ python /scripts/lookup.py --map `--query` 无匹配或候选全部为 `Weak: yes` 时改进查询词重试。仍无强命中即为事实缺口:按上面「缺口时停止」处理。 +`--map`、`--query`、`--section`、`--check` 共用安装前置检查。参考目录不存在或没有 +参考页时,任何动作都以 rc=1 报告路径/安装错误;这不属于事实零命中,不能靠改查询词 +重试。 + ## 事实边界 本 Skill 只拥有 TSL/TSF 的语言语法、文件模型、表达式、控制流、对象、运行时语言 结构和 TS-SQL 外形。混合请求先逐项拆分,只回答其中的语法部分。 API 名称、签名、参数、返回值、平台 scope、解释器可用性和金融取数事实属于 -`tsl-api-reference` skill。语法示例里可以出现 API 名称来展示调用位置,但这只证明 -源码外形,不证明该 API 的签名、返回行为、平台 scope 或在目标解释器上可用;需要这 -些结论时查 API skill,没有事实支持时按「缺口时停止」处理,不把两个 skill 的片段 -拼成「可运行」结论。 +`tsl-api-reference` skill。准备面向用户交付的 TSL/TSF 代码时,按下面顺序处理: + +1. 先枚举代码中除语言关键字和用户定义符号外的调用标识符。 +2. 语言结构逐项用本 Skill 取回;每个 builtin/API 都逐项用 `tsl-api-reference` 取回 + 名称、签名和目标 scope。 +3. 任一 API 依赖没有取回时,整段交付代码按「缺口时停止」处理;不能只给语法已查的 + 半成品,也不能凭语法页示例中的 API 名称补齐。 + +纯语法说明可以使用明确标为占位的用户定义调用来展示调用位置,而不必为该占位符查 +API;但不得声称该占位调用的 API 行为或输出,也不得把真实 builtin/API 当成无需核对 +的占位符。解释一旦包含真实 API 的签名、可用性、返回值、输出,或代码将面向用户 +交付,就必须执行上面的 API 取回步骤。 ## 运行方式不属于本 Skill @@ -178,10 +192,17 @@ TSL 的运行方式、解释器路径、平台检测和环境选择都不是本 脚本或 CI,并严格照其规定执行。 本 Skill 只能确认语法外形正确,不能确认代码在目标解释器上可运行。给出未经执行的 -代码时,据此区分「语法已取回」与「运行时未验证」。 +代码时,据此区分「语法已取回」与「运行时未验证」。用户未要求实际执行时,解释器 +不可用是受支持状态,不构成事实缺口,也不要求提供可重放运行证据。 ## 维护校验 -改动本 Skill、参考页、`data/` 词表或 lookup 实现后运行 `scripts/lookup.py --check` -校验参考页结构与词表覆盖,rc=0 才算通过。口语同义词和页级意图短语维护在 -`data/lexicon.json`,策展纪律见 `data/README.md`。 +Section ID 来自标题下的显式 `` 元数据,不从标题派生;改标题 +时保留原 ID。新增章节时分配新 ID,不重排或复用旧 ID。 + +改动本 Skill、参考页、`data/` 词表或 lookup 实现后,先运行 +`scripts/lookup.py --check` 校验显式 ID、标题层级、quickstart 派生规则、代码块身份、 +链接与词表页覆盖。在本 playbook 仓库维护时,再从仓库根目录运行 +`python -m unittest test.test_tsl_syntax_reference -v` 校验检索排序、退出码和 CLI +行为。`--check` 只是结构检查,不证明检索排序、事实语义或示例运行结果。口语同义词 +和页级意图短语维护在 `data/lexicon.json`,策展纪律见 `data/README.md`。 diff --git a/skills/tsl-syntax-reference/data/README.md b/skills/tsl-syntax-reference/data/README.md index 8d7fb13f..6463f40d 100644 --- a/skills/tsl-syntax-reference/data/README.md +++ b/skills/tsl-syntax-reference/data/README.md @@ -1,7 +1,8 @@ # 检索词表(lexicon.json) `lexicon.json` 是 `scripts/lookup.py` 的策展检索数据,与引擎逻辑分离维护。 -两张表都只影响检索,不是事实正文。 +两张表都只影响检索,不是事实正文。查询输入应从用户原话提取错误文本、标识符 +或单一语法要素,保留原写法但不传完整礼貌句。 ## query_synonyms —— 口语同义词 @@ -20,14 +21,16 @@ - 键必须与 `references/` 下的文件名完全一致;每个参考页至少一条短语。 键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言 - 入口——`lookup.py --check` 和回归测试会拦住这两种漂移。 + 入口。`lookup.py --check` 负责拦截页键漂移,行为回归测试负责拦截排序漂移。 ## 改动后的校验 ```bash -python scripts/lookup.py --check +python skills/tsl-syntax-reference/scripts/lookup.py --check +python -m unittest test.test_tsl_syntax_reference -v ``` `--check` 会校验 `page_intent_aliases` 的键与 `references/` 参考页一一对应: 键指向不存在的页、或某页没有自然语言入口都会报错。新增参考页时必须同时 -在这里补一条页级意图短语。 +在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;原始 alias、 +助词变体、弱命中和已知真实问法由专属测试覆盖。 diff --git a/skills/tsl-syntax-reference/data/lexicon.json b/skills/tsl-syntax-reference/data/lexicon.json index 2e3afc95..7e540e85 100644 --- a/skills/tsl-syntax-reference/data/lexicon.json +++ b/skills/tsl-syntax-reference/data/lexicon.json @@ -28,8 +28,16 @@ }, "page_intent_aliases": { "01_quickstart.md": ["最简单能跑", "最简单的脚本", "天软脚本", "tinysoft"], - "02_core_model.md": ["脚本和可复用", "可复用函数文件", "声明函数后面写代码"], - "03_values_and_literals.md": ["字符串和数组下标", "下标从几开始", "下标起点"], + "02_core_model.md": [ + "脚本和可复用", + "可复用函数文件", + "声明函数后面写代码" + ], + "03_values_and_literals.md": [ + "字符串和数组下标", + "下标从几开始", + "下标起点" + ], "04_variables_and_constants.md": ["常量怎么声明", "变量能不能直接赋值"], "05_functions_and_calls.md": ["默认参数", "函数怎么带"], "06_expressions_and_operators.md": ["赋值和相等比较"], @@ -46,7 +54,14 @@ "10_runtime_context_and_with.md": ["临时切换系统参数"], "11_matrix_and_collections.md": ["某行存在", "二维数组怎么判断"], "12_resultset_and_filters.md": ["保留匹配行", "按某一列"], - "13_ts_sql.md": ["左连接", "左外连接", "左联接", "数据库", "分组排序", "聚合排序"], + "13_ts_sql.md": [ + "左连接", + "左外连接", + "左联接", + "数据库", + "分组排序", + "聚合排序" + ], "14_debug_and_profiler.md": [ "程序慢", "计时找瓶颈", @@ -57,11 +72,17 @@ "耗时", "debug" ], - "15_lexical_structure_and_compile_options.md": ["变量名区分大小写", "注释怎么写"], + "15_lexical_structure_and_compile_options.md": [ + "变量名区分大小写", + "注释怎么写" + ], "16_types_and_conversions.md": ["字符串转整数", "类型转换"], "17_external_calls_and_threads.md": ["调用 dll", "dll", "动态库", "开线程"], "18_namespace_libpath_and_unit_runtime.md": ["找不到 tsf", "搜索路径"], - "19_object_runtime_and_introspection.md": ["查看对象属于哪个类", "运行时对象"], + "19_object_runtime_and_introspection.md": [ + "查看对象属于哪个类", + "运行时对象" + ], "20_builtin_runtime_objects.md": ["内存流"], "21_matrix_deep_dive.md": ["矩阵求逆", "矩阵转置", "求逆和转置"], "22_fmarray.md": ["高性能矩阵", "fmarray"], diff --git a/skills/tsl-syntax-reference/references/01_quickstart.md b/skills/tsl-syntax-reference/references/01_quickstart.md index 7364063b..b51c9aba 100644 --- a/skills/tsl-syntax-reference/references/01_quickstart.md +++ b/skills/tsl-syntax-reference/references/01_quickstart.md @@ -1,30 +1,81 @@ # TSL 快速落代码规则 -本篇提供一份单点的语言核心事实速查,以及没有任何复用或模块需求时的默认起手骨架。 +本篇提供一份由专题页逐条派生的语言核心事实速查,以及没有任何复用或模块需求时的默认起手骨架。 ## 本篇职责 -在落代码前一次性给出跨专题的语言硬规则(赋值、文件形态、声明位置、命名参数、下标起点、类与 `unit` 外形),并提供术语对照和默认可执行脚本骨架。 + + +在落代码前一次性汇总跨专题的语言硬规则(赋值、文件形态、声明位置、命名参数、下标起点、类与 `unit` 外形),并提供术语对照和默认脚本骨架。本页是强制读取的派生摘要,不独立拥有完整事实;每条规则的完整上下文仍由对应专题页拥有。 ## 语言核心事实速查 + + -这一节是本语法手册默认的语言硬规则收口点。涉及赋值、`.tsl` 语句区 / 声明区、`.tsf` 模块、命名参数、类写法、`unit` 骨架和下标规则时,统一先看这里。 +这一节是专题事实的强制派生速查,不是第二份事实源。写代码时先核对这里,再按当前任务的语法要素取回对应专题 Section;`--check` 会逐条比较下面的摘要与专题页 canonical 规则。 -- 赋值:普通赋值用 `:=`,不要把 `=` 当成普通赋值。 -- 后缀:用户已给出 `.tsl` / `.tsf` 后缀时,后缀就是判断依据;未给后缀时,入口流程、脚本任务或一次性执行逻辑对应 `.tsl`,可复用交付物(函数、过程、类、模块或扩展文件)对应 `.tsf`,只是脚本内部封装函数或类时仍按 `.tsl` 处理;仍不明确时向用户确认。 -- `.tsl`:可执行脚本,语句区在前并按顺序执行;函数 / 类声明区在后,供前面的语句调用或运行时解析。不要在声明区后面继续追加脚本语句。 -- `.tsf`:可复用模块 / 函数扩展文件;非 `unit` 顶层函数 / 过程部署到解释器 `funcext` 后,脚本可以直接调用;顶层类声明只按可复用声明理解;`unit` 按模块组织理解。 -- `.tsf` 文件名:文件名(不含扩展名)必须与第一个顶层声明同名,`UserAccount.tsf` 的顶层声明必须是 `function UserAccount`、`type UserAccount = class` 或 `unit UserAccount`。 -- `function` / `procedure`:用户只说“写一个函数”时默认用 `function`;只有用户明确要求 `procedure` / 过程时才用 `procedure Name(...); begin ... end;`。 -- 类:顶层类定义统一写成 `type Name = class ... end;`,不要写裸 `class Name`;创建对象有两种方式,`new ClassName()` 最常用,`createObject(...)` 作为次选;需要字符串类名、类类型变量或跨 `unit` 路径时,更适合用 `createObject(...)`。 -- `unit`:涉及多文件组织时,先按 `unit Name; interface ... implementation ... end.` 骨架理解;不要把 `unit` 当默认最小起手。 -- 命名参数:写法是 `Func(a:1, b:2)`,不要写成 `Func(a = 1, b = 2)`。 -- 下标:`array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。 + + +- 普通变量赋值使用 `:=`;`=` 在普通表达式里用于比较,不用于赋值。 + + + +- 未给后缀时,入口流程、脚本任务或一次性执行逻辑对应 `.tsl`;可复用交付物(函数、过程、类、模块或扩展文件)对应 `.tsf`;只是脚本内部封装函数或类时,仍按 `.tsl` 处理;仍不明确时向用户确认,不要把脚本入口和可复用模块合并成一个猜测文件。 + + + +- `.tsl` 脚本按两段理解:语句区在前并按顺序执行;声明区在后,可放 `function / procedure` 或 `type Name = class`。写 `.tsl` 时先写语句区,需要函数、过程或类时把声明区放在语句区之后。 + + + +- 写 `.tsf` 时只写顶层函数 / 过程 / 类声明,或 `unit`;不要写成会直接顺序执行的脚本入口。 + + + +- `.tsf` 文件名(不含扩展名)必须与第一个顶层声明同名;第一个声明可以是同名 `function`、`type Name = class` 或 `unit`。 + + + +- 用户提示词里的“函数”默认对应 `function`,不要自动改写成 `procedure`。 + + + +- `procedure Name(...); begin ... end;` 只在用户明确要求 `procedure` / 过程时生成;不要因为没有返回值就自动改用 `procedure`。 + + + +- 类定义统一按 `type Name = class ... end;` 写。 + + + +- 普通本地类实例化默认生成 `new ClassName()`;`createObject("ClassName")`、`createObject(ClassType)` 只在字符串类名、类类型变量或跨 `unit` 路径场景生成。 + + + +- `unit` 是完整的顶层主体;常见完整形态是 `unit Name; interface ... implementation ... end.`。 + + + +- 如果没有特殊需求,默认优先用完整形态;简写形态只在不需要显式区分 `interface` / `implementation` 时再用。 + + + +- 调用时支持命名参数,写法是 `name: value`。 + + + +- 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。 + + + +- `array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。 ## 术语对照 + + - “脚本语句区”:`.tsl` 文件开头会按顺序执行的语句。 - “声明区”:`.tsl` 语句区之后的 `function / procedure` 或 `type Name = class` 声明。 - “顶层 `function / procedure`”:在 `.tsf` 中指模块暴露的顶层函数 / 过程,在 `.tsl` 中指脚本声明区里的函数 / 过程。 @@ -33,6 +84,8 @@ ## 默认起手骨架 + + 如果用户只要求写一段代码、脚本或示例,且没有明确 `.tsf`、`unit`、模块复用或对象建模需求,直接从 `.tsl` 脚本版本开始: diff --git a/skills/tsl-syntax-reference/references/02_core_model.md b/skills/tsl-syntax-reference/references/02_core_model.md index c2491dc7..75b33b6e 100644 --- a/skills/tsl-syntax-reference/references/02_core_model.md +++ b/skills/tsl-syntax-reference/references/02_core_model.md @@ -4,17 +4,24 @@ ## 本篇职责 + + 回答“目标文件到底是 `.tsl` 脚本还是 `.tsf` 可复用声明文件,以及 `.tsl` 里的哪些内容会顺序执行、哪些内容只是后置声明”。 本页是文件模型的唯一事实源:后缀判断、语句区 / 声明区顺序、`.tsf` 顶层声明形态和文件名约束都在这里收口。函数体、类体、`unit` 内部的语法外形由各自专题页拥有。 ## 文件模型核心规则 + + - 用户已给出 `.tsl` / `.tsf` 后缀时,后缀就是判断依据;未给后缀时,再按交付目标判断。 + - 未给后缀时,入口流程、脚本任务或一次性执行逻辑对应 `.tsl`;可复用交付物(函数、过程、类、模块或扩展文件)对应 `.tsf`;只是脚本内部封装函数或类时,仍按 `.tsl` 处理;仍不明确时向用户确认,不要把脚本入口和可复用模块合并成一个猜测文件。 + - `.tsl` 脚本按两段理解:语句区在前并按顺序执行;声明区在后,可放 `function / procedure` 或 `type Name = class`。写 `.tsl` 时先写语句区,需要函数、过程或类时把声明区放在语句区之后。 + - 写 `.tsf` 时只写顶层函数 / 过程 / 类声明,或 `unit`;不要写成会直接顺序执行的脚本入口。 - `.tsf` 里的非 `unit` 顶层函数 / 过程可按函数扩展理解:部署到解释器 `funcext` 后,`.tsl` 可以直接调用;顶层类声明只按可复用声明理解;`unit` 按模块组织理解。 - `uses` 可以出现在顶层,但这里只把它当成辅助语句,不把它当成主体声明;函数体和类定义体里的位置限制见 [09_units_and_scope.md](09_units_and_scope.md)。 @@ -23,12 +30,14 @@ - `unit` 默认先按完整形态理解;它也可以省略 `interface` / `implementation` 写成简写形态,见 [09_units_and_scope.md](09_units_and_scope.md)。 - 不要把 `.tsl` 写成只有顶层函数的模块;如果用户要通用可复用函数,优先写 `.tsf`。 - 不要把 `.tsf` 写成会直接执行脚本语句的入口;如果用户要顺序执行入口,优先写 `.tsl`。 -- `.tsf` 文件名(不含扩展名)必须与第一个顶层声明同名: - - `UserAccount.tsf` 中的顶层声明必须是 `function UserAccount` 或 `type UserAccount = class` 或 `unit UserAccount`。 - - TSL 语言大小写无关,因此 `userAccount.tsf` 和 `UserAccount.tsf` 在语法层面都合法。 + +- `.tsf` 文件名(不含扩展名)必须与第一个顶层声明同名;第一个声明可以是同名 `function`、`type Name = class` 或 `unit`。 +- TSL 语言大小写无关,因此 `userAccount.tsf` 和 `UserAccount.tsf` 在语法层面都合法。 ## 文件模型示例 + + 使用这些示例时遵守: - 可以模仿已经出现的文件模型、语句顺序和块级结构。 @@ -38,6 +47,8 @@ ### `.tsl` 文件模型 + + `.tsl` 脚本语句区的最小形态: @@ -96,6 +107,8 @@ end; ### `.tsf` 文件模型 + + `.tsf` 顶层函数的最小形态: @@ -165,6 +178,8 @@ end. ### 文件模型反例 + + 顶层裸 `class` 声明: @@ -211,6 +226,8 @@ function:__main__:line 9: invalid statement ## 任务到文件模型的选择规则 + + 已经确定任务目标时,按下表选择起手形态: @@ -231,6 +248,8 @@ function:__main__:line 9: invalid statement ## 文件模型禁止项 + + - 把 `.tsl` 当成 `.tsf` 来写,只给一个顶层函数,不写任何会执行的脚本语句。 diff --git a/skills/tsl-syntax-reference/references/03_values_and_literals.md b/skills/tsl-syntax-reference/references/03_values_and_literals.md index 92ba5e39..4cf29a97 100644 --- a/skills/tsl-syntax-reference/references/03_values_and_literals.md +++ b/skills/tsl-syntax-reference/references/03_values_and_literals.md @@ -4,20 +4,24 @@ ## 本篇职责 + + 回答“基本类型怎么写、数组和字符串怎么索引、字符串编码边界怎么写、哪些值规则属于语言级事实”。本页拥有整数、实数、字符串、布尔、`array(...)` 字面量和下标 / 区间规则;矩阵与集合扩展不在本页展开。 ## 核心规则 + + + + - 最先掌握的几类值是:整数、实数、普通字符串、布尔和 `array(...)`。 - 普通字符串既可以用双引号,也可以用单引号。 - 同类引号本身可以通过连续写两个同类引号放进字符串里。 - `\\`、`\"`、`\n` 这类基础转义可用。 - `\t` 和 `\xNN` 这类转义也可用。 - `\r`、`\r\n`、`\a`、`\b`、`\f`、`\v` 这些经典转义也可用。 -- `array(...)` 既可以写顺序数组,也可以写字符串键表。 -- 顺序数组下标从 `0` 开始。 -- `binary(...)` 创建的二进制缓冲区也用 `[]` 访问,并且下标从 `0` 开始。 -- 字符串下标从 `1` 开始。 + +- `array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。 - `s[0]` 在运行时会越界,不要把字符串当成 0 基下标。 - 字符串取子串用 `s[start:end]`,并且 `end` 是包含在结果里的。 - 字符串替换子串用 `s[start:end] := "..."`。 @@ -33,6 +37,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - 只复制任务需要的值、字面量、下标或字符串片段,不要把多个示例拼成未写入文档的新语法。 @@ -164,6 +170,8 @@ C ### 字符串边界规则 + + 原始字符串 `%%` 的空白分隔、标识符和多行: @@ -305,6 +313,8 @@ writeLn(b[1] = "1"); ### 字符串子串读写 + + 字符串子串统一用 `s[start:end]` 区间访问,`end` 包含在结果里;同一形态既能读取,也能替换、删除和插入。 @@ -371,6 +381,8 @@ writeLn(s); ### 字符串 `$` 连接与拼装 + + `$` 用于字符串连接和类型转字符串拼装: @@ -404,6 +416,8 @@ A=1 B=2.34 C=AAA ### 字符串 `like` 模式匹配 + + `like` 用于判断字符串是否符合指定模式(支持通配符和正则表达式): @@ -450,6 +464,8 @@ writeLn(result); ### `format` 格式化占位符 + + `format` 用于按占位符格式化输出: @@ -474,6 +490,8 @@ writeLn(result); ### `formatdatetime` 日期格式化 + + `formatdatetime` 用于将日期按指定格式输出: @@ -499,6 +517,8 @@ writeLn(result); ## 默认生成模板 + + 如果你只是要抓住“基本类型 + array”的第一层,用这个最短例子: 代码块身份:可直接照写示例 @@ -513,6 +533,8 @@ items := array(1, 2, 3); ## 禁止项 + + - 用 `s[0]` 访问字符串首字符。 - 误以为 `array(...)` 只能写顺序数组,不能写字符串键表。 - 误以为字符串区间和数组一样从 `0` 开始。 diff --git a/skills/tsl-syntax-reference/references/04_variables_and_constants.md b/skills/tsl-syntax-reference/references/04_variables_and_constants.md index 7210eee6..f024fd61 100644 --- a/skills/tsl-syntax-reference/references/04_variables_and_constants.md +++ b/skills/tsl-syntax-reference/references/04_variables_and_constants.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“普通变量怎样直接使用、`var` 在什么位置出现、常量必须怎样初始化、哪些名字一旦绑定就不能再赋值”。 ## 核心规则 + + - 默认变量模型是“直接赋值即得到变量”,不要求先写 `var`。 - 例如:`a := 1; b := array(1, 2, 3);` 这种写法可直接编译。 - `var name;` 只作为显式声明写法,不能当成默认必需步骤。 @@ -26,6 +30,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - 默认生成普通变量时直接用 `:=`;只有用户明确要求或 `{$explicit+}` 场景才复制 `var`。 @@ -34,6 +40,8 @@ ### 普通变量与显式声明 + + 默认变量模型: @@ -82,6 +90,8 @@ writeLn(a); ### 常量初始化 + + 顶层最稳的常量写法: @@ -170,6 +180,8 @@ value := kMaxRetries; ### 多参数赋值 + + 多参数赋值: @@ -297,6 +309,8 @@ end; ## 默认生成模板 + + 普通变量和顶层常量的默认骨架如下: 代码块身份:可直接照写示例 @@ -309,6 +323,8 @@ items := array(1, 2, 3); ## 禁止项 + + - 以为普通变量必须先写 `var` 才能使用。 - 把常量初始化写成普通变量赋值风格。 - 以为 `const =` 顶层单独写一行就一定成立。 diff --git a/skills/tsl-syntax-reference/references/05_functions_and_calls.md b/skills/tsl-syntax-reference/references/05_functions_and_calls.md index 7a25993b..6cedeec4 100644 --- a/skills/tsl-syntax-reference/references/05_functions_and_calls.md +++ b/skills/tsl-syntax-reference/references/05_functions_and_calls.md @@ -4,12 +4,18 @@ ## 本篇职责 + + 回答“如何正确声明 `function` 和 `procedure`、`.tsl` 脚本语句区如何调用后置函数声明、怎样使用参数修饰、普通函数默认参数与可变参数,以及哪些函数写法会直接编译失败”。 ## 核心规则 + + - 最稳妥的函数骨架仍然是 `function Name(...); begin ... end;`。 + - 用户提示词里的“函数”默认对应 `function`,不要自动改写成 `procedure`。 + - `procedure Name(...); begin ... end;` 只在用户明确要求 `procedure` / 过程时生成;不要因为没有返回值就自动改用 `procedure`。 - `procedure` 头后不允许写返回类型;返回类型注解只用于 `function`。 - 在 `.tsl` 文件模型层,脚本语句后可以接函数声明;语句区在前顺序执行,声明区在后提供函数/过程定义。见 [02_core_model.md](02_core_model.md)。 @@ -29,8 +35,10 @@ - 如果不确定任务是否需要写回语义,优先显式用 `const` 形参,或先切到 `{$varByRef-}`;不要依赖未修饰参数的运行时默认行为。 - `return expr;` 会直接返回当前函数结果。 - `exit;` 会立即结束当前函数;在本页最小样例里,如果此前没有写入返回结果,调用方观察到的是默认值 `0`。 + - 调用时支持命名参数,写法是 `name: value`。 - 命名参数也支持 `call(...)` 这类按函数名或函数指针转调的模型。 + - 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。 - 对二进制函数 / 系统函数直接使用命名参数,会报 `named parameter mode can't support here`;这类函数要先用 TSL 再封一层。 - 函数参数支持默认值。 @@ -50,6 +58,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - 普通运行示例默认按 `.tsl` 脚本语句区书写;入口语句放前面,函数 / 过程 / 类型声明放在后置声明区。 @@ -59,6 +69,8 @@ ### 基础函数 / 过程骨架 + + `.tsl` 语句区调用后置函数声明: @@ -141,6 +153,8 @@ end; ### 签名增强:参数类型与返回值类型注解 + + 带参数类型和返回值类型: @@ -200,6 +214,8 @@ end; ### 参数传递方式 + + 未修饰参数默认写回调用方: @@ -317,6 +333,8 @@ end; ### `return` 与 `exit` + + 代码块身份:可直接照写示例 @@ -349,6 +367,8 @@ end; ### 调用增强:命名参数与混用规则 + + 命名参数调用: @@ -428,6 +448,8 @@ end; ### 默认参数 + + 默认值参数: @@ -514,6 +536,8 @@ function F(a, b = 100, c = default_value); ### 可变参数 `...` + + 尾部可变参数: @@ -652,6 +676,8 @@ end; ### 匿名函数与函数指针 + + 匿名函数变量: @@ -775,6 +801,8 @@ writeLn(a(7, 8)); ### `::` 指向全局函数 + + 当当前作用域里有同名局部函数时,可以用 `::FuncName(...)` 指定去调全局/系统函数: @@ -813,10 +841,14 @@ end; ### 系统交互专题 + + `external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。这一篇只保留“普通函数怎样定义和调用”的主线。 ## 默认生成模板 + + 如果你只是要写一个能被智能体稳定续写的 `.tsl` 脚本,从语句区起步,需要函数时把声明区放在后面: 代码块身份:可直接照写示例 @@ -853,6 +885,8 @@ end; ## 禁止项 + + - 在 `.tsl` 的声明区后面继续写脚本语句。 - 用户只说“写一个函数”时,默认改成 `procedure`。 - 因为任务没有返回值,就自动改成 `procedure`。 diff --git a/skills/tsl-syntax-reference/references/06_expressions_and_operators.md b/skills/tsl-syntax-reference/references/06_expressions_and_operators.md index 33bde082..a65c4e86 100644 --- a/skills/tsl-syntax-reference/references/06_expressions_and_operators.md +++ b/skills/tsl-syntax-reference/references/06_expressions_and_operators.md @@ -4,14 +4,20 @@ ## 本篇职责 + + 回答“赋值、算术、比较、逻辑、位运算、集合/类型关系、条件求值、表达式对象、访问/调用、点前缀运算符和专题运算符入口在 TSL 里怎样写”。 ## 核心规则 + + - 本页是 TSL 表达式与运算符的生成规则页;写代码时只使用本页或对应专题页明确记录的运算符和表达式形态。 ### 本页直接生成的运算符 + + @@ -46,6 +52,8 @@ ### 专题运算符入口 + + | 运算符 / 形态 | 责任页 | 生成规则 | @@ -66,8 +74,10 @@ 默认生成规则: -- 普通变量赋值使用 `:=`;常量初始化不按普通赋值判断,见 [04_variables_and_constants.md](04_variables_and_constants.md)。 -- `=` 在普通表达式里用于比较,不用于赋值。 + + +- 普通变量赋值使用 `:=`;`=` 在普通表达式里用于比较,不用于赋值。 +- 常量初始化不按普通赋值判断,见 [04_variables_and_constants.md](04_variables_and_constants.md)。 - `const name = value;` 和函数签名默认参数 `name = value` 不按本页普通表达式比较判断。 - 普通算术使用 `+`、`-`、`*`、`/`、`\`、`%`、`div`、`mod`、`^`、`~`。 - 需要数值倒数时用 `!x`;整型和实型输入都会得到实型结果。 @@ -100,6 +110,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - `:=` 是普通变量赋值写法;常量初始化规则见 [04_variables_and_constants.md](04_variables_and_constants.md)。 @@ -110,6 +122,8 @@ ### 基础赋值和条件求值 + + 代码块身份:可直接照写示例 @@ -554,6 +568,8 @@ writeLn(if 2 > 1 then 2 else 1); ### 表达式对象 + + `@` 表达式前导: @@ -634,6 +650,8 @@ writeLn(result_value); ### 空安全访问 + + 代码块身份:可直接照写示例 @@ -679,6 +697,8 @@ writeLn(c?.a?.[1] = nil); ### 否定形式运算 + + 代码块身份:可直接照写示例 @@ -707,6 +727,8 @@ end; ### 标量链式比较 + + 代码块身份:可直接照写示例 @@ -733,6 +755,8 @@ writeLn(1 :<> 2 :<> 3); ### 矩阵链式比较 + + 代码块身份:可直接照写示例 @@ -763,6 +787,8 @@ writeLn(s[2]); ### 条件编译探测 + + 代码块身份:可直接照写示例 @@ -791,6 +817,8 @@ writeLn(0); ## 默认生成模板 + + 如果你只需要最小的“比较 + 三目”例子,直接用这个: 代码块身份:可直接照写示例 @@ -809,6 +837,8 @@ writeLn(value); ## 禁止项 + + - 用 `=` 当赋值运算符。 - 把 `==` / `!=` 当成等值 / 不等值比较;本页比较使用 `=` / `<>`。 - 把 `!` 当成逻辑非;本页逻辑非使用 `not`,`!` 是一元倒数/矩阵逆相关运算符。 diff --git a/skills/tsl-syntax-reference/references/07_control_flow.md b/skills/tsl-syntax-reference/references/07_control_flow.md index b70e5cc0..1b54aaee 100644 --- a/skills/tsl-syntax-reference/references/07_control_flow.md +++ b/skills/tsl-syntax-reference/references/07_control_flow.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“`if`、`case`、`for`、`while`、`repeat`、`break`、`continue`、`try`、`raise` 这些流程结构在 TSL 里到底怎么写,哪些写法可以直接生成”。 ## 核心规则 + + - `if ... then ... else ...` 默认写成块式分支:`then begin ... end else begin ... end`。 - 块式分支内部的普通语句必须用分号结尾。 - 控制流块的 `begin ... end` 后可以加分号也可以不加(语法都允许)。 @@ -26,6 +30,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - 条件表达式、比较、布尔值和普通赋值的事实见 [06_expressions_and_operators.md](06_expressions_and_operators.md)。 @@ -34,6 +40,8 @@ ### `if`、`while`、`repeat ... until` + + 代码块身份:可直接照写示例 @@ -69,6 +77,8 @@ writeLn(counter); ### `for` 的几种主干写法 + + 最基础的递增循环: @@ -155,6 +165,8 @@ for i, value in numbers do ### `break` 与 `continue` + + `break`: @@ -216,6 +228,8 @@ writeLn(sum); ### `case` 语句形态 + + 普通分支: @@ -305,6 +319,8 @@ mid ### `try ... except` + + 代码块身份:可直接照写示例 @@ -364,6 +380,8 @@ end ### `try ... finally` + + 正常路径: @@ -425,6 +443,8 @@ finally ### `raise` + + 代码块身份:可直接照写示例 @@ -448,6 +468,8 @@ before ## 默认生成模板 + + 最短条件分支的默认骨架如下: 代码块身份:可直接照写示例 @@ -466,6 +488,8 @@ end ## 禁止项 + + - 在 `else` 前面误加分号。 - 生成没有分号的裸分支赋值,例如 `then value := 1 else ...`;带 `else` 时用块式分支。 - 以为 `try ... finally` 会吞掉异常。 diff --git a/skills/tsl-syntax-reference/references/08_objects_and_classes.md b/skills/tsl-syntax-reference/references/08_objects_and_classes.md index ad011baf..d844bccf 100644 --- a/skills/tsl-syntax-reference/references/08_objects_and_classes.md +++ b/skills/tsl-syntax-reference/references/08_objects_and_classes.md @@ -4,10 +4,16 @@ ## 本篇职责 + + 回答“`type Name = class`、字段、`static`、方法、`property`、析构、类类型、继承和对象创建在 TSL 里怎样写”。 ## 核心规则 + + + + - 类定义统一按 `type Name = class ... end;` 写。 - 顶层类声明可以放在松散语句之后(单向允许)。 - 顶层类声明也可以放在顶层 `function / procedure` 之后。 @@ -36,6 +42,7 @@ - 基础覆盖写法是:父类方法声明为 `virtual`,子类对应方法声明为 `override`。 - 基础祖先类调用:可以用 `Inherited;`、`Inherited MethodName(...)` 或 `class(BaseClass, ObjectName).MethodName()`。 - 创建对象有两种方式:`new ClassName()` 最常用,`createObject(...)` 作为次选。 + - 普通本地类实例化默认生成 `new ClassName()`;`createObject("ClassName")`、`createObject(ClassType)` 只在字符串类名、类类型变量或跨 `unit` 路径场景生成。 - 如果类里定义了 `function create(...)`,`new`、`createObject("ClassName", ...)` 和 `createObject(ClassType, ...)` 都可以透传构造参数,也都支持默认参数和命名参数。 - 析构写法是无参 `function destroy();`;对象的最后一个引用被清空(如设为 `nil`)时会触发它。存在别名引用时,只清空其中一个引用不会触发。 @@ -51,6 +58,8 @@ ## 可直接照写示例 + + 使用这些示例时遵守: - 普通本地类创建优先复制 `new ClassName()` 形态。 @@ -59,6 +68,8 @@ ### 最小类与声明位置 + + 最短类骨架: @@ -169,6 +180,8 @@ writeLn(a); ### 字段、静态成员、常量与可见性 + + `static` 字段: @@ -383,6 +396,8 @@ end; ### 构造函数边界 + + `create` 建议保持 `public`: @@ -412,6 +427,8 @@ end; ### 属性、类型注解与类外实现 + + 基础 `property`: @@ -745,6 +762,8 @@ end; ### 对象创建与类类型 + + `new` 关键字: @@ -897,6 +916,8 @@ end; ### 重载、继承与析构 + + `overload` 方法: @@ -1302,6 +1323,8 @@ end; ### 跨 unit 类路径 + + `unit` 中的嵌套类路径创建属于跨 `unit` 边界,这里用 `text` 展示骨架: @@ -1396,6 +1419,8 @@ writeLn(0); ## 默认生成模板 + + 最短类骨架直接复用本页开头的“最短类骨架”。 代码块身份:可直接照写示例 @@ -1407,6 +1432,8 @@ end; ## 禁止项 + + - 以下误写不可照写;本节只收容易被智能体从相近语言或相邻 TSL 写法外推出来的边界,不重复列已经在核心规则里明确的普通规则。 - 不把裸类名成员访问当成文档事实;类方法和静态字段默认用 `class(Name).Member` 或文档明确反射入口。 - 把 `self()` 当成本页明确的工厂式写法。 diff --git a/skills/tsl-syntax-reference/references/09_units_and_scope.md b/skills/tsl-syntax-reference/references/09_units_and_scope.md index 22f9591c..fdb659cb 100644 --- a/skills/tsl-syntax-reference/references/09_units_and_scope.md +++ b/skills/tsl-syntax-reference/references/09_units_and_scope.md @@ -4,6 +4,8 @@ ## 本篇职责 + + 回答下面几类最常见的问题: - 什么时候该把一组函数写成 `unit` @@ -14,8 +16,13 @@ ## 核心规则 + + + + - `unit` 是完整的顶层主体;常见完整形态是 `unit Name; interface ... implementation ... end.`。 - `unit` 也可以省略 `interface` / `implementation` 写成简写形态;这种简写里定义的函数对外可调用。 + - 如果没有特殊需求,默认优先用完整形态;简写形态只在不需要显式区分 `interface` / `implementation` 时再用。 - 完整 `unit` 示例中,`implementation` 前保留空行,避免接口段和实现段挤在一起。 - `unit` 允许接口声明与实现段分离:`interface` 段可以只声明函数签名或类方法签名,函数体和类方法体放到 `implementation` 段。 @@ -33,8 +40,12 @@ ## 示例与行为 + + ### 最小 `unit` + + 代码块身份:可直接照写示例 @@ -71,6 +82,8 @@ end. ### 简写 `unit` + + 代码块身份:可直接照写示例 @@ -104,6 +117,8 @@ end. ### 顶层、函数体、类体里的 `uses` + + 沿用同一个 `DemoUnit.tsf`,分别看顶层、函数体、类体三种 `uses` 位置: @@ -159,6 +174,8 @@ end; ### `uses` 的位置限制 + + 代码块身份:反例 / 不可照写 @@ -208,6 +225,8 @@ end; ### `interface` 段 `uses` 和 `implementation` 段 `uses` + + 代码块身份:配置片段 / 概念骨架 @@ -289,6 +308,8 @@ writeLn(obj.FromImplementation()); ### 接口声明与实现段分离 + + 代码块身份:可直接照写示例 @@ -332,6 +353,8 @@ end. ### 实现段私有函数 + + 代码块身份:配置片段 / 概念骨架 @@ -380,6 +403,8 @@ writeLn(PrivateFunc()); ### 重名解析与限定调用 + + 重名函数最小对照: @@ -476,6 +501,8 @@ writeLn(objB.value); ## 默认生成模板 + + 如果你只是要先写出一个最小、可被其他文件引用的 `unit`,从下面这个骨架起步: 代码块身份:可直接照写示例 @@ -499,6 +526,8 @@ end. ## 禁止项 + + - 把依赖外部 `unit` 的 `uses` 例子误当成单文件最小示例。 - 在还没把目标 `unit` 放进查找路径前,就把“找不到接口”误判成 `uses` 语法错。 - 把函数体或类定义体里的 `uses` 写在第一条语句之后。 diff --git a/skills/tsl-syntax-reference/references/10_runtime_context_and_with.md b/skills/tsl-syntax-reference/references/10_runtime_context_and_with.md index a521c379..6f555312 100644 --- a/skills/tsl-syntax-reference/references/10_runtime_context_and_with.md +++ b/skills/tsl-syntax-reference/references/10_runtime_context_and_with.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“`setSysParam` / `getSysParam` 怎样用、`sysParams[...]` 是什么、块环境 `with *, values do` / `with **, values do` 怎样写、`#Func() with array(...)` 这种后缀环境调用怎样写,网格调用怎样取回结果,以及全局缓存最小读写规则是什么”。 ## 核心规则 + + 本页示例只说明运行时上下文语法中的调用位置和源码外形。API 的精确签名、参数、返回行为、平台 scope、目标环境或解释器可用性必须使用 `tsl-api-reference` skill 重新核对,不能由本页示例反推。 - TSL 有一组运行时系统参数;本页只写通用语法形态。 @@ -33,8 +37,12 @@ ## 可直接照写示例 + + ### `setSysParam` / `getSysParam` / `sysParams[...]` 基础读写 + + 直接设置和读取系统参数: @@ -78,6 +86,8 @@ writeLn(getSysParam("a")); ### 块环境 `with *` + + 代码块身份:可直接照写示例 @@ -131,6 +141,8 @@ end ### 块环境 `with **` + + 代码块身份:可直接照写示例 @@ -158,6 +170,8 @@ writeLn(getSysParam("b")); ### 后缀 `with array(...)`:网格调用的临时覆盖 + + `#Func(...) with array(...)` 在一次网格调用里临时覆盖系统参数,调用结束后恢复外层原值。下面用一个 `TestDo.tsf` 演示覆盖边界: @@ -212,6 +226,8 @@ writeLn(getSysParam("b") = nil); ### `#` 网格调用与 `dupvalue` + + 网格调用函数: @@ -263,6 +279,8 @@ array(2,4,6,8,10) ### 网格调用的 `timeout` + + 代码块身份:可直接照写示例 @@ -283,6 +301,8 @@ end; ### `setGlobalCache`、`getGlobalCache` 与 `ifCache` + + 代码块身份:可直接照写示例 @@ -310,6 +330,8 @@ writeLn(cached_data[0], ',', cached_data[1], ',', cached_data[2]); ### `checkGlobalCacheExpired` + + 代码块身份:可直接照写示例 @@ -329,6 +351,8 @@ writeLn(checkGlobalCacheExpired(cache_ref)); ### 缓存值本地写入后会实例化 + + 代码块身份:可直接照写示例 @@ -350,6 +374,8 @@ writeLn(local_copy[0], ',', local_copy[1], ',', local_copy[2]); ### 全局缓存参与 `select` + + 代码块身份:可直接照写示例 @@ -373,6 +399,8 @@ writeLn(query_result[0][0], ',', query_result[0][1], ';', query_result[1][0], ', ## 本页不生成的范围 + + - 网格超时触发错误时的完整边界 - `getGlobalCacheInfo`、`listGlobalCache`、`listGlobalCacheRemoved` - 初始化 TSL、监控线程、回收策略与兼容旧系统方案 @@ -383,6 +411,8 @@ writeLn(query_result[0][0], ',', query_result[0][1], ';', query_result[1][0], ', ## 默认生成模板 + + 最小运行时环境入口的默认骨架如下: 代码块身份:可直接照写示例 @@ -409,6 +439,8 @@ end; ## 禁止项 + + - 把系统参数页直接写成金融函数页。 - 把 `#Func() with array(...)` 误判成也能直接套在本地函数 `Demo()` 后面。 - 把 `with *` 误判成会自动恢复外层系统参数。 diff --git a/skills/tsl-syntax-reference/references/11_matrix_and_collections.md b/skills/tsl-syntax-reference/references/11_matrix_and_collections.md index d045be1c..ab29c5e5 100644 --- a/skills/tsl-syntax-reference/references/11_matrix_and_collections.md +++ b/skills/tsl-syntax-reference/references/11_matrix_and_collections.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“`array(...)` 在 TSL 里除了最普通的一维数组,还能怎样组织数据;哪些矩阵样写法属于本页文档明确形态;元素、子集、整行和行集合关系应该怎样判断”。 ## 核心规则 + + - `array(...)` 既可以写顺序数组,也可以写字符串键表。 - 顺序数组下标从 `0` 开始;字符串仍然从 `1` 开始。 - `array(...)` 可以继续嵌套,形成二维或矩阵样数据。 @@ -21,9 +25,13 @@ ## 可直接照写示例 + + ### 基础数组与键表 - + + + 顺序数组与字符串键表: @@ -71,6 +79,8 @@ writeLn(r2[1]); ### 矩阵样比较 + + 矩阵链式比较: @@ -96,6 +106,8 @@ writeLn(s[2]); ### `in`、`not in`、`sqlin`、`not sqlin` + + `in` 既可以判断单个元素是否存在,也可以判断左侧数组是否是右侧结果集的子集: @@ -156,6 +168,8 @@ writeLn(array(5, 6) not sqlin array((1, 2), (3, 4))); ### 行集合并、交、差、对称差 + + 下面这组最小例子展示“按行运算”和“结果会折叠重复行”: @@ -192,6 +206,8 @@ outersect_rows := left_rows outersect right_rows; ## 和过滤运算的区别 + + - 集合运算先把数据当成“行集合”来看,再做包含、并交差。 @@ -201,6 +217,8 @@ outersect_rows := left_rows outersect right_rows; ## 默认生成模板 + + 按需求语义从下面两种模板中选择: 代码块身份:可直接照写示例 @@ -212,6 +230,8 @@ row_matched := array(1, 2) sqlin array((1, 2), (3, 4)); ## 点前缀比较算符产生逻辑数组 + + 点前缀比较(`.=`、`.<>`、`.>`、`.>=`、`.<`、`.<=`)对数组/矩阵逐元素比较,返回真假值数组: @@ -277,6 +297,8 @@ writeLn("子集 (1,0):", subset[1][0]); ## 非完全矩阵缺位当 0 处理与标量广播 + + 基础算符作用于非完全矩阵(行长度不一致或字符串键不对齐的数组)时,对应位置不存在或为 `nil` 时**默认当 0 处理**: @@ -336,6 +358,8 @@ writeLn("(1,1):", result[1][1]); ## 本页不生成的范围 + + - 专门的结果集过滤函数 - TS-SQL 查询与写回 - 更大范围的矩阵函数族 @@ -344,6 +368,8 @@ writeLn("(1,1):", result[1][1]); ## 禁止项 + + - 不要把字符串下标按数组的 `0` 起始规则来写;字符串下标从 `1` 开始,详见 [03_values_and_literals.md](03_values_and_literals.md)。 - 不要把矩阵链式比较 `::...` 和标量链式比较混写成同一种语法。 - 不要把 `in` 和 `sqlin` 当成同一个概念。 diff --git a/skills/tsl-syntax-reference/references/12_resultset_and_filters.md b/skills/tsl-syntax-reference/references/12_resultset_and_filters.md index 00b17dac..bb0fab97 100644 --- a/skills/tsl-syntax-reference/references/12_resultset_and_filters.md +++ b/skills/tsl-syntax-reference/references/12_resultset_and_filters.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“已有数组或二维结果集时,想按某个过滤集保留命中的行、排除命中的行,或者只拿到符合条件的行下标时,应该怎么写”。 ## 核心规则 + + - 结果集过滤主函数是 `filterIn(...)` 和 `filterNotIn(...)`。 - `filterIn(rows, filter_array, field)` 返回命中的子结果集;`filterNotIn(...)` 返回未命中的子结果集。 - 第四个参数写成 `false` 时,返回值不再是子结果集,而是符合条件的行下标数组。 @@ -20,8 +24,12 @@ ## 可直接照写示例 + + ### 按单列过滤 + + 按 `"Code"` 列做包含与排除: @@ -60,6 +68,8 @@ writeLn(drop_rows[1]["Code"]); ### 返回行下标 + + 第四个参数写成 `false` 时,返回行下标而不是子结果集: @@ -101,6 +111,8 @@ picked := rows[keep_index, array("Code", "V1")]; ### `Field = nil` 时按整行过滤 + + 整行过滤与一维数组过滤都走 `Field = nil`: @@ -131,6 +143,8 @@ left_only := filterNotIn(array(1, 2, 3, 4, 5, 5, 6, 7), array(1, 2, 3, 4), nil); ### 按字段列表过滤 + + 如果只想按部分列判断是否命中,把 `Field` 写成字段名数组: @@ -161,6 +175,8 @@ sub_result := rows[keep_index, array("A", "C")]; ## 和集合运算的区别 + + - 过滤运算按“原结果集中的每一行是否命中”来决定保留或排除。 @@ -170,6 +186,8 @@ sub_result := rows[keep_index, array("A", "C")]; ## 默认生成模板 + + 结果集过滤的默认骨架如下: 代码块身份:可直接照写示例 @@ -185,6 +203,8 @@ keep_rows := filterIn(rows, code_arr, "Code"); ## 禁止项 + + - 把 `filterIn(...)` 当成去重工具。 - 过滤整行时忘了把 `Field` 写成 `nil`。 - 需要返回下标时漏掉第四个参数 `false`。 diff --git a/skills/tsl-syntax-reference/references/13_ts_sql.md b/skills/tsl-syntax-reference/references/13_ts_sql.md index c901fd23..5ce3f011 100644 --- a/skills/tsl-syntax-reference/references/13_ts_sql.md +++ b/skills/tsl-syntax-reference/references/13_ts_sql.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“写 TS-SQL 查询和写回时,怎样从最小 `select ... from ... end` 骨架开始,逐步处理筛选、分组、排序、多表联接(含 LEFT JOIN)、组内子查询、极值引用,以及如何用 `insert`/`update`/`delete` 修改内存数组”。 ## 核心规则 + + - TS-SQL 是 TSL 自带的类 SQL 查询语法,不是金融业务函数库。 - 基础查询文档骨架是:以 `select` / `sselect` / `vselect` / `mselect` 开始,以 `end` 收尾。 - `from` 后面可以直接跟内存数组结果集。 @@ -29,8 +33,12 @@ ## 可直接照写示例 + + ### 最小查询骨架 + + 代码块身份:可直接照写示例 @@ -58,6 +66,8 @@ writeLn(length(query_result)); ### 字段选择 + + 代码块身份:可直接照写示例 @@ -80,6 +90,8 @@ writeLn(length(query_result)); ### 四个查询入口怎样分工 + + 代码块身份:可直接照写示例 @@ -106,6 +118,8 @@ col_index := mcols(matrix_result, 1); ### `where` 和 `order by` + + 代码块身份:可直接照写示例 @@ -127,6 +141,8 @@ query_result := select * from source_rows where ["B"] > 1 order by ["B"] end; ### `group by` + + 代码块身份:可直接照写示例 @@ -153,6 +169,8 @@ group_result := select ["A"], sumOf(["B"]) as "SumB" ### 一维数组上的 `thisRow` 与 `thisRowIndex` + + 代码块身份:可直接照写示例 @@ -176,6 +194,8 @@ query_result := select thisRow as "Value", thisRowIndex as "Idx" ### `join` + + 代码块身份:可直接照写示例 @@ -215,6 +235,8 @@ writeLn(join_result[0]["V2"]); ### `thisGroup` + + 代码块身份:可直接照写示例 @@ -242,6 +264,8 @@ group_result := select ["A"], max_b := maxOf(["B"]) as "MaxB", ### `thisRowIndex` 在排序后仍指向原始位置 + + 代码块身份:可直接照写示例 @@ -265,6 +289,8 @@ query_result := select thisRowIndex as "Idx", ["B"] ### `refMaxOf` 与 `refMinOf` + + 代码块身份:可直接照写示例 @@ -283,6 +309,8 @@ min_ref_result := select minOf([0]) as "MinA", refMinOf([1]) as "RefB" from sour ### `LEFT JOIN` 多表联接 + + `left join` 保留左表所有行,右表不匹配时用 `nil` 填充: @@ -322,6 +350,8 @@ writeLn("(1,1):", result[1]["vb"]); ### `right join` / `full join` / `cross join` 与逗号联接 + + `right join` / `full join` 的不匹配行同样用 `nil` 填充: @@ -423,6 +453,8 @@ writeLn("(0,0):", result[0]["id"]); ### `INSERT` 写回 + + `insert into` 向内存数组插入新行: @@ -480,6 +512,8 @@ writeLn(a[2]["id"], a[2]["cls"]); ### `UPDATE` 写回 + + `update` 修改符合条件的行: @@ -533,6 +567,8 @@ writeLn(r[0], r[1], r[2]); ### `DELETE` 写回 + + `delete` 删除符合条件的行: @@ -582,6 +618,8 @@ writeLn(mrows(a)); ### `distinct` 结果集去重 + + `select distinct` 对结果集去重;聚集函数内也可用 `distinct` 前缀: @@ -613,6 +651,8 @@ writeLn(plain_sum); ### `as` 别名、`as nil` 与字段区间 + + 代码块身份:可直接照写示例 @@ -643,6 +683,8 @@ writeLn(mcols(range_rows)); ### `drange` 取行区间 + + 代码块身份:可直接照写示例 @@ -672,6 +714,8 @@ writeLn(mrows(part_rows)); ### `selectopt` 位选项 + + 代码块身份:可直接照写示例 @@ -697,6 +741,8 @@ writeLn(opt_rows[0]); ### 条件聚集、移动聚集与 `refof` + + 聚集函数统一支持 `(Expr, BoolConditionExp, N, MovingFirst, CacheId)` 形态: @@ -729,6 +775,8 @@ writeLn(ref_prev[1]["Expr1"]); ### `group by ... having` + + `having` 用聚集条件筛选分组(`where` 不能用聚集): @@ -764,6 +812,8 @@ having_rows := select ["cls"] from a group by ["cls"] having countof(*) > 1 end; ### `thisOrder` 与多列 `order by` + + 代码块身份:可直接照写示例 @@ -791,6 +841,8 @@ writeLn(desc_rows[0]["id"]); ### `refsof` 引用上级结果集 + + 在嵌套子查询里,`refsof(Exp, UpLevel)` 用上 N 级结果集计算 `Exp`: @@ -818,6 +870,8 @@ writeLn(r[1]["id"], ",", r[1]["up"]); ### `[@Field]` 取字段类型 + + 代码块身份:可直接照写示例 @@ -840,6 +894,8 @@ integer ### `aggof` 自定义聚集扩展 + + `aggof('名称', 表达式)` 调用一个自定义回调函数做聚集。本地 `TSL.exe` 会报 `AggOf Init Error`;下例在服务端(pyTSL)验证通过: @@ -877,6 +933,8 @@ end; ## 本页不生成的范围 + + - `TSQLInsert` / `TSQLSetValue` / `TSQLBatchInsert` / `TSQLEdit` / `TSQLPost` / `TSQLFinal` 对象被 TS-SQL 查询的回调机制 - 面向 SQL 表、业务表或时间序列的数据查询与写回(`marketTable` / `infoTable` / `tradeTable` / `sqlTable` / `hugeSqlTable` 等数据源) @@ -884,6 +942,8 @@ end; ## 默认生成模板 + + TS-SQL 的最短默认骨架如下: 代码块身份:可直接照写示例 @@ -895,6 +955,8 @@ query_result := select * from source_rows end; ## 禁止项 + + - 不要把数据库 SQL 方言直接迁移成 TS-SQL 代码。 - 不要把 `select` 当成普通函数调用,忘了以 `end` 收尾。 - 在二维结果集里直接写 `A` 而不是 `["A"]`。 diff --git a/skills/tsl-syntax-reference/references/14_debug_and_profiler.md b/skills/tsl-syntax-reference/references/14_debug_and_profiler.md index 169edc52..3cbd0ea1 100644 --- a/skills/tsl-syntax-reference/references/14_debug_and_profiler.md +++ b/skills/tsl-syntax-reference/references/14_debug_and_profiler.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“`goto`、`debugReturn`、`debugRunEnv`、`mtic` / `mtoc`、`setProfiler`、`__line__` 和 `__stack_frame` 怎样写、会怎样表现”。 ## 核心规则 + + 本页示例只说明调试与分析结构中的调用位置和源码外形。相关 API 的精确签名、参数、返回行为、平台 scope、目标环境或解释器可用性必须使用 `tsl-api-reference` skill 重新核对,不能由本页示例反推。 - `goto label_name;` 属于文档明确写法,但目标位置以 `label label_name; statement` 这种内联形式作为默认生成形态。 @@ -23,8 +27,12 @@ ## 可直接照写示例 + + ### `goto` + + 代码块身份:可直接照写示例 @@ -91,6 +99,8 @@ writeLn("after"); ### `debugReturn` + + 代码块身份:可直接照写示例 @@ -114,6 +124,8 @@ end; ### `debugRunEnv` 与 `debugRunEnvDo` + + `debugRunEnv(0)` / `debugRunEnv(1)`: @@ -156,6 +168,8 @@ end; ### `mtic` 与 `mtoc` + + 代码块身份:可直接照写示例 @@ -181,6 +195,8 @@ writeLn(elapsed2 >= 0); ### `setProfiler` 与 `getProfilerInfo` + + 代码块身份:可直接照写示例 @@ -203,6 +219,8 @@ writeLn(length(info) > 0); ### `__line__` 与 `__stack_frame` + + `__line__`: @@ -252,6 +270,8 @@ array( ## 禁止项 + + - 不要把 `debugReturn` 当成普通函数 `return` 使用。 - 不要假设 `goto` 可以跨函数、跨脚本体或跳到单独成行的 `label`。 - 不要给计时或性能分析器调用补未写入文档参数。 diff --git a/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md b/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md index b6896311..6037ff66 100644 --- a/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md +++ b/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“TSL 的词法层规则和编译期开关有哪些”:标识符与大小写、注释形态、保留字清单,以及 `{$...}` 编译开关的书写位置和作用范围。 ## 核心规则 + + - 标识符大小写无关;下划线可出现在标识符中。 - `//` 是行注释;首行 `#!` 可作为 CGI 风格注释;`{ ... }` 与 `(* ... *)` 是块注释。 - 条件编译指令使用 `{$define}`、`{$undef}`、`{$ifdef}`、`{$ifndef}`、`{$else}`、`{$endif}`。 @@ -20,6 +24,8 @@ ## 保留字/关键字速查 + + TSL 关键字大小写无关;本表统一按文档推荐写法展示。生成代码时不要把这些名称用作普通标识符。 @@ -46,8 +52,12 @@ TSL 关键字大小写无关;本表统一按文档推荐写法展示。生成 ## 可直接照写示例 + + ### 标识符、注释与条件编译 + + 大小写无关与下划线标识符: @@ -106,6 +116,8 @@ writeLn(40); ### 显式变量声明开关 + + `{$explicit+}` 的文档明确形态: @@ -144,6 +156,8 @@ writeLn(a + b); ### 条件编译分支边界 + + 条件编译不会去编译未命中的坏代码分支: @@ -166,6 +180,8 @@ writeLn(1); ### `{$CompileOption}` 编译选项 + + `{$CompileOption}` 用于设置编译期开关,改变编译器的默认行为: @@ -192,6 +208,8 @@ echo 1 + 1; ### 参数默认传递开关 + + `{$varByRef-}` 与 `{$varByRef+}`: @@ -242,6 +260,8 @@ end; ## 禁止项 + + - 不要在 `{$explicit+}` 后继续直接使用未声明变量。 - 不要把 `{$i ...}` / `{$include ...}` 包含文件写法当成可用能力。 - 不要把 `反例 / 不可照写` 代码块复制进正向示例。 diff --git a/skills/tsl-syntax-reference/references/16_types_and_conversions.md b/skills/tsl-syntax-reference/references/16_types_and_conversions.md index 599a7596..a4b78d3c 100644 --- a/skills/tsl-syntax-reference/references/16_types_and_conversions.md +++ b/skills/tsl-syntax-reference/references/16_types_and_conversions.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“运行时值会变成什么类型、默认值是什么、怎样显式转换、复数怎样表示”。本页拥有数值 / 日期时间 / 真假值 / nil / 复数字面量与类型转换边界;变量声明形式和编译期开关不在本页收口。 ## 核心规则 + + - 整数常量支持十进制、`0x` 十六进制、`0b` 二进制和 `0o` 八进制。 - `100L` 这类后缀写法表示 `Int64` 常量。 - 超过 32 位整数范围的十进制整数字面量 `2147483648` 按 `Int64`,不按实数处理。 @@ -35,8 +39,12 @@ ## 可直接照写示例 + + ### 数值、日期与真值 + + 基础数字字面量与类型判定: @@ -126,6 +134,8 @@ writeLn(nan = nan); ### 初始值与空值 + + 变量初始值与缺失值: @@ -205,6 +215,8 @@ writeLn(ifNumber("100")); ### 显式类型转换 + + 显式类型转换的正向例子: @@ -225,6 +237,8 @@ writeLn(c); ### 复数 + + 复数常量、类型与基础读取: @@ -357,6 +371,8 @@ writeLn(imag(z)); ## 禁止项 + + - 不要把“能编译”误解成“会自动按期望类型转换”。 - 不要把数字字符串直接当成数值参与算术;需要数值语义时先显式转换。 - 不要从其他语言的 truthy/falsy 或隐式转换规则反推 TSL。 diff --git a/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md b/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md index c6779cee..32ec4542 100644 --- a/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md +++ b/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“外部 DLL 声明、原生函数指针包装、C 回调和多线程调用有哪些文档明确写法”。本页只覆盖系统交互能力,普通 TSL 函数的定义与调用不在本页收口。 ## 核心规则 + + - 外部函数声明的文档明确形态是 `function Name(...): Type; stdcall|cdecl; external "dll" [name "symbol"];`。 - 当 TSL 函数名和 DLL 导出名一致时,`name "symbol"` 可以省略。 - Windows 示例默认显式写调用约定;不要把省略调用约定当成跨平台默认规则。 @@ -21,8 +25,12 @@ ## 可直接照写示例 + + ### 最小 `external` 声明 + + 代码块身份:可直接照写示例 @@ -99,6 +107,8 @@ function TickCdecl(): int64; cdecl; external "kernel32.dll" name "GetTickCount64 ### `procedure external` + + 代码块身份:可直接照写示例 @@ -120,6 +130,8 @@ procedure SleepMs(ms: integer); stdcall; external "kernel32.dll" name "Sleep"; ### 原生函数指针包装 + + 代码块身份:可直接照写示例 @@ -143,6 +155,8 @@ function GetProcAddress(module_handle: pointer; proc_name: string): pointer; std ### DLL 名的文档边界 + + 类常量字符串: @@ -181,6 +195,8 @@ function TickFromExpr(): int64; stdcall; external "kernel32"$"."$"dll" name "Get ### `makeInstance` + + 代码块身份:可直接照写示例 @@ -205,6 +221,8 @@ end; ### 线程模式最小正例 + + 代码块身份:可直接照写示例 @@ -238,6 +256,8 @@ end; ## 默认生成模板 + + DLL 引入的最小默认骨架如下: 代码块身份:可直接照写示例 @@ -250,6 +270,8 @@ function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCou ## 禁止项 + + - 把 `external` 的 DLL 名直接写成字符串拼接表达式。 - 省略了外部函数的参数类型或返回类型。 - 把 `makeInstance(...)` 生成的结果默认写成普通函数名直调,而不是先包装或用 `##f(...)`。 diff --git a/skills/tsl-syntax-reference/references/18_namespace_libpath_and_unit_runtime.md b/skills/tsl-syntax-reference/references/18_namespace_libpath_and_unit_runtime.md index 7174a1c6..58479a2a 100644 --- a/skills/tsl-syntax-reference/references/18_namespace_libpath_and_unit_runtime.md +++ b/skills/tsl-syntax-reference/references/18_namespace_libpath_and_unit_runtime.md @@ -4,6 +4,8 @@ ## 本篇职责 + + 回答下面这些“写法已经会了,但运行起来为什么这样”的问题: - `unit` 什么时候进入 `initialization` @@ -15,6 +17,8 @@ ## 核心规则 + + - 完整 `unit` 形态可以包含 `interface`、`implementation`、`initialization`、`finalization`,并以 `end.` 结束。 - `initialization` 在 `unit` 第一次被实际使用时触发,不是只因为顶层写了 `uses` 就立刻执行。 - `finalization` 会在脚本结束前触发。 @@ -33,8 +37,12 @@ ## 示例与行为 + + ### `unit` 生命周期 + + 代码块身份:配置片段 / 概念骨架 @@ -99,6 +107,8 @@ FINAL ### `unit` 成员的读取边界 + + 直接限定读取: @@ -242,6 +252,8 @@ DemoUnit.unit_counter := 13; ### `tslfilename()` + + 代码块身份:可直接照写示例 @@ -257,6 +269,8 @@ writeLn(tslfilename()); ### `namespace` 与默认命名空间 + + 把默认命名空间、脚本覆盖、以及“全局函数与命名空间函数同名”的三组结论压成同一套最小实验: @@ -304,6 +318,8 @@ writeLn(Hello()); ### `-LIBPATH` + + 查找顺序: @@ -359,6 +375,8 @@ tsl .\main.tsl -LIBPATH "D:\libA\,D:\libB\" ### `syssettsllibpath()` 与 `sysgettsllibpath()` + + 代码块身份:配置片段 / 概念骨架 @@ -378,6 +396,8 @@ writeLn(call("FnB")); ### `tsl.conf` 的 `Libpath=` + + 代码块身份:配置片段 / 概念骨架 @@ -468,6 +488,8 @@ writeLn(Hello()); ## 禁止项 + + - 把 `DemoUnit.var_name := value` 当成可用的限定赋值。 - 以为 `implementation` 里的常量和变量一定都不能从 `DemoUnit.Member` 读到。 - 以为 `findFunction("DemoUnit")` 暴露的成员范围和 `DemoUnit.Member` 完全相同。 diff --git a/skills/tsl-syntax-reference/references/19_object_runtime_and_introspection.md b/skills/tsl-syntax-reference/references/19_object_runtime_and_introspection.md index e252f2b9..ea788960 100644 --- a/skills/tsl-syntax-reference/references/19_object_runtime_and_introspection.md +++ b/skills/tsl-syntax-reference/references/19_object_runtime_and_introspection.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“怎样检查类信息、函数信息、对象运行时状态、弱引用创建和弱引用访问判定”。本页拥有反射与运行时状态查询;普通类编写、对象创建和方法调用不在本页收口,也不要因为本页存在反射能力就把普通调用改写成动态查找。 ## 核心规则 + + - 对象值可以用 `ifObj(...)` 做显式判定。 - 普通本地类创建仍默认使用 `new ClassName()`;本页的 `createObject(cls)` 只用于运行时已经拿到类类型变量后的创建场景。 - `class(Name)` 和 `findClass("Name")` 都可以拿到类类型。 @@ -36,8 +40,12 @@ ## 可直接照写示例 + + ### 对象值与类类型 + + 对象值的最小显式判定: @@ -122,6 +130,8 @@ end; ### 类信息与对象状态 + + `classInfo(1)` 返回类类型: @@ -225,6 +235,8 @@ end; ### 函数句柄与重载 + + `findFunction(...)` 查找实例方法和类方法: @@ -315,6 +327,8 @@ end; ### 函数信息、对象枚举与生命周期 + + `functionInfo` 的文档字段: @@ -406,6 +420,8 @@ end; ### 弱引用与自动弱引用 + + 弱引用能力的条件编译判定: @@ -543,6 +559,8 @@ end; ## 禁止项 + + - 不要在运行时/反射页发明类声明、继承或构造语法;类基础事实见 [08_objects_and_classes.md](08_objects_and_classes.md)。 - 不要为了普通对象创建先写 `findClass(...)` / `createObject(...)`;普通本地类创建以 [08_objects_and_classes.md](08_objects_and_classes.md) 为准,默认用 `new ClassName()`。 - 不要把函数句柄直接当普通函数直调;默认调用方式是 `f.do(...)`。 @@ -572,6 +590,8 @@ end; ### 段落式 `weakRef` / `autoRef` 的正确写法 + + 段落式 `weakRef` / `autoRef`(不带分号)像 `public` / `private` 一样切换后续成员的弱引用属性: diff --git a/skills/tsl-syntax-reference/references/20_builtin_runtime_objects.md b/skills/tsl-syntax-reference/references/20_builtin_runtime_objects.md index dd617ab7..86a51f52 100644 --- a/skills/tsl-syntax-reference/references/20_builtin_runtime_objects.md +++ b/skills/tsl-syntax-reference/references/20_builtin_runtime_objects.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“写普通 TSL/TSF 脚本时,哪些内置对象可以直接创建,最小可用接口是什么,哪些对象其实依赖特定运行上下文”。 ## 核心规则 + + - `TStringList` 可以直接用 `new TStringList()` 创建。 - `TStringList` 支持数字下标、字符串键下标、`Count` 和 `Add(...)`。 - `THashedStringList` 可以直接创建,最小用法与 `TStringList` 接近,适合按名称快速取值。 @@ -27,8 +31,12 @@ ## 可直接照写示例 + + ### `TStringList` + + 代码块身份:可直接照写示例 @@ -50,6 +58,8 @@ writeLn(list.Count); ### `THashedStringList` + + 代码块身份:可直接照写示例 @@ -69,6 +79,8 @@ writeLn(list["B"]); ### `TStream` 家族的最小可靠入口:`TMemoryStream` 与 `TFileStream` + + 代码块身份:可直接照写示例 @@ -126,6 +138,8 @@ writeLn(ifObj(base)); ### `TCipher` + + 无参创建反例: @@ -161,6 +175,8 @@ writeLn(cipher.Decrypt(enc)); ### `TRsa` + + 代码块身份:可直接照写示例 @@ -184,6 +200,8 @@ writeLn(length(rsa.PrivateKey) > 0); ### `TIniFile` 与 `TMemIniFile` + + 磁盘 INI 文件: @@ -220,6 +238,8 @@ writeLn(ini.ReadString("S", "K", "")); ### `MailMsg` 与 `MessagePart` + + 代码块身份:可直接照写示例 @@ -249,6 +269,8 @@ writeLn(part.Body); ### `TWebRequest` / `TWebResponse` 的普通脚本边界 + + 代码块身份:反例 / 不可照写 @@ -266,6 +288,8 @@ resp := new TWebResponse(); ## 本页不生成的范围 + + - `FTP` / `SMTP` / `POP3` 的连接、认证、上传下载、发送邮件和收取邮件流程。 - `TWebRequest` / `TWebResponse` 的 Web 请求响应流程。 - `TCookie` / `TCookieCollection`。 @@ -277,6 +301,8 @@ resp := new TWebResponse(); ## 禁止项 + + - 不要直接把 `new TStream()` 写成普通脚本里的可靠创建方式。 - 不要无参创建 `TCipher`;最小可靠写法是 `new TCipher(2)` 后再设置 `Password`。 - 不要把 `TWebRequest` / `TWebResponse` 写进普通脚本模板。 diff --git a/skills/tsl-syntax-reference/references/21_matrix_deep_dive.md b/skills/tsl-syntax-reference/references/21_matrix_deep_dive.md index 5ee3fbb0..8a88194c 100644 --- a/skills/tsl-syntax-reference/references/21_matrix_deep_dive.md +++ b/skills/tsl-syntax-reference/references/21_matrix_deep_dive.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“怎样直接构造全零矩阵、全一矩阵、随机矩阵、单位矩阵、空矩阵和数列数组,怎样写矩阵逆/广义逆,怎样进行矩阵乘除乘方,怎样转置矩阵,怎样拼接矩阵,怎样拿到矩阵的行数、列数、行索引和列索引,怎样遍历矩阵、取/改子矩阵,以及怎样用 `mfind` 找到或替换符合条件的单元格”。 ## 核心规则 + + 本页示例只说明矩阵语法中的调用位置、运算结构和源码外形。矩阵 API 的精确签名、参数、返回行为、平台 scope、目标环境或解释器可用性必须使用 `tsl-api-reference` skill 重新核对,不能由本页示例反推。 - 矩阵初始化和基础函数的具体参数规格使用 `tsl-api-reference` skill 按名查询;本页只保留矩阵行为示例、返回形态边界和跨函数通用的广播 / `ErrDefine` / `ErrReplace` 机制。 @@ -38,8 +42,12 @@ ## 可直接照写示例 + + ### 矩阵初始化 + + 代码块身份:可直接照写示例 @@ -77,6 +85,8 @@ writeLn(mcols(zeros_2d)); ### `->` 数列数组初始化 + + 默认步长为 `1`: @@ -119,6 +129,8 @@ seq_indexed := array(0, 1, array("A", "B", "C", "D", "E", "F")) -> 5; ### 矩阵一元倒数 / 逆 / 广义逆:`!A` + + 方阵输入返回普通矩阵逆: @@ -186,6 +198,8 @@ writeLn(inverse_value[2][1]); ### `msize`、`mrows`、`mcols` + + 代码块身份:可直接照写示例 @@ -215,6 +229,8 @@ col_index := mcols(matrix_rows, 1); ### 矩阵遍历:`::` 与 `::=` + + `::` 执行语句块: @@ -267,6 +283,8 @@ writeLn(matrix_value[1][1]); ### 深度遍历:`:.` 与 `:.=` + + `:.` 会遍历到嵌套数组的最深节点: @@ -310,6 +328,8 @@ writeLn(values[2]); ### 子矩阵 + + 按行列范围提取: @@ -406,6 +426,8 @@ writeLn(matrix_value[1][1]); ### `mfind` 与 `mfindSparse` + + 一维数组无条件查找会返回一维下标数组: @@ -528,6 +550,8 @@ B ### 矩阵乘法、除法、左除、乘方:`:*`、`:/`、`:\`、`:^` + + `:*` 是矩阵乘法(区别于逐元素乘 `*`): @@ -658,6 +682,8 @@ writeLn(a[0][0]); ### 基础函数的矩阵广播与无效输入替换参数 + + 多参数基础函数支持逐参数广播: @@ -757,6 +783,8 @@ array(1,-999,-999,100) ### 矩阵转置:反引号 `` ` `` + + 单次转置交换行列: @@ -839,6 +867,8 @@ writeLn("行向量 (0,2):", row_vector[0][2]); ### 矩阵拼接:`union`、`&=`、`|`、`:|` + + `union` 按行拼接(一维或二维): @@ -935,6 +965,8 @@ bar 结果 (1,2):2 ## 默认生成模板 + + 需要矩阵构造时,优先从这个最短模板开始: 代码块身份:可直接照写示例 @@ -945,6 +977,8 @@ matrix_value := zeros(2, 3); ## 决策边界和禁止项 + + - 把 `eye(3)` 当成一维数组。 - 把 `!A` 当成逻辑非表达式。 - 把 `*` 当成矩阵乘法;矩阵乘法使用 `:*`。 diff --git a/skills/tsl-syntax-reference/references/22_fmarray.md b/skills/tsl-syntax-reference/references/22_fmarray.md index 497e0209..1fa36cc1 100644 --- a/skills/tsl-syntax-reference/references/22_fmarray.md +++ b/skills/tsl-syntax-reference/references/22_fmarray.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“什么时候该用 `FMArray` 而不是普通 `array`,以及文档明确 `FMArray` 写法有哪些”。 ## 核心规则 + + - `fmarray[...]` 可以直接构造 `FMArray` 常量。 - `dataType(v)` 对 `FMArray` 返回 `27`。 - `dataType(v, 1)` 可以读出 `FMArray` 单元格类型;本页文档类型包括 `0` 整型、`1` 浮点、`20` 64 位整型。 @@ -26,8 +30,12 @@ ## 可直接照写示例 + + ### 常量构造与类型判断 + + 代码块身份:可直接照写示例 @@ -74,6 +82,8 @@ writeLn(f2[0,0], ',', f2[0,1], ',', f2[1,0], ',', f2[1,1]); ### `mInit`、`mInitDiag`、`mRand` + + 代码块身份:可直接照写示例 @@ -105,6 +115,8 @@ writeLn(mcols(fr1)); ### `arrayToFm`、`matrixToArray` 与单元格类型转换 + + 代码块身份:可直接照写示例 @@ -135,6 +147,8 @@ writeLn(a2[0][0], ',', a2[0][1], ',', a2[1][0], ',', a2[1][1]); ### 尺寸与重构 + + 二维和三维尺寸: @@ -173,6 +187,8 @@ writeLn(r1[0], ',', r1[1], ',', r1[2], ',', r1[3], ',', r1[4], ',', r1[5]); ### 标量运算与基础算符 + + 代码块身份:可直接照写示例 @@ -200,6 +216,8 @@ writeLn(s2[0], ',', s2[1], ',', s2[2]); ### `union2` 与左值类型 + + 代码块身份:可直接照写示例 @@ -228,6 +246,8 @@ writeLn(u2[0], ',', u2[1], ',', u2[2], ',', u2[3], ',', u2[4], ',', u2[5]); ### 多维转置与维度交换 + + 三维 `FMArray` 上,反引号转置会把全部维度倒置: @@ -272,6 +292,8 @@ writeLn(t[0,0,0], ',', t[0,1,0], ',', t[0,2,0], ';', t[1,0,0], ',', t[1,1,0], ', ### 矩阵连接 / 矩阵并右方:`union`、`|`、`:|` + + `union` 会按行拼接,不做去重: @@ -366,6 +388,8 @@ writeLn(m2[2,0], ',', m2[2,1], ',', m2[2,2], ',', m2[2,3]); ### 排序 + + 一维排序: @@ -398,6 +422,8 @@ writeLn(f1[0,0], ',', f1[0,1], ';', f1[1,0], ',', f1[1,1], ';', f1[2,0], ',', f1 ### TS-SQL 对 `FMArray` 的支持 + + 代码块身份:可直接照写示例 @@ -422,6 +448,8 @@ writeLn(mcols(q2)); ### `insert` / `delete` / `update` + + `insert`: @@ -482,6 +510,8 @@ writeLn(u[0,0], ',', u[0,1], ';', u[1,0], ',', u[1,1], ';', u[2,0], ',', u[2,1]) ## 本页不生成的范围 + + - `mInitDiag` 更高维行为 - `mRand` 的随机分布参数变体 - `union` / `|` / `:|` 与普通 `array` 的更多混合边界 @@ -492,6 +522,8 @@ writeLn(u[0,0], ',', u[0,1], ';', u[1,0], ',', u[1,1], ';', u[2,0], ',', u[2,1]) ## 默认生成模板 + + 需要 `FMArray` 常量时,优先从这个最短模板开始: 代码块身份:可直接照写示例 @@ -502,6 +534,8 @@ f := fmarray[1, 2, 3]; ## 禁止项 + + - 在一个 `FMArray` 常量里混用不同单元格类型。 - 把 `FMArray` 当成支持字符串下标的 `array`。 - 以为对 `FMArray` 做普通 `select` 后,结果还是 `dataType=27`。 diff --git a/skills/tsl-syntax-reference/references/23_object_overloads_and_iteration.md b/skills/tsl-syntax-reference/references/23_object_overloads_and_iteration.md index 229ed265..c8dda9a0 100644 --- a/skills/tsl-syntax-reference/references/23_object_overloads_and_iteration.md +++ b/skills/tsl-syntax-reference/references/23_object_overloads_and_iteration.md @@ -4,10 +4,14 @@ ## 本篇职责 + + 回答“当类不只是普通对象,而要直接参与 `obj + x`、`obj[index]`、`for v in obj`、`mrows(obj)` 这类语言级操作时,支持哪些文档明确写法”。 ## 核心规则 + + - 对象二元算符重载的最小可靠形态是成员方法 `function operator + (other);` 这一类写法。 - 比较算符可写成 `function operator < (other, is_left);`,用 `is_left` 区分对象在左边还是右边。 - 对象 `[]` 读取有两种文档明确写法:`function operator[](index);` 和 `function operator[0](index, s1);`。 @@ -21,8 +25,12 @@ ## 可直接照写示例 + + ### 二元算符重载 + + 代码块身份:可直接照写示例 @@ -92,6 +100,8 @@ end; ### `[]` 重载:`operator[]` / `operator[1]` + + 代码块身份:可直接照写示例 @@ -128,6 +138,8 @@ end; ### `[]` 重载:`operator[0]` / `operator[1]` + + 读取签名也可以写成 `function operator[0](index, s1);`(与上一段 `operator[]` 等价的另一组写法): @@ -166,6 +178,8 @@ end; ### `for in` 重载 + + 代码块身份:可直接照写示例 @@ -218,6 +232,8 @@ end; ### `mrows` / `mcols` / `msize` 重载(带参形态,支持下标列表) + + 代码块身份:可直接照写示例 @@ -276,6 +292,8 @@ end; ### `++` / `+=` / `--` / `-=` 自增自减重载 + + 代码块身份:可直接照写示例 @@ -366,6 +384,8 @@ end; ### 二进制函数重载:`operator funcName` + + 除了符号算符,`operator` 还能重载具名的全局二进制函数(如 `DateToStr`、`TryStrToInt` 等)。定义写成 `[class] function operator funcName(...)`:`class` 关键字可选,加上表示类方法,不加表示成员函数。 @@ -456,6 +476,8 @@ end; ### `::` / `:.` 遍历重载与 `mcell` / `mrow` / `mcol` / `mIndexCount` / `mIndex` + + 重载 `::`(二维遍历)或 `:.`(深度遍历)后,对象就能像矩阵一样被 `obj::begin ... end` 遍历。遍历体里用到的 `mcell` / `mrow` / `mcol` / `mIndexCount` / `mIndex(n)` 也各自重载,返回当前单元的值、行下标、列下标、维度数和第 `n` 维下标。`operator ::(flag)` 的 `flag` 为 `0` 表示第一次循环、`1` 表示后续循环,返回 `0` 或 `nil` 结束遍历、返回非零数字继续: @@ -532,6 +554,8 @@ end; ### `mrows` / `mcols` / `msize` 重载(无参形态,只取数量且免 `::`) + + `msize` / `mrows` / `mcols` 这类关键字函数也能重载,形态同二进制函数重载 `[class] function operator KeyWord(...)`,但**关键字重载不需要 `::` 指定全局**: @@ -563,6 +587,8 @@ end; ## 本页不生成的范围 + + - 多级 `[]` 下标重载 - 右侧算术如 `value + obj` @@ -570,6 +596,8 @@ end; ## 禁止项 + + - 不要从本页 `operator` 示例外推未写入文档的重载族。 - 重载 `::` / `:.` 遍历时,不要漏掉配套的 `mcell` / `mrow` / `mcol` / `mIndexCount` / `mIndex` 重载,否则遍历体会报 `override function not found`。 - 不要把多级 `[]` 下标重载或 `value + obj` 这类右侧算术写成文档事实。 diff --git a/skills/tsl-syntax-reference/scripts/lookup.py b/skills/tsl-syntax-reference/scripts/lookup.py index 6c92f1c8..608c19cb 100644 --- a/skills/tsl-syntax-reference/scripts/lookup.py +++ b/skills/tsl-syntax-reference/scripts/lookup.py @@ -26,6 +26,21 @@ FENCED_CODE_RE = re.compile(r"```[^\n]*\n(.*?)```", re.DOTALL) # 用户说"传引用" / "中括号")。逗号或顿号分隔,只影响检索,不是事实正文。 SECTION_TAG_RE = re.compile(r"", re.DOTALL | re.IGNORECASE) TAG_SEPARATOR_RE = re.compile(r"[,,、]\s*") +SECTION_ID_RE = re.compile( + r"^$", re.IGNORECASE +) +SECTION_ID_PREFIX_RE = re.compile(r"^$", + re.IGNORECASE, +) +QUICKSTART_RULE_PREFIX_RE = re.compile( + r"^", + re.DOTALL | re.IGNORECASE, +) IDENTITY_PREFIX = "代码块身份:" BLOCK_DESCRIPTION_PREFIX = "代码块说明:" ALLOWED_IDENTITIES = { @@ -61,10 +76,28 @@ GENERIC_HEADINGS = frozenset( } ) SUSPICIOUS_FENCE_RE = re.compile(r"^(?:\s+`{3}|`{4,})") -WRITE_PRELUDE_ANCHORS = ( - ("02_core_model.md", "文件模型核心规则"), - ("01_quickstart.md", "语言核心事实速查"), +WRITE_PRELUDE_SECTIONS = ( + ("syntax-02-002", "02_core_model.md", "文件模型核心规则"), + ("syntax-01-002", "01_quickstart.md", "语言核心事实速查"), ) +QUICKSTART_PAGE = "01_quickstart.md" +QUICKSTART_SUMMARY_HEADING = "语言核心事实速查" +QUICKSTART_RULE_OWNER_PAGES = { + "assignment": "06_expressions_and_operators.md", + "file-choice": "02_core_model.md", + "tsl-layout": "02_core_model.md", + "tsf-layout": "02_core_model.md", + "tsf-filename": "02_core_model.md", + "function-default": "05_functions_and_calls.md", + "procedure-explicit": "05_functions_and_calls.md", + "class-shape": "08_objects_and_classes.md", + "object-creation": "08_objects_and_classes.md", + "unit-shape": "09_units_and_scope.md", + "unit-default": "09_units_and_scope.md", + "named-arguments": "05_functions_and_calls.md", + "named-argument-order": "05_functions_and_calls.md", + "index-origins": "03_values_and_literals.md", +} HEADING_TOKEN_SCORE = 12 HEADING_EXACT_SCORE = 20 @@ -108,6 +141,18 @@ CHINESE_STOP_TOKENS = { "里面", } +CHINESE_QUERY_FILLERS = ( + "请帮我", + "请问", + "麻烦", + "帮我", + "解释一下", + "说明一下", + "看一下", + "一下", +) +CHINESE_QUERY_PARTICLES = ("的", "是", "吗", "呢", "吧") + ASCII_FILTER_STOP_TOKENS = { "debug", "please", @@ -178,6 +223,23 @@ class ValidationProblem: message: str +@dataclass(frozen=True) +class QuickstartRule: + page: Path + line: int + bullet_line: int + key: str + text: str + + +class ReferenceInstallationError(RuntimeError): + pass + + +class ReferenceStructureError(RuntimeError): + pass + + @dataclass(frozen=True) class QueryMatch: section: Section @@ -258,29 +320,6 @@ def normalize(value: str) -> str: return unicodedata.normalize("NFKC", value).casefold() -SYMBOL_SLUG_REPLACEMENTS = ( - ("**", " double-star "), - ("[]", " index "), - ("::", " double-colon "), - (":.", " colon-dot "), - ("*", " star "), -) - - -def _slug(value: str) -> str: - normalized = normalize(value) - for symbol, replacement in SYMBOL_SLUG_REPLACEMENTS: - normalized = normalized.replace(symbol, replacement) - slug = re.sub(r"[^\w]+", "-", normalized, flags=re.UNICODE).strip("-_") - return slug or "section" - - -def section_id(relative_page: Path | str, heading_path: tuple[str, ...]) -> str: - page = Path(relative_page) - parts = [_slug(page.with_suffix("").as_posix()), *(_slug(item) for item in heading_path)] - return "--".join(parts) - - def _reference_pages(references_dir: Path) -> list[Path]: return [ page @@ -289,6 +328,49 @@ def _reference_pages(references_dir: Path) -> list[Path]: ] +def _reference_installation_problem( + references_dir: Path, +) -> ValidationProblem | None: + references_dir = Path(references_dir) + if references_dir.is_dir() and _reference_pages(references_dir): + return None + return ValidationProblem( + references_dir, + 1, + "参考目录不可用:目录不存在或没有参考页;检查 --references-dir 或重新安装 skill", + ) + + +def _require_reference_pages(references_dir: Path) -> list[Path]: + references_dir = Path(references_dir) + problem = _reference_installation_problem(references_dir) + if problem is not None: + raise ReferenceInstallationError(problem.message) + return _reference_pages(references_dir) + + +def _next_nonblank_line(lines: list[str], start: int) -> int | None: + for index in range(start, len(lines)): + if lines[index].strip(): + return index + return None + + +def _heading_section_id(lines: list[str], heading_line: int) -> tuple[str, int]: + metadata_line = _next_nonblank_line(lines, heading_line + 1) + if metadata_line is None: + raise ReferenceStructureError( + f"第 {heading_line + 1} 行标题缺少显式 section ID" + ) + metadata = lines[metadata_line].strip() + match = SECTION_ID_RE.fullmatch(metadata) + if match is None: + raise ReferenceStructureError( + f"第 {heading_line + 1} 行标题缺少显式 section ID" + ) + return match.group(1), metadata_line + + def _heading_records(lines: list[str]) -> tuple[str, list[tuple[int, int, str]]]: page_title = "" records: list[tuple[int, int, str]] = [] @@ -311,7 +393,9 @@ def _heading_records(lines: list[str]) -> tuple[str, list[tuple[int, int, str]]] return page_title, records -def _associated_identity(lines: list[str], opening_fence: int) -> str | None: +def _associated_identity( + lines: list[str], opening_fence: int +) -> tuple[str | None, int | None]: previous = opening_fence - 1 while previous >= 0 and not lines[previous].strip(): previous -= 1 @@ -320,17 +404,19 @@ def _associated_identity(lines: list[str], opening_fence: int) -> str | None: while previous >= 0 and not lines[previous].strip(): previous -= 1 if previous < 0: - return None + return None, None metadata = lines[previous].strip() if not metadata.startswith(IDENTITY_PREFIX): - return None + return None, None identity = metadata[len(IDENTITY_PREFIX) :].strip() earlier = previous - 1 while earlier >= 0 and not lines[earlier].strip(): earlier -= 1 if earlier >= 0 and lines[earlier].strip().startswith(IDENTITY_PREFIX): - return None - return identity if identity in ALLOWED_IDENTITIES else None + return None, None + if identity not in ALLOWED_IDENTITIES: + return None, previous + return identity, previous def _identities(body: str) -> tuple[str, ...]: @@ -341,7 +427,7 @@ def _identities(body: str) -> tuple[str, ...]: if not FENCE_RE.match(line): continue if not in_fence: - identity = _associated_identity(lines, index) + identity, _ = _associated_identity(lines, index) if identity is not None: identities.append(identity) in_fence = not in_fence @@ -359,8 +445,10 @@ def _section_tags(body: str) -> tuple[str, ...]: def load_sections(references_dir: Path = DEFAULT_REFERENCES_DIR) -> list[Section]: + references_dir = Path(references_dir) sections: list[Section] = [] - for page in _reference_pages(Path(references_dir)): + seen_ids: set[str] = set() + for page in _require_reference_pages(references_dir): text = page.read_text(encoding="utf-8") lines = text.splitlines(keepends=True) page_title, headings = _heading_records(lines) @@ -387,10 +475,14 @@ def load_sections(references_dir: Path = DEFAULT_REFERENCES_DIR) -> list[Section else len(lines) ) local_body = "".join(lines[start:local_end]) - base_id = section_id(page.relative_to(references_dir), heading_path) + base_id, _ = _heading_section_id(lines, start) + if base_id in seen_ids: + raise ReferenceStructureError(f"重复 section ID:{base_id}") + seen_ids.add(base_id) tags = _section_tags(local_body) + searchable_body = STRUCTURAL_METADATA_RE.sub(" ", local_body) searchable_text = normalize( - "\n".join((page.stem, page_title, *heading_path, *tags, local_body)) + "\n".join((page.stem, page_title, *heading_path, *tags, searchable_body)) ) sections.append( Section( @@ -417,13 +509,14 @@ def _identity_problems(page: Path, lines: list[str]) -> list[ValidationProblem]: if identity not in ALLOWED_IDENTITIES: problems.append(ValidationProblem(page, index, f"未知身份:{identity}")) in_fence = False + associated_identity_lines: set[int] = set() for index, line in enumerate(lines): fence = FENCE_RE.match(line) if fence: if in_fence: in_fence = False else: - identity = _associated_identity(lines, index) + identity, identity_line = _associated_identity(lines, index) if identity is None: problems.append( ValidationProblem( @@ -431,6 +524,8 @@ def _identity_problems(page: Path, lines: list[str]) -> list[ValidationProblem]: ) ) else: + if identity_line is not None: + associated_identity_lines.add(identity_line) expected = IDENTITY_FENCE_LANGUAGES.get(identity) language = (fence.group(1) or "").strip() if expected is not None and language != expected: @@ -452,6 +547,15 @@ def _identity_problems(page: Path, lines: list[str]) -> list[ValidationProblem]: ) if in_fence: problems.append(ValidationProblem(page, len(lines), "代码围栏未闭合")) + for index, line in enumerate(lines): + stripped = line.strip() + if not stripped.startswith(IDENTITY_PREFIX): + continue + identity = stripped[len(IDENTITY_PREFIX) :].strip() + if identity in ALLOWED_IDENTITIES and index not in associated_identity_lines: + problems.append( + ValidationProblem(page, index + 1, "孤立的代码块身份:后面没有关联代码围栏") + ) return problems @@ -529,28 +633,261 @@ def _tag_problems(sections: list[Section]) -> list[ValidationProblem]: return problems +def _section_id_problems( + page: Path, lines: list[str] +) -> list[ValidationProblem]: + problems: list[ValidationProblem] = [] + _, headings = _heading_records([f"{line}\n" for line in lines]) + claimed_metadata_lines: set[int] = set() + for heading_line, _, title in headings: + metadata_line = _next_nonblank_line(lines, heading_line + 1) + if metadata_line is None: + problems.append( + ValidationProblem( + page, + heading_line + 1, + f"标题缺少显式 section ID:{title}", + ) + ) + continue + metadata = lines[metadata_line].strip() + if not SECTION_ID_PREFIX_RE.match(metadata): + problems.append( + ValidationProblem( + page, + heading_line + 1, + f"标题缺少显式 section ID:{title}", + ) + ) + continue + claimed_metadata_lines.add(metadata_line) + if SECTION_ID_RE.fullmatch(metadata) is None: + problems.append( + ValidationProblem( + page, + metadata_line + 1, + "显式 section ID 格式无效;只允许 ASCII 字母、数字、点、下划线和连字符", + ) + ) + + in_fence = False + for index, line in enumerate(lines): + if FENCE_RE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + if SECTION_ID_PREFIX_RE.match(line.strip()) and index not in claimed_metadata_lines: + problems.append( + ValidationProblem(page, index + 1, "孤立的 section ID:前面没有可索引标题") + ) + return problems + + +def _duplicate_section_id_problems( + pages: dict[Path, list[str]], +) -> list[ValidationProblem]: + problems: list[ValidationProblem] = [] + seen: dict[str, tuple[Path, int]] = {} + for page, lines in pages.items(): + in_fence = False + for index, line in enumerate(lines): + if FENCE_RE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + match = SECTION_ID_RE.fullmatch(line.strip()) + if match is None: + continue + section_id_value = match.group(1) + if section_id_value in seen: + first_page, first_line = seen[section_id_value] + problems.append( + ValidationProblem( + page, + index + 1, + f"重复 section ID:{section_id_value};首次位于 " + f"{first_page}:{first_line}", + ) + ) + else: + seen[section_id_value] = (page, index + 1) + return problems + + +def _quickstart_rule_records( + page: Path, lines: list[str] +) -> tuple[list[QuickstartRule], list[ValidationProblem]]: + records: list[QuickstartRule] = [] + problems: list[ValidationProblem] = [] + in_fence = False + for index, line in enumerate(lines): + if FENCE_RE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + stripped = line.strip() + if not QUICKSTART_RULE_PREFIX_RE.match(stripped): + continue + marker = QUICKSTART_RULE_RE.fullmatch(stripped) + if marker is None: + problems.append( + ValidationProblem(page, index + 1, "quickstart-rule 标记格式无效") + ) + continue + bullet_line = _next_nonblank_line(lines, index + 1) + if bullet_line is None or not lines[bullet_line].startswith("- "): + problems.append( + ValidationProblem( + page, + index + 1, + "quickstart-rule 标记后必须紧跟一个顶格列表项", + ) + ) + continue + text_parts = [lines[bullet_line].strip()] + continuation = bullet_line + 1 + while continuation < len(lines): + line = lines[continuation] + if not line.strip(): + break + if not line.startswith((" ", "\t")): + break + text_parts.append(line.strip()) + continuation += 1 + records.append( + QuickstartRule( + page=page, + line=index + 1, + bullet_line=bullet_line + 1, + key=marker.group(1), + text=" ".join(text_parts), + ) + ) + return records, problems + + +def _quickstart_rule_problems( + pages: dict[Path, list[str]], + require_complete: bool = False, +) -> list[ValidationProblem]: + quickstart = next( + (page for page in pages if page.name == QUICKSTART_PAGE), + None, + ) + if quickstart is None: + return [] + + problems: list[ValidationProblem] = [] + records: list[QuickstartRule] = [] + for page, lines in pages.items(): + page_records, page_problems = _quickstart_rule_records(page, lines) + records.extend(page_records) + problems.extend(page_problems) + + quick_lines = pages[quickstart] + _, headings = _heading_records([f"{line}\n" for line in quick_lines]) + summary = next( + ( + (position, start, level) + for position, (start, level, title) in enumerate(headings) + if level == 2 and title == QUICKSTART_SUMMARY_HEADING + ), + None, + ) + if summary is None: + problems.append( + ValidationProblem( + quickstart, 1, f"quickstart 缺少「{QUICKSTART_SUMMARY_HEADING}」派生摘要" + ) + ) + return problems + + position, start, level = summary + end = len(quick_lines) + for next_start, next_level, _ in headings[position + 1 :]: + if next_level <= level: + end = next_start + break + marked_bullets = { + record.bullet_line - 1 for record in records if record.page == quickstart + } + for index in range(start + 1, end): + if quick_lines[index].startswith("- ") and index not in marked_bullets: + problems.append( + ValidationProblem( + quickstart, + index + 1, + "quickstart 派生摘要列表项缺少 quickstart-rule 标记", + ) + ) + + by_key: dict[str, list[QuickstartRule]] = {} + for record in records: + by_key.setdefault(record.key, []).append(record) + expected_keys = set(QUICKSTART_RULE_OWNER_PAGES) if require_complete else set() + for key in sorted(set(by_key) | expected_keys): + key_records = by_key.get(key, []) + summaries = [record for record in key_records if record.page == quickstart] + canonicals = [record for record in key_records if record.page != quickstart] + if len(summaries) != 1 or len(canonicals) != 1: + problems.append( + ValidationProblem( + quickstart, + 1, + f"quickstart-rule {key!r} 必须恰好有一条派生摘要和一条专题事实", + ) + ) + continue + expected_page = QUICKSTART_RULE_OWNER_PAGES.get(key) + if expected_page is not None and canonicals[0].page.name != expected_page: + problems.append( + ValidationProblem( + canonicals[0].page, + canonicals[0].line, + f"quickstart-rule {key!r} 的专题 owner 应为 {expected_page}", + ) + ) + if summaries[0].text != canonicals[0].text: + problems.append( + ValidationProblem( + quickstart, + summaries[0].bullet_line, + f"派生摘要规则与专题事实不一致:{key}", + ) + ) + if not any(record.page == quickstart for record in records): + problems.append( + ValidationProblem(quickstart, 1, "quickstart 派生摘要没有可校验规则") + ) + return problems + + def validate_references( references_dir: Path = DEFAULT_REFERENCES_DIR, ) -> list[ValidationProblem]: references_dir = Path(references_dir) problems: list[ValidationProblem] = [] - # 逐页校验在零页时全部静默通过;参考页缺失属于安装/路径错误,必须报错, - # 否则 --check 会为一个空目录返回成功。 - if not _reference_pages(references_dir): - problems.append( - ValidationProblem( - references_dir, 1, "references 中没有可校验的参考页;检查路径或重新安装 skill" - ) - ) + installation_problem = _reference_installation_problem(references_dir) + if installation_problem is not None: + problems.append(installation_problem) return problems index_page = references_dir / "index.md" if index_page.exists(): problems.append(ValidationProblem(index_page, 1, "references 中不得保留 index.md")) + pages: dict[Path, list[str]] = {} + invalid_section_ids = False for page in sorted(references_dir.glob("*.md"), key=lambda item: item.name): text = page.read_text(encoding="utf-8") lines = text.splitlines() + pages[page] = lines problems.extend(_identity_problems(page, lines)) problems.extend(_local_link_problems(page, text, references_dir)) + section_id_problems = _section_id_problems(page, lines) + problems.extend(section_id_problems) + invalid_section_ids = invalid_section_ids or bool(section_id_problems) page_title, headings = _heading_records(text.splitlines(keepends=True)) h1_count = 0 in_fence = False @@ -569,6 +906,15 @@ def validate_references( h1_count += 1 previous_level = 1 continue + if level in (5, 6): + problems.append( + ValidationProblem( + page, + index + 1, + "H5/H6 标题不会被索引;参考页只允许使用 H2/H3/H4", + ) + ) + continue if level not in (2, 3, 4): continue if level > previous_level + 1: @@ -587,7 +933,12 @@ def validate_references( if next_level <= level: end = next_start break - body = "\n".join(lines[start + 1 : end]).strip() + body = re.sub( + r"", + " ", + "\n".join(lines[start + 1 : end]), + flags=re.DOTALL, + ).strip() duty_sections.append(body) if len(duty_sections) != 1 or not duty_sections[0]: problems.append( @@ -601,6 +952,21 @@ def validate_references( problems.append( ValidationProblem(page, index, f"包含人工路由协议:{phrase}") ) + duplicate_id_problems = _duplicate_section_id_problems(pages) + problems.extend(duplicate_id_problems) + invalid_section_ids = invalid_section_ids or bool(duplicate_id_problems) + problems.extend( + _quickstart_rule_problems( + pages, + require_complete=( + references_dir.resolve() == DEFAULT_REFERENCES_DIR.resolve() + ), + ) + ) + # load_sections intentionally refuses headings without explicit IDs. Keep + # --check diagnostic by returning the collected structural problems first. + if invalid_section_ids: + return problems sections = load_sections(references_dir) problems.extend(_tag_problems(sections)) ids: dict[str, Section] = {} @@ -608,22 +974,22 @@ def validate_references( if section.id in ids: problems.append(ValidationProblem(section.page, 1, f"重复 section ID:{section.id}")) ids[section.id] = section - for page_name, heading in WRITE_PRELUDE_ANCHORS: - page_sections = [ - section for section in sections if section.page.name == page_name - ] - # Only enforce the anchor when the page is present, so validating a - # synthetic references dir (tests) does not demand the bundled pages. - if page_sections and not any( - heading in section.heading_path for section in page_sections - ): - problems.append( - ValidationProblem( - references_dir / page_name, - 1, - f"write 模式前置章节缺失:{page_name} 的「{heading}」", + if references_dir.resolve() == DEFAULT_REFERENCES_DIR.resolve(): + for section_id_value, page_name, heading in WRITE_PRELUDE_SECTIONS: + section = ids.get(section_id_value) + if ( + section is None + or section.page.name != page_name + or section.heading_path != (heading,) + ): + problems.append( + ValidationProblem( + references_dir, + 1, + f"write 模式前置章节错误:{section_id_value} 必须指向 " + f"{page_name} 的「{heading}」", + ) ) - ) # 概念地图逐页从「本篇职责」段生成;有该段的页必须产出非空摘要, # 否则某页职责段被清空/写坏时地图会静默缺页。 mapped_pages = {page_name for page_name, _, _ in build_concept_map(references_dir)} @@ -671,8 +1037,17 @@ def validate_references( return problems -def _base_query_tokens(text: str) -> set[str]: +def _normalized_retrieval_text(text: str) -> str: normalized = normalize(text) + for filler in CHINESE_QUERY_FILLERS: + normalized = normalized.replace(filler, "") + for particle in CHINESE_QUERY_PARTICLES: + normalized = normalized.replace(particle, "") + return normalized + + +def _base_query_tokens(text: str) -> set[str]: + normalized = _normalized_retrieval_text(text) tokens = set(ASCII_TOKEN_RE.findall(normalized)) for run in CHINESE_RUN_RE.findall(normalized): tokens.add(run) @@ -698,9 +1073,13 @@ def _query_contains_phrase(query: str, phrase: str) -> bool: # 含中文的短语按去空白后的串比较。SKILL.md 要求智能体传「术语」而不是 # 用户原话,术语常以空格分隔(「数组 下标 起点」),逐字子串匹配会 # 整条落空;去空白后 phrase 仍要求连续出现,不放宽词序。 - return _WHITESPACE_RE.sub("", normalized_phrase) in _WHITESPACE_RE.sub( - "", normalize(query) + normalized_alias = _WHITESPACE_RE.sub( + "", _normalized_retrieval_text(phrase) ) + normalized_query = _WHITESPACE_RE.sub( + "", _normalized_retrieval_text(query) + ) + return normalized_alias in normalized_query phrase_tokens = _ascii_token_sequence(phrase) query_tokens_in_order = _ascii_token_sequence(query) if not phrase_tokens: @@ -749,7 +1128,7 @@ def _intent_score(section: Section, query: str) -> int: def _has_chinese_context(section: Section, query: str) -> bool: - runs = CHINESE_RUN_RE.findall(normalize(query)) + runs = CHINESE_RUN_RE.findall(_normalized_retrieval_text(query)) for run in runs: tokens = ( [run] @@ -790,7 +1169,8 @@ def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown: tokens = query_tokens(query) heading_text = normalize("\n".join(section.heading_path)) page_title_text = normalize(section.page_title) - body_text = normalize(SECTION_TAG_RE.sub(" ", section.local_body)) + body_without_metadata = STRUCTURAL_METADATA_RE.sub(" ", section.local_body) + body_text = normalize(SECTION_TAG_RE.sub(" ", body_without_metadata)) tag_text = normalize("\n".join(section.tags)) term_text = _code_text(section.local_body) expanded_only_tokens = _synonym_tokens(query) - _base_query_tokens(query) @@ -867,19 +1247,12 @@ def _score_reasons(score: ScoreBreakdown) -> tuple[str, ...]: def _write_prelude(sections: list[Section]) -> list[Section]: - prelude: list[Section] = [] - for page_name, heading in WRITE_PRELUDE_ANCHORS: - match = next( - ( - section - for section in sections - if section.page.name == page_name and heading in section.heading_path - ), - None, - ) - if match is not None: - prelude.append(match) - return prelude + by_id = {section.id: section for section in sections} + return [ + by_id[section_id] + for section_id, _, _ in WRITE_PRELUDE_SECTIONS + if section_id in by_id + ] STRING_LITERAL_RE = re.compile(r'"[^"\n]*"|\'[^\'\n]*\'') @@ -1028,7 +1401,8 @@ def _safe_json_string(value: str) -> str: def _plain_text_summary(body: str, limit: int = 180) -> str: # 标签是检索元数据,不是事实正文;不能泄进候选摘要。 - without_tags = SECTION_TAG_RE.sub(" ", body) + without_metadata = STRUCTURAL_METADATA_RE.sub(" ", body) + without_tags = SECTION_TAG_RE.sub(" ", without_metadata) without_fences = FENCED_CODE_RE.sub(" ", without_tags) without_links = re.sub( r"!?\[([^\]]*)\]\([^)]+\)", lambda match: match.group(1), without_fences @@ -1090,13 +1464,15 @@ def render_candidates(result: QueryResult) -> str: def render_section(section: Section) -> str: + body = STRUCTURAL_METADATA_RE.sub("", section.body) + body = re.sub(r"\n{3,}", "\n\n", body).rstrip() lines = [ "# TSL Syntax Section", "", f"Section ID: `{section.id}`", f"Source: `{_logical_source(section)}`", "", - section.body.rstrip(), + body, ] return "\n".join(lines).rstrip() + "\n" @@ -1138,15 +1514,22 @@ def _configure_utf8() -> None: reconfigure(encoding="utf-8") +def _reference_error_message( + error: ReferenceInstallationError | ReferenceStructureError, +) -> str: + if isinstance(error, ReferenceInstallationError): + return str(error) + return f"参考资料结构不可用:{error}" + + HELP_EPILOG = """\ 检索分两步,缺一步都不算取回事实: 1. 先取候选(只有摘要和 Section ID,不含事实正文) - lookup.py --query "命名参数 默认参数" --mode write + lookup.py --query "命名参数" --mode write 2. 再按候选里的 Section ID 取回正文;多个要素各自跑完第 1 步后, 选定的 Section ID 可以合并成一次取回 - lookup.py --section "05_functions_and_calls--可直接照写示例--基础函数-过程骨架" \\ - "02_core_model--文件模型核心规则" + lookup.py --section "syntax-05-004" "syntax-02-002" 查询词无从下手时先 --map 把需求映射到 TSL 概念;改动参考页或 data/ 词表后用 --check 校验。 弱命中(候选标 Weak: yes)没有意图/标题/标识符/标签命中,只靠正文低分撞词; @@ -1175,7 +1558,7 @@ def _parser() -> argparse.ArgumentParser: ) action.add_argument( "--query", - help="按术语、报错原文或语法要素名检索候选章节(不要传用户原话);需配合 " + help="从用户原话提取术语、报错原文或单一语法要素名,保留原写法但不传完整用户句;需配合 " "--mode。候选标 `Weak: yes` 表示只有正文低分撞词,全部候选皆弱时退出码为 2", ) action.add_argument( @@ -1194,8 +1577,8 @@ def _parser() -> argparse.ArgumentParser: action.add_argument( "--check", action="store_true", - help="校验参考页的结构、代码块身份、本地链接,以及 data/lexicon.json 的页级" - "意图短语与参考页是否一一对应;发现问题时退出码为 1", + help="只校验参考页结构、显式 Section ID、代码块身份、本地链接、quickstart " + "派生规则和词表页覆盖;检索排序由测试套件校验。发现问题时退出码为 1", ) parser.add_argument( "--mode", @@ -1245,8 +1628,21 @@ def main(argv: list[str] | None = None) -> int: parser.error("--query 必须同时指定 --mode") if args.mode is not None and args.query is None: parser.error("--mode 仅用于 --query") + installation_problem = _reference_installation_problem(args.references_dir) + if installation_problem is not None: + print( + f"{installation_problem.page}:{installation_problem.line}: " + f"{installation_problem.message}", + file=sys.stderr, + ) + return 1 if args.show_map: - print(render_concept_map(build_concept_map(args.references_dir)), end="") + try: + entries = build_concept_map(args.references_dir) + except (ReferenceInstallationError, ReferenceStructureError) as error: + print(_reference_error_message(error), file=sys.stderr) + return 1 + print(render_concept_map(entries), end="") return 0 if args.check: problems = validate_references(args.references_dir) @@ -1254,7 +1650,11 @@ def main(argv: list[str] | None = None) -> int: print(f"{problem.page}:{problem.line}: {problem.message}", file=sys.stderr) return 1 if problems else 0 if args.section is not None: - sections = load_sections(args.references_dir) + try: + sections = load_sections(args.references_dir) + except (ReferenceInstallationError, ReferenceStructureError) as error: + print(_reference_error_message(error), file=sys.stderr) + return 1 by_id = {item.id: item for item in sections} requested = list(dict.fromkeys(args.section)) missing = [item for item in requested if item not in by_id] @@ -1272,7 +1672,11 @@ def main(argv: list[str] | None = None) -> int: end="", ) return 0 - result = query_sections(args.query, args.mode, args.limit, args.references_dir) + try: + result = query_sections(args.query, args.mode, args.limit, args.references_dir) + except (ReferenceInstallationError, ReferenceStructureError) as error: + print(_reference_error_message(error), file=sys.stderr) + return 1 print(render_candidates(result), end="") if not result.matches: print("no matching sections", file=sys.stderr) diff --git a/test/test_tsl_syntax_reference.py b/test/test_tsl_syntax_reference.py new file mode 100644 index 00000000..205ea5a7 --- /dev/null +++ b/test/test_tsl_syntax_reference.py @@ -0,0 +1,394 @@ +import importlib.util +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SKILL_ROOT = ROOT / "skills" / "tsl-syntax-reference" +LOOKUP_PATH = SKILL_ROOT / "scripts" / "lookup.py" +SKILL_PATH = SKILL_ROOT / "SKILL.md" +CI_PATH = ROOT / ".gitea" / "workflows" / "checks.yml" +PREPARE_PATH = ROOT / ".gitea" / "workflows" / "prepare.yml" + + +def load_lookup_module(): + spec = importlib.util.spec_from_file_location("tsl_syntax_lookup", LOOKUP_PATH) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {LOOKUP_PATH}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +lookup = load_lookup_module() + + +def write_reference(directory: Path, name: str, content: str) -> Path: + page = directory / name + page.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8", newline="\n") + return page + + +def run_lookup(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(LOOKUP_PATH), *args], + cwd=ROOT, + capture_output=True, + text=True, + ) + + +class TslSyntaxReferenceTests(unittest.TestCase): + def test_bundled_references_pass_structural_check(self): + self.assertEqual([], lookup.validate_references()) + + def test_bundled_sections_use_unique_explicit_ids(self): + sections = lookup.load_sections() + ids = [section.id for section in sections] + + self.assertEqual(len(ids), len(set(ids))) + self.assertTrue(ids) + for section_id in ids: + with self.subTest(section_id=section_id): + self.assertRegex(section_id, r"^syntax-\d{2}-\d{3}$") + + def test_explicit_section_id_survives_heading_rename(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + page = write_reference( + references, + "99_fixture.md", + """ + # Fixture + + ## 本篇职责 + + + + 测试职责。 + + ## 原标题 + + + + 事实正文。 + """, + ) + before = lookup.load_sections(references)[1].id + page.write_text( + page.read_text(encoding="utf-8").replace("## 原标题", "## 新标题"), + encoding="utf-8", + newline="\n", + ) + after = lookup.load_sections(references)[1].id + + self.assertEqual("syntax-99-002", before) + self.assertEqual(before, after) + + def test_missing_explicit_section_id_fails_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "99_fixture.md", + """ + # Fixture + + ## 本篇职责 + + 测试职责。 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + + self.assertTrue(any("缺少显式 section ID" in message for message in messages)) + + def test_duplicate_explicit_section_id_fails_check_and_loading(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "99_fixture.md", + """ + # Fixture + + ## 本篇职责 + + + + 测试职责。 + + ## 重复 ID + + + + 事实正文。 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + with self.assertRaisesRegex(lookup.ReferenceStructureError, "重复 section ID"): + lookup.load_sections(references) + + self.assertTrue(any("重复 section ID" in message for message in messages)) + + def test_orphan_identity_line_fails_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "99_fixture.md", + """ + # Fixture + + ## 本篇职责 + + + + 测试职责。 + + ## 示例 + + + + 代码块身份:可直接照写示例 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + + self.assertTrue(any("孤立的代码块身份" in message for message in messages)) + + def test_h5_and_h6_headings_fail_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "99_fixture.md", + """ + # Fixture + + ## 本篇职责 + + + + 测试职责。 + + ##### 不会被索引的标题 + + 隐藏事实。 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + + self.assertTrue(any("H5/H6" in message for message in messages)) + + def test_quickstart_rule_drift_fails_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "01_quickstart.md", + """ + # Quickstart + + ## 本篇职责 + + + + 派生摘要。 + + ## 语言核心事实速查 + + + + + - 普通赋值使用 `=`。 + """, + ) + write_reference( + references, + "02_topic.md", + """ + # Topic + + ## 本篇职责 + + + + 完整事实源。 + + ## 核心规则 + + + + + - 普通赋值使用 `:=`。 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + + self.assertTrue( + any("派生摘要规则与专题事实不一致" in message for message in messages) + ) + + def test_complete_quickstart_contract_detects_rule_deleted_from_both_pages(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + quickstart = write_reference( + references, + "01_quickstart.md", + """ + # Quickstart + + ## 本篇职责 + + + + 派生摘要。 + + ## 语言核心事实速查 + + + + 没有任何规则。 + """, + ) + pages = {quickstart: quickstart.read_text(encoding="utf-8").splitlines()} + + messages = [ + item.message + for item in lookup._quickstart_rule_problems( + pages, + require_complete=True, + ) + ] + + self.assertTrue(any("assignment" in message for message in messages)) + + def test_missing_references_are_installation_errors_for_every_action(self): + with tempfile.TemporaryDirectory() as temp_dir: + missing = Path(temp_dir) / "missing" + actions = ( + ("--map",), + ("--query", "数组下标", "--mode", "explain"), + ("--section", "syntax-03-002"), + ("--check",), + ) + for action in actions: + with self.subTest(action=action): + result = run_lookup( + *action, + "--references-dir", + str(missing), + ) + self.assertEqual(1, result.returncode) + self.assertEqual("", result.stdout) + self.assertIn("参考目录不可用", result.stderr) + + def test_documented_array_index_query_ranks_basic_array_section_first(self): + for query in ("数组下标", "请帮我解释数组的下标"): + with self.subTest(query=query): + result = lookup.query_sections(query, "explain", limit=1) + self.assertEqual("11_matrix_and_collections.md", result.matches[0].section.page.name) + self.assertEqual("基础数组与键表", result.matches[0].section.heading_path[-1]) + + def test_documented_write_and_diagnose_queries_rank_expected_sections(self): + write_result = lookup.query_sections("命名参数", "write", limit=1) + diagnose_result = lookup.query_sections( + "invalid statement 声明区", + "diagnose", + limit=1, + ) + + self.assertEqual( + ["syntax-02-002", "syntax-01-002"], + [section.id for section in write_result.prelude], + ) + self.assertEqual("syntax-05-008", write_result.matches[0].section.id) + self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id) + + def test_index_origin_variants_rank_value_rules_first(self): + for query in ("下标从几开始", "下标是从几开始", "请问下标是从几开始"): + with self.subTest(query=query): + result = lookup.query_sections(query, "explain", limit=1) + self.assertEqual("03_values_and_literals.md", result.matches[0].section.page.name) + self.assertEqual("核心规则", result.matches[0].section.heading_path[-1]) + + def test_every_curated_page_alias_still_ranks_its_page_first(self): + for page_name, aliases in lookup.PAGE_INTENT_ALIASES.items(): + for alias in aliases: + with self.subTest(page=page_name, alias=alias): + result = lookup.query_sections(alias, "explain", limit=1) + self.assertTrue(result.matches, alias) + self.assertEqual(page_name, result.matches[0].section.page.name) + + def test_weak_only_query_remains_a_no_match(self): + result = run_lookup("--query", "只能确认", "--mode", "explain") + + self.assertEqual(2, result.returncode) + self.assertIn("only weak candidates", result.stderr) + + def test_batch_section_retrieval_is_atomic(self): + ids = [section.id for section in lookup.load_sections()[:2]] + success = run_lookup("--section", *ids) + failure = run_lookup("--section", ids[0], "syntax-99-999") + + self.assertEqual(0, success.returncode) + self.assertTrue(all(section_id in success.stdout for section_id in ids)) + self.assertEqual(2, failure.returncode) + self.assertEqual("", failure.stdout) + self.assertIn("section not found: syntax-99-999", failure.stderr) + + def test_structural_metadata_is_not_exposed_in_lookup_output(self): + sections = {section.id: section for section in lookup.load_sections()} + candidates = lookup.render_candidates( + lookup.query_sections("命名参数", "write", limit=1) + ) + quickstart = lookup.render_section(sections["syntax-01-002"]) + + for output in (candidates, quickstart): + with self.subTest(output=output[:40]): + self.assertNotIn("