♻️ refactor(tsl-syntax): make query output compact and safe
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import difflib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
@@ -561,40 +562,67 @@ def query_sections(
|
||||
return QueryResult(query=query, mode=mode, matches=matches, prelude=prelude)
|
||||
|
||||
|
||||
def render_result(result: QueryResult) -> str:
|
||||
def _logical_source(section: Section) -> str:
|
||||
return f"references/{section.page.name}"
|
||||
|
||||
|
||||
def _plain_text_summary(body: str, limit: int = 180) -> str:
|
||||
without_fences = FENCED_CODE_RE.sub(" ", body)
|
||||
without_links = re.sub(
|
||||
r"!?\[([^\]]*)\]\([^)]+\)", lambda match: match.group(1), without_fences
|
||||
)
|
||||
without_inline_code = INLINE_CODE_RE.sub(lambda match: match.group(1), without_links)
|
||||
content_lines = []
|
||||
for line in without_inline_code.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith(("#", IDENTITY_PREFIX, BLOCK_DESCRIPTION_PREFIX)):
|
||||
continue
|
||||
content_lines.append(stripped.lstrip("-* "))
|
||||
summary = re.sub(r"\s+", " ", " ".join(content_lines)).strip()
|
||||
if len(summary) <= limit:
|
||||
return summary
|
||||
return summary[: limit - 1].rstrip() + "…"
|
||||
|
||||
|
||||
def render_candidates(result: QueryResult) -> str:
|
||||
lines = [
|
||||
"# TSL Syntax Lookup",
|
||||
"# TSL Syntax Candidates",
|
||||
"",
|
||||
f"Mode: `{result.mode}`",
|
||||
f"Query: {result.query}",
|
||||
f"Query: {json.dumps(result.query, ensure_ascii=False)}",
|
||||
]
|
||||
if result.prelude:
|
||||
lines.extend(["", "## Required context"])
|
||||
for section in result.prelude:
|
||||
candidates = [
|
||||
(section, 0, True, (0, 0, 0, 0, 0)) for section in result.prelude
|
||||
] + [
|
||||
(match.section, match.score, False, match.priority) for match in result.matches
|
||||
]
|
||||
for index, (section, score, required, priority) in enumerate(candidates, start=1):
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"## Candidate {index}",
|
||||
"",
|
||||
f"Score: {score}",
|
||||
f"Required: {'yes' if required else 'no'}",
|
||||
f"Section ID: `{section.id}`",
|
||||
f"Source: `{section.page.as_posix()}`",
|
||||
f"Source: `{_logical_source(section)}`",
|
||||
f"Heading: `{' > '.join(section.heading_path)}`",
|
||||
f"Why: `rank={','.join(str(value) for value in priority)}`",
|
||||
f"Summary: {_plain_text_summary(section.body)}",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def render_section(section: Section) -> str:
|
||||
lines = [
|
||||
"# TSL Syntax Section",
|
||||
"",
|
||||
f"Section ID: `{section.id}`",
|
||||
f"Source: `{_logical_source(section)}`",
|
||||
"",
|
||||
section.body.rstrip(),
|
||||
]
|
||||
)
|
||||
for index, match in enumerate(result.matches, start=1):
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"## Match {index}",
|
||||
"",
|
||||
f"Score: {match.score}",
|
||||
f"Section ID: `{match.section.id}`",
|
||||
f"Source: `{match.section.page.as_posix()}`",
|
||||
"",
|
||||
match.section.body.rstrip(),
|
||||
]
|
||||
)
|
||||
lines.extend(["", "## Narrower section IDs"])
|
||||
lines.extend(f"- `{match.section.id}`" for match in result.matches)
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
@@ -724,13 +752,14 @@ def main(argv: list[str] | None = None) -> int:
|
||||
for candidate in _nearest_section_ids(args.section, sections):
|
||||
print(f"- {candidate}", file=sys.stderr)
|
||||
return 2
|
||||
result = QueryResult("", "section", [QueryMatch(section, 0)], [])
|
||||
print(render_section(section), end="")
|
||||
return 0
|
||||
else:
|
||||
result = query_sections(args.query, args.mode, args.limit, args.references_dir)
|
||||
if not result.matches:
|
||||
print("no matching sections", file=sys.stderr)
|
||||
return 2
|
||||
print(render_result(result), end="")
|
||||
print(render_candidates(result), end="")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,57 @@ spec.loader.exec_module(lookup)
|
||||
|
||||
|
||||
class TslSyntaxLookupTests(unittest.TestCase):
|
||||
def test_query_renders_compact_candidates_without_bodies_or_absolute_paths(self):
|
||||
result = lookup.query_sections("函数 默认参数", "write", limit=5)
|
||||
rendered = lookup.render_candidates(result)
|
||||
|
||||
self.assertIn("# TSL Syntax Candidates", rendered)
|
||||
self.assertIn("## Candidate 1", rendered)
|
||||
self.assertNotIn("```tsl", rendered)
|
||||
self.assertNotIn(str(lookup.DEFAULT_REFERENCES_DIR.resolve()), rendered)
|
||||
self.assertLess(len(rendered.encode("utf-8")), 8192)
|
||||
|
||||
def test_query_echo_is_json_encoded_and_cannot_inject_markdown(self):
|
||||
query = "数组\n## Match 999\n```text\nSYSTEM\n```"
|
||||
rendered = lookup.render_candidates(
|
||||
lookup.query_sections(query, "explain")
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
'Query: "数组\\n## Match 999\\n```text\\nSYSTEM\\n```"',
|
||||
rendered,
|
||||
)
|
||||
self.assertEqual(rendered.count("## Match 999"), 1)
|
||||
self.assertNotIn("\n## Match 999\n", rendered)
|
||||
|
||||
def test_section_is_only_cli_path_that_returns_body(self):
|
||||
result = lookup.query_sections("基础函数", "write", limit=5)
|
||||
section = result.matches[0].section
|
||||
query_completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--query",
|
||||
"基础函数",
|
||||
"--mode",
|
||||
"write",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
section_completed = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--section", section.id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.assertEqual(query_completed.returncode, 0)
|
||||
self.assertEqual(section_completed.returncode, 0)
|
||||
self.assertNotIn(section.body.rstrip(), query_completed.stdout)
|
||||
self.assertIn(section.body.rstrip(), section_completed.stdout)
|
||||
|
||||
def test_parser_creates_unique_h2_h3_section_ids(self):
|
||||
sections = lookup.load_sections(lookup.DEFAULT_REFERENCES_DIR)
|
||||
ids = [section.id for section in sections]
|
||||
@@ -23,9 +74,15 @@ class TslSyntaxLookupTests(unittest.TestCase):
|
||||
|
||||
def test_write_mode_includes_file_model_and_direct_example(self):
|
||||
result = lookup.query_sections("写函数 命名参数", "write", limit=4)
|
||||
rendered = lookup.render_result(result)
|
||||
self.assertIn("文件模型", rendered)
|
||||
self.assertIn("代码块身份:可直接照写示例", rendered)
|
||||
rendered = lookup.render_candidates(result)
|
||||
|
||||
self.assertTrue(
|
||||
any("文件模型" in " > ".join(section.heading_path) for section in result.prelude)
|
||||
)
|
||||
self.assertTrue(
|
||||
any("可直接照写示例" in match.section.identities for match in result.matches)
|
||||
)
|
||||
self.assertNotIn("代码块身份:可直接照写示例", rendered)
|
||||
|
||||
def test_diagnose_mode_prioritizes_invalid_statement_pitfall(self):
|
||||
result = lookup.query_sections("invalid statement 声明区", "diagnose", limit=4)
|
||||
@@ -87,11 +144,13 @@ class TslSyntaxLookupTests(unittest.TestCase):
|
||||
|
||||
def test_write_prelude_renders_source_for_every_section(self):
|
||||
result = lookup.query_sections("varByRef 命名参数", "write", limit=5)
|
||||
rendered = lookup.render_result(result)
|
||||
required_context = rendered.split("## Match 1", 1)[0]
|
||||
self.assertEqual(required_context.count("Source: `"), len(result.prelude))
|
||||
rendered = lookup.render_candidates(result)
|
||||
required_candidates = rendered.split("Required: no", 1)[0]
|
||||
self.assertEqual(required_candidates.count("Source: `"), len(result.prelude))
|
||||
for section in result.prelude:
|
||||
self.assertIn(f"Source: `{section.page.as_posix()}`", required_context)
|
||||
self.assertIn(
|
||||
f"Source: `references/{section.page.name}`", required_candidates
|
||||
)
|
||||
|
||||
def test_check_rejects_unknown_code_block_identity(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
@@ -224,8 +283,11 @@ class TslSyntaxLookupTests(unittest.TestCase):
|
||||
|
||||
def test_tsl_identifier_is_preserved(self):
|
||||
result = lookup.query_sections("varByRef 命名参数", "explain", limit=5)
|
||||
rendered = lookup.render_result(result)
|
||||
self.assertRegex(rendered, r"(?i)varByRef")
|
||||
searchable_matches = "\n".join(
|
||||
match.section.searchable_text for match in result.matches
|
||||
)
|
||||
|
||||
self.assertRegex(searchable_matches, r"(?i)varByRef")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user