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
@@ -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: