diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index feaa71a8..970ba2cf 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -1,6 +1,7 @@ +--- name: 🧪 Playbook 测试套件 -on: +"on": push: branches: - main @@ -54,7 +55,8 @@ jobs: git clone "$REPO_URL" "$REPO_DIR" - if git -C "$REPO_DIR" cat-file -e "${TARGET_SHA}^{commit}" 2>/dev/null; then + if git -C "$REPO_DIR" cat-file -e \ + "${TARGET_SHA}^{commit}" 2>/dev/null; then git -C "$REPO_DIR" checkout -f "$TARGET_SHA" else if [ -n "$TARGET_REF" ]; then @@ -90,16 +92,11 @@ jobs: set -euo pipefail echo "========================================" - echo "🧪 Python CLI 测试" + echo "🧪 Python 测试" echo "========================================" cd "$REPO_DIR" - python3 -m unittest discover -s tests/cli -v - - echo "========================================" - echo "🧪 Python 扩展测试" - echo "========================================" - + echo "覆盖:CLI、subtree/snapshot 部署路线、模板同步、文档一致性" python3 -m unittest discover -s tests -p "test_*.py" -v echo "========================================" diff --git a/README.md b/README.md index 9102e386..ce03de8a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ python scripts/playbook.py -config playbook.toml ```toml [playbook] project_root = "/path/to/project" +playbook_root = "docs/standards/playbook" +install_mode = "snapshot" [sync_rules] # force = true # 可选 @@ -81,12 +83,12 @@ project_name = "MyProject" ```bash # spec 写完后 -python /scripts/playbook.py \ +python /scripts/playbook.py \ -record-spec docs/superpowers/specs/-design.md \ -progress memory-bank/progress.md # plan 写完后 -python /scripts/playbook.py \ +python /scripts/playbook.py \ -record-plan docs/superpowers/plans/.md \ -progress memory-bank/progress.md ``` @@ -199,10 +201,11 @@ TSL 相关问题直接查阅 `rulesets/tsl/index.md` 与 `docs/tsl/`。 先区分三个路径概念: - `project_root`:目标项目根目录。 -- `deploy_root`:相对于 `project_root` 的项目内目标目录。 -- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行部署脚本的位置,不是部署目标。 +- `playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录。 +- `install_mode`:`subtree` 表示 Playbook 已由 git subtree 放在项目内;`snapshot` 表示从外部 clone 安装裁剪快照。 +- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行安装脚本的位置,不是项目内 Playbook 根目录。 -以 TSL 为例,Playbook 在项目内的默认部署根是 `docs/standards/playbook`;如果你把 `deploy_root` 改成 `custom/playbook`,则部署结果会落到 `/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。 +以 TSL 为例,Playbook 在项目内的默认根是 `docs/standards/playbook`;如果你把 `playbook_root` 改成 `custom/playbook`,则快照会落到 `/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。 #### 方式一:`git subtree` @@ -214,6 +217,8 @@ git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbo cat <<'EOF' > playbook.toml [playbook] project_root = "." +playbook_root = "docs/standards/playbook" +install_mode = "subtree" [sync_standards] langs = ["tsl"] @@ -249,6 +254,8 @@ git commit -m ":package: deps(playbook): add tsl standards" # playbook.toml [playbook] project_root = "." + playbook_root = "docs/standards/playbook" + install_mode = "subtree" [sync_standards] langs = ["tsl", "cpp"] @@ -277,19 +284,17 @@ git commit -m ":package: deps(playbook): add tsl standards" git clone https://git.mytsl.cn/csh/playbook.git /opt/playbook ``` -2. 在目标项目根创建 `playbook.toml`,并用 `deploy_root` 指定项目内的部署根。例如: - +2. 在目标项目根创建 `playbook.toml`,并用 `playbook_root` 指定项目内的 Playbook 根。例如: - `project_root` 写目标项目根目录。 - - `deploy_root` 写目标项目内的相对路径。 - - 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `deploy_root`;那只是你执行脚本的位置。 + - `playbook_root` 写目标项目内的相对路径。 + - `install_mode = "snapshot"` 表示由外部 clone 安装项目内快照。 + - 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `playbook_root`;那只是你执行脚本的位置。 ```toml [playbook] project_root = "." - deploy_root = "custom/playbook" - - [vendor] - langs = ["tsl"] + playbook_root = "custom/playbook" + install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -308,10 +313,10 @@ git commit -m ":package: deps(playbook): add tsl standards" 说明: -- 这里的 `[vendor]` 是“把 Playbook 快照部署进目标项目”的执行步骤,不是第三种正式部署路线。 -- `deploy_root` 永远表示目标项目内的部署目录;它不是外部 clone 出来的 Playbook 仓库路径。 -- 外部 clone 场景下必须显式填写 `deploy_root`;脚本不会替你补默认部署目录。 -- 如果 `deploy_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。 +- `install_mode = "snapshot"` 会先把 Playbook 裁剪快照安装到目标项目的 `playbook_root`,再执行后续同步动作。 +- `playbook_root` 永远表示目标项目内的 Playbook 根;它不是外部 clone 出来的 Playbook 仓库路径。 +- 外部 clone 场景下必须显式填写 `playbook_root`;脚本不会替你补默认项目内根目录。 +- 如果 `playbook_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。 --- @@ -347,7 +352,7 @@ git commit -m ":package: deps(playbook): add tsl standards" ├── .gitattributes # 行尾/文本规范 ├── AGENTS.md # Codex 入口(由 playbook 自动生成/更新) ├── CLAUDE.md # Claude Code 入口(自动注入 @AGENTS.md) -├── / # 本 Playbook 在项目内的部署根(默认 docs/standards/playbook) +├── / # 本 Playbook 在项目内的根(默认 docs/standards/playbook) │ ├── docs/ │ ├── rulesets/ │ ├── scripts/ diff --git a/SKILLS.md b/SKILLS.md index 468dcdaa..8a6c3e01 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -110,10 +110,10 @@ no_backup = true 如果你的项目已经把本 Playbook 部署到项目内,则在目标项目里执行: ```bash -python /scripts/playbook.py -config playbook.toml +python /scripts/playbook.py -config playbook.toml ``` -其中 `` 默认为 `docs/standards/playbook`。 +其中 `` 默认为 `docs/standards/playbook`。 --- @@ -158,7 +158,7 @@ python /scripts/playbook.py -config playbook.toml - Python:`docs/python/style_guide.md`、`docs/python/tooling.md`、`docs/python/configuration.md` 若你的项目把本 Playbook 部署到项目内,文档根路径为 -`/docs/...`;其中 `` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。 +`/docs/...`;其中 `` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。 --- diff --git a/playbook.toml.example b/playbook.toml.example index 27e597cc..f99660de 100644 --- a/playbook.toml.example +++ b/playbook.toml.example @@ -2,14 +2,10 @@ # 配置文件所在目录默认作为 project_root。 [playbook] -# project_root = "." # 可选:目标项目根目录 -# deploy_root = "docs/standards/playbook" # 项目内已部署时可省略;从外部 clone 执行时必填;值始终是相对于 project_root 的项目内路径,不是外部 clone 的 playbook 路径 -# claude_md = "CLAUDE.md" # 可选:CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建) - -[vendor] -# 当从外部 clone 的 playbook 向项目内部署快照时启用 -# 外部 clone 路径只用于执行脚本;快照仍会写入 / -# langs = ["tsl"] # 可选:默认仅 tsl +# project_root = "." # 可选:目标项目根目录 +# playbook_root = "docs/standards/playbook" # 项目内 Playbook 根;相对于 project_root;不是外部 clone 的 playbook 路径 +# install_mode = "subtree" # subtree|snapshot;subtree 表示已在项目内,snapshot 表示从外部 clone 安装快照 +# claude_md = "CLAUDE.md" # 可选:CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建) [sync_rules] # 同步 AGENT_RULES.md(配置节存在即启用) diff --git a/scripts/playbook.py b/scripts/playbook.py index 08eb25e9..8eb29507 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -14,7 +14,6 @@ except ModuleNotFoundError: # Python < 3.11 tomllib = None ORDER = [ - "vendor", "sync_rules", "sync_memory_bank", "sync_prompts", @@ -29,7 +28,14 @@ MAIN_LOOP_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", MA assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC) MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP) -PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home", "skill_link"} +PATH_CONFIG_KEYS = { + "project_root", + "playbook_root", + "deploy_root", + "agents_home", + "codex_home", + "skill_link", +} DOCS_INDEX_SECTION_HEADINGS = { "common": "## 跨语言(common)", "tsl": "## TSL(tsl/tsf)", @@ -275,13 +281,13 @@ def normalize_relative_dir(raw: object, label: str) -> str: return "." if normalized == "" else normalized -def join_deploy_subpath(root: str, child: str) -> str: +def join_playbook_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: +def resolve_in_project_playbook_root(project_root: Path) -> str | None: try: rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve()) if str(rel) != ".": @@ -291,9 +297,8 @@ def resolve_in_project_deploy_root(project_root: Path) -> str | None: return None -def config_requires_deploy_root(config: dict) -> bool: +def config_uses_playbook_root(config: dict) -> bool: for key in ( - "vendor", "sync_rules", "sync_memory_bank", "sync_prompts", @@ -305,46 +310,74 @@ def config_requires_deploy_root(config: dict) -> bool: return False -def resolve_configured_deploy_root(config: dict, project_root: Path) -> str: +def validate_removed_config(config: dict) -> None: + if "vendor" in config: + raise ValueError( + "[vendor] is no longer supported; use [playbook].install_mode = " + '"snapshot" and configure languages in [sync_standards]' + ) + + playbook_config = config.get("playbook", {}) + if not isinstance(playbook_config, dict): + raise ValueError("[playbook] must be a table") + + if "deploy_root" in playbook_config: + raise ValueError( + "playbook.deploy_root is no longer supported; use playbook.playbook_root" + ) + if "intall_mode" in playbook_config: + raise ValueError( + "playbook.intall_mode is not supported; use playbook.install_mode" + ) + + +def resolve_install_mode(config: dict) -> str: + playbook_config = config.get("playbook", {}) + raw = "subtree" + if ( + isinstance(playbook_config, dict) + and playbook_config.get("install_mode") is not None + ): + raw = playbook_config.get("install_mode") + mode = str(raw).strip().lower() + if mode not in ("subtree", "snapshot"): + raise ValueError( + "playbook.install_mode must be one of: subtree, snapshot" + ) + return mode + + +def resolve_configured_playbook_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" - ) + raw = playbook_config.get("playbook_root") if raw is not None and str(raw).strip(): - return normalize_relative_dir(raw, "deploy_root") + return normalize_relative_dir(raw, "playbook_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 + in_project_playbook_root = resolve_in_project_playbook_root(project_root) + if in_project_playbook_root is not None: + return in_project_playbook_root - if config_requires_deploy_root(config): + if resolve_install_mode(config) == "snapshot" or config_uses_playbook_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" + "playbook.playbook_root is required when running from an external clone; " + "set it to the target project's relative Playbook 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_playbook_root(context: dict) -> str: + return context["playbook_root"] def resolve_docs_prefix(context: dict) -> str: - return join_deploy_subpath(resolve_deploy_root(context), "docs") + return join_playbook_subpath(resolve_playbook_root(context), "docs") def resolve_playbook_scripts(context: dict) -> str: - return join_deploy_subpath(resolve_deploy_root(context), "scripts") + return join_playbook_subpath(resolve_playbook_root(context), "scripts") def read_git_commit(root: Path) -> str: @@ -408,9 +441,11 @@ def write_docs_index(dest_prefix: Path, langs: list[str]) -> None: docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") -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") +def write_snapshot_readme( + dest_prefix: Path, playbook_root: str, langs: list[str] +) -> None: + scripts_path = join_playbook_subpath(playbook_root, "scripts/playbook.py") + docs_index_path = join_playbook_subpath(playbook_root, "docs/index.md") lines = [ "# Playbook(裁剪快照)", "", @@ -424,7 +459,7 @@ def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str]) f"python {scripts_path} -config playbook.toml", "```", "", - f"配置示例:`{join_deploy_subpath(deploy_root, 'playbook.toml.example')}`", + f"配置示例:`{join_playbook_subpath(playbook_root, 'playbook.toml.example')}`", "", "文档入口:", "", @@ -453,23 +488,51 @@ def write_source_file(dest_prefix: Path, langs: list[str]) -> None: ) -def vendor_action(config: dict, context: dict) -> int: +def snapshot_langs(config: dict) -> list[str]: + sync_standards = config.get("sync_standards") + if isinstance(sync_standards, dict): + return normalize_langs(sync_standards.get("langs")) + return normalize_langs(None) + + +def is_generated_snapshot(path: Path) -> bool: + source = path / "SOURCE.md" + if not source.is_file(): + return False try: - langs = normalize_langs(config.get("langs")) + return "Generated-by: scripts/playbook.py" in source.read_text(encoding="utf-8") + except OSError: + return False + + +def install_snapshot(config: dict, context: dict) -> int: + try: + langs = snapshot_langs(config) except ValueError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 - deploy_root = context["deploy_root"] - target_path = Path(deploy_root) - + playbook_root = context["playbook_root"] + target_path = Path(playbook_root) project_root: Path = context["project_root"] dest_prefix = project_root / target_path dest_standards = dest_prefix.parent + if dest_prefix.resolve() == PLAYBOOK_ROOT.resolve(): + log(f"Snapshot already installed at {dest_prefix}") + return 0 + ensure_dir(dest_standards) if dest_prefix.exists(): + if not is_generated_snapshot(dest_prefix): + print( + "ERROR: refusing to replace existing playbook_root because it is " + "not a generated snapshot; use install_mode = \"subtree\" for a " + "git subtree-managed Playbook", + file=sys.stderr, + ) + return 2 timestamp = datetime.now().strftime("%Y%m%d%H%M%S") backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}" dest_prefix.rename(backup) @@ -535,13 +598,29 @@ 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, deploy_root, langs) + write_snapshot_readme(dest_prefix, playbook_root, langs) write_source_file(dest_prefix, langs) - log(f"Vendored snapshot -> {dest_prefix}") + log(f"Installed snapshot -> {dest_prefix}") return 0 +def validate_subtree_install(context: dict) -> int: + project_root: Path = context["project_root"] + playbook_root = context["playbook_root"] + expected_root = (project_root / playbook_root).resolve() + if PLAYBOOK_ROOT.resolve() == expected_root: + return 0 + + print( + "ERROR: install_mode = \"subtree\" requires running the project-local " + f"Playbook script at " + f"{join_playbook_subpath(playbook_root, 'scripts/playbook.py')}", + file=sys.stderr, + ) + return 2 + + def replace_placeholders( text: str, project_name: str | None, @@ -1413,8 +1492,6 @@ def format_md_action(config: dict, context: dict) -> int: def run_action(name: str, config: dict, context: dict) -> int: print(f"[action] {name}") - if name == "vendor": - return vendor_action(config, context) if name == "sync_rules": return sync_rules_action(config, context) if name == "sync_memory_bank": @@ -1489,6 +1566,13 @@ def main(argv: list[str]) -> int: return 2 config = load_config(config_path) + try: + validate_removed_config(config) + install_mode = resolve_install_mode(config) + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + playbook_config = config.get("playbook", {}) project_root = playbook_config.get("project_root") if project_root: @@ -1499,7 +1583,7 @@ def main(argv: list[str]) -> int: root = config_path.parent resolved_root = root.resolve() try: - deploy_root = resolve_configured_deploy_root(config, resolved_root) + playbook_root = resolve_configured_playbook_root(config, resolved_root) except ValueError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 @@ -1507,9 +1591,19 @@ def main(argv: list[str]) -> int: "project_root": resolved_root, "config_path": config_path.resolve(), "config": config, - "deploy_root": deploy_root, + "install_mode": install_mode, + "playbook_root": playbook_root, } + if install_mode == "snapshot": + result = install_snapshot(config, context) + if result != 0: + return result + elif config_uses_playbook_root(config): + result = validate_subtree_install(context) + if result != 0: + return result + if should_sync_agents(config): result = sync_agents_template(context) if result != 0: diff --git a/templates/README.md b/templates/README.md index e56e8c4f..beceb536 100644 --- a/templates/README.md +++ b/templates/README.md @@ -82,6 +82,8 @@ templates/ # playbook.toml [playbook] project_root = "/path/to/project" +playbook_root = "docs/standards/playbook" +install_mode = "snapshot" # 同步 AGENT_RULES.md(配置节存在即启用) [sync_rules] @@ -98,15 +100,15 @@ project_name = "MyProject" ``` ```bash -python /scripts/playbook.py -config playbook.toml +python /scripts/playbook.py -config playbook.toml ``` 参数说明见 `playbook.toml.example`(仓库根目录)或项目内的 -`/playbook.toml.example`。 +`/playbook.toml.example`。 -其中 `` 默认为 `docs/standards/playbook`, +其中 `` 默认为 `docs/standards/playbook`, 也可以按项目配置改成 `custom/playbook` 等自定义目录; -对应文档入口会变成 `/docs/...`。 +对应文档入口会变成 `/docs/...`。 如果你当前是在 **外部 clone 的 playbook 仓库** 中执行,而不是在目标项目内执行快照,请使用: @@ -114,8 +116,8 @@ python /scripts/playbook.py -config playbook.toml python scripts/playbook.py -config playbook.toml ``` -此时 `[vendor]` 会把快照写入 `/`; -后续再在目标项目内使用 `/scripts/playbook.py` +此时 `install_mode = "snapshot"` 会把快照写入 `/`; +后续再在目标项目内使用 `/scripts/playbook.py` 做同步更新。 ### 配置节说明 @@ -296,8 +298,7 @@ project/ ### ci/、cpp/、python/ -语言和 CI 配置模板。通过 playbook.py 的 `[vendor]` -复制到快照中: +语言和 CI 配置模板。`install_mode = "snapshot"` 安装快照时会复制这些模板: - `ci/gitea/`:Gitea Actions 工作流与辅助脚本。 部署到快照 `templates/ci/` @@ -307,7 +308,7 @@ project/ - `python/`:`pyproject.toml`、`.editorconfig` 等文件。 部署到快照 `templates/python/` -> 注意:这些模板通过 `[vendor]` 复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。 +> 注意:这些模板会复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。 > 其中 `ci/gitea/` 应按 `templates/ci/README.md` 的说明,整块复制 `.gitea/` 目录,而不只是复制 workflows。 **使用方式**: @@ -316,14 +317,16 @@ project/ # playbook.toml - 生成包含这些模板的快照 [playbook] project_root = "/path/to/project" +playbook_root = "docs/standards/playbook" +install_mode = "snapshot" -[vendor] +[sync_standards] langs = ["tsl", "cpp", "python"] ``` ```bash python scripts/playbook.py -config playbook.toml -# 然后手动从 /templates/ 复制所需配置到项目根目录 +# 然后手动从 /templates/ 复制所需配置到项目根目录 ``` ## 与 playbook 其他部分的关系 @@ -335,14 +338,14 @@ playbook/ ├── docs/ # 权威静态文档 ├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等 └── scripts/ - └── playbook.py # 统一入口:vendor / sync_* + └── playbook.py # 统一入口:snapshot install / sync_* ``` ## 完整部署流程 ```bash # 1. 准备配置并执行统一入口 -python /scripts/playbook.py -config playbook.toml +python /scripts/playbook.py -config playbook.toml # 2. 编辑 memory-bank/*.md 填写项目信息 diff --git a/tests/README.md b/tests/README.md index 312f63f5..8e76a776 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,7 +16,7 @@ tests/ ├── test_no_backup_flags.py # no_backup 行为测试 ├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试 ├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试 -├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试 +├── test_vendor_snapshot_templates.py # snapshot 快照模板完整性测试 ├── test_main_loop_cli.py # main_loop CLI 测试 ├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试 ├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards) @@ -70,7 +70,7 @@ sh tests/integration/check_doc_links.sh - CLI 参数解析与帮助信息 - TOML 配置解析与动作顺序 -- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地 +- snapshot install/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地 ### 2. 模板验证测试 (templates/) diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index 92d69bfd..1c570fc7 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -125,7 +125,8 @@ class PlaybookCliTests(unittest.TestCase): config_body = f""" [playbook] project_root = "." -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [format_md] @@ -142,7 +143,7 @@ langs = ["tsl"] self.assertIn("sync_standards", output) self.assertIn("format_md", output) - def test_format_md_only_does_not_require_deploy_root(self): + def test_format_md_only_does_not_require_playbook_root(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] @@ -157,15 +158,55 @@ project_root = "{tmp_dir}" self.assertEqual(result.returncode, 0) - def test_vendor_creates_snapshot(self): + def test_vendor_section_is_rejected(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" + +[vendor] +langs = ["tsl"] +""" + config_path = write_config(root, "playbook.toml", config_body) + + result = run_cli("-config", str(config_path)) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("[vendor]", result.stdout + result.stderr) + + def test_deploy_root_is_rejected(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" -[vendor] +[sync_standards] +langs = ["tsl"] +""" + config_path = write_config(root, "playbook.toml", config_body) + + result = run_cli("-config", str(config_path)) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("deploy_root", result.stdout + result.stderr) + self.assertIn("playbook_root", result.stdout + result.stderr) + + def test_snapshot_install_creates_snapshot(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" + +[sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) @@ -176,15 +217,16 @@ langs = ["tsl"] self.assertEqual(result.returncode, 0) self.assertTrue(snapshot.is_file()) - def test_vendor_docs_index_uses_new_tsl_entrypoints(self): + def test_snapshot_docs_index_uses_new_tsl_entrypoints(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" -[vendor] +[sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) @@ -201,14 +243,15 @@ langs = ["tsl"] self.assertIn("`tsl/reference/index.md`", text) self.assertNotIn("`tsl/syntax_book/index.md`", text) - def test_external_clone_requires_explicit_deploy_root(self): + def test_external_clone_requires_explicit_playbook_root(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" +install_mode = "snapshot" -[vendor] +[sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) @@ -216,14 +259,34 @@ langs = ["tsl"] result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) - self.assertIn("deploy_root", result.stdout + result.stderr) + self.assertIn("playbook_root", result.stdout + result.stderr) + + def test_subtree_mode_requires_project_local_script(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "subtree" + +[sync_standards] +langs = ["tsl"] +""" + config_path = write_config(root, "playbook.toml", config_body) + + result = run_cli("-config", str(config_path)) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("project-local Playbook script", result.stdout + result.stderr) def test_sync_memory_bank_creates_memory_bank(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -242,7 +305,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -265,7 +329,8 @@ langs = ["tsl"] f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -282,7 +347,8 @@ no_backup = true f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl", "cpp"] @@ -304,7 +370,8 @@ no_backup = true config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl", "markdown"] @@ -326,7 +393,8 @@ langs = ["tsl", "markdown"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -352,7 +420,8 @@ langs = ["tsl"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{target}" @@ -453,7 +522,8 @@ skills = ["brainstorming"] config_body = f""" [playbook] project_root = "{tmp_root}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{target}" @@ -475,7 +545,8 @@ skills = ["karpathy-guidelines"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{target}" @@ -496,7 +567,8 @@ skills = ["tsl-guide"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] codex_home = "{target}" @@ -511,17 +583,15 @@ skills = ["brainstorming"] self.assertNotEqual(result.returncode, 0) self.assertIn("codex_home", result.stdout + result.stderr) - def test_external_clone_flow_rewrites_links_with_configured_deploy_root(self): + def test_external_clone_flow_rewrites_links_with_configured_playbook_root(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) agents_home = root / "agents-home" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" - -[vendor] -langs = ["tsl"] +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -545,23 +615,28 @@ skills = ["style-cleanup"] def test_deployed_snapshot_rewrites_links_from_snapshot_location(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) - vendor_config = write_config( + install_config = write_config( root, - "vendor.toml", + "install.toml", f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" -[vendor] +[sync_standards] langs = ["tsl"] """, ) - vendor_result = run_cli("-config", str(vendor_config)) - self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr) + install_result = run_cli("-config", str(install_config)) + self.assertEqual( + install_result.returncode, + 0, + msg=install_result.stdout + install_result.stderr, + ) - vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py" + snapshot_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py" agents_home = root / "local-agents" sync_config = write_config( root, @@ -569,7 +644,8 @@ langs = ["tsl"] f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -582,7 +658,7 @@ skills = ["style-cleanup"] """, ) - sync_result = run_script(vendored_script, "-config", str(sync_config)) + sync_result = run_script(snapshot_script, "-config", str(sync_config)) self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr) self.assert_style_cleanup_tsl_docs_prefix( root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" @@ -593,7 +669,8 @@ skills = ["style-cleanup"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -619,7 +696,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -650,7 +728,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -682,7 +761,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -708,7 +788,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -730,7 +811,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" @@ -759,7 +841,8 @@ skills = ["commit-message"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" @@ -784,7 +867,8 @@ no_backup = true config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{CUSTOM_DEPLOY_ROOT}" +playbook_root = "{CUSTOM_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" diff --git a/tests/test_deployment_routes_e2e.py b/tests/test_deployment_routes_e2e.py index 2738c98a..989d62f9 100644 --- a/tests/test_deployment_routes_e2e.py +++ b/tests/test_deployment_routes_e2e.py @@ -85,6 +85,8 @@ class DeploymentRoutesE2ETests(unittest.TestCase): """ [playbook] project_root = "." +playbook_root = "docs/standards/playbook" +install_mode = "subtree" [sync_rules] no_backup = true @@ -119,7 +121,7 @@ no_backup = true self.assertIn("@AGENT_RULES.md", text) self.assertIn("", text) - def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self): + def test_external_clone_deployment_installs_snapshot_and_updates_claude(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_root = Path(tmp_dir) external_clone = tmp_root / "playbook" @@ -137,10 +139,8 @@ no_backup = true f""" [playbook] project_root = "." -deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}" - -[vendor] -langs = ["tsl", "markdown"] +playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}" +install_mode = "snapshot" [sync_rules] no_backup = true diff --git a/tests/test_gitattributes_modes.py b/tests/test_gitattributes_modes.py index 3e92131c..8c8421ad 100644 --- a/tests/test_gitattributes_modes.py +++ b/tests/test_gitattributes_modes.py @@ -34,7 +34,8 @@ class GitattributesModeTests(unittest.TestCase): config_body = f""" [playbook] project_root = \"{root}\" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = [\"tsl\"] diff --git a/tests/test_no_backup_flags.py b/tests/test_no_backup_flags.py index 6c246289..3608b900 100644 --- a/tests/test_no_backup_flags.py +++ b/tests/test_no_backup_flags.py @@ -27,7 +27,8 @@ class NoBackupFlagsTests(unittest.TestCase): config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_rules] force = true @@ -56,7 +57,8 @@ no_backup = true config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["tsl"] @@ -85,7 +87,8 @@ no_backup = true config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [install_skills] agents_home = "{root / 'agents'}" diff --git a/tests/test_sync_directory_actions.py b/tests/test_sync_directory_actions.py index af7aa947..2e07f4fc 100644 --- a/tests/test_sync_directory_actions.py +++ b/tests/test_sync_directory_actions.py @@ -29,7 +29,8 @@ class SyncDirectoryActionsTests(unittest.TestCase): config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" @@ -54,7 +55,8 @@ project_name = "Demo" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_prompts] """ @@ -82,7 +84,8 @@ deploy_root = "{DEFAULT_DEPLOY_ROOT}" config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" diff --git a/tests/test_sync_templates_placeholders.py b/tests/test_sync_templates_placeholders.py index 7ec8cb3b..04a478e8 100644 --- a/tests/test_sync_templates_placeholders.py +++ b/tests/test_sync_templates_placeholders.py @@ -222,7 +222,8 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): config_body = f""" [playbook] project_root = \"{tmp_dir}\" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_rules] @@ -267,31 +268,16 @@ langs = [\"cpp\", \"tsl\"] self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text) self.assertFalse(rules_text.endswith("\n\n")) - def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self): + def test_sync_standards_rewrites_typescript_docs_prefix_for_snapshot_playbook(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) - vendor_config = root / "vendor.toml" - vendor_config.write_text( + install_config = root / "install.toml" + install_config.write_text( f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" - -[vendor] -langs = ["typescript"] -""", - encoding="utf-8", - ) - - vendor_result = run_cli("-config", str(vendor_config)) - self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stderr) - - sync_config = root / "sync.toml" - sync_config.write_text( - f""" -[playbook] -project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_standards] langs = ["typescript"] @@ -299,11 +285,28 @@ langs = ["typescript"] encoding="utf-8", ) - vendored_script = ( + install_result = run_cli("-config", str(install_config)) + self.assertEqual(install_result.returncode, 0, msg=install_result.stderr) + + sync_config = root / "sync.toml" + sync_config.write_text( + f""" +[playbook] +project_root = "{tmp_dir}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" + +[sync_standards] +langs = ["typescript"] +""", + encoding="utf-8", + ) + + snapshot_script = ( root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py" ) sync_result = run_script( - vendored_script, "-config", str(sync_config), cwd=root + snapshot_script, "-config", str(sync_config), cwd=root ) self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr) @@ -317,7 +320,8 @@ langs = ["typescript"] config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" [sync_rules] diff --git a/tests/test_tsl_entrypoints_consistency.py b/tests/test_tsl_entrypoints_consistency.py index abcdc3cf..bd4303ab 100644 --- a/tests/test_tsl_entrypoints_consistency.py +++ b/tests/test_tsl_entrypoints_consistency.py @@ -66,19 +66,20 @@ class TslEntrypointsConsistencyTests(unittest.TestCase): self.assertIn("方式一:git subtree", text) self.assertIn("方式二:外部 clone 后执行部署", text) self.assertIn("`project_root`:目标项目根目录", text) - self.assertIn("`deploy_root`:相对于 `project_root` 的项目内目标目录", text) + self.assertIn("`playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录", text) self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text) - self.assertIn("外部 clone 场景下必须显式填写 `deploy_root`", text) + self.assertIn("外部 clone 场景下必须显式填写 `playbook_root`", text) self.assertNotIn("方式二:手动复制快照", text) self.assertNotIn("方式三:CLI 裁剪复制", text) - self.assertNotIn("如果省略 `deploy_root`,默认仍部署到 `docs/standards/playbook`", text) + self.assertNotIn("如果省略 `playbook_root`,默认仍部署到 `docs/standards/playbook`", text) - def test_playbook_example_defines_deploy_root_as_target_path(self): + def test_playbook_example_defines_playbook_root_as_target_path(self): text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8") - self.assertIn('deploy_root = "docs/standards/playbook"', text) + self.assertIn('playbook_root = "docs/standards/playbook"', text) + self.assertIn('install_mode = "subtree"', text) self.assertIn("相对于 project_root", text) self.assertIn("不是外部 clone 的 playbook 路径", text) - self.assertIn("从外部 clone 执行时必填", text) + self.assertIn("snapshot 表示从外部 clone 安装快照", text) self.assertNotIn("target_dir", text) def test_deployment_docs_do_not_reference_legacy_terms(self): diff --git a/tests/test_vendor_snapshot_templates.py b/tests/test_vendor_snapshot_templates.py index 8e84a6bc..1bd259ff 100644 --- a/tests/test_vendor_snapshot_templates.py +++ b/tests/test_vendor_snapshot_templates.py @@ -17,15 +17,16 @@ def run_cli(*args): ) -class VendorSnapshotTemplatesTests(unittest.TestCase): - def test_vendor_includes_core_templates(self): +class SnapshotTemplatesTests(unittest.TestCase): + def test_snapshot_install_includes_core_templates(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" -deploy_root = "{DEFAULT_DEPLOY_ROOT}" +playbook_root = "{DEFAULT_DEPLOY_ROOT}" +install_mode = "snapshot" -[vendor] +[sync_standards] langs = ["tsl"] """ config_path = Path(tmp_dir) / "playbook.toml"