diff --git a/skills/tsl-syntax-reference/SKILL.md b/skills/tsl-syntax-reference/SKILL.md index 56513b2d..9cca1514 100644 --- a/skills/tsl-syntax-reference/SKILL.md +++ b/skills/tsl-syntax-reference/SKILL.md @@ -1,6 +1,6 @@ --- name: tsl-syntax-reference -description: 当用户需要编写、修改、审查或解释 TSL/TSF、TS-SQL、Tinysoft/天软脚本或公式,涉及 `.tsl` / `.tsf` 文件,排查 invalid statement 等语法错误,或核对语言与运行时结构规则时使用。 +description: "当用户需要编写、修改、审查或解释 TSL/TSF、TS-SQL、Tinysoft/天软脚本或公式,涉及 `.tsl` / `.tsf` 文件,排查 invalid statement 等语法错误,或核对语言与运行时结构规则时使用;包括你自认为已经确定的写法。不用于查询 API 签名或天软数据字典字段(改用 tsl-api-reference),也不用于选择解释器或运行方式(读取目标文件附近的 AGENTS.md)。" --- # TSL Syntax Reference @@ -12,67 +12,15 @@ description: 当用户需要编写、修改、审查或解释 TSL/TSF、TS-SQL 下标起点这类"看起来确定"的规则同样要取回。`` 指本 `SKILL.md` 所在目录。 -检索必须分两步: - -1. `--query` 只返回紧凑候选和 Section ID,不返回事实正文。 -2. 从候选中选择支持当前结论的章节,再用 `--section` 取回唯一事实正文。 - -一次 `--query` 只覆盖一个语法要素。一段代码涉及多个要素时(如文件模型、函数 -骨架、赋值运算符),逐个要素分别执行 `--query`;不用一个要素的取回结果推断另 -一个要素。各要素选定的 Section ID 可以合并成一次 `--section` 批量取回。 - -根据任务意图选择模式: - ```bash -python /scripts/lookup.py --query "命名参数" --mode write -python /scripts/lookup.py --query "invalid statement 声明区" --mode diagnose -python /scripts/lookup.py --query "数组下标" --mode explain +python /scripts/lookup.py --help ``` -- `write`:编写或修改代码。 -- `diagnose`:定位语法错误或错误写法。 -- `explain`:解释语言规则或代码含义。 - -用 `--limit N`(1..10,默认 5)控制返回的查询候选条数。 - -`--mode write` 会在查询候选之前额外附加 `Required: yes` 的前置章节(文件模型与 -语言核心事实速查)。它们不占 `--limit` 预算,也不是本次查询的命中结果:先按它们 -核对文件模型和硬规则,再从 `Required: no` 的候选里挑选支持当前结论的章节。前置 -章节在同一任务内取回一次即可;后续 `--query` 重复列出它们时,不必再次 `--section`。 -其中 `syntax-01-002` 是从专题页逐条派生的强制速查,不是另一份完整事实源;生成结论 -仍要取回拥有该规则的专题 Section。 - -`--query` 输出候选后,必须执行下面这种精确取回。多个已选定的 Section ID 可以 -一次传入: - -```bash -python /scripts/lookup.py --section "syntax-05-004" "syntax-02-002" -``` - -只有 `--section` 返回的章节正文可作为本次任务的语法事实来源;候选摘要和概念地图都不能直接支持代码结论。需要补充时改进查询词再次检索。 - -## 构造查询词 - -从用户原话中提取错误原文、TSL 标识符和单一语法要素名;保留这些词在用户原话中的 -写法,不传完整句,也不自行翻译成猜测术语。脚本会自动处理两类词,不必手工调整: - -- 中文口语词自动扩展到 TSL 术语(如"打印"→输出/writeLn,"列表"→数组, - "程序慢"→性能分析)。保留提取词的原写法即可。 -- `tsl`、`tsf`、`tinysoft`、`debug`、`please` 这几个词不参与逐词匹配, - 但可能整体把查询导向某个专题页。无结果时靠加这类词补救没有用,改为换更具体的 - 语法要素名。`program` 是真实 TSL 关键字,会参与精确匹配。 - -## 弱命中视同无匹配 - -候选里的 `Weak: yes` 表示该节没有任何强信号命中(意图短语、标题、标识符、 -标签),只靠正文低分撞词进入候选。弱候选不作为选择对象: - -- 候选全部为弱命中时,lookup 返回 rc=2,按无匹配处理——改进查询词重试, - 仍全弱即为事实缺口。 -- 个别候选为弱命中、其余为强命中时,只从强命中里挑选。 - -没有 `Weak` 行的候选也不等于相关:词面命中不代表该节支持当前结论,仍要按 -Summary 判断后再取回正文核对。 +构造任一命令前先运行上面的 `--help`。帮助输出独占动作、mode、参数范围、查询词、 +弱命中、概念地图、前置章节、Owner Section、结构校验和退出码的命令契约;不要在本 +文件中推断或复用旧参数。检索必须完成 `--query` 到 `--section` 两步,只有 +`--section` 返回的正文可作为语法事实。`syntax-01-002` 是派生速查;命中其中规则后, +按紧邻规则的 `Owner Section` 取回真正拥有该事实的专题 Section。 ## 代码块必须标注来源 @@ -145,32 +93,6 @@ Section ID 抄自 `--section` 输出首部的 `Section ID:` 行。多个要素 - 用候选摘要或概念地图代替章节正文。 - 给出未标注来源的代码块。 -## 查询词无从下手时先取概念地图 - -若需求只有自然语言、无法从中取出 TSL 概念词或错误文本来构造 `--query`,先运行: - -```bash -python /scripts/lookup.py --map -``` - -它列出全部专题的纯文本职责摘要,用于把需求映射到 TSL 特有概念。地图不含可照写事实;从地图取得概念词后,仍按上面的两步执行 `--query` 和 `--section`。 - -## 退出码 - -| 退出码 | 含义 | 处理方式 | -| ------ | --------------------------------------------------------------------- | ------------------------------------------------------- | -| 0 | 查询或取回成功 | 读取输出并继续两步流程 | -| 1 | `--check` 发现参考页问题,或参考目录内没有可校验的参考页 | 修复参考页,或检查 `--references-dir` 路径与 skill 安装 | -| 2 | 参数不合法、`--query` 无匹配或候选全部为弱命中、`--section` ID 不存在 | 见下 | - -`--section` ID 不存在时,脚本会在 stderr 打印最接近的若干 Section ID;从中挑选正确 ID 重试,不要凭猜测拼写 ID。一次传入多个 ID 时只要有一个不存在就不返回任何正文,修正后整批重试。 - -`--query` 无匹配或候选全部为 `Weak: yes` 时改进查询词重试。仍无强命中即为事实缺口:按上面「缺口时停止」处理。 - -`--map`、`--query`、`--section`、`--check` 共用安装前置检查。参考目录不存在或没有 -参考页时,任何动作都以 rc=1 报告路径/安装错误;这不属于事实零命中,不能靠改查询词 -重试。 - ## 事实边界 本 Skill 只拥有 TSL/TSF 的语言语法、文件模型、表达式、控制流、对象、运行时语言 @@ -202,12 +124,5 @@ TSL 的运行方式、解释器路径、平台检测和环境选择都不是本 ## 维护校验 -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`。 +维护本 Skill、参考页、词表或检索器时,先读 [`data/README.md`](data/README.md),按其中 +的 Section ID、派生规则、粒度和验证纪律执行。 diff --git a/skills/tsl-syntax-reference/data/README.md b/skills/tsl-syntax-reference/data/README.md index 6463f40d..2d883f31 100644 --- a/skills/tsl-syntax-reference/data/README.md +++ b/skills/tsl-syntax-reference/data/README.md @@ -23,7 +23,16 @@ 键写错或参考页改名后 `_intent_score` 会静默返回 0 分,该页失去自然语言 入口。`lookup.py --check` 负责拦截页键漂移,行为回归测试负责拦截排序漂移。 -## 改动后的校验 +## Skill 维护与校验 + +Section ID 来自标题下的显式 `` 元数据,不从标题派生。改标题 +时保留原 ID;新增章节时分配新 ID,不重排或复用旧 ID。quickstart 派生规则必须与 +专题事实逐字一致,并显式给出可由 `--section` 直接取回的 Owner Section。 + +大型参考页必须把可检索事实切成受控大小的叶子 Section,避免一次精确取回返回数百行。 +`--check` 会校验大页的叶子 Section 粒度;拆分时保留已有 ID,把新主题分配给新 ID。 + +改动本 Skill、参考页、`data/` 词表或 lookup 实现后运行: ```bash python skills/tsl-syntax-reference/scripts/lookup.py --check @@ -34,3 +43,7 @@ python -m unittest test.test_tsl_syntax_reference -v 键指向不存在的页、或某页没有自然语言入口都会报错。新增参考页时必须同时 在这里补一条页级意图短语。它只做结构检查,不验证自然语言排序;原始 alias、 助词变体、弱命中和已知真实问法由专属测试覆盖。 + +`--check` 还覆盖显式 ID、标题层级、quickstart 派生与 Owner Section、代码块身份、 +本地链接和大页粒度;它不证明事实语义、检索排序或示例运行结果。仓库级 unittest +负责 CLI、排序与退出码行为。 diff --git a/skills/tsl-syntax-reference/references/01_quickstart.md b/skills/tsl-syntax-reference/references/01_quickstart.md index 5b922dc0..c9221f5e 100644 --- a/skills/tsl-syntax-reference/references/01_quickstart.md +++ b/skills/tsl-syntax-reference/references/01_quickstart.md @@ -19,66 +19,82 @@ - 普通变量赋值使用 `:=`;`=` 在普通表达式里用于比较,不用于赋值。 + Owner Section:`syntax-06-004` - 未给后缀时,入口流程、脚本任务或一次性执行逻辑对应 `.tsl`;可复用交付物(函数、过程、类、模块或扩展文件)对应 `.tsf`;只是脚本内部封装函数或类时,仍按 `.tsl` 处理;仍不明确时向用户确认,不要把脚本入口和可复用模块合并成一个猜测文件。 + Owner Section:`syntax-02-002` - `.tsl` 脚本按两段理解:语句区在前并按顺序执行;声明区在后,可放 `function / procedure` 或 `type Name = class`。写 `.tsl` 时先写语句区,需要函数、过程或类时把声明区放在语句区之后。 + Owner Section:`syntax-02-002` - 写 `.tsf` 时只写顶层函数 / 过程 / 类声明,或 `unit`;不要写成会直接顺序执行的脚本入口。 + Owner Section:`syntax-02-002` - `.tsf` 文件名(不含扩展名)必须与第一个顶层声明同名;第一个声明可以是同名 `function`、`type Name = class` 或 `unit`。 + Owner Section:`syntax-02-002` - 用户提示词里的“函数”默认对应 `function`,不要自动改写成 `procedure`。 + Owner Section:`syntax-05-002` - `procedure Name(...); begin ... end;` 只在用户明确要求 `procedure` / 过程时生成;不要因为没有返回值就自动改用 `procedure`。 + Owner Section:`syntax-05-002` - 类定义统一按 `type Name = class ... end;` 写。 + Owner Section:`syntax-08-002` - 普通本地类实例化默认生成 `new ClassName()`;`createObject("ClassName")`、`createObject(ClassType)` 只在字符串类名、类类型变量或跨 `unit` 路径场景生成。 + Owner Section:`syntax-08-002` - `unit` 是完整的顶层主体;常见完整形态是 `unit Name; interface ... implementation ... end.`。 + Owner Section:`syntax-09-002` - 如果没有特殊需求,默认优先用完整形态;简写形态只在不需要显式区分 `interface` / `implementation` 时再用。 + Owner Section:`syntax-09-002` - 调用时支持命名参数,写法是 `name: value`。 + Owner Section:`syntax-05-002` - 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。 + Owner Section:`syntax-05-002` - 普通单行文本默认使用 `"..."` 或 `'...'`;根据内容选择不冲突的引号,必要时再使用转义或连续同类引号。`%% ...%%` 仅用于多行文本、引号非常密集等原始字符串场景;不得因为内容是中文、非 ASCII 或较长就自动改用 `%%`。 + Owner Section:`syntax-03-004` - `U`、`L` 前缀只由目标字符串类型或已确认的 API 编码要求决定,不能因为内容是中文就自动添加;普通中文内容优先直接写成 `"中文内容"`。 + Owner Section:`syntax-03-004` - `array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。 + Owner Section:`syntax-03-002` ## 术语对照 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 d41e071f..aa64c532 100644 --- a/skills/tsl-syntax-reference/references/06_expressions_and_operators.md +++ b/skills/tsl-syntax-reference/references/06_expressions_and_operators.md @@ -121,7 +121,7 @@ - 字符串字面量、拼接与文本边界见 [03_values_and_literals.md](03_values_and_literals.md);数组扩展和矩阵样数据见 [11_matrix_and_collections.md](11_matrix_and_collections.md)。 - `{$ifdef ...}` 能力探测见 [15_lexical_structure_and_compile_options.md](15_lexical_structure_and_compile_options.md),不要写成普通业务逻辑。 -### 基础赋值和条件求值 +## 基础赋值和条件求值 @@ -195,6 +195,12 @@ writeLn(a); 3 ``` +## 基础算术与比较 + + + + + 基础算术: 代码块身份:可直接照写示例 @@ -308,6 +314,12 @@ writeLn(dataType(rb)); - 整型 `a` 和实型 `b` 都可以用 `!` 求倒数。 - 上面两个 `dataType(...)` 都输出 `1`,表示结果是实型。 +## 逻辑与位运算 + + + + + 逻辑运算: 代码块身份:可直接照写示例 @@ -364,6 +376,12 @@ writeLn(2 ror 1); 1 ``` +## 复合赋值与字符串运算 + + + + + 基础算术复合赋值: 代码块身份:可直接照写示例 @@ -513,6 +531,12 @@ writeLn("abc" like "a%"); 因此 `like` 更接近“正则匹配”,不是 SQL 那套 `%` / `_` 通配语义。 +## 自增、自减与 if 表达式 + + + + + 自增与自减: 代码块身份:可直接照写示例 @@ -567,7 +591,7 @@ writeLn(if 2 > 1 then 2 else 1); `if condition then true_value else false_value` 必须带 `else`,否则不是本页可照写的表达式形态。 -### 运算符优先级 +## 运算符优先级 @@ -614,7 +638,7 @@ writeLn(flag); 稀有矩阵运算符的详细优先级以其专题页为准;不要用本表外推尚未写入正式文档的符号。 -### 静态计算表达式 `static` +## 静态计算表达式 `static` @@ -674,7 +698,7 @@ end; 这不是类成员的 `static` 字段;类静态成员见 [08_objects_and_classes.md](08_objects_and_classes.md)。缓存结果具有运行时状态,不要用它保存每次调用都必须重新计算的值。 -### 表达式对象 +## 表达式对象 @@ -756,7 +780,7 @@ writeLn(result_value); 逗号表达式本身可以作为一个普通子表达式继续参与后续运算。 -### 空安全访问 +## 空安全访问 @@ -803,7 +827,7 @@ writeLn(c?.a?.[1] = nil); 不要从这一段外推成所有深度、所有成员/下标组合都可写。 -### 否定形式运算 +## 否定形式运算 @@ -833,7 +857,7 @@ end; 1 ``` -### 标量链式比较 +## 标量链式比较 @@ -861,7 +885,7 @@ writeLn(1 :<> 2 :<> 3); 1 ``` -### 矩阵链式比较 +## 矩阵链式比较 @@ -893,7 +917,7 @@ writeLn(s[2]); 矩阵链式比较会按元素位置分别得到结果数组,并且可以和标量混用。 -### 条件编译探测 +## 条件编译探测 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 0a97f929..c05da38a 100644 --- a/skills/tsl-syntax-reference/references/08_objects_and_classes.md +++ b/skills/tsl-syntax-reference/references/08_objects_and_classes.md @@ -68,7 +68,7 @@ - `createObject(...)` 示例只在字符串类名、类类型变量或跨 `unit` 路径场景复制。 - `property` 类型注解只有在已有类型名证据时生成;不要为了完整性发明说明性类型名。 -### 最小类与声明位置 +## 最小类与声明位置 @@ -180,7 +180,7 @@ writeLn(a); - 函数体内部声明类会报 `invalid statement` - 在松散语句脚本里,`type MyClass = class ... end;` 之后继续写 `writeLn(a);` 也会报 `invalid statement` -### 字段、静态成员、常量与可见性 +## 字段、静态成员、常量与成员访问 @@ -275,6 +275,12 @@ end; - `c.Inc()` 输出 `11` +## 成员可见性与继承访问 + + + + + 可见性 `private` / `protected` / `public`: 代码块身份:可直接照写示例 @@ -396,7 +402,7 @@ end; - 子类里直接调用父类 `private` 方法也会在执行时报错。 - `private` / `protected` 方法访问也遵循同样边界:外部不能调 `private` / `protected` 方法,子类只能调 `protected` 方法,不能调 `private` 方法。 -### 构造函数边界 +## 构造函数边界 @@ -427,11 +433,11 @@ end; - 上述例子里的 `a.value` 输出 ``,说明 `private create` 没有执行。 - `protected create`、`createObject("A", ...)` 和 `createObject(class(A), ...)` 也按同一规则处理;构造函数应保持 `public`。 -### 属性、类型注解与类外实现 +## 基础 property 与类型注解 - + 基础 `property`: @@ -530,6 +536,12 @@ abc abc ``` +## 类内声明与类外实现 + + + + + 类内声明、类外实现的带类型重载方法: 代码块身份:可直接照写示例 @@ -590,6 +602,12 @@ left left ``` +## 索引与固定 index property + + + + + 索引型 `property`: 代码块身份:可直接照写示例 @@ -695,6 +713,12 @@ end; - `obj.school` 输出 `math` - `obj.idx("High school")` 也输出 `math` +## 参数化 property 与 accessor + + + + + 参数化 `property`: 代码块身份:可直接照写示例 @@ -762,7 +786,7 @@ end; - `write setItem` 这种“写方法接参数个数 + 赋值值”的写法可以通过 - 上述例子中的 `obj.Item(2)` 输出 `x` -### 对象创建与类类型 +## 对象创建与类类型 @@ -916,11 +940,11 @@ end; - `findClass("MathBox").Add(...)` 可以调用类方法 - 上述例子依次输出 `7`、`11` -### 重载、继承与析构 +## 重载与继承 - + `overload` 方法: @@ -1036,6 +1060,12 @@ end; - 当多个父类存在同名方法时,本例优先命中第一个父类 `A` - 上述例子中的 `obj.Speak()` 输出 `1` +## 虚方法、覆盖与隐藏 + + + + + 基础 `virtual` / `override`: 代码块身份:可直接照写示例 @@ -1133,6 +1163,12 @@ end; - `c.Ask()` 输出 `child`:`virtual` + `override` 后,父类方法内部的 `Who()` 定向到子类实现 - 这就是 hide 与 override 的关键区别:hide 只影响直接调用,override 改变了所有经由基类的间接调用 +## 祖先类调用与类型视图 + + + + + `class(BaseClass, ObjectName).MethodName()`: 代码块身份:可直接照写示例 @@ -1257,6 +1293,12 @@ end; - `Inherited BaseValue(5)` 可以显式调用父类指定方法 - 上述例子依次输出 `6`、`9` +## 析构与 self 工厂 + + + + + 析构函数 `destroy`: 代码块身份:可直接照写示例 @@ -1323,7 +1365,7 @@ end; - `self(1)` 返回的对象在这个例子里是 `ChildBox`,因此输出 `1` - `self(0)` 返回的对象在这个例子里不是 `ChildBox`,因此输出 `0` -### 跨 unit 类路径 +## 跨 unit 类路径 diff --git a/skills/tsl-syntax-reference/scripts/lookup.py b/skills/tsl-syntax-reference/scripts/lookup.py index bd6d4543..e10b0f4f 100644 --- a/skills/tsl-syntax-reference/scripts/lookup.py +++ b/skills/tsl-syntax-reference/scripts/lookup.py @@ -37,6 +37,9 @@ QUICKSTART_RULE_RE = re.compile( QUICKSTART_RULE_PREFIX_RE = re.compile( r"^", re.DOTALL | re.IGNORECASE, @@ -83,23 +86,24 @@ WRITE_PRELUDE_SECTIONS = ( ) 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", - "string-literal-default": "03_values_and_literals.md", - "string-prefix-by-type": "03_values_and_literals.md", - "index-origins": "03_values_and_literals.md", +MAX_LEAF_SECTION_LINES = 180 +QUICKSTART_RULE_OWNERS = { + "assignment": ("06_expressions_and_operators.md", "syntax-06-004"), + "file-choice": ("02_core_model.md", "syntax-02-002"), + "tsl-layout": ("02_core_model.md", "syntax-02-002"), + "tsf-layout": ("02_core_model.md", "syntax-02-002"), + "tsf-filename": ("02_core_model.md", "syntax-02-002"), + "function-default": ("05_functions_and_calls.md", "syntax-05-002"), + "procedure-explicit": ("05_functions_and_calls.md", "syntax-05-002"), + "class-shape": ("08_objects_and_classes.md", "syntax-08-002"), + "object-creation": ("08_objects_and_classes.md", "syntax-08-002"), + "unit-shape": ("09_units_and_scope.md", "syntax-09-002"), + "unit-default": ("09_units_and_scope.md", "syntax-09-002"), + "named-arguments": ("05_functions_and_calls.md", "syntax-05-002"), + "named-argument-order": ("05_functions_and_calls.md", "syntax-05-002"), + "string-literal-default": ("03_values_and_literals.md", "syntax-03-004"), + "string-prefix-by-type": ("03_values_and_literals.md", "syntax-03-004"), + "index-origins": ("03_values_and_literals.md", "syntax-03-002"), } HEADING_TOKEN_SCORE = 12 @@ -232,6 +236,9 @@ class QuickstartRule: bullet_line: int key: str text: str + section_id: str | None + owner_section: str | None + owner_line: int | None class ReferenceInstallationError(RuntimeError): @@ -724,6 +731,7 @@ def _quickstart_rule_records( records: list[QuickstartRule] = [] problems: list[ValidationProblem] = [] in_fence = False + current_section_id: str | None = None for index, line in enumerate(lines): if FENCE_RE.match(line): in_fence = not in_fence @@ -731,6 +739,14 @@ def _quickstart_rule_records( if in_fence: continue stripped = line.strip() + heading = HEADING_RE.match(line) + if heading is not None and len(heading.group(1)) in (2, 3, 4): + metadata_line = _next_nonblank_line(lines, index + 1) + metadata = lines[metadata_line].strip() if metadata_line is not None else "" + section_id_match = SECTION_ID_RE.fullmatch(metadata) + current_section_id = ( + section_id_match.group(1) if section_id_match is not None else None + ) if not QUICKSTART_RULE_PREFIX_RE.match(stripped): continue marker = QUICKSTART_RULE_RE.fullmatch(stripped) @@ -750,6 +766,8 @@ def _quickstart_rule_records( ) continue text_parts = [lines[bullet_line].strip()] + owner_section: str | None = None + owner_line: int | None = None continuation = bullet_line + 1 while continuation < len(lines): line = lines[continuation] @@ -757,7 +775,21 @@ def _quickstart_rule_records( break if not line.startswith((" ", "\t")): break - text_parts.append(line.strip()) + owner_match = OWNER_SECTION_RE.fullmatch(line) + if owner_match is not None: + if owner_section is not None: + problems.append( + ValidationProblem( + page, + continuation + 1, + f"quickstart-rule {marker.group(1)!r} 重复 Owner Section", + ) + ) + else: + owner_section = owner_match.group(1) + owner_line = continuation + 1 + else: + text_parts.append(line.strip()) continuation += 1 records.append( QuickstartRule( @@ -766,6 +798,9 @@ def _quickstart_rule_records( bullet_line=bullet_line + 1, key=marker.group(1), text=" ".join(text_parts), + section_id=current_section_id, + owner_section=owner_section, + owner_line=owner_line, ) ) return records, problems @@ -829,7 +864,13 @@ def _quickstart_rule_problems( 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() + expected_keys = set(QUICKSTART_RULE_OWNERS) if require_complete else set() + declared_section_ids = { + match.group(1) + for lines in pages.values() + for line in lines + if (match := SECTION_ID_RE.fullmatch(line.strip())) is not None + } 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] @@ -843,20 +884,57 @@ def _quickstart_rule_problems( ) ) continue - expected_page = QUICKSTART_RULE_OWNER_PAGES.get(key) - if expected_page is not None and canonicals[0].page.name != expected_page: + summary = summaries[0] + canonical = canonicals[0] + expected_owner = QUICKSTART_RULE_OWNERS.get(key) + if expected_owner is not None and canonical.page.name != expected_owner[0]: problems.append( ValidationProblem( - canonicals[0].page, - canonicals[0].line, - f"quickstart-rule {key!r} 的专题 owner 应为 {expected_page}", + canonical.page, + canonical.line, + f"quickstart-rule {key!r} 的专题 owner 应为 {expected_owner[0]}", ) ) - if summaries[0].text != canonicals[0].text: + if expected_owner is not None and canonical.section_id != expected_owner[1]: + problems.append( + ValidationProblem( + canonical.page, + canonical.line, + f"quickstart-rule {key!r} 的专题 owner Section 应为 " + f"{expected_owner[1]}", + ) + ) + if summary.owner_section is None: problems.append( ValidationProblem( quickstart, - summaries[0].bullet_line, + summary.bullet_line, + f"quickstart-rule {key!r} 缺少 Owner Section", + ) + ) + elif summary.owner_section not in declared_section_ids: + problems.append( + ValidationProblem( + quickstart, + summary.owner_line or summary.bullet_line, + f"quickstart-rule {key!r} 的 Owner Section 不存在:" + f"{summary.owner_section}", + ) + ) + elif canonical.section_id is not None and summary.owner_section != canonical.section_id: + problems.append( + ValidationProblem( + quickstart, + summary.owner_line or summary.bullet_line, + f"quickstart-rule {key!r} 的 Owner Section 与专题事实所在 Section " + f"不一致:{summary.owner_section} != {canonical.section_id}", + ) + ) + if summary.text != canonical.text: + problems.append( + ValidationProblem( + quickstart, + summary.bullet_line, f"派生摘要规则与专题事实不一致:{key}", ) ) @@ -867,6 +945,35 @@ def _quickstart_rule_problems( return problems +def _leaf_section_granularity_problems( + sections: list[Section], +) -> list[ValidationProblem]: + """Reject exact-retrieval units whose body has grown beyond the safe budget.""" + problems: list[ValidationProblem] = [] + for section in sections: + has_child = any( + other.page == section.page + and len(other.heading_path) > len(section.heading_path) + and other.heading_path[: len(section.heading_path)] + == section.heading_path + for other in sections + ) + if has_child: + continue + line_count = len(section.body.splitlines()) + if line_count <= MAX_LEAF_SECTION_LINES: + continue + problems.append( + ValidationProblem( + section.page, + 1, + f"叶子 Section 正文超过 {MAX_LEAF_SECTION_LINES} 行:" + f"{section.id}({line_count} 行)", + ) + ) + return problems + + def validate_references( references_dir: Path = DEFAULT_REFERENCES_DIR, ) -> list[ValidationProblem]: @@ -971,6 +1078,7 @@ def validate_references( return problems sections = load_sections(references_dir) problems.extend(_tag_problems(sections)) + problems.extend(_leaf_section_granularity_problems(sections)) ids: dict[str, Section] = {} for section in sections: if section.id in ids: @@ -1534,9 +1642,24 @@ HELP_EPILOG = """\ 选定的 Section ID 可以合并成一次取回 lookup.py --section "syntax-05-004" "syntax-02-002" -查询词无从下手时先 --map 把需求映射到 TSL 概念;改动参考页或 data/ 词表后用 --check 校验。 +一次 --query 只覆盖一个语法要素;从用户原话提取错误原文、标识符或要素名, +保留原写法但不传完整用户句。多个要素分别查询,再批量 --section。 + +write 模式额外列出 Required: yes 前置章节,不占 --limit,同一任务只需取回一次。 +syntax-01-002 是派生速查;每条规则后的 Owner Section 才是该事实的专题来源。 + +查询词无从下手时先 --map 把需求映射到 TSL 概念;地图不含可照写事实。 弱命中(候选标 Weak: yes)没有意图/标题/标识符/标签命中,只靠正文低分撞词; -全部候选皆弱时视同无匹配并返回 rc=2,应改进查询词重试而不是从弱候选里挑。 +混合候选只从强命中里选择;全部候选皆弱时视同无匹配并返回 rc=2。 +没有 Weak 行也不保证相关,仍按 Summary 选定后取回正文核对。 + +--section 中任一 ID 不存在时整批不返回正文,并在 stderr 给出最近的 Section ID。 +改动参考页或 data/ 词表后用 --check;它不校验事实语义、排序或示例运行结果。 + +退出码: + 0 动作成功 + 1 参考目录/结构/安装错误,或 --check 发现问题 + 2 参数错误、无匹配、全弱命中或 Section ID 不存在 """ @@ -1581,7 +1704,8 @@ def _parser() -> argparse.ArgumentParser: "--check", action="store_true", help="只校验参考页结构、显式 Section ID、代码块身份、本地链接、quickstart " - "派生规则和词表页覆盖;检索排序由测试套件校验。发现问题时退出码为 1", + "派生规则、词表页覆盖和叶子 Section 180 行粒度上限;检索排序由测试套件校验。" + "发现问题时退出码为 1", ) parser.add_argument( "--mode", diff --git a/test/test_tsl_syntax_reference.py b/test/test_tsl_syntax_reference.py index 7a8de5bd..0a862723 100644 --- a/test/test_tsl_syntax_reference.py +++ b/test/test_tsl_syntax_reference.py @@ -1,4 +1,5 @@ import importlib.util +import re import subprocess import sys import tempfile @@ -275,6 +276,104 @@ class TslSyntaxReferenceTests(unittest.TestCase): self.assertTrue(any("assignment" in message for message in messages)) + def test_quickstart_owner_drift_fails_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + write_reference( + references, + "01_quickstart.md", + """ + # Quickstart + + ## 本篇职责 + + + + 派生摘要。 + + ## 语言核心事实速查 + + + + + - 普通赋值使用 `:=`。 + Owner Section:`syntax-02-999` + """, + ) + write_reference( + references, + "02_topic.md", + """ + # Topic + + ## 本篇职责 + + + + 完整事实源。 + + ## 核心规则 + + + + + - 普通赋值使用 `:=`。 + """, + ) + + messages = [item.message for item in lookup.validate_references(references)] + + self.assertTrue(any("Owner Section 不存在" in message for message in messages)) + + def test_quickstart_routes_every_derived_rule_to_its_owner_section(self): + quickstart = run_lookup("--section", "syntax-01-002") + + self.assertEqual(0, quickstart.returncode, quickstart.stderr) + owner_ids = set( + re.findall(r"Owner Section:`(syntax-\d{2}-\d{3})`", quickstart.stdout) + ) + self.assertEqual( + { + "syntax-02-002", + "syntax-03-002", + "syntax-03-004", + "syntax-05-002", + "syntax-06-004", + "syntax-08-002", + "syntax-09-002", + }, + owner_ids, + ) + for owner_id in owner_ids: + with self.subTest(owner_id=owner_id): + owner = run_lookup("--section", owner_id) + self.assertEqual(0, owner.returncode, owner.stderr) + + def test_oversized_leaf_section_fails_structure_check(self): + with tempfile.TemporaryDirectory() as temp_dir: + references = Path(temp_dir) + long_body = "\n".join(f"事实行 {index}" for index in range(181)) + write_reference( + references, + "99_fixture.md", + "# Fixture\n\n" + "## 本篇职责\n\n" + "\n\n" + "测试职责。\n\n" + "## 过长事实段\n\n" + "\n\n" + f"{long_body}\n", + ) + + result = run_lookup( + "--check", + "--references-dir", + str(references), + ) + + self.assertEqual(1, result.returncode) + self.assertIn("叶子 Section 正文超过 180 行", result.stderr) + def test_missing_references_are_installation_errors_for_every_action(self): with tempfile.TemporaryDirectory() as temp_dir: missing = Path(temp_dir) / "missing" @@ -317,6 +416,25 @@ class TslSyntaxReferenceTests(unittest.TestCase): self.assertEqual("syntax-05-008", write_result.matches[0].section.id) self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id) + def test_object_and_class_queries_return_focused_sections(self): + cases = { + "成员访问可见性": "syntax-08-013", + "类外实现": "syntax-08-014", + "固定索引 property": "syntax-08-015", + "参数化 property": "syntax-08-016", + "方法隐藏 hide": "syntax-08-017", + "调用父类 inherited": "syntax-08-018", + "析构 destroy": "syntax-08-019", + } + for query, expected_id in cases.items(): + with self.subTest(query=query): + result = lookup.query_sections(query, "explain", limit=1) + self.assertTrue(result.matches, query) + self.assertEqual(expected_id, result.matches[0].section.id) + section = run_lookup("--section", expected_id) + self.assertEqual(0, section.returncode, section.stderr) + self.assertLessEqual(len(section.stdout.splitlines()), 186) + def test_external_call_queries_retrieve_platform_and_abi_boundaries(self): cases = { "动态库常驻": "syntax-17-012", @@ -440,17 +558,40 @@ class TslSyntaxReferenceTests(unittest.TestCase): def test_skill_contract_uses_extracted_queries_and_deliverable_api_checks(self): skill = SKILL_PATH.read_text(encoding="utf-8") - help_text = lookup._parser().format_help() + help_result = run_lookup("--help") + self.assertEqual(0, help_result.returncode, help_result.stderr) + help_text = help_result.stdout - self.assertIn("保留这些词在用户原话中", skill) - self.assertIn("不传完整句", skill) + self.assertNotIn("## 构造查询词", skill) self.assertNotIn("用户怎么说就怎么传", skill) + self.assertIn("从用户原话提取", help_text) + self.assertIn("保留原写法但不传完整用户句", help_text) self.assertIn("不传完整用户句", help_text) self.assertIn("面向用户交付的 TSL/TSF 代码", skill) self.assertIn("每个 builtin/API", skill) self.assertIn("纯语法说明", skill) self.assertIn("不得声称该占位调用的 API 行为或输出", skill) + def test_help_owns_cli_details_and_skill_defers_to_it(self): + skill = SKILL_PATH.read_text(encoding="utf-8") + result = run_lookup("--help") + + self.assertEqual(0, result.returncode, result.stderr) + for text in ( + "一次 --query 只覆盖一个语法要素", + "Required: yes", + "Owner Section", + "混合候选", + "180 行粒度上限", + "退出码", + ): + with self.subTest(text=text): + self.assertIn(text, result.stdout) + self.assertIn("构造任一命令前先运行", skill) + self.assertIn("scripts/lookup.py --help", skill) + self.assertIn("不用于查询 API 签名", skill) + self.assertIn("不用于选择解释器或运行方式", skill) + def test_ci_runs_syntax_structure_and_format_gates(self): workflow = CI_PATH.read_text(encoding="utf-8") prepare = PREPARE_PATH.read_text(encoding="utf-8")