📝 docs(tsl): rebuild canonical syntax and routing manual
This commit is contained in:
+174
-100
@@ -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": "## TSL(tsl/tsf)",
|
||||
"cpp": "## C++(cpp)",
|
||||
"python": "## Python(python)",
|
||||
"typescript": "## TypeScript(typescript)",
|
||||
"markdown": "## Markdown(markdown)",
|
||||
}
|
||||
|
||||
|
||||
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"本快照为裁剪版 Playbook(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 跨语言(common)",
|
||||
"",
|
||||
"- 提交信息与版本号:`common/commit_message.md`",
|
||||
]
|
||||
for lang in langs:
|
||||
if lang == "tsl":
|
||||
lines += [
|
||||
"",
|
||||
"## TSL(tsl)",
|
||||
"",
|
||||
"- 代码风格:`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 += [
|
||||
"",
|
||||
"## Python(python)",
|
||||
"",
|
||||
"- 代码风格:`python/style_guide.md`",
|
||||
"- 工具链:`python/tooling.md`",
|
||||
"- 配置清单:`python/configuration.md`",
|
||||
]
|
||||
elif lang == "typescript":
|
||||
lines += [
|
||||
"",
|
||||
"## TypeScript(typescript)",
|
||||
"",
|
||||
"- 代码风格:`typescript/code_style.md`",
|
||||
"- 命名规范:`typescript/naming.md`",
|
||||
"- 工具链:`typescript/toolchain.md`",
|
||||
"- 配置清单:`typescript/configuration.md`",
|
||||
]
|
||||
elif lang == "markdown":
|
||||
lines += [
|
||||
"",
|
||||
"## Markdown(markdown)",
|
||||
"",
|
||||
"- 代码块与行内代码格式:`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):
|
||||
|
||||
Reference in New Issue
Block a user