feat(tsl-codegen): add decoupled documentation toolkit

Generate TSL API Markdown from YAML or JSON into a configurable project scope.\nAdd file and directory lint modes, tags-aware indexing, and keyword search across tags and descriptions.\nBundle the toolkit through the playbook build and sync workflows.
This commit is contained in:
csh
2026-07-20 09:08:38 +08:00
parent 1d5304e7b6
commit c69278283f
18 changed files with 13955 additions and 12821 deletions
@@ -0,0 +1,83 @@
import importlib.util
import io
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[1]
/ "scripts"
/ "build_index.py"
)
def load_script():
spec = importlib.util.spec_from_file_location(
"tsl_codegen_function_index", SCRIPT_PATH
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class FunctionIndexTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.skill_dir = Path(self.temp_dir.name) / "tsl-api-reference"
self.codegen_root = self.skill_dir / "references" / "codegen"
self.data_dir = self.skill_dir / "data"
leaf = self.codegen_root / "builtin" / "base" / "array.md"
leaf.parent.mkdir(parents=True)
leaf.write_text(
"# Builtin - 基础 / 数组\n\n"
"## `demo()`\n\n"
"<!-- tags: 数组 列表 -->\n\n"
"返回示例值。\n\n"
"返回:integer\n",
encoding="utf-8",
)
self.module = load_script()
def tearDown(self):
self.temp_dir.cleanup()
def run_main(self, *args):
stdout = io.StringIO()
stderr = io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
result = self.module.main(["--skill-dir", str(self.skill_dir), *args])
return result, stdout.getvalue(), stderr.getvalue()
def test_rebuild_writes_only_tsv(self):
result, _, _ = self.run_main()
self.assertEqual(0, result)
self.assertTrue((self.data_dir / "function_index.tsv").is_file())
self.assertEqual([], list(self.codegen_root.rglob("index.md")))
def test_tags_and_summary_are_stored_separately(self):
rows = self.module.build_rows(self.codegen_root)
row = dict(zip(self.module.HEADER, rows[0]))
self.assertEqual("数组 列表", row["tags"])
self.assertEqual("返回示例值。", row["summary"])
def test_check_does_not_require_index_pages(self):
self.data_dir.mkdir(parents=True)
rows = self.module.build_rows(self.codegen_root)
(self.data_dir / "function_index.tsv").write_text(
self.module.render_tsv(rows),
encoding="utf-8",
newline="\n",
)
result, stdout, _ = self.run_main("--check")
self.assertEqual(0, result)
self.assertIn("matches md tree", stdout)
if __name__ == "__main__":
unittest.main()
+85
View File
@@ -0,0 +1,85 @@
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).parents[1] / "scripts" / "generate.py"
class DocGenCliTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.input = self.root / "entry.json"
self.input.write_text(
json.dumps(
{
"module": "项目 / 示例",
"path": "base/my_functions",
"functions": [
{
"signature": "demo()",
"desc": "示例函数。",
"returns": "nil",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
def tearDown(self):
self.temp_dir.cleanup()
def run_cli(self, *args):
return subprocess.run(
[sys.executable, str(SCRIPT), str(self.input), *args],
capture_output=True,
text=True,
encoding="utf-8",
cwd=self.root,
)
def test_default_scope_writes_configured_path_under_project(self):
result = self.run_cli()
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "project"
/ "base"
/ "my_functions.md"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(output.is_file())
self.assertTrue(output.read_text(encoding="utf-8").startswith("# 项目 / 示例\n"))
def test_custom_scope_changes_first_destination_directory(self):
result = self.run_cli("--scope", "my-project")
output = (
self.root
/ "skills"
/ "tsl-api-reference"
/ "references"
/ "codegen"
/ "my-project"
/ "base"
/ "my_functions.md"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(output.is_file())
def test_output_option_is_rejected(self):
result = self.run_cli("--output", str(self.root / "out.md"))
self.assertNotEqual(result.returncode, 0)
self.assertIn("unrecognized arguments: --output", result.stderr)
if __name__ == "__main__":
unittest.main()
+43
View File
@@ -0,0 +1,43 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).parents[1] / "scripts" / "lint.py"
VALID_PAGE = "# 项目 / 示例\n\n## `demo()`\n\n示例函数\n\n返回:nil\n"
class DocLintCliTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.page = self.root / "base" / "demo.md"
self.page.parent.mkdir()
self.page.write_text(VALID_PAGE, encoding="utf-8")
def tearDown(self):
self.temp_dir.cleanup()
def run_cli(self, *args):
return subprocess.run(
[sys.executable, str(SCRIPT), *map(str, args)],
capture_output=True,
text=True,
encoding="utf-8",
)
def test_accepts_one_markdown_file(self):
result = self.run_cli("--file", self.page)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("1 files", result.stderr)
def test_accepts_one_directory(self):
result = self.run_cli("--dir", self.root)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("1 files", result.stderr)
if __name__ == "__main__":
unittest.main()
+40
View File
@@ -0,0 +1,40 @@
import importlib.util
import unittest
from pathlib import Path
SCRIPT = (
Path(__file__).resolve().parents[3]
/ "skills"
/ "tsl-api-reference"
/ "scripts"
/ "lookup.py"
)
def load_script():
spec = importlib.util.spec_from_file_location("tsl_lookup", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class LookupTest(unittest.TestCase):
def test_keyword_search_includes_tags_and_summary(self):
module = load_script()
rows = [
{
"name": "demo",
"signature": "demo()",
"module": "base",
"tags": "数组 列表",
"summary": "返回示例值",
}
]
self.assertEqual(rows, module.search_keyword(rows, ["数组"]))
self.assertEqual(rows, module.search_keyword(rows, ["返回示例"]))
if __name__ == "__main__":
unittest.main()