🐛 fix(tsl_syntax): repair lookup engine and reconcile reference facts

lookup.py: exempt code-anchored ASCII identifiers from the mixed
zh/en gate so exact hits are no longer dropped; dedup parent/child
sections in results; validate write-prelude anchors in --check.

references: correct interpreter-verified facts (case-as-expression,
control-flow semicolons, __line__/__stack_frame, tslObjects order,
destroy timing, ErrDefine, truncated outputs), fix headings, scope
qualifiers and reversed quotes; strip dead preamble metadata from all
24 pages.

packaging: exclude __pycache__/*.pyc from playbook and bundle copies;
update build test file-count assertion to match.

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 923bce91b0
commit b597edfc70
29 changed files with 356 additions and 256 deletions
+86 -12
View File
@@ -6,13 +6,22 @@ import tempfile
import unittest
from pathlib import Path
from scripts.build_tsl_playbook import build_agents_text
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "build_tsl_playbook.py"
SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml"
README = ROOT / "README.md"
class BuildTslPlaybookTests(unittest.TestCase):
def test_readme_names_tsl_syntax_skill_as_unique_fact_owner(self):
text = README.read_text(encoding="utf-8")
self.assertIn("TSL 语法事实唯一由 `tsl-syntax-reference` 管理", text)
self.assertIn("TSL 领域路由与事实边界", text)
def test_builds_minimal_tsl_playbook_tree(self):
with tempfile.TemporaryDirectory() as tmp_dir:
output = Path(tmp_dir) / "tsl-playbook"
@@ -37,6 +46,9 @@ class BuildTslPlaybookTests(unittest.TestCase):
self.assertTrue((syntax_skill / "SKILL.md").is_file())
self.assertTrue((syntax_skill / "scripts" / "lookup.py").is_file())
self.assertFalse((syntax_skill / "references" / "index.md").exists())
self.assertTrue(
(output / "skills" / "tsl-syntax-reference" / "SKILL.md").is_file()
)
self.assertTrue(
(output / "skills" / "tsl-api-reference" / "SKILL.md").is_file()
)
@@ -61,9 +73,54 @@ class BuildTslPlaybookTests(unittest.TestCase):
output_docs = count_files(output / "docs" / "tsl")
self.assertEqual(output_docs, source_docs)
source_skill = count_files(ROOT / "skills" / "tsl-api-reference")
output_skill = count_files(output / "skills" / "tsl-api-reference")
self.assertEqual(output_skill, source_skill)
for skill_name in ("tsl-syntax-reference", "tsl-api-reference"):
source_skill = count_files(ROOT / "skills" / skill_name)
output_skill = count_files(output / "skills" / skill_name)
self.assertEqual(output_skill, source_skill)
def test_missing_tsl_skill_reports_specific_source_path(self):
for skill_name in ("tsl-syntax-reference", "tsl-api-reference"):
with self.subTest(skill=skill_name), tempfile.TemporaryDirectory() as tmp_dir:
repo = Path(tmp_dir) / "repo"
repo.mkdir()
copy_required_sources(repo)
missing_path = repo / "skills" / skill_name
shutil.rmtree(missing_path)
result = subprocess.run(
[
sys.executable,
str(repo / "scripts" / "build_tsl_playbook.py"),
"--output",
str(repo / "output"),
],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn(str(missing_path), result.stderr)
def test_build_agents_text_only_rewrites_title_and_trailing_newline(self):
marker = (
"- 需要任何 TSL 语法、文件模型、函数、模块或金融数据事实时,"
"第一跳统一进 `docs/tsl/index.md`,由它路由到具体页;"
"不在本文件内猜文件路径,也不全目录搜索。"
)
with tempfile.TemporaryDirectory() as tmp_dir:
ruleset = Path(tmp_dir) / "index.md"
ruleset.write_text(
f"# TSL 智能体规则\n\n{marker}\n\n",
encoding="utf-8",
newline="\n",
)
agents_text = build_agents_text(ruleset)
self.assertEqual(
agents_text,
f"# TSL Agent Instructions\n\n{marker}\n",
)
def test_sync_workflow_does_not_remove_entire_target_branch(self):
text = SYNC_WORKFLOW.read_text(encoding="utf-8")
@@ -109,6 +166,7 @@ class BuildTslPlaybookTests(unittest.TestCase):
for path in (
"AGENTS.md",
"docs/tsl/index.md",
"skills/tsl-syntax-reference/SKILL.md",
"skills/tsl-api-reference/SKILL.md",
):
git(repo, "cat-file", "-e", f"HEAD:{path}")
@@ -125,6 +183,7 @@ class BuildTslPlaybookTests(unittest.TestCase):
for path in (
"AGENTS.md",
"docs/tsl/index.md",
"skills/tsl-syntax-reference/SKILL.md",
"skills/tsl-api-reference/SKILL.md",
):
git(repo, "cat-file", "-e", f"HEAD:{path}")
@@ -164,20 +223,18 @@ def run_sync(repo: Path) -> None:
env = os.environ.copy()
env.update(
{
"REPO_DIR": str(repo),
"TARGET_BRANCH": "tsl-playbook",
"COMMIT_AUTHOR_NAME": "test",
"COMMIT_AUTHOR_EMAIL": "test@example.invalid",
}
)
with tempfile.NamedTemporaryFile(
"w", suffix=".sh", encoding="utf-8", newline="\n", delete=False
) as script_file:
script_file.write(extract_sync_workflow_script())
script_path = script_file.name
script_path = repo / ".sync-test.sh"
script_path.write_text(
extract_sync_workflow_script(), encoding="utf-8", newline="\n"
)
try:
result = subprocess.run(
["bash", script_path],
["bash", script_path.name],
cwd=repo,
env=env,
capture_output=True,
@@ -186,7 +243,7 @@ def run_sync(repo: Path) -> None:
errors="replace",
)
finally:
os.unlink(script_path)
script_path.unlink(missing_ok=True)
if result.returncode != 0:
raise AssertionError(result.stderr + result.stdout)
@@ -223,6 +280,17 @@ def copy_required_sources(repo: Path) -> None:
"print('lookup')\n", encoding="utf-8", newline="\n"
)
syntax_skill = repo / "skills" / "tsl-syntax-reference"
(syntax_skill / "references").mkdir(parents=True)
(syntax_skill / "SKILL.md").write_text(
"---\nname: tsl-syntax-reference\n---\n",
encoding="utf-8",
newline="\n",
)
(syntax_skill / "references" / "index.md").write_text(
"# TSL Syntax Reference\n", encoding="utf-8", newline="\n"
)
(repo / "rulesets" / "tsl").mkdir(parents=True)
(repo / "rulesets" / "tsl" / "index.md").write_text(
"# TSL Agent Instructions\n", encoding="utf-8", newline="\n"
@@ -230,7 +298,13 @@ def copy_required_sources(repo: Path) -> None:
def count_files(path: Path) -> int:
return sum(1 for item in path.rglob("*") if item.is_file())
return sum(
1
for item in path.rglob("*")
if item.is_file()
and "__pycache__" not in item.parts
and item.suffix != ".pyc"
)
def extract_sync_workflow_script() -> str: