From 59fe4eb83da4710fa73b901466fc0ddfdabb0395 Mon Sep 17 00:00:00 2001 From: csh Date: Sat, 11 Jul 2026 23:15:48 +0800 Subject: [PATCH] :sparkles: feat(tsl_syntax): add concept map to lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 --map 模式,扫描 24 页「本篇职责」段实时生成 TSL 概念地图, 帮助从零写代码时把自然语言需求映射到该查的 TSL 概念(property、 unit、operator 重载等),随后仍用 --query 取精确语法。地图从 references 实时生成,零漂移;--check 增加护栏,职责段被清空即报错。 Co-Authored-By: Claude Fable 5 --- skills/tsl-syntax-reference/SKILL.md | 10 +++ skills/tsl-syntax-reference/scripts/lookup.py | 82 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/skills/tsl-syntax-reference/SKILL.md b/skills/tsl-syntax-reference/SKILL.md index 29050cbb..30287c66 100644 --- a/skills/tsl-syntax-reference/SKILL.md +++ b/skills/tsl-syntax-reference/SKILL.md @@ -21,6 +21,16 @@ python /scripts/lookup.py --query "数组下标" --mode explain 以 lookup 返回的章节作为本次任务的事实来源;需要补充时改进查询词并再次检索,不得绕过 lookup 手工路由。需要复取输出中列出的某个章节时,用 `--section
` 单独取回。 +## 从零起手先看概念地图 + +面对自然语言需求、还不确定该往哪个 TSL 概念上想时(尤其从零编写、周围无参考代码),先运行: + +```bash +python /scripts/lookup.py --map +``` + +它列出全部专题的职责摘要,用于把需求映射到 TSL 特有概念(如 `property`、`unit`、`operator` 重载、原生矩阵运算、命名参数、`with` 环境等,这些在别的语言里未必存在或写法不同)。地图只负责提示该查什么概念,不含语法细节;选定概念后仍用 `--query` 取精确语法。不要把地图当成可照写来源。 + ## 事实边界 本 Skill 只拥有 TSL/TSF 的语言语法、文件模型、表达式、控制流、对象、运行时语言结构和 TS-SQL 外形。其他事实交给对应所有者: diff --git a/skills/tsl-syntax-reference/scripts/lookup.py b/skills/tsl-syntax-reference/scripts/lookup.py index 1b2e72fa..f458d14b 100644 --- a/skills/tsl-syntax-reference/scripts/lookup.py +++ b/skills/tsl-syntax-reference/scripts/lookup.py @@ -29,6 +29,7 @@ ALLOWED_IDENTITIES = { } ROUTER_PHRASES = ("路由中心", "选择一个主专题", "候选页继续判断") EXCLUDED_REFERENCE_FILES = {"index.md"} +DUTY_HEADING = "本篇职责" SUSPICIOUS_FENCE_RE = re.compile(r"^(?:\s+`{3}|`{4,})") WRITE_PRELUDE_ANCHORS = ( ("02_core_model.md", "文件模型核心规则"), @@ -351,6 +352,20 @@ def validate_references( f"write 模式前置章节缺失:{page_name} 的「{heading}」", ) ) + # 概念地图逐页从「本篇职责」段生成;有该段的页必须产出非空摘要, + # 否则某页职责段被清空/写坏时地图会静默缺页。 + mapped_pages = {page_name for page_name, _, _ in build_concept_map(references_dir)} + for section in sections: + if section.heading_path != (DUTY_HEADING,): + continue + if section.page.name not in mapped_pages: + problems.append( + ValidationProblem( + section.page, + 1, + f"概念地图摘要为空:{section.page.name} 的「{DUTY_HEADING}」", + ) + ) return problems @@ -583,6 +598,69 @@ def render_result(result: QueryResult) -> str: return "\n".join(lines).rstrip() + "\n" +def _duty_summary(body: str) -> str: + lines = body.splitlines() + paragraph: list[str] = [] + for line in lines[1:]: + stripped = line.strip() + if not stripped: + if paragraph: + break + continue + paragraph.append(stripped) + summary = " ".join(paragraph) + # 职责段以冒号引出列表时(如 09),首段本身空洞,把随后的 + # 列表项折叠进摘要才能保留实际覆盖面。 + if summary.endswith((":", ":")): + items: list[str] = [] + seen_paragraph = False + for line in lines[1:]: + stripped = line.strip() + if not stripped: + continue + if not seen_paragraph: + if stripped == summary or stripped in summary: + seen_paragraph = True + continue + if stripped.startswith(("-", "*", "·")): + items.append(stripped.lstrip("-*· ").strip()) + elif items: + break + if items: + summary = summary + ";".join(items) + "。" + return summary + + +def build_concept_map(references_dir: Path = DEFAULT_REFERENCES_DIR) -> list[tuple[str, str, str]]: + entries: list[tuple[str, str, str]] = [] + seen_pages: set[str] = set() + for section in load_sections(references_dir): + page_name = section.page.name + if page_name in seen_pages: + continue + if section.heading_path != (DUTY_HEADING,): + continue + summary = _duty_summary(section.body) + if not summary: + continue + seen_pages.add(page_name) + entries.append((page_name, section.page_title, summary)) + entries.sort(key=lambda item: item[0]) + return entries + + +def render_concept_map(entries: list[tuple[str, str, str]]) -> str: + lines = [ + "# TSL 概念地图", + "", + "把自然语言需求映射到该查哪个 TSL 概念,随后仍用 `--query` 取精确语法。", + "本清单不含语法细节,也不替代 lookup;用命中专题里的关键语法词组成查询。", + ] + for page_name, page_title, summary in entries: + lines.extend(["", f"## {page_title}", "", summary]) + return "\n".join(lines).rstrip() + "\n" + + def _configure_utf8() -> None: for stream in (sys.stdout, sys.stderr): reconfigure = getattr(stream, "reconfigure", None) @@ -595,6 +673,7 @@ def _parser() -> argparse.ArgumentParser: action = parser.add_mutually_exclusive_group(required=True) action.add_argument("--query") action.add_argument("--section") + action.add_argument("--map", dest="show_map", action="store_true") action.add_argument("--check", action="store_true") parser.add_argument("--mode", choices=("write", "diagnose", "explain")) parser.add_argument("--limit", type=int, default=5) @@ -625,6 +704,9 @@ def main(argv: list[str] | None = None) -> int: parser.error("--mode is required with --query") if args.mode is not None and args.query is None: parser.error("--mode only applies to --query") + if args.show_map: + print(render_concept_map(build_concept_map(args.references_dir)), end="") + return 0 if args.check: problems = validate_references(args.references_dir) for problem in problems: