📝 docs(tsl): rebuild canonical syntax and routing manual

This commit is contained in:
csh
2026-04-22 16:59:20 +08:00
parent 3ed5052e61
commit 96b705b00b
83 changed files with 46446 additions and 323 deletions
+174 -100
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
@@ -21,7 +22,15 @@ ORDER = [
]
SCRIPT_DIR = Path(__file__).resolve().parent
PLAYBOOK_ROOT = SCRIPT_DIR.parent
PATH_CONFIG_KEYS = {"project_root", "target_dir", "agents_home", "codex_home"}
PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home"}
DOCS_INDEX_SECTION_HEADINGS = {
"common": "## 跨语言(common",
"tsl": "## TSLtsl/tsf",
"cpp": "## C++cpp",
"python": "## Pythonpython",
"typescript": "## TypeScripttypescript",
"markdown": "## Markdownmarkdown",
}
def usage() -> str:
@@ -232,6 +241,85 @@ def normalize_langs(raw: object) -> list[str]:
return cleaned
def normalize_relative_dir(raw: object, label: str) -> str:
value = str(raw).strip()
if not value:
raise ValueError(f"{label} is empty")
path = Path(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError(f"invalid {label}: {value}")
normalized = path.as_posix()
return "." if normalized == "" else normalized
def join_deploy_subpath(root: str, child: str) -> str:
if root in ("", "."):
return child.lstrip("/")
return f"{root.rstrip('/')}/{child.lstrip('/')}"
def resolve_in_project_deploy_root(project_root: Path) -> str | None:
try:
rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
if str(rel) != ".":
return rel.as_posix()
except ValueError:
pass
return None
def config_requires_deploy_root(config: dict) -> bool:
for key in (
"vendor",
"sync_rules",
"sync_memory_bank",
"sync_prompts",
"sync_standards",
"install_skills",
):
if key in config:
return True
return False
def resolve_configured_deploy_root(config: dict, project_root: Path) -> str:
playbook_config = config.get("playbook", {})
raw = None
if isinstance(playbook_config, dict):
raw = playbook_config.get("deploy_root")
vendor_config = config.get("vendor", {})
if isinstance(vendor_config, dict) and vendor_config.get("target_dir") is not None:
raise ValueError(
"vendor.target_dir is no longer supported; use [playbook].deploy_root"
)
if raw is not None and str(raw).strip():
return normalize_relative_dir(raw, "deploy_root")
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
if in_project_deploy_root is not None:
return in_project_deploy_root
if config_requires_deploy_root(config):
raise ValueError(
"playbook.deploy_root is required when running from an external clone; "
"set it to the target project's relative deployment path"
)
return "docs/standards/playbook"
def resolve_deploy_root(context: dict) -> str:
project_root: Path = context["project_root"]
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
if in_project_deploy_root is not None:
return in_project_deploy_root
return context["deploy_root"]
def resolve_docs_prefix(context: dict) -> str:
return join_deploy_subpath(resolve_deploy_root(context), "docs")
def resolve_main_language(config: dict, context: dict) -> str:
raw = config.get("main_language")
if raw is not None and str(raw).strip():
@@ -254,21 +342,8 @@ def resolve_main_language(config: dict, context: dict) -> str:
def resolve_playbook_scripts(project_root: Path, context: dict) -> str:
playbook_scripts = PLAYBOOK_ROOT / "scripts"
try:
rel = playbook_scripts.resolve().relative_to(project_root.resolve())
return rel.as_posix()
except ValueError:
full_config = context.get("config", {})
if isinstance(full_config, dict):
vendor_conf = full_config.get("vendor")
if isinstance(vendor_conf, dict):
target_dir = vendor_conf.get("target_dir")
if target_dir:
target_str = str(target_dir).strip().rstrip("/").rstrip("\\")
if target_str:
return f"{target_str}/scripts"
return "docs/standards/playbook/scripts"
_ = project_root
return join_deploy_subpath(resolve_deploy_root(context), "scripts")
def read_git_commit(root: Path) -> str:
@@ -284,88 +359,75 @@ def read_git_commit(root: Path) -> str:
return result.stdout.strip() or "N/A"
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
lines = [
"# 文档导航(Docs Index",
def extract_docs_index_sections(lines: list[str]) -> dict[str, list[str]]:
heading_to_key = {value: key for key, value in DOCS_INDEX_SECTION_HEADINGS.items()}
starts: list[tuple[int, str]] = []
for idx, line in enumerate(lines):
key = heading_to_key.get(line)
if key is not None:
starts.append((idx, key))
sections: dict[str, list[str]] = {}
for idx, (start, key) in enumerate(starts):
end = starts[idx + 1][0] if idx + 1 < len(starts) else len(lines)
section_lines = lines[start:end]
while section_lines and section_lines[-1] == "":
section_lines = section_lines[:-1]
sections[key] = section_lines
return sections
def build_docs_index_lines(langs: list[str], source_path: Path | None = None) -> list[str]:
docs_index_path = source_path or (PLAYBOOK_ROOT / "docs" / "index.md")
source_lines = docs_index_path.read_text(encoding="utf-8").splitlines()
title = source_lines[0] if source_lines else "# 文档导航(Docs Index"
sections = extract_docs_index_sections(source_lines)
ordered_keys = ["common", *langs]
result = [
title,
"",
f"本快照为裁剪版 Playbooklangs: {','.join(langs)})。",
"",
"## 跨语言(common",
"",
"- 提交信息与版本号:`common/commit_message.md`",
]
for lang in langs:
if lang == "tsl":
lines += [
"",
"## TSLtsl",
"",
"- 代码风格:`tsl/code_style.md`",
"- 命名规范:`tsl/naming.md`",
"- 语法手册:`tsl/syntax_book/index.md`",
"- 工具链与验证命令(模板):`tsl/toolchain.md`",
]
elif lang == "cpp":
lines += [
"",
"## C++cpp",
"",
"- 代码风格:`cpp/code_style.md`",
"- 命名规范:`cpp/naming.md`",
"- 工具链与验证命令(模板):`cpp/toolchain.md`",
"- 第三方依赖(Conan):`cpp/dependencies_conan.md`",
"- clangd 配置:`cpp/clangd.md`",
]
elif lang == "python":
lines += [
"",
"## Pythonpython",
"",
"- 代码风格:`python/style_guide.md`",
"- 工具链:`python/tooling.md`",
"- 配置清单:`python/configuration.md`",
]
elif lang == "typescript":
lines += [
"",
"## TypeScripttypescript",
"",
"- 代码风格:`typescript/code_style.md`",
"- 命名规范:`typescript/naming.md`",
"- 工具链:`typescript/toolchain.md`",
"- 配置清单:`typescript/configuration.md`",
]
elif lang == "markdown":
lines += [
"",
"## Markdownmarkdown",
"",
"- 代码块与行内代码格式:`markdown/index.md`",
]
for idx, key in enumerate(ordered_keys):
section = sections.get(key)
if section is None:
raise ValueError(f"docs/index.md is missing section for {key}")
if idx > 0:
result.append("")
result.extend(section)
return result
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
lines = build_docs_index_lines(langs)
docs_index = dest_prefix / "docs/index.md"
ensure_dir(docs_index.parent)
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_snapshot_readme(dest_prefix: Path, langs: list[str]) -> None:
def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str]) -> None:
scripts_path = join_deploy_subpath(deploy_root, "scripts/playbook.py")
docs_index_path = join_deploy_subpath(deploy_root, "docs/index.md")
lines = [
"# Playbook(裁剪快照)",
"",
f"本目录为从 Playbook vendoring 的裁剪快照(langs: {','.join(langs)})。",
f"本目录为从 Playbook 部署到项目内的裁剪快照(langs: {','.join(langs)})。",
"",
"## 使用",
"",
"在目标项目根目录执行:",
"",
"```sh",
"python docs/standards/playbook/scripts/playbook.py -config playbook.toml",
f"python {scripts_path} -config playbook.toml",
"```",
"",
"配置示例:`docs/standards/playbook/playbook.toml.example`",
f"配置示例:`{join_deploy_subpath(deploy_root, 'playbook.toml.example')}`",
"",
"文档入口:",
"",
"- `docs/standards/playbook/docs/index.md`",
f"- `{docs_index_path}`",
"- `.agents/index.md`",
]
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -393,11 +455,8 @@ def vendor_action(config: dict, context: dict) -> int:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
target_dir = config.get("target_dir", "docs/standards/playbook")
target_path = Path(target_dir)
if target_path.is_absolute() or ".." in target_path.parts:
print(f"ERROR: invalid target_dir: {target_dir}", file=sys.stderr)
return 2
deploy_root = context["deploy_root"]
target_path = Path(deploy_root)
project_root: Path = context["project_root"]
dest_prefix = project_root / target_path
@@ -471,7 +530,7 @@ def vendor_action(config: dict, context: dict) -> int:
copy2(example_config, dest_prefix / "playbook.toml.example")
write_docs_index(dest_prefix, langs)
write_snapshot_readme(dest_prefix, langs)
write_snapshot_readme(dest_prefix, deploy_root, langs)
write_source_file(dest_prefix, langs)
log(f"Vendored snapshot -> {dest_prefix}")
@@ -910,32 +969,45 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str |
"",
"标准快照文档入口:",
"",
f"- {docs_prefix or 'docs/standards/playbook/docs/'}",
f"- {docs_prefix or 'docs/'}",
]
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
log("Synced .agents/index.md")
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
def rewrite_docs_links_in_markdown(root: Path, docs_prefix: str, recursive: bool) -> None:
replacements = {
"`docs/tsl/": f"`{docs_prefix}/tsl/",
"`docs/cpp/": f"`{docs_prefix}/cpp/",
"`docs/python/": f"`{docs_prefix}/python/",
"`docs/typescript/": f"`{docs_prefix}/typescript/",
"`docs/markdown/": f"`{docs_prefix}/markdown/",
"`docs/common/": f"`{docs_prefix}/common/",
"tsl": f"{docs_prefix}/tsl/",
"cpp": f"{docs_prefix}/cpp/",
"python": f"{docs_prefix}/python/",
"typescript": f"{docs_prefix}/typescript/",
"markdown": f"{docs_prefix}/markdown/",
"common": f"{docs_prefix}/common/",
}
for md_path in agents_dir.glob("*.md"):
iterator = root.rglob("*.md") if recursive else root.glob("*.md")
patterns = [
(re.compile(rf"(?<![\w./-])docs/{section}/"), replacement)
for section, replacement in replacements.items()
]
for md_path in iterator:
if not md_path.is_file():
continue
text = md_path.read_text(encoding="utf-8")
updated = text
for old, new in replacements.items():
updated = updated.replace(old, new)
for pattern, replacement in patterns:
updated = pattern.sub(replacement, updated)
if updated != text:
md_path.write_text(updated, encoding="utf-8")
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
rewrite_docs_links_in_markdown(agents_dir, docs_prefix, recursive=False)
def rewrite_skill_docs_links(skill_dir: Path, docs_prefix: str) -> None:
rewrite_docs_links_in_markdown(skill_dir, docs_prefix, recursive=True)
def read_gitattributes_entries(path: Path) -> list[str]:
entries: list[str] = []
for line in path.read_text(encoding="utf-8").splitlines():
@@ -1047,13 +1119,7 @@ def sync_standards_action(config: dict, context: dict) -> int:
copytree(src, dst)
log(f"Synced .agents/{lang} from standards.")
docs_prefix = None
try:
rel_snapshot = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
if str(rel_snapshot) != ".":
docs_prefix = f"{rel_snapshot.as_posix()}/docs"
except ValueError:
docs_prefix = None
docs_prefix = resolve_docs_prefix(context)
if docs_prefix:
for lang in langs:
@@ -1165,6 +1231,7 @@ def install_skills_action(config: dict, context: dict) -> int:
dst.rename(backup)
log(f"Backed up existing skill: {name} -> {backup.name}")
copytree(src, dst)
rewrite_skill_docs_links(dst, resolve_docs_prefix(context))
log(f"Installed: {name}")
return 0
@@ -1246,10 +1313,17 @@ def main(argv: list[str]) -> int:
root = (config_path.parent / root).resolve()
else:
root = config_path.parent
resolved_root = root.resolve()
try:
deploy_root = resolve_configured_deploy_root(config, resolved_root)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
context = {
"project_root": root.resolve(),
"project_root": resolved_root,
"config_path": config_path.resolve(),
"config": config,
"deploy_root": deploy_root,
}
if should_sync_agents(config):