feat(tsl_syntax): add concept map to lookup

新增 --map 模式,扫描 24 页「本篇职责」段实时生成 TSL 概念地图,
帮助从零写代码时把自然语言需求映射到该查的 TSL 概念(property、
unit、operator 重载等),随后仍用 --query 取精确语法。地图从
references 实时生成,零漂移;--check 增加护栏,职责段被清空即报错。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
csh
2026-07-13 09:16:57 +08:00
co-authored by Claude Fable 5
parent b597edfc70
commit 59fe4eb83d
2 changed files with 92 additions and 0 deletions
+10
View File
@@ -21,6 +21,16 @@ python <this-skill-dir>/scripts/lookup.py --query "数组下标" --mode explain
以 lookup 返回的章节作为本次任务的事实来源;需要补充时改进查询词并再次检索,不得绕过 lookup 手工路由。需要复取输出中列出的某个章节时,用 `--section <Section ID>` 单独取回。
## 从零起手先看概念地图
面对自然语言需求、还不确定该往哪个 TSL 概念上想时(尤其从零编写、周围无参考代码),先运行:
```bash
python <this-skill-dir>/scripts/lookup.py --map
```
它列出全部专题的职责摘要,用于把需求映射到 TSL 特有概念(如 `property``unit``operator` 重载、原生矩阵运算、命名参数、`with` 环境等,这些在别的语言里未必存在或写法不同)。地图只负责提示该查什么概念,不含语法细节;选定概念后仍用 `--query` 取精确语法。不要把地图当成可照写来源。
## 事实边界
本 Skill 只拥有 TSL/TSF 的语言语法、文件模型、表达式、控制流、对象、运行时语言结构和 TS-SQL 外形。其他事实交给对应所有者:
@@ -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: