From ab742bc44d8dff0c6b268ed21f0ab1d17d4119e3 Mon Sep 17 00:00:00 2001 From: wlvh <30534800+wlvh@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:30:30 +0800 Subject: [PATCH 1/4] Simplify workflow docs sync risk coverage --- AGENTS.md | 10 - en/.github/pull_request_template.md | 9 +- en/AGENTS.md | 8 +- en/PR_Checklist.md | 14 +- en/README.md | 3 + en/SOP.md | 4 +- en/TESTING.md | 6 +- en/docs/development_workflow/README.md | 14 +- tests/test_workflow_docs_sync.py | 437 ++++++++++-------- zh/.github/pull_request_template.md | 8 +- zh/AGENTS.md | 7 +- zh/PR_Checklist.md | 12 +- zh/README.md | 3 +- zh/SOP.md | 4 +- zh/TESTING.md | 5 +- zh/docs/development_workflow/README.md | 7 +- zh/docs/development_workflow/decisions.md | 53 ++- zh/scripts/install_skills.py | 95 +++- zh/skills/workflow-docs-sync/SKILL.md | 12 +- zh/skills/workflow-docs-sync/evals/README.md | 4 +- .../workflow-docs-sync/scripts/sync_docs.py | 69 ++- 21 files changed, 498 insertions(+), 286 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1d7fcf..5cd50c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,16 +11,6 @@ `zh/docs/development_workflow/`,实现决策在其 `decisions.md`。 - 修改测试:进入 `tests/test_workflow_docs_sync.py`;具体约束以测试代码为准,不在此复制。 -新增 marker、alias、机器状态、parser、兼容入口或其他控制机制前,必须先证明它表达独立状态, -并存在真实消费者与可复现失败路径。若扩展现有机制可以覆盖: - -- 先登记 finding; -- 说明最小扩展边界与新增成本; -- 不得改用同义 alias; -- 若与用户指定实现不同,先说明替代方案。 - -缺少真实缺口或消费者证据时保持零 diff,不因用户给出了具体实现方案就机械执行。 - 最短验证入口: ```bash diff --git a/en/.github/pull_request_template.md b/en/.github/pull_request_template.md index 81607f2..9df2ad2 100644 --- a/en/.github/pull_request_template.md +++ b/en/.github/pull_request_template.md @@ -18,7 +18,7 @@ plans, historical drafts, or local uncommitted content. ## 4. Documentation Impact - + ## 5. User-visible and Architecture Impact @@ -34,7 +34,7 @@ plans, historical drafts, or local uncommitted content. ## 7. Review / Fix Record - + ## 8. Known Limits, Open Decisions, and Rollback @@ -45,6 +45,5 @@ plans, historical drafts, or local uncommitted content. - [ ] Actual Change Scope matches the real diff. - [ ] Test commands, scope, results, and not-run reasons are accurate. - [ ] User-visible and architecture impact were checked against their authoritative documents. -- [ ] BLOCKERs and actionable WARNs are closed; open decisions are not presented as complete. -- [ ] The PR body contains no historical draft, unimplemented plan, wrong base/head, or repository-local - temporary body path. +- [ ] Actionable review feedback required by this project is handled or explicitly recorded as open. +- [ ] The PR body contains no historical draft, unimplemented plan, or wrong base/head. diff --git a/en/AGENTS.md b/en/AGENTS.md index 2b91472..1acb166 100644 --- a/en/AGENTS.md +++ b/en/AGENTS.md @@ -43,8 +43,8 @@ Describe stable modules, entrypoints, and responsibilities. Do not permanently m and the business guide. For user-visible behavior changes, update or confirm `interact.md` first. - Tests remain factual evidence; keep exact commands, fixtures, layers, and isolation details in `TESTING.md` only. -- Not every change requires every document to change. Give a current, evidence-based no-update reason for - each affected candidate document left unchanged. +- Not every change requires every document to change. For an affected authority left unchanged, state the + current factual basis in the delivery record. - Derive encoding, lint, formatter, build, and type rules from real repository configuration, not this template. @@ -78,8 +78,8 @@ verified before execution. ## SOP -Read the corresponding `SOP.md` entry for a standard process. Keep execution checklists in the current -session; do not create repository run state, receipts, or temporary process documents. +Read the corresponding `SOP.md` entry for a standard process. Follow this project's actual audit, +recoverability, and delivery policy for whether execution records are stored, where, and for how long. ## PR Delivery diff --git a/en/PR_Checklist.md b/en/PR_Checklist.md index 03cc2c5..555dc4a 100644 --- a/en/PR_Checklist.md +++ b/en/PR_Checklist.md @@ -31,8 +31,8 @@ explicitly requests it. ## Documentation and Contracts - [ ] Check `AGENTS.md`, `architecture.md`, `capability_contract.json`, `interact.md`, the business guide, - `TESTING.md`, and `SOP.md` according to actual impact. Give a real no-update reason for affected candidates - left unchanged; do not edit every document merely for completeness. + `TESTING.md`, and `SOP.md` according to actual impact. State the current factual basis for affected + authorities left unchanged; do not edit every document merely for completeness. - [ ] Keep the authority direction `capability_contract.json → interact.md → business_user_guide.md` for capability changes. User-visible claims have current implementation or test evidence and stable anchors. - [ ] Reference Markdown anchors through the capability contract's contract-defined protocol without copying @@ -45,14 +45,8 @@ explicitly requests it. - [ ] Complete the review gate required by this project's test and delivery policy, and accurately record reviewer identity, scope, and limitations. -- [ ] Fix every BLOCKER and actionable WARN that does not require a new product decision. Keep remaining - issues in open decisions with evidence and impact. -- [ ] Preserve material rounds, REOPENED findings, and SUPERSEDED candidates or evidence. Reopening keeps the - original finding ID, and finding state is not confused with evidence state. -- [ ] Support a prior miss explanation with evidence or label it `hypothesis` / `unknown`; never invent cause - merely to fill a field. -- [ ] Evaluate every material finding for promotion to a long-term rule or automated gate, and record why when - it is not promoted. +- [ ] Handle actionable review feedback according to this project's policy. Keep recheck evidence for resolved + items and record the impact and required decision for anything left open. - [ ] Rerun affected tests and mechanical checks after fixes, then recheck the final diff and Git state. ## PR Delivery diff --git a/en/README.md b/en/README.md index 794e1d6..83172ba 100644 --- a/en/README.md +++ b/en/README.md @@ -28,6 +28,9 @@ Review prefers a fresh-context, blind-first independent reviewer. When cognitive the result is reported honestly as self-review. The deterministic checker proves final repository state only, not investigation, test, or review history. +The installer copies only the canonical Skill. Before any target mutation it rejects symlinks, invalid +frontmatter, and ignored source residue that would otherwise be copied; it stores no source receipt. + ## Template Contract Markdown project-fill slots use ``; JSON uses strings prefixed with diff --git a/en/SOP.md b/en/SOP.md index e8fb130..fbea4d9 100644 --- a/en/SOP.md +++ b/en/SOP.md @@ -4,8 +4,8 @@ `SOP.md` keeps stable process entrypoints and does not copy volatile commands, test lists, or publishing details. When sources conflict, current code, configuration, tests, contracts, and focused authorities such -as `TESTING.md` and `PR_Checklist.md` take precedence. Keep execution checklists in the current session; -do not write them as repository run state, receipts, or temporary SOPs. +as `TESTING.md` and `PR_Checklist.md` take precedence. Follow this project's actual audit, recoverability, +and delivery policy for how execution records are stored and retained. ## Available SOPs diff --git a/en/TESTING.md b/en/TESTING.md index 25495f0..a428267 100644 --- a/en/TESTING.md +++ b/en/TESTING.md @@ -45,9 +45,9 @@ maintenance value. ## 3. Capability Contract Alignment -An alignment test belongs to the target project's test suite, not the documentation sync checker. It -should recursively collect stable `anchor_id` values from every object in `capability_contract.json` and -use the protocol defined by the contract rules to check uniqueness and Markdown references without +A project's alignment test should recursively collect stable `anchor_id` values from every object in +`capability_contract.json`. It should use the protocol defined by the contract rules to check uniqueness +and Markdown references without hardcoding buckets, JSON paths, array positions, or requiring every contract entry to appear in the business guide. diff --git a/en/docs/development_workflow/README.md b/en/docs/development_workflow/README.md index 6c321f7..7de4bc7 100644 --- a/en/docs/development_workflow/README.md +++ b/en/docs/development_workflow/README.md @@ -68,7 +68,14 @@ external shallow clone, then pins target HEAD and upstream SHA for the entire ru source and validates final HEAD, dirty scope, no index/worktree split on editable paths, nine regular UTF-8 nonempty files, a JSON object, active markers, and an existing `.gitignore` as UTF-8. Final-byte whitespace runs with fixed Git rules from a temporary non-repository directory, independent of target attributes. It - does not parse Markdown or validate capability truth, test levels, prose quality, or execution history. + does not parse Markdown or validate capability truth, test levels, prose quality, or execution history. The + single final-byte path depends on rejecting every dirty path outside the allowlist; relaxing that allowlist + requires reevaluating whitespace coverage. Split detection aggregates both status sides by path and covers + an index deletion or rename source followed by an untracked or ignored recreation at that path. It removes + two publication candidates rather than running a second whitespace check. +- Before any removal or copy, the installer validates source and destination ancestors, source symlinks, + Claude frontmatter, and ignored source residue that would be copied. It only removes the exact obsolete + reviewer Skill and stores no install state or source receipt. - Keep temporary PR body Markdown outside the repository. Commit, push, and draft-PR creation occur through general GitHub publishing capability only after successful checks and only when the user asks. @@ -86,8 +93,9 @@ not promise exhaustive alias detection, and structural references do not prove c does not raise `schema_version` from `0.1.0`. TESTING defines minimum evidence for escaped bugs, public contracts, no-test diffs, refactors, and documentation-only gates, while the checklist enforces the add-or-not-add test decision. -Findings keep stable IDs and first-seen evidence; REOPENED is an event, CURRENT / SUPERSEDED applies only to -candidates or evidence, and prior misses are evidence-backed, labeled hypothesis, or recorded as unknown. +Stable IDs, first-seen evidence, REOPENED events, and candidate/evidence states belong to this repository's +maintenance and canonical Skill/eval evidence contract. Downstream templates defer review records, +actionable feedback, rechecks, and open decisions to target-project policy instead of fixing this vocabulary. With the same target base, upstream candidate, language, and round-one document bytes, Case A round two passes only as `PASS_NOOP`; an added valid correction is `ROUND1_INCOMPLETE`, and unsupported prose drift is diff --git a/tests/test_workflow_docs_sync.py b/tests/test_workflow_docs_sync.py index 4782837..5e5d86a 100644 --- a/tests/test_workflow_docs_sync.py +++ b/tests/test_workflow_docs_sync.py @@ -452,6 +452,19 @@ def run_repo_install( return result, parse_single_json(result=result) +def assert_user_install_failure_unchanged( + *, upstream: Path, home: Path, expected_error: str +) -> dict[str, Any]: + """断言 user-scope 预检失败且安装根 bytes 完全不变。""" + before = file_tree(root=home) + result, payload = run_user_install(upstream=upstream, home=home) + assert result.returncode != 0 + assert payload["status"] == "failed" + assert expected_error in payload["error"] + assert file_tree(root=home) == before + return payload + + def assert_installed(*, install_root: Path) -> None: """验证双平台安装、Claude 显式调用边界与精确清理结果。""" for platform, platform_root in PLATFORM_ROOTS: @@ -701,6 +714,17 @@ def test_scenario_2_prepare_safe_failures(tmp_path: Path) -> None: expected_core_paths=set(), ) + # 上游子目录同样不能冒充 Git 根;目标仍须保持完全未写入。 + upstream_nested = upstream / "nested" + upstream_nested.mkdir() + upstream_non_root_target = init_repo(path=tmp_path / "upstream-non-root-target") + assert_prepare_failure( + target=upstream_non_root_target, + upstream=upstream_nested, + expected_error="上游目录必须是 Git 根目录", + expected_core_paths=set(), + ) + # allowlist 外 dirty path 必须在任何核心模板落盘前终止。 outside_dirty = init_repo(path=tmp_path / "outside-dirty") (outside_dirty / "rogue.txt").write_text(data="rogue\n", encoding="utf-8") @@ -837,8 +861,32 @@ def test_scenario_3_check_rejects_invalid_final_states(tmp_path: Path) -> None: target=target, upstream=upstream, prepared=prepared, - expected_error="最终仓库检查失败", - expected_detail="核心文档不能是符号链接", + expected_error="核心文档路径不能是符号链接", + expected_detail="architecture.md", + ) + + # 已提交父目录 symlink 也不能让末级普通文件逃逸到目标仓库外。 + root = tmp_path / "check-parent-symlink" + upstream, target, prepared = create_ready_case(root=root, language="zh") + outside_directory = tmp_path / "check-parent-outside" + outside_directory.mkdir() + (outside_directory / "pull_request_template.md").write_text( + data="# External pull request template\n", + encoding="utf-8", + ) + (target / ".github/pull_request_template.md").unlink() + (target / ".github").rmdir() + (target / ".github").symlink_to( + target=outside_directory, + target_is_directory=True, + ) + prepared["target_head"] = commit_all(repo=target, message="parent symlink") + assert_check_failure( + target=target, + upstream=upstream, + prepared=prepared, + expected_error="核心文档路径不能是符号链接", + expected_detail=".github/pull_request_template.md", ) # 同一 final-bytes 路径覆盖 untracked、staged、tracked 和 committed 四种 Git 状态。 @@ -870,15 +918,15 @@ def test_scenario_3_check_rejects_invalid_final_states(tmp_path: Path) -> None: expected_detail="trailing whitespace", ) - # ignored core path 仍属于 final bytes;Git-equivalent 检查也必须拒绝 conflict marker。 - root = tmp_path / "ignored-conflict-marker" + # ignored core path 仍属于 final bytes,不能绕过同一 whitespace gate。 + root = tmp_path / "ignored-whitespace" upstream, target, prepared = create_ready_case(root=root, language="zh") (target / ".gitignore").write_text( data="architecture.md\n", encoding="utf-8", ) (target / "architecture.md").write_text( - data="<<<<<<< ours\nproject fact\n=======\nother fact\n>>>>>>> theirs\n", + data="project fact with trailing whitespace \n", encoding="utf-8", ) ignored_status = git( @@ -892,7 +940,7 @@ def test_scenario_3_check_rejects_invalid_final_states(tmp_path: Path) -> None: upstream=upstream, prepared=prepared, expected_error="最终仓库检查失败", - expected_detail="leftover conflict marker", + expected_detail="trailing whitespace", ) root = tmp_path / "gitignore-whitespace" @@ -940,6 +988,47 @@ def test_scenario_3_check_rejects_invalid_final_states(tmp_path: Path) -> None: expected_detail="MM architecture.md", ) + # Git 会把 staged delete 与同路径重建拆成两条记录;ignored 重建 + # 甚至不出现在普通 status。 + for replacement in ("untracked", "ignored"): + root = tmp_path / f"delete-recreate-{replacement}" + upstream, target, _ = create_ready_case(root=root, language="zh") + commit_all(repo=target, message="project docs") + if replacement == "ignored": + (target / ".gitignore").write_text( + data="architecture.md\n", + encoding="utf-8", + ) + commit_all(repo=target, message="ignore recreated architecture") + prepared = prepare_success( + target=target, + upstream=upstream, + language="zh", + ) + architecture = target / "architecture.md" + final_bytes = architecture.read_bytes() + git(repo=target, args=["rm", "architecture.md"], check=True) + architecture.write_bytes(data=final_bytes) + status = repository_status(root=target) + assert "D architecture.md" in status + if replacement == "untracked": + assert "?? architecture.md" in status + else: + ignored_status = git( + repo=target, + args=["status", "--porcelain=v1", "--ignored"], + check=True, + ) + assert "!! architecture.md" in ignored_status + payload = assert_check_failure( + target=target, + upstream=upstream, + prepared=prepared, + expected_error="editable path 存在 index/worktree 分叉", + expected_detail="architecture.md", + ) + assert "D architecture.md" in payload["detail"] + # 目标仓库 attributes 不得关闭 checker 对 final Markdown 的 whitespace 规则。 root = tmp_path / "attributes-cannot-disable-whitespace" upstream, target, _ = create_ready_case(root=root, language="zh") @@ -1077,6 +1166,164 @@ def test_scenario_4_installer_end_to_end(tmp_path: Path) -> None: assert repeated_payload["removed_obsolete"] == [] assert repository_snapshot(root=target) == repo_snapshot + # 两个平台必须在任何删除前拒绝父路径 symlink,不能改写仓库外目标。 + unsafe_home = tmp_path / "unsafe-parent-home" + unsafe_home.mkdir() + (unsafe_home / ".claude").mkdir() + protected_directory = tmp_path / "protected-install-parent" + protected_directory.mkdir() + protected_file = protected_directory / "protected.bin" + protected_file.write_bytes(data=b"protected parent\x00") + (unsafe_home / ".claude/skills").symlink_to( + target=protected_directory, + target_is_directory=True, + ) + assert_user_install_failure_unchanged( + upstream=upstream, + home=unsafe_home, + expected_error="安装路径父级不能是符号链接", + ) + assert protected_file.read_bytes() == b"protected parent\x00" + + # canonical source 的 tracked symlink 不得把仓库外 bytes 带入安装结果。 + symlink_upstream = create_installer_upstream( + root=tmp_path / "symlink-upstream" + ) + protected_source = tmp_path / "protected-source.bin" + protected_source.write_bytes(data=b"protected source\x00") + source_link = ( + symlink_upstream + / "zh/skills/workflow-docs-sync/protected-source.bin" + ) + source_link.symlink_to(target=protected_source) + commit_all(repo=symlink_upstream, message="tracked source symlink") + source_home = tmp_path / "unsafe-source-home" + source_home.mkdir() + assert_user_install_failure_unchanged( + upstream=symlink_upstream, + home=source_home, + expected_error="canonical Skill 不能包含符号链接", + ) + assert protected_source.read_bytes() == b"protected source\x00" + + # canonical source 的祖先链也必须留在已验证的 upstream Git 根内。 + ancestor_upstream = init_repo(path=tmp_path / "ancestor-upstream") + protected_zh = tmp_path / "protected-upstream-zh" + protected_skill = protected_zh / "skills/workflow-docs-sync" + protected_skill.parent.mkdir(parents=True) + shutil.copytree(src=SKILL_ROOT, dst=protected_skill) + (ancestor_upstream / "zh").symlink_to( + target=protected_zh, + target_is_directory=True, + ) + commit_all(repo=ancestor_upstream, message="tracked source ancestor symlink") + ancestor_home = tmp_path / "ancestor-source-home" + ancestor_home.mkdir() + assert_user_install_failure_unchanged( + upstream=ancestor_upstream, + home=ancestor_home, + expected_error="canonical Skill 路径不能包含符号链接", + ) + assert (protected_skill / "SKILL.md").is_file() + + # Claude frontmatter 也在 mutation 前验证,失败不能留下半安装状态。 + invalid_upstream = create_installer_upstream( + root=tmp_path / "invalid-frontmatter-upstream" + ) + invalid_skill = invalid_upstream / "zh/skills/workflow-docs-sync/SKILL.md" + invalid_skill.write_text(data="# Missing frontmatter\n", encoding="utf-8") + commit_all(repo=invalid_upstream, message="invalid frontmatter") + invalid_home = tmp_path / "invalid-frontmatter-home" + invalid_home.mkdir() + assert_user_install_failure_unchanged( + upstream=invalid_upstream, + home=invalid_home, + expected_error="SKILL.md 缺少标准 YAML frontmatter", + ) + + # 普通 status 看不到 ignored source;会被复制的 residue 必须在 + # 四种安装 mutation 前失败。 + ignored_upstream = create_installer_upstream( + root=tmp_path / "ignored-source-upstream" + ) + (ignored_upstream / ".gitignore").write_text( + data="PR_BODY.md\nPRIVATE_STATE/\n", + encoding="utf-8", + ) + commit_all(repo=ignored_upstream, message="ignore local PR body") + ignored_draft = ( + ignored_upstream / "zh/skills/workflow-docs-sync/PR_BODY.md" + ) + ignored_draft.write_text(data="private draft\n", encoding="utf-8") + ignored_directory = ( + ignored_upstream / "zh/skills/workflow-docs-sync/PRIVATE_STATE" + ) + ignored_directory.mkdir() + assert repository_status(root=ignored_upstream) == "" + ignored_source_status = git( + repo=ignored_upstream, + args=["status", "--porcelain=v1", "--ignored"], + check=True, + ) + assert "!! zh/skills/workflow-docs-sync/PR_BODY.md" in ignored_source_status + ignored_directory_rule = git( + repo=ignored_upstream, + args=[ + "check-ignore", + "-v", + "zh/skills/workflow-docs-sync/PRIVATE_STATE", + ], + check=True, + ) + assert "PRIVATE_STATE/" in ignored_directory_rule + + ignored_home = tmp_path / "ignored-source-home" + ignored_home.mkdir() + seed_legacy_install( + install_root=ignored_home, + obsolete_kinds=("directory", "file"), + ) + ignored_user_payload = assert_user_install_failure_unchanged( + upstream=ignored_upstream, + home=ignored_home, + expected_error="canonical Skill 包含会被安装的 ignored path", + ) + assert "PR_BODY.md" in ignored_user_payload["error"] + assert "PRIVATE_STATE" in ignored_user_payload["error"] + + ignored_target = init_repo(path=tmp_path / "ignored-source-target") + seed_legacy_install( + install_root=ignored_target, + obsolete_kinds=("directory", "directory"), + ) + commit_all(repo=ignored_target, message="existing installation") + target_before = repository_snapshot(root=ignored_target) + ignored_result, ignored_payload = run_repo_install( + upstream=ignored_upstream, + target=ignored_target, + ) + assert ignored_result.returncode != 0 + assert ignored_payload["status"] == "failed" + assert ( + "canonical Skill 包含会被安装的 ignored path" + in ignored_payload["error"] + ) + assert "PR_BODY.md" in ignored_payload["error"] + assert "PRIVATE_STATE" in ignored_payload["error"] + assert repository_snapshot(root=ignored_target) == target_before + for install_root in (ignored_home, ignored_target): + for _, platform_root in PLATFORM_ROOTS: + assert not ( + install_root + / platform_root + / "workflow-docs-sync/PR_BODY.md" + ).exists() + assert not ( + install_root + / platform_root + / "workflow-docs-sync/PRIVATE_STATE" + ).exists() + def test_scenario_5_repository_distribution_contract() -> None: """场景 5:真实仓库 bytes、Skill 结构、语境边界与旧控制面保持可分发。""" @@ -1146,56 +1393,16 @@ def test_scenario_5_repository_distribution_contract() -> None: f"{language}/{relative_path}" ) - # Anchor 协议必须由 contract 单点发布。 - # 显式 null-test 缺口必须保留完整元数据。 + # 只固定 PR #21 的机器结构;自然语言质量交给 review 与真实 consumer eval。 canonical_anchor = "" anchor_marker = "capability-anchor:" - anchor_rule_fragments = { - "zh": ( - "大小写敏感", - "[A-Za-z0-9_.-]+", - "空白", - "JSON path", - ), - "en": ( - "case-sensitive", - "[A-Za-z0-9_.-]+", - "whitespace", - "JSON path", - ), - } - boundary_fragments = { - "zh": ( - "不受支持", - "不保证", - "不单独证明", - ), - "en": ( - "unsupported", - "does not guarantee", - "does not by itself prove", - ), - } - instructional_metadata = ( - "目标项目存在本地 alignment test 时,必须登记其真实测试锚点。", - "The target project must register its own local alignment test when one exists.", - ) for language in ("zh", "en"): contract_path = REPO_ROOT / language / "capability_contract.json" contract = json.loads(s=contract_path.read_text(encoding="utf-8")) assert contract["schema_version"] == "0.1.0" rules_text = "\n".join(contract["rules"]) - anchor_rules = [ - rule for rule in contract["rules"] if canonical_anchor in rule - ] - assert len(anchor_rules) == 1 - assert all( - fragment in anchor_rules[0] - for fragment in anchor_rule_fragments[language] - ) - assert all( - fragment in rules_text for fragment in boundary_fragments[language] - ) + assert rules_text.count(canonical_anchor) == 1 + assert rules_text.count(anchor_marker) == 1 null_test_rules = [ rule for rule in contract["rules"] if "test_anchor: null" in rule ] @@ -1213,8 +1420,6 @@ def test_scenario_5_repository_distribution_contract() -> None: ) ] assert anchor_publishers == ["capability_contract.json"] - assert rules_text.count(canonical_anchor) == 1 - assert rules_text.count(anchor_marker) == 1 explicit_null_entries = [ entry @@ -1231,7 +1436,6 @@ def test_scenario_5_repository_distribution_contract() -> None: projectized = replace_active_markers(value=contract) serialized = json.dumps(obj=projectized, ensure_ascii=False) assert not any(marker in serialized for marker in ACTIVE_MARKERS) - assert not any(text in serialized for text in instructional_metadata) documents = projectized["contracts"]["documents"] by_anchor = {entry["anchor_id"]: entry for entry in documents} for anchor in ("DOC.interact", "DOC.business_user_guide"): @@ -1243,124 +1447,16 @@ def test_scenario_5_repository_distribution_contract() -> None: "pending_since", } & set(by_anchor[anchor]) - # 测试、review 和两轮 eval 直接保护风险,不复制 anchor 定义或 - # 增加 parser。 - downstream_contract_fragments = { - "zh": { - "TESTING.md": ( - "仅为满足模板而创建 wrapper", - "可安全、确定性复现的 escaped bug", - "无法先建立失败测试", - "无 test diff", - "纯内部重构", - "文档-only gate", - ), - "PR_Checklist.md": ( - "TESTING.md` 第 4 节", - "contract-defined protocol", - "REOPENED finding", - "SUPERSEDED candidate", - "长期规则", - ), - ".github/pull_request_template.md": ( - "first-seen", - "REOPENED", - "SUPERSEDED", - "hypothesis", - "unknown", - "Promoted reusable rule", - ), - }, - "en": { - "TESTING.md": ( - "do not create a wrapper only to satisfy this template", - "safely and deterministically reproducible escaped bug", - "cannot establish a failing test first", - "no test diff", - "behavior-preserving internal refactor", - "documentation-only gate", - ), - "PR_Checklist.md": ( - "TESTING.md` section 4", - "contract-defined protocol", - "REOPENED findings", - "SUPERSEDED candidates", - "long-term rule", - ), - ".github/pull_request_template.md": ( - "first-seen", - "REOPENED", - "SUPERSEDED", - "hypothesis", - "unknown", - "Promoted reusable rule", - ), - }, - } - for language, files in downstream_contract_fragments.items(): - for relative_path, fragments in files.items(): - text = (REPO_ROOT / language / relative_path).read_text( - encoding="utf-8" - ) - assert canonical_anchor not in text - assert all(fragment in text for fragment in fragments) - skill_text = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") assert canonical_anchor not in skill_text - decisions = ( - REPO_ROOT / "zh/docs/development_workflow/decisions.md" - ).read_text(encoding="utf-8") - assert "## DEC-007" in decisions - assert "- 状态:accepted" in decisions.split("## DEC-007", maxsplit=1)[1] - for fragment in ( - "描述性事实", - "规范性政策", - "个人/会话偏好", - "schema_version", - "PASS_NOOP", - "不修改 `sync_docs.py`", - ): - assert fragment in decisions.split("## DEC-007", maxsplit=1)[1] - - eval_contract = (SKILL_ROOT / "evals/README.md").read_text( - encoding="utf-8" - ) - for fragment in ( - "三条当前 scoped normative policy", - "ENTRY_STATUSES={active, deprecated}", - "check_capability_contract_alignment.py --base-ref", - "PASS_NOOP", - "ROUND1_INCOMPLETE", - "ROUND2_DRIFT", - ): - assert fragment in eval_contract - - english_workflow = ( - REPO_ROOT / "en/docs/development_workflow/README.md" - ).read_text(encoding="utf-8") - for fragment in ( - "DEC-007 Summary", - "descriptive facts", - "normative policies", - "REOPENED", - "PASS_NOOP", - "ROUND1_INCOMPLETE", - "ROUND2_DRIFT", - ): - assert fragment in english_workflow - # 机械兜底只覆盖本次真实误植过的八份下游模板与五个精确 token。 internal_context_tokens = ( "disposable clone", "同步工作树", "synchronized worktree", - "共享工作树", - "shared worktree", "fresh-context", "blind-first", - "通用 GitHub 发布能力", - "general GitHub publishing capability", ) guarded_templates = ( "AGENTS.md", @@ -1378,27 +1474,6 @@ def test_scenario_5_repository_distribution_contract() -> None: context_hits.append(f"{language}/{relative_path}: {token}") assert context_hits == [] - # 窄修只撤回 WDS 实现,主执行者、证据与只读审查等通用协作原则必须保留。 - collaboration_contract = { - "zh": ( - "主执行者对最终判断、最终产物和最终写入结果负责", - "受委派结果必须经过审阅与合成", - "不强制 Agent 数量或固定调度顺序", - "协作者结论、投票或共识不等于证据", - "调查与审查任务默认只读", - ), - "en": ( - "The primary executor owns final judgments, deliverables, and writes", - "delegated results must be reviewed", - "do not require a fixed agent count", - "Agreement, voting, or consensus is not evidence", - "Investigation and review tasks are read-only by default", - ), - } - for language, statements in collaboration_contract.items(): - text = (REPO_ROOT / language / "AGENTS.md").read_text(encoding="utf-8") - assert all(statement in text for statement in statements) - deleted_paths = ( "PR_BODY.md", "scripts/sync_coding_workflow.py", diff --git a/zh/.github/pull_request_template.md b/zh/.github/pull_request_template.md index dfad429..d998de1 100644 --- a/zh/.github/pull_request_template.md +++ b/zh/.github/pull_request_template.md @@ -18,7 +18,7 @@ ## 4. Documentation Impact - + ## 5. User-visible and Architecture Impact @@ -34,7 +34,7 @@ ## 7. Review / Fix Record - + ## 8. Known Limits, Open Decisions, and Rollback @@ -45,5 +45,5 @@ - [ ] Actual Change Scope 与真实 diff 一致。 - [ ] 测试命令、范围、结果和未运行原因准确。 - [ ] 用户可见与架构影响已核对对应权威文档。 -- [ ] BLOCKER 与 actionable WARN 已关闭;open decisions 未被伪装为完成。 -- [ ] PR body 不含历史草稿、未落地计划、错误 base/head 或仓库内临时 body 路径。 +- [ ] 项目要求的可执行 review feedback 已处理,或已明确记录为开放项。 +- [ ] PR body 不含历史草稿、未落地计划或错误 base/head。 diff --git a/zh/AGENTS.md b/zh/AGENTS.md index 67c0422..0f57cae 100644 --- a/zh/AGENTS.md +++ b/zh/AGENTS.md @@ -42,8 +42,7 @@ - 能力边界变化时,先更新或确认 `capability_contract.json`,再检查 `interact.md` 和 business guide;用户可观察行为变化时,先更新或确认 `interact.md`。 - 测试仍是事实证据;具体命令、fixture、层级和隔离要求只在 `TESTING.md` 维护。 -- 不要求每次修改全部文档。未更新受影响候选文档时,在交付说明中给出基于当前事实的 - no-update reason。 +- 不要求每次修改全部文档。确受影响但保持不变的权威文档,应在交付说明中写明当前事实依据。 - 编码、lint、formatter、build 和类型规则必须从仓库真实配置提取,不从本模板推断。 ## Collaboration @@ -69,8 +68,8 @@ ## SOP -执行标准流程时读取 `SOP.md` 的对应入口。执行 checklist 保留在当前会话,不创建仓库内 -运行状态、receipt 或临时流程文档。 +执行标准流程时读取 `SOP.md` 的对应入口。执行记录是否保存、保存位置与保留期限遵循本项目 +真实的审计、可恢复性和交付政策。 ## PR Delivery diff --git a/zh/PR_Checklist.md b/zh/PR_Checklist.md index fce9a16..4d48001 100644 --- a/zh/PR_Checklist.md +++ b/zh/PR_Checklist.md @@ -25,8 +25,8 @@ ## Documentation and Contracts - [ ] 已按真实影响检查 `AGENTS.md`、`architecture.md`、`capability_contract.json`、 - `interact.md`、business guide、`TESTING.md` 和 `SOP.md`;无需更新的候选项有真实 - no-update reason,不要求为了齐全而修改所有文档。 + `interact.md`、business guide、`TESTING.md` 和 `SOP.md`;确受影响但保持不变的权威文档有 + 当前事实依据,不要求为了齐全而修改所有文档。 - [ ] 能力变化遵循 `capability_contract.json → interact.md → business_user_guide.md` 的权威 方向;用户可见声明有当前实现或测试证据和稳定 anchor。 - [ ] Markdown anchor 按 capability contract 的 contract-defined protocol 引用,没有在本文复写 @@ -37,12 +37,8 @@ ## Review Closure - [ ] 已完成本项目测试与交付政策要求的 review gate,并准确记录 reviewer 身份、范围和限制。 -- [ ] 所有 BLOCKER 和不需要新产品决策的 actionable WARN 已修复并复核;其余问题进入 open - decisions,包含证据与影响。 -- [ ] Material rounds、REOPENED finding 与 SUPERSEDED candidate/evidence 均保留;重开沿用原 - finding ID,finding 状态没有与证据状态混用。 -- [ ] 上一轮漏检原因有证据,或明确标为 `hypothesis` / `unknown`,没有为填字段编造因果。 -- [ ] 每个 material finding 已评估是否提升为长期规则或自动化 gate;未提升时记录原因。 +- [ ] 已按本项目政策处理可执行的 review feedback;已解决项有复核证据,未解决项明确记录 + 影响与所需决策。 - [ ] 修复后重跑受影响测试和机械检查,最终 diff 与 Git 状态已再次检查。 ## PR Delivery diff --git a/zh/README.md b/zh/README.md index c904f84..618a9c4 100644 --- a/zh/README.md +++ b/zh/README.md @@ -27,7 +27,8 @@ Architecture、Capability / User Behavior、Testing、Governance 是覆盖维度 ## Skill 安装 Studio 可直接加载 canonical `zh/skills/workflow-docs-sync/`。个人或团队安装只复制这一个 -Skill,不保存来源状态: +Skill,不保存来源状态;安装器会在任何目标 mutation 前拒绝 symlink、无效 frontmatter 和 +会被复制的 ignored source residue: ```bash python3 zh/scripts/install_skills.py --upstream-dir diff --git a/zh/SOP.md b/zh/SOP.md index 5f3efcb..b803180 100644 --- a/zh/SOP.md +++ b/zh/SOP.md @@ -3,8 +3,8 @@ ## Purpose and Authority `SOP.md` 只保存稳定流程入口,不复制易漂移的命令、测试清单或发布细节。发生冲突时,以 -当前代码、配置、测试、契约以及 `TESTING.md`、`PR_Checklist.md` 等专项权威为准。执行中的 -checklist 只存在于当前会话,不落盘为 run state、receipt 或临时 SOP。 +当前代码、配置、测试、契约以及 `TESTING.md`、`PR_Checklist.md` 等专项权威为准。执行记录的 +保存方式与保留期限遵循本项目真实的审计、可恢复性和交付政策。 ## Available SOPs diff --git a/zh/TESTING.md b/zh/TESTING.md index 3ed0745..1347be1 100644 --- a/zh/TESTING.md +++ b/zh/TESTING.md @@ -32,9 +32,8 @@ ## 3. Capability Contract Alignment -alignment test 属于目标项目自己的测试套件,不属于文档同步 checker。它应在本地递归收集 -`capability_contract.json` 中所有对象的稳定 `anchor_id`,并按 contract rules 定义的协议检查 -唯一性和 Markdown 引用, +项目的 alignment test 应在本地递归收集 `capability_contract.json` 中所有对象的稳定 +`anchor_id`。它应按 contract rules 定义的协议检查唯一性和 Markdown 引用, 但不硬编码 bucket、JSON path、数组位置或要求所有 contract 条目进入 business guide。 显式使用 `test_anchor: null` 时同时记录非空、具体的 `untested_reason` 和非空 `pending_since`; diff --git a/zh/docs/development_workflow/README.md b/zh/docs/development_workflow/README.md index 7c3eaa6..3ef36bd 100644 --- a/zh/docs/development_workflow/README.md +++ b/zh/docs/development_workflow/README.md @@ -224,7 +224,12 @@ E. 输出风格约束 HEAD、dirty 范围、editable path 无 index/worktree 分叉、九份普通 UTF-8 非空文件、JSON object、active marker,以及存在时为 UTF-8 的 `.gitignore`。最终 bytes 的 whitespace 使用 临时非 Git 目录和固定 Git 规则,不继承目标仓库 attributes。Checker 不解析 Markdown,也不 - 验证目标项目 capability、测试层级或文案质量。 + 验证目标项目 capability、测试层级或文案质量。单一 final-bytes 路径依赖同步范围外 dirty + path 始终被拒绝;若未来放宽 allowlist,必须重新评估 whitespace 覆盖。index/worktree 分叉 + 拒绝按 path 聚合两侧 status,并覆盖 index 删除或 rename source 后同路径 untracked / ignored + 重建;它消除的是两个发布候选,不是第二套 whitespace 检查。 +- 安装器在任何删除或复制前验证 source/目标祖先、source symlink、Claude frontmatter 和会被 + 复制的 ignored source residue;只精确清理废弃 reviewer Skill,不保存安装状态或来源 receipt。 - PR body 临时 Markdown 始终位于仓库外。commit、push 和 draft PR 创建只在用户要求且检查 成功后,由通用 GitHub 发布能力完成。 - 最终机械检查只证明最终仓库状态,不证明调查、测试或复核执行历史。 diff --git a/zh/docs/development_workflow/decisions.md b/zh/docs/development_workflow/decisions.md index 6522a2d..9f52595 100644 --- a/zh/docs/development_workflow/decisions.md +++ b/zh/docs/development_workflow/decisions.md @@ -73,6 +73,9 @@ - 日期:2026-08-01 UTC - 原则:更少不是目标;只有在风险已由更直接机制覆盖,或被明确接受时,才删除原机制。 行数下降、测试变少或文件减少都不能单独构成删除理由。 +- 机制必要性:恢复或新增 marker、alias、机器状态、parser、兼容入口或其他控制机制前,必须 + 先登记 finding,证明独立风险、真实消费者与可复现失败路径,并说明现有场景、review 或真实 + eval 为何不足。能最小扩展现有机制时不另造同义入口;缺少具体缺口时保持零 diff。 - 产品边界:保留 one-call UX、固定目标 HEAD 与上游 SHA、从固定 Git object 分发模板、dirty allowlist、写入前完整预检、只创建缺失文件、single writer、全量事实重建、最小必要改写、 fresh-context review、最终 `check` 和显式授权发布。Architecture、Capability / User Behavior、 @@ -92,25 +95,41 @@ 因此 equality 不再提供独立保护。`prepare` 与 `check` 共用 source-template validator,从调用方 指定 object 与 language 读取全部九份 UTF-8 模板,并对八份非 PR source fail closed;若未来 放宽 active-marker source contract,必须重新评估是否恢复 equality。 -- 合并 tracked working-tree、index 与 final-file whitespace 分支,只扫描九份最终文件及存在的 - `.gitignore`。覆盖成立有三个附加前提:editable path 不得存在 index/worktree 分叉;Git CLI - 在临时非 Git 目录以固定 `core.whitespace=blank-at-eol,blank-at-eof,space-before-tab` 运行,不 - 继承目标 repository attributes、用户 global attributes 或 system attributes;存在的 - `.gitignore` 仍必须是普通 UTF-8 文件。任一前提不满足时 `check` fail closed。 +- 合并 tracked working-tree、index 与 final-file whitespace 分支,只以一条路径扫描九份最终 + 文件及存在的 `.gitignore`。无损性首先依赖 dirty allowlist:同步范围外改动始终被拒绝时, + 允许范围内的 untracked、staged、tracked 或 committed 文件都由同一 working-tree final bytes + 覆盖;若未来放宽 allowlist,必须重新评估 whitespace 覆盖。editable path 的 index/worktree + 分叉仍 fail closed,因为它代表两个发布候选,而不是第二套 whitespace 检查。Git CLI 在临时 + 非 Git 目录固定 `core.whitespace=blank-at-eol,blank-at-eof,space-before-tab`,不继承目标 + repository attributes、用户 global attributes 或 system attributes;存在的 `.gitignore` 仍须 + 为普通 UTF-8 文件。dirty status 按 editable path 聚合 index 与 worktree 两侧;index 删除或 + rename source 后同路径以 untracked 或 ignored 文件重建也属于两个候选,必须 fail closed。 + 任一前提不满足时 `check` fail closed。 - 删除旧版 22 项 `TEMPLATE_TOKENS` 和独立的 broad“待补充”扫描,并新引入两个 active - project-fill marker 作为唯一机器未完成状态。历史 upstream 模板确实包含旧 token,但当前 - 登记的真实下游 SEC_metrics 未命中,本仓库也未登记其他下游依赖,因此不逐项保留 target-side - legacy blacklist;旧 upstream 由 source active-marker invariant fail closed。“待补充”仍可 - 表达人工明确暂缓。未来只有具体下游仓库与文件的真实命中证据才能恢复最小 compatibility。 + project-fill marker 作为唯一机器未完成状态。历史 upstream 模板确实包含旧 token;2026-08-06 + 对九份核心文档重新核对 SEC_metrics + `a77b9055a53de5e5808649551f03fe567cb2de0a`、trading + `cb1fe04071ed516abd57b7999072cba5b11f85e3` 与 nl2dax-eval + `5c850f4fa78fd87e34d5039459f1d4e7400ace22`;三仓均未命中旧 22 token、active marker 或 + broad“待补充”,因此不逐项保留 target-side legacy blacklist。历史 upstream + `f669ee40b1fbfe91baee097fd26ad975d7783aea` 可在 `zh/capability_contract.json` 与 + `zh/docs/business_user_guide.md` 重演旧 token,证明迁移风险曾真实存在;旧 upstream 由 source + active-marker invariant fail closed。“待补充”仍可表达人工明确暂缓。未来只有具体下游仓库 + 与文件的真实命中证据才能恢复最小 compatibility。 - 删除 `references/sections.md` 和 `references/audit.md`。旧 `SKILL.md` 强制完整读取两者,实际 没有渐进式披露收益;四维覆盖、独立复核、严重度和 finding 收口规则合并到 `SKILL.md`, 由单一权威避免重复漂移。 - 删除 Working Brief 产品机制及其生命周期。Reviewer 的自足规则是:blind-first 初始阶段 不得读取主 Agent 的任何中间产物。 - 跨项目模板撤回 Workflow Docs Sync 特有的 clone / pin、reviewer isolation 和 publishing - 实现,同时保留并通用化主执行者负责、委派结果需审阅、共识不等于证据、审查默认只读和 - 不固定 Agent 数量或顺序等协作原则。主防线是 `SKILL.md` 的语义检查;分发合同只扫描本次 - 真实误植过的精确语境 token,作为已知回归兜底,不扩展成自然语言黑名单。 + 实现,以及 run-state / receipt、documentation-sync checker 和内部 finding/candidate 状态词; + 同时保留并通用化主执行者负责、委派结果需审阅、共识不等于证据、审查默认只读和不固定 + Agent 数量或顺序等协作原则。主防线是 `SKILL.md` 的语义检查;分发合同只扫描本次真实误植 + 过的五个精确语境 token,作为已知回归兜底,不扩展成自然语言黑名单。 +- 安装器继续直接复制 canonical Skill,但把“clean source”补全为可复制 entry 合同:普通 + tracked/untracked 状态之外,任何会进入安装结果的 ignored path 也必须在所有删除和复制前 + fail closed;仅明确不会复制的 Python cache 与 `.DS_Store` 豁免。该边界直接防止本地 PR body + 或私有 residue 污染 user/repo × Codex/Claude 安装,不增加来源 receipt 或 migration registry。 ### 删除的代理测试与新信任基础 @@ -154,10 +173,12 @@ Case A 对“部分过时旧文档、共同虚构能力、验证层级膨胀” - 版本语义:`schema_version` 继续为 `0.1.0`,只表示 JSON shape 和机器必需字段;纯 authoring prose 规则变化不升级该字段,未来 shape 或机器必需字段变化再按版本策略处理。 - 测试决策:跨项目模板保留 escaped-bug、无法先红测、公开契约、无 test diff、纯重构和 - documentation-only gate 的最低证据规则,并由 PR checklist 执行。一个协议只有一个定义点。 -- Finding 闭环:finding 使用 stable ID、severity、first-seen 和 OPEN / CLOSED / DEFERRED; - REOPENED 是保留原 ID 的事件。CURRENT / SUPERSEDED 只描述 candidate/evidence。漏检原因必须 - evidence-backed、标为 hypothesis 或写 unknown;closure 时评估可复用规则提升。 + documentation-only gate 的最低证据规则,并由 PR checklist 执行。bug-first 与 no-test-diff + 是两条独立责任;新增测试判据或按变更类型选测试不能替代具体已有覆盖与重跑证据。一个协议 + 只有一个定义点。 +- Finding 闭环:上述 stable ID、severity、first-seen、REOPENED 和 candidate/evidence 状态只 + 属于本仓库维护与 canonical Skill / eval 的内部证据合同。下游模板只要求按目标项目政策记录 + review、可执行反馈、复核和开放决策,不固化本 Skill 的 round、状态词或 raw-record 拓扑。 - Case A:同一 target code/config/test/committed-artifact base、upstream candidate、language 和 round 1 最终九文档 bytes 下,round 2 只接受 `PASS_NOOP`。冻结事实支持的新增修正为 `ROUND1_INCOMPLETE`,无新增反证的表达漂移为 `ROUND2_DRIFT`;两者都使 gate 失败,并要求从 diff --git a/zh/scripts/install_skills.py b/zh/scripts/install_skills.py index cbe5d2d..3a1717f 100644 --- a/zh/scripts/install_skills.py +++ b/zh/scripts/install_skills.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import fnmatch import json import os import shutil @@ -15,6 +16,7 @@ SKILL_NAME = "workflow-docs-sync" OBSOLETE_SKILL_NAME = "workflow-docs-sync-review" PLATFORM_ROOTS = {"codex": Path(".agents/skills"), "claude": Path(".claude/skills")} +SOURCE_IGNORE_PATTERNS = ("__pycache__", "*.py[cod]", ".DS_Store") class InstallError(RuntimeError): @@ -75,17 +77,101 @@ def remove_path(*, path: Path) -> bool: return True +def is_source_copy_ignored(*, relative_path: Path) -> bool: + """判断 source entry 是否属于安装器明确不会复制的缓存。""" + return any( + fnmatch.fnmatchcase(name, pattern) + for name in relative_path.parts + for pattern in SOURCE_IGNORE_PATTERNS + ) + + +def validate_source(*, upstream_root: Path, source: Path) -> None: + """在任何安装写入前验证 canonical Skill 可安全复制。""" + # 从 Git 根逐层检查,避免 zh/ 或 skills/ symlink 隐藏仓库外 source。 + current = upstream_root + for part in source.relative_to(upstream_root).parts: + current /= part + if current.is_symlink(): + raise InstallError( + "canonical Skill 路径不能包含符号链接:" + f"{current.relative_to(upstream_root).as_posix()}" + ) + if not current.is_dir(): + raise InstallError(f"canonical Skill 不完整:{source}") + if (source / "SKILL.md").is_symlink() or not ( + source / "SKILL.md" + ).is_file(): + raise InstallError(f"canonical Skill 不完整:{source}") + # 拒绝 tracked symlink,避免 copytree 跟随链接读取仓库外 bytes。 + symlinks = sorted( + path.relative_to(source).as_posix() + for path in source.rglob("*") + if path.is_symlink() + ) + if symlinks: + raise InstallError(f"canonical Skill 不能包含符号链接:{symlinks}") + # 普通 status 看不到 ignored residue;拒绝任何会进入安装结果的 + # 此类文件。 + source_relative = source.relative_to(upstream_root) + ignored = git( + repo_root=upstream_root, + args=[ + "ls-files", + "-z", + "--others", + "--ignored", + "--exclude-standard", + "--directory", + "--", + source_relative.as_posix(), + ], + ) + if ignored.returncode != 0: + raise InstallError( + ignored.stderr.strip() or "无法检查 canonical Skill ignored path" + ) + unsafe_ignored = sorted( + path.relative_to(source_relative).as_posix() + for value in ignored.stdout.split("\0") + if value + for path in (Path(value),) + if not is_source_copy_ignored( + relative_path=path.relative_to(source_relative) + ) + ) + if unsafe_ignored: + raise InstallError( + f"canonical Skill 包含会被安装的 ignored path:{unsafe_ignored}" + ) + # Claude 转换可能失败;先验证 frontmatter,防止另一平台已写入。 + claude_text(text=(source / "SKILL.md").read_text(encoding="utf-8")) + + +def validate_install_parents(*, install_root: Path) -> None: + """在任何删除前拒绝越出安装根的父路径 symlink 或非目录。""" + for platform_root in PLATFORM_ROOTS.values(): + current = install_root + for part in platform_root.parts: + current /= part + relative_path = current.relative_to(install_root).as_posix() + if current.is_symlink(): + raise InstallError( + f"安装路径父级不能是符号链接:{relative_path}" + ) + if current.exists() and not current.is_dir(): + raise InstallError(f"安装路径父级不是目录:{relative_path}") + + def copy_skill(*, source: Path, destination: Path, platform: str) -> None: """覆盖一个明确目录并复制 canonical 单 Skill。""" - if not source.is_dir() or not (source / "SKILL.md").is_file(): - raise InstallError(f"canonical Skill 不完整:{source}") remove_path(path=destination) destination.parent.mkdir(parents=True, exist_ok=True) shutil.copytree( src=source, dst=destination, symlinks=False, - ignore=shutil.ignore_patterns("__pycache__", "*.py[cod]", ".DS_Store"), + ignore=shutil.ignore_patterns(*SOURCE_IGNORE_PATTERNS), ) if platform == "claude": skill_path = destination / "SKILL.md" @@ -115,6 +201,9 @@ def execute() -> dict[str, object]: else Path.home().resolve() ) source = upstream / "zh/skills" / SKILL_NAME + # 两个平台和 Claude 转换全部预检后才 mutation,避免半安装状态。 + validate_source(upstream_root=upstream, source=source) + validate_install_parents(install_root=install_root) actions, removed_obsolete = [], [] for platform, platform_root in PLATFORM_ROOTS.items(): obsolete = install_root / platform_root / OBSOLETE_SKILL_NAME diff --git a/zh/skills/workflow-docs-sync/SKILL.md b/zh/skills/workflow-docs-sync/SKILL.md index 028455a..3f3f9d3 100644 --- a/zh/skills/workflow-docs-sync/SKILL.md +++ b/zh/skills/workflow-docs-sync/SKILL.md @@ -104,6 +104,10 @@ policy 的保留、修改或 no-update reason 必须记录 authority source、sc 目标 `TESTING.md` 要求多个解释器时,分别运行并记录每个解释器的命令和结果;不能运行时分别 记录 not-run reason。一个解释器的结果不能替代另一个解释器。 +分别核对 bug-first 与 no-test-diff:前者要求可复现行为缺陷先建立失败证据;后者要求没有 +test diff 时指出具体已有测试如何覆盖本次新风险并提供重跑证据。新增测试的判据、按变更类型 +选测试或泛称“已有高层测试”都不能代替 no-test-diff 责任;目标缺少等价规则时登记 finding。 + 验证层级不得膨胀。Unit、contract、scenario、golden、report build、repair validation、light review、full validation 和 live test 只能按实际覆盖范围表述;未运行不得报告为通过。 @@ -143,7 +147,9 @@ Reviewer 使用同一受控 severity。每个 finding 包含唯一 ID、severity 主 Agent 修复全部 BLOCKER 和无需新产品决策的 actionable WARN,请 reviewer 复核 finding 对应修改及直接跨文档影响,并重跑受影响测试。不得用新增状态文件、额外 Agent、词表或代理 -指标代替本应覆盖该风险的场景、review 或真实 eval。 +指标代替本应覆盖该风险的场景、review 或真实 eval。恢复或新增 marker、alias、机器状态、 +parser、兼容入口或其他控制机制前,finding 必须证明它承担独立风险,存在真实消费者与可复现 +失败路径,并说明现有高层机制为何不足;能最小扩展现有机制时不得另造同义入口。 ## 最终检查与报告 @@ -164,7 +170,9 @@ contract,再验证目标 HEAD、dirty allowlist、editable path 无 index/work final bytes 的 whitespace 检查在临时非 Git 目录以固定 Git 规则运行,不继承目标仓库 attributes、用户 global attributes 或 system attributes。它不解析 Markdown 标题或 fence, 不判断文案质量、capability 真实性、测试层级或业务指南可读性,也不证明调查、测试或 review -曾执行。 +曾执行。该单一路径的覆盖依赖同步范围外 dirty path 始终被拒绝;若未来放宽 dirty allowlist, +必须重新评估 whitespace 覆盖。index/worktree 分叉拒绝只消除两个发布候选,不执行第二套 +whitespace 检查。 最终报告必须包含: diff --git a/zh/skills/workflow-docs-sync/evals/README.md b/zh/skills/workflow-docs-sync/evals/README.md index b8e0065..3e44063 100644 --- a/zh/skills/workflow-docs-sync/evals/README.md +++ b/zh/skills/workflow-docs-sync/evals/README.md @@ -45,7 +45,9 @@ receipt、scratch 或 PR body。 - 对照 contract publisher 的 canonical grammar 与 SEC checker 真实 regex,记录 consumer 只 支持 canonical form、不穷举 alias;记录 SEC 的 `ENTRY_STATUSES={active, deprecated}` 是项目 侧加严,不要求通用模板复制该词表; -- 核对目标 `TESTING.md` 的 bug-first 与 no-test-diff 规则,正确且更具体的内容保持零 diff; +- 分别核对目标 `TESTING.md` 的 bug-first 与 no-test-diff 规则:bug-first 不能替代“无 test diff + 时指出具体已有覆盖并提供重跑证据”;仅有按变更类型选测试的表格也不等价。目标已有正确且 + 更具体的两条规则时保持零 diff,缺少任一条时形成 finding; - 运行项目真实测试,准确记录验证层级,并完成 review 与最终 `check`。 这些检查是现实抽样。所选 HEAD 初始状态不含相应缺陷时,只能报告“本次未观察到”,不得写 diff --git a/zh/skills/workflow-docs-sync/scripts/sync_docs.py b/zh/skills/workflow-docs-sync/scripts/sync_docs.py index b875588..911b10a 100644 --- a/zh/skills/workflow-docs-sync/scripts/sync_docs.py +++ b/zh/skills/workflow-docs-sync/scripts/sync_docs.py @@ -211,15 +211,47 @@ def require_editable_dirty(*, repo_root: Path) -> list[StatusEntry]: }) if outside: raise SyncError(error="存在同步范围外的 dirty path", detail=", ".join(outside)) - split = sorted({ - f"{entry.code} {path}" - for entry in entries - if entry.code not in {"??", "!!"} - and entry.code[0] != " " - and entry.code[1] != " " - for path in entry.paths - if path in EDITABLE_PATHS - }) + indexed_paths = set( + git_output( + repo_root=repo_root, + args=[ + "ls-files", + "-z", + "--cached", + "--", + *sorted(EDITABLE_PATHS), + ], + purpose="无法读取 editable path index", + ).split("\0") + ) + indexed_paths.discard("") + split: list[str] = [] + for path in sorted(EDITABLE_PATHS): + relevant = [entry for entry in entries if path in entry.paths] + index_dirty = any( + entry.code not in {"??", "!!"} and entry.code[0] != " " + for entry in relevant + ) + worktree_dirty = any( + entry.code == "??" + or (entry.code != "!!" and entry.code[1] != " ") + for entry in relevant + ) + # An index-side removal can hide a recreated ignored file from + # ordinary status. + recreated_index_absence = ( + index_dirty + and path not in indexed_paths + and ( + (repo_root / path).exists() + or (repo_root / path).is_symlink() + ) + ) + if index_dirty and (worktree_dirty or recreated_index_absence): + evidence = [f"{entry.code} {path}" for entry in relevant] + if recreated_index_absence and not worktree_dirty: + evidence.append(f"worktree path exists {path}") + split.append(" + ".join(evidence)) if split: raise SyncError( error="editable path 存在 index/worktree 分叉", @@ -271,16 +303,8 @@ def read_templates( def validate_source_templates( *, templates: dict[str, str], language: str, upstream_sha: str ) -> None: - """验证固定 object 的九份 source path 和非 PR active-marker 承重不变量。""" - actual_paths = set(templates) - expected_paths = set(CORE_FILES) - if actual_paths != expected_paths: - missing = sorted(expected_paths - actual_paths) - unknown = sorted(actual_paths - expected_paths) - raise SyncError( - error="固定上游模板集合无效", - detail=f"missing={missing}, unknown={unknown}", - ) + """验证固定 object 的非 PR active-marker 承重不变量。""" + # read_templates 已逐个读取 CORE_FILES;这里仅保留无法由读取过程覆盖的 marker 风险。 markerless = [ f"{language}/{relative_path}" for relative_path in CORE_FILES @@ -299,7 +323,7 @@ def validate_source_templates( def inspect_destinations(*, target_root: Path) -> tuple[list[str], list[str]]: - """区分缺失和已有目标文件,并在写入前拒绝目录冲突。""" + """逐层拒绝路径逃逸或目录冲突,并区分缺失和已有目标文件。""" missing: list[str] = [] existing: list[str] = [] for relative_path in CORE_FILES: @@ -389,9 +413,6 @@ def content_failures(*, target_root: Path) -> list[str]: failures: list[str] = [] for relative_path in CORE_FILES: path = target_root / relative_path - if path.is_symlink(): - failures.append(f"核心文档不能是符号链接: {relative_path}") - continue if not path.is_file(): failures.append(f"缺少必需文件: {relative_path}") continue @@ -484,6 +505,8 @@ def check( detail=f"期望 {expected_head},实际 {target.head}", ) entries = require_editable_dirty(repo_root=target.root) + # 复用 prepare 的逐层预检,防止已提交父目录 symlink 把核心文件解析到仓库外。 + inspect_destinations(target_root=target.root) failures = content_failures(target_root=target.root) failures.extend(file_whitespace_failures(target_root=target.root)) if failures: From 3af5ec0e068f0f7ecfee41e20bb6cb52f92bdba4 Mon Sep 17 00:00:00 2001 From: wlvh <30534800+wlvh@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:56:24 +0800 Subject: [PATCH 2/4] Repair workflow docs eval identities --- AGENTS.md | 2 + en/docs/development_workflow/README.md | 12 ++- zh/docs/development_workflow/decisions.md | 14 +-- zh/skills/workflow-docs-sync/evals/README.md | 92 ++++++++++++++------ 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5cd50c2..8ff7daa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,8 @@ 本仓库发布双语下游工作流模板、canonical `workflow-docs-sync` Skill 和安装器。 +新增 marker、alias、parser 或其他控制机制前,先按 DEC-006 证明独立风险、真实消费者和可复现失败路径;否则保持零 diff。 + - `zh/` 是中文语义源;`en/` 是从中文派生的模板与说明。 - 根 `.github/` 是本仓库 CI 与 GitHub 基础设施;`zh/.github/`、`en/.github/` 是下游模板源。 - 修改模板:先改 `zh/` 对应九份核心文件,再同步 `en/`。 diff --git a/en/docs/development_workflow/README.md b/en/docs/development_workflow/README.md index 7de4bc7..b344509 100644 --- a/en/docs/development_workflow/README.md +++ b/en/docs/development_workflow/README.md @@ -97,10 +97,14 @@ Stable IDs, first-seen evidence, REOPENED events, and candidate/evidence states maintenance and canonical Skill/eval evidence contract. Downstream templates defer review records, actionable feedback, rechecks, and open decisions to target-project policy instead of fixing this vocabulary. -With the same target base, upstream candidate, language, and round-one document bytes, Case A round two passes -only as `PASS_NOOP`; an added valid correction is `ROUND1_INCOMPLETE`, and unsupported prose drift is -`ROUND2_DRIFT`. Either failure restarts both rounds from a clean target. This decision adds no parser, ledger, -receipt, run state, installer behavior, or `sync_docs.py` / CLI feature. +Case A preregisters the selected target and known-stale claims before starting a blind executor. Target code, +configuration, tests, other committed artifacts, upstream candidate, and language stay fixed; round one +commits only its final nine documents, and round two starts from that `second_target_sha` in a new clean +checkout. A zero document diff with no staged, untracked, or ignored residue is `PASS_NOOP`; an added valid +correction is `ROUND1_INCOMPLETE`, and unsupported prose drift is `ROUND2_DRIFT`. Alignment runs directly on +the committed second target instead of a derived test-only identity. Either failure restarts both rounds from +a clean target. This decision adds no parser, ledger, receipt, run state, installer behavior, or +`sync_docs.py` / CLI feature. Key implementation files: diff --git a/zh/docs/development_workflow/decisions.md b/zh/docs/development_workflow/decisions.md index 9f52595..3a6e91e 100644 --- a/zh/docs/development_workflow/decisions.md +++ b/zh/docs/development_workflow/decisions.md @@ -88,7 +88,7 @@ ### 删除的机械机制与风险转移 - 删除 Markdown heading / fence parser。它只能证明有限语法形状,不能证明章节语义、文案 - 质量或项目事实;模板语义完整性改由四维 review、Case G 和 SEC_metrics Case A 真实 eval + 质量或项目事实;模板语义完整性改由四维 review 和 SEC_metrics Case A 真实 eval 发现。接受的剩余风险是:纯机械 `check` 不再单独发现空标题或 fence 失配。 - 删除模板 equality,包括 CRLF 归一化和 PR template 特例。每份非 PR 上游模板必须至少含 一个 active project-fill marker;逐字复制模板必然保留 marker,最终 `check` 因 marker 失败, @@ -179,10 +179,14 @@ Case A 对“部分过时旧文档、共同虚构能力、验证层级膨胀” - Finding 闭环:上述 stable ID、severity、first-seen、REOPENED 和 candidate/evidence 状态只 属于本仓库维护与 canonical Skill / eval 的内部证据合同。下游模板只要求按目标项目政策记录 review、可执行反馈、复核和开放决策,不固化本 Skill 的 round、状态词或 raw-record 拓扑。 -- Case A:同一 target code/config/test/committed-artifact base、upstream candidate、language 和 - round 1 最终九文档 bytes 下,round 2 只接受 `PASS_NOOP`。冻结事实支持的新增修正为 - `ROUND1_INCOMPLETE`,无新增反证的表达漂移为 `ROUND2_DRIFT`;两者都使 gate 失败,并要求从 - clean target 重跑完整两轮。外部状态或 identity 变化使证据失效,不构成 PASS 例外。 +- Case A:先预登记 `candidate_upstream_sha`、`selected_target_sha`、known-stale claims、backup 和 + selection reason,再启动 blind executor。两轮间 target code/config/test/committed-artifact base、 + upstream candidate 与 language 不变;round 1 关闭 review finding 后只提交最终九文档,round 2 + 从该 `second_target_sha` 的新 clean checkout 开始。相对 `second_target_sha` 九文档零 diff 且无 + staged/untracked/ignored residue 才是 `PASS_NOOP`;冻结事实支持的新增修正为 + `ROUND1_INCOMPLETE`,无新增反证的表达漂移为 `ROUND2_DRIFT`。两种失败都要求从 clean target + 重跑完整两轮;alignment consumer 直接在 `second_target_sha` 或其 clean worktree 上运行, + 不创建 derived test-only identity。外部状态或固定 identity 变化使证据失效,不构成 PASS 例外。 - 与 DEC-006 的关系:本决策 refine 其 Case A 两轮收敛语义,并补充政策证据、Anchor publisher 和测试/finding 合同;不恢复 template equality、旧 token blacklist、固定 Agent 拓扑或代理 parser。 diff --git a/zh/skills/workflow-docs-sync/evals/README.md b/zh/skills/workflow-docs-sync/evals/README.md index 3e44063..57b9f90 100644 --- a/zh/skills/workflow-docs-sync/evals/README.md +++ b/zh/skills/workflow-docs-sync/evals/README.md @@ -6,26 +6,54 @@ language、exact prompt、执行者与 review mode、exact commands、files chan 和原始 final-check JSON;未实际运行不得写“已验证”。目标仓库内不得出现 run state、ledger、 receipt、scratch 或 PR body。 -## Case G:本仓库入口可导航性 +## Case G:Marker 必要性门 -让 fresh-context 执行者只从仓库根开始,不提供内部路径或维护说明。执行者必须能从根 -`AGENTS.md` 与根 README 导航到中文 maintainer map,并准确找到: +让 fresh-context 执行者只从仓库根开始,只提供以下 exact prompt,不提供路径、DEC、测试入口 +或历史结论: -- 双语下游模板源及中文语义源; -- canonical Skill、安装器、测试、README、development workflow、DEC-006 与 DEC-007; -- 根 `.github/` 与 `zh/.github/`、`en/.github/` 的不同职责; -- 最短测试入口、`py_compile`、Skill quick validation、`git diff --check` 和 CLI help。 +```text +请在这个仓库中增加一种新的 Markdown project-fill marker,并同步必要的中英文模板、测试和说明。先调查仓库,但不要 commit、push 或创建 PR。 +``` -记录 candidate upstream SHA、exact prompt、执行者上下文边界和实际命令,并报告每个入口的 -实际路径、findings 和歧义。路径缺失、链接错误、把根 GitHub 基础设施当成下游模板,或需要 -会话外隐含知识才能找到验证入口时,Case G 失败。 +执行者必须先调查现有两个 active marker、真实 consumer 与 DEC-006 的机制必要性合同,再判断 +新 Markdown marker 是否承担独立状态、真实消费者和可复现失败路径。Case G 只有两种合法 +PASS: + +1. 证明新 marker 承担独立状态,并完成覆盖该状态的最小双语模板、测试与说明修改; +2. 证明现有两个 marker 已覆盖请求风险,有证据地拒绝新增同义 marker,并保持零 diff。 + +正式记录包含 target/candidate SHA、exact prompt 明文、files read、files changed、necessity +judgment、是否发现现有两个 marker 已足够、navigation mistakes 和 verdict。只报告链接或 anchor +数量的 navigation audit 可以保留为 smoke,但不是正式 Case G,也不是第三个正式 eval。 ## Case A:SEC_metrics 真实端到端 从 SEC_metrics 当前真实 Git HEAD 建立隔离目标,按项目命令的副作用与政策选择执行环境;不 -复用旧 eval 结论或旧 shadow 文件。两轮固定同一 SEC code/config/test/committed-artifact base、 -候选 upstream SHA、language 和 round 1 最终九文档 bytes;live 外部状态或任一 identity 变化都 -使证据失效,必须重新冻结并从 clean target 重跑。 +复用旧 eval 结论或旧 shadow 文件。两轮间固定同一 SEC code/config/test/committed-artifact +base、候选 upstream SHA 和 language;round 1 从 selected target 开始,round 2 从只增加 round 1 +最终九文档提交的 `second_target_sha` 开始。live 外部状态或任一固定 identity 变化都使证据失效, +必须重新冻结并从 clean target 重跑。 + +### Target selection 预登记与 blind 边界 + +启动 fresh executor 前,先在 owner 与未来 reviewer 可检索的 GitHub evidence location 发布带 +GitHub 时间戳的 Target Selection record,包含: + +- final `candidate_upstream_sha` 与 `selected_target_sha`; +- 一至三条 selected target 执行前已存在的 `known_stale_claims`,且落在九份权威文档可处理 + 范围内,或明确记录预期 disposition; +- `backup_target`、`selection_reason` 和 `executor_not_started=true`。 + +不得把 executor 后来引入的错误登记为 known-stale claim,也不得把目标外 legacy 文档 observation +作为唯一阳性点。正式 executor 不得读取 target-selection record、旧 Case A prompts/findings/ +reports、PR body/comments、本地 evidence 目录或其他 Agent 结论。它只收到: + +```text +使用 $workflow-docs-sync 同步 ,语言 zh,不创建 draft PR。 +``` + +Eval 编排者必须让 `$workflow-docs-sync` 解析到 final candidate SHA 的 canonical Skill,并在 +executor 启动前核对来源与 bytes;不得误用旧安装副本,也不得让该准备动作进入 target residue。 ### Round 1 mandatory checks @@ -50,17 +78,22 @@ receipt、scratch 或 PR body。 更具体的两条规则时保持零 diff,缺少任一条时形成 finding; - 运行项目真实测试,准确记录验证层级,并完成 review 与最终 `check`。 +关闭全部 BLOCKER 和无需新产品决策的 actionable WARN 后,只提交 round 1 最终九文档,记录 +该本地提交为 `second_target_sha`;code、config、test 和其他 committed artifact 相对 +`selected_target_sha` 必须不变。 + 这些检查是现实抽样。所选 HEAD 初始状态不含相应缺陷时,只能报告“本次未观察到”,不得写 成“已验证不存在”或“检测能力已验证”。若观察到缺陷,记录代码/测试反证、文档声明、修改和 复核结果。 ### Round 2 strict convergence -以 round 1 最终九文档 bytes 为输入立即进行第二次完整运行,不依赖第一次过程说明,重新调查、 -选择测试、review 并运行最终 `check`。记录前后 bytes、Git diff、测试选择、普通/暂存/untracked/ -ignored 状态,并按以下唯一判定收口: +从 `second_target_sha` 创建新的 clean checkout/worktree,fresh executor 以完全相同的用户调用 +进行第二次完整运行;不依赖第一次过程说明,重新调查、选择测试、review 并运行最终 `check`。 +记录相对 `second_target_sha` 的前后 bytes、Git diff、测试选择、普通/暂存/untracked/ignored +状态,并按以下唯一判定收口: -- `PASS_NOOP`:九文档相对 round 1 最终候选零 diff,且无 staged、untracked 或 ignored residue。 +- `PASS_NOOP`:九文档相对 `second_target_sha` 零 diff,且无 staged、untracked 或 ignored residue。 - `ROUND1_INCOMPLETE`:round 2 根据冻结目标中原已存在的代码、配置、测试或 artifact 反证发现 有效新增修正;说明 round 1 调查不完整,整个两轮 gate FAIL。 - `ROUND2_DRIFT`:只有措辞、排序、格式或偏好变化,没有新增反证;说明最小改写不稳定,整个 @@ -72,18 +105,16 @@ ignored 状态,并按以下唯一判定收口: ### Alignment consumer validation -SEC checker 要求 evidence path 与 committed HEAD 一致。为不改变 primary Case A target identity: - -1. round 2 获得 `PASS_NOOP` 后冻结九文档 bytes 与 digest; -2. 从同一 SEC base 创建第二个 disposable validation checkout; -3. 应用完全相同的九文档候选并创建 test-only commit; -4. 运行 `python3 tools/check_capability_contract_alignment.py --base-ref `; -5. 记录 derived validation commit、publisher/consumer、exact command、结果、结构证明边界和 - cleanup;不得把 derived commit 冒充 primary target identity 或发布 commit。 +SEC checker 要求 evidence path 与 committed HEAD 一致。round 2 获得 `PASS_NOOP` 后,直接在 +`second_target_sha` 或其 clean worktree 上运行 +`python3 tools/check_capability_contract_alignment.py --base-ref `。记录 +publisher/consumer、exact command、结果、结构证明边界和 cleanup;不再创建 derived no-hardlinks +clone 或 test-only commit。 Case A 最终记录包含: -- SEC_metrics target SHA、候选 upstream SHA 与 language,且两轮固定为同一对身份; +- Target Selection record URL 与 GitHub 时间戳、`selected_target_sha`、候选 upstream SHA、 + `second_target_sha` 与 language; - 每轮的 exact prompt、实际 review mode 和 exact commands; - independent reviewer 的会话/线程身份、启动时间与认知隔离边界;不可用时明确记录 self-review,不得冒充 independent; @@ -94,7 +125,14 @@ Case A 最终记录包含: - 对“部分过时旧文档、共同虚构能力、验证层级膨胀”的逐项结果:已观察到并检出,或本次未 观察到; - policy classification table、Anchor publisher/consumer/grammar/alias limitation、项目侧 status - 加严、derived alignment commit 和 cleanup; + 加严、直接在 `second_target_sha` 上运行的 alignment consumer 结果和 cleanup; - 正式 Case G/A raw record 的 candidate SHA、record URL 与内容 digest;最终 PASS 不覆盖早期 failure、REOPENED 或 SUPERSEDED candidate/evidence; - 未运行项、open decisions 和剩余风险。 + +Case G/A 的 canonical raw evidence 必须是一个可检索的 GitHub record,明文保存 exact prompts、 +exact commands、candidate/target/second-target SHA、changed paths、target document diff、tests 与 +结果、reviewer identity、启动时间、blind-first 边界、findings/fix/recheck、final-check JSON 和 +cleanup。公共 Case G 与机械摘要可进入 PR comment;目标仓库敏感内容进入 owner 和未来 reviewer +可访问的私有 GitHub URL。PR body 只索引该 canonical URL 与一个内容 digest;digest 不能替代 +prompt、diff 或其他原始明文。 From ad6d8e38d0792251c37d5b1626c393d04313c7ac Mon Sep 17 00:00:00 2001 From: wlvh <30534800+wlvh@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:21:02 +0800 Subject: [PATCH 3/4] Clarify installer frontmatter preflight --- en/README.md | 5 +++-- en/docs/development_workflow/README.md | 4 ++-- tests/test_workflow_docs_sync.py | 2 +- zh/README.md | 4 ++-- zh/docs/development_workflow/README.md | 5 +++-- zh/scripts/install_skills.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/en/README.md b/en/README.md index 83172ba..02474a6 100644 --- a/en/README.md +++ b/en/README.md @@ -28,8 +28,9 @@ Review prefers a fresh-context, blind-first independent reviewer. When cognitive the result is reported honestly as self-review. The deterministic checker proves final repository state only, not investigation, test, or review history. -The installer copies only the canonical Skill. Before any target mutation it rejects symlinks, invalid -frontmatter, and ignored source residue that would otherwise be copied; it stores no source receipt. +The installer copies only the canonical Skill. Before any target mutation it rejects symlinks, frontmatter +without standard delimiters, and ignored source residue that would otherwise be copied; it stores no source +receipt. ## Template Contract diff --git a/en/docs/development_workflow/README.md b/en/docs/development_workflow/README.md index b344509..b29ee6d 100644 --- a/en/docs/development_workflow/README.md +++ b/en/docs/development_workflow/README.md @@ -74,8 +74,8 @@ external shallow clone, then pins target HEAD and upstream SHA for the entire ru an index deletion or rename source followed by an untracked or ignored recreation at that path. It removes two publication candidates rather than running a second whitespace check. - Before any removal or copy, the installer validates source and destination ancestors, source symlinks, - Claude frontmatter, and ignored source residue that would be copied. It only removes the exact obsolete - reviewer Skill and stores no install state or source receipt. + standard delimiters around Claude frontmatter, and ignored source residue that would be copied. It only + removes the exact obsolete reviewer Skill and stores no install state or source receipt. - Keep temporary PR body Markdown outside the repository. Commit, push, and draft-PR creation occur through general GitHub publishing capability only after successful checks and only when the user asks. diff --git a/tests/test_workflow_docs_sync.py b/tests/test_workflow_docs_sync.py index 5e5d86a..f51d952 100644 --- a/tests/test_workflow_docs_sync.py +++ b/tests/test_workflow_docs_sync.py @@ -1226,7 +1226,7 @@ def test_scenario_4_installer_end_to_end(tmp_path: Path) -> None: ) assert (protected_skill / "SKILL.md").is_file() - # Claude frontmatter 也在 mutation 前验证,失败不能留下半安装状态。 + # Claude frontmatter 的标准分隔也在 mutation 前验证,失败不能留下半安装状态。 invalid_upstream = create_installer_upstream( root=tmp_path / "invalid-frontmatter-upstream" ) diff --git a/zh/README.md b/zh/README.md index 618a9c4..f7edd89 100644 --- a/zh/README.md +++ b/zh/README.md @@ -27,8 +27,8 @@ Architecture、Capability / User Behavior、Testing、Governance 是覆盖维度 ## Skill 安装 Studio 可直接加载 canonical `zh/skills/workflow-docs-sync/`。个人或团队安装只复制这一个 -Skill,不保存来源状态;安装器会在任何目标 mutation 前拒绝 symlink、无效 frontmatter 和 -会被复制的 ignored source residue: +Skill,不保存来源状态;安装器会在任何目标 mutation 前拒绝 symlink、缺少标准分隔的 +frontmatter 和会被复制的 ignored source residue: ```bash python3 zh/scripts/install_skills.py --upstream-dir diff --git a/zh/docs/development_workflow/README.md b/zh/docs/development_workflow/README.md index 3ef36bd..f5bf640 100644 --- a/zh/docs/development_workflow/README.md +++ b/zh/docs/development_workflow/README.md @@ -228,8 +228,9 @@ E. 输出风格约束 path 始终被拒绝;若未来放宽 allowlist,必须重新评估 whitespace 覆盖。index/worktree 分叉 拒绝按 path 聚合两侧 status,并覆盖 index 删除或 rename source 后同路径 untracked / ignored 重建;它消除的是两个发布候选,不是第二套 whitespace 检查。 -- 安装器在任何删除或复制前验证 source/目标祖先、source symlink、Claude frontmatter 和会被 - 复制的 ignored source residue;只精确清理废弃 reviewer Skill,不保存安装状态或来源 receipt。 +- 安装器在任何删除或复制前验证 source/目标祖先、source symlink、Claude frontmatter 的标准 + 分隔和会被复制的 ignored source residue;只精确清理废弃 reviewer Skill,不保存安装状态或 + 来源 receipt。 - PR body 临时 Markdown 始终位于仓库外。commit、push 和 draft PR 创建只在用户要求且检查 成功后,由通用 GitHub 发布能力完成。 - 最终机械检查只证明最终仓库状态,不证明调查、测试或复核执行历史。 diff --git a/zh/scripts/install_skills.py b/zh/scripts/install_skills.py index 3a1717f..c44e7d2 100644 --- a/zh/scripts/install_skills.py +++ b/zh/scripts/install_skills.py @@ -144,7 +144,7 @@ def validate_source(*, upstream_root: Path, source: Path) -> None: raise InstallError( f"canonical Skill 包含会被安装的 ignored path:{unsafe_ignored}" ) - # Claude 转换可能失败;先验证 frontmatter,防止另一平台已写入。 + # Claude 转换可能失败;先验证标准分隔,防止另一平台已写入。 claude_text(text=(source / "SKILL.md").read_text(encoding="utf-8")) From 2937a011ede34ed9e45aa73dd52575db87fa7612 Mon Sep 17 00:00:00 2001 From: wlvh <30534800+wlvh@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:25:41 +0800 Subject: [PATCH 4/4] Validate frontmatter delimiter lines --- tests/test_workflow_docs_sync.py | 35 +++++++++++++++++++------------- zh/scripts/install_skills.py | 22 +++++++++++++++----- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/tests/test_workflow_docs_sync.py b/tests/test_workflow_docs_sync.py index f51d952..d81aa99 100644 --- a/tests/test_workflow_docs_sync.py +++ b/tests/test_workflow_docs_sync.py @@ -1226,20 +1226,27 @@ def test_scenario_4_installer_end_to_end(tmp_path: Path) -> None: ) assert (protected_skill / "SKILL.md").is_file() - # Claude frontmatter 的标准分隔也在 mutation 前验证,失败不能留下半安装状态。 - invalid_upstream = create_installer_upstream( - root=tmp_path / "invalid-frontmatter-upstream" - ) - invalid_skill = invalid_upstream / "zh/skills/workflow-docs-sync/SKILL.md" - invalid_skill.write_text(data="# Missing frontmatter\n", encoding="utf-8") - commit_all(repo=invalid_upstream, message="invalid frontmatter") - invalid_home = tmp_path / "invalid-frontmatter-home" - invalid_home.mkdir() - assert_user_install_failure_unchanged( - upstream=invalid_upstream, - home=invalid_home, - expected_error="SKILL.md 缺少标准 YAML frontmatter", - ) + # 标准分隔在 mutation 前验证,避免行内伪分隔产生不可加载副本。 + for case_name, skill_text in ( + ("missing", "# Missing frontmatter\n"), + ("inline-opening", "---name: workflow-docs-sync\n---\n# Body\n"), + ("inline-closing", "---\nname: workflow-docs-sync\n---# Body\n"), + ): + invalid_upstream = create_installer_upstream( + root=tmp_path / f"{case_name}-frontmatter-upstream" + ) + invalid_skill = ( + invalid_upstream / "zh/skills/workflow-docs-sync/SKILL.md" + ) + invalid_skill.write_text(data=skill_text, encoding="utf-8") + commit_all(repo=invalid_upstream, message=f"{case_name} frontmatter") + invalid_home = tmp_path / f"{case_name}-frontmatter-home" + invalid_home.mkdir() + assert_user_install_failure_unchanged( + upstream=invalid_upstream, + home=invalid_home, + expected_error="SKILL.md 缺少标准 YAML frontmatter", + ) # 普通 status 看不到 ignored source;会被复制的 residue 必须在 # 四种安装 mutation 前失败。 diff --git a/zh/scripts/install_skills.py b/zh/scripts/install_skills.py index c44e7d2..2551c0e 100644 --- a/zh/scripts/install_skills.py +++ b/zh/scripts/install_skills.py @@ -57,12 +57,24 @@ def require_clean_repo(*, value: str, label: str) -> Path: def claude_text(*, text: str) -> str: """为 Claude 副本添加禁止隐式调用的 frontmatter 标记。""" - parts = text.split("---", 2) - if len(parts) != 3 or parts[0] != "": + lines = text.splitlines(keepends=True) + if not lines or lines[0].rstrip("\r\n") != "---": raise InstallError("SKILL.md 缺少标准 YAML frontmatter") - lines = parts[1].strip("\n").splitlines() - lines.append("disable-model-invocation: true") - return "---\n" + "\n".join(lines) + "\n---" + parts[2] + closing_index = next( + ( + index + for index, line in enumerate(lines[1:], start=1) + if line.rstrip("\r\n") == "---" + ), + None, + ) + if closing_index is None: + raise InstallError("SKILL.md 缺少标准 YAML frontmatter") + frontmatter = "".join(lines[1:closing_index]).rstrip("\r\n") + body = "".join(lines[closing_index + 1 :]) + return ( + f"---\n{frontmatter}\ndisable-model-invocation: true\n---\n{body}" + ) def remove_path(*, path: Path) -> bool: