diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d120ee5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# 仓库维护入口 + +本仓库发布双语下游工作流模板、canonical `workflow-docs-sync` Skill 和安装器。 + +- `zh/` 是中文语义源;`en/` 是从中文派生的模板与说明。 +- 根 `.github/` 是本仓库 CI 与 GitHub 基础设施;`zh/.github/`、`en/.github/` 是下游模板源。 +- 修改模板:先改 `zh/` 对应九份核心文件,再同步 `en/`。 +- 修改 Skill:进入 `zh/skills/workflow-docs-sync/`。 +- 修改安装器:进入 `zh/scripts/install_skills.py`。 +- 修改入口说明:更新根 README、`zh/README.md` 与 `en/README.md`;详细工作流在 + `zh/docs/development_workflow/`,实现决策在其 `decisions.md`。 +- 修改测试:进入 `tests/test_workflow_docs_sync.py`;具体约束以测试代码为准,不在此复制。 + +最短验证入口: + +```bash +python3 -m pytest -q +``` + +完整维护地图与补充验证见 [zh/README.md](zh/README.md#维护者地图)。 diff --git a/README.md b/README.md index ca71862..4b149d8 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,20 @@ [中文](zh/README.md) | English -This repository publishes bilingual workflow-document templates and the -`workflow-docs-sync` Skill that specializes them against a real target repository. +This repository publishes bilingual downstream workflow-document templates, the canonical +`workflow-docs-sync` Skill, and its installer. Chinese under `zh/` is the semantic source; `en/` is derived. -## Quick Start - -Invoke the Skill once. Provide only the target repository, optional language -(`zh` or `en`), and whether to create a draft PR after successful validation. +Invoke the Skill once with only the target Git repository, `zh` or `en`, and whether a draft PR should be +created after success: ```text Use $workflow-docs-sync for /absolute/path/to/repository in English. Do not create a draft PR. ``` -The main agent is the only workspace writer. Four domain analyses and the -adversarial audit are read-only; the final checker validates repository state, -not execution history. Upstream checkout and commit resolution are internal. - -## Directory Map - -- [en/](en/): English templates and workflow documentation. -- [zh/](zh/): Chinese source templates, prompts, decisions, and Skill implementation. -- [zh/skills/workflow-docs-sync/](zh/skills/workflow-docs-sync/): single-session orchestration, read-only analysis references, and the deterministic checker. -- [tests/test_workflow_docs_sync.py](tests/test_workflow_docs_sync.py): sync and installer regression tests. - -## Install Path Rule - -The `zh/` and `en/` directories are upstream source prefixes. The Skill strips -only the selected leading language directory and installs the remaining paths at -the target repository root. Inner paths such as `.github/` remain unchanged. +The repository root contains CI and GitHub infrastructure. Downstream `.github/` templates live under the +language directories and are installed without that leading language prefix. -Chinese remains the source-of-truth workflow. English is its derived language -path; unfinished English coverage must be identified explicitly as `en-pending`. +For source ownership, edit locations, and validation commands, use the +[Chinese maintainer map](zh/README.md#维护者地图). English template and directory guidance is available in +[en/README.md](en/README.md). diff --git a/en/.github/pull_request_template.md b/en/.github/pull_request_template.md index cc8edd8..836d62b 100644 --- a/en/.github/pull_request_template.md +++ b/en/.github/pull_request_template.md @@ -1,114 +1,50 @@ ## 1. Background and Goal ---- + -## 2. Implementation +## 2. Implementation and Tradeoffs - - ---- - -## 3. Change Scope - - + -| File / Directory | Change Type | Notes | -|---|---|---| -| | Added / Modified / Deleted | | +## 3. Actual Change Scope ---- + ## 4. Documentation Impact - - -Affected documents: - -- None - -Notes: - -- - ---- - -## 5. User and Architecture Impact - -User-visible change: - -- Yes / No -- Notes: - -Architecture change: - -- Yes / No -- Notes: - ---- - -## 6. Review / Fix Record - - + -| Round | Source | Issue Summary | Judgment | Result | Evidence | -|---|---|---|---|---|---| -| R0 | Initial submission | N/A | N/A | Initial implementation | | -| R1 | Codex / Claude / Human | | Real / Invalid / Defer | Fixed / Won't fix / N/A | | +## 5. User-visible and Architecture Impact ---- + -## 7. Known Limits and Rollback +## 6. Testing Evidence -Known limits: +- Exact command: Record the command verbatim, or write `Not run`. +- Scope: State the layer, entrypoint, and boundary the command actually proves. +- Result: Record pass, failure, skip, and important counts or errors. +- Not-run reason: Write `Not applicable` when run; otherwise give the concrete reason and risk. +- Environment: Record the actual execution environment, isolation method, side effects, and cleanup result. -- +## 7. Review / Fix Record -Rollback: + -- +## 8. Known Limits, Open Decisions, and Rollback ---- + -## 8. Final Self-Check +## 9. Final Self-check -- [ ] Current branch is not the main branch. -- [ ] Ran `git diff --name-only ...HEAD`. -- [ ] Change Scope matches the actual diff. -- [ ] PR body contains no stale drafts, old branch names, or unimplemented plans. -- [ ] Testing and test evidence follow `TESTING.md`. -- [ ] User-visible changes were checked against `interact.md`. -- [ ] Architecture changes were checked against `architecture.md`. -- [ ] Every review / fix round is recorded in "Review / Fix Record". +- [ ] 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. diff --git a/en/AGENTS.md b/en/AGENTS.md index 0fae0b8..0c8a03a 100644 --- a/en/AGENTS.md +++ b/en/AGENTS.md @@ -1,72 +1,95 @@ -## File Overview +# Agent Working Guide -### Core Config +## Authority Map -- `AGENTS.md`: Agent entrypoint, file overview, coding rules, and user-facing document relationships. -- `capability_contract.json`: Cross-project sample registry for capability boundaries, responsibility boundaries, and agent behavior commitments. -- `.github/pull_request_template.md`: Long-term PR body template used to draft local `PR_BODY.md`. +- Current code, configuration, tests, committed artifacts, and reproducible results are project facts. +- `architecture.md` defines system structure and boundaries; `TESTING.md` defines test entrypoints and + evidence levels; `PR_Checklist.md` defines delivery checks; `SOP.md` keeps only stable process entrypoints. +- `capability_contract.json` defines capability boundaries, `interact.md` defines user-visible behavior and + acceptance, and `docs/business_user_guide.md` only derives explanations from the first two. +- Existing documents are claims to verify against implementation; they cannot prove themselves correct. -### Core Modules + -### Business Logic +## Repository Overview -### Notes +Describe stable modules, entrypoints, and responsibilities. Do not permanently mirror `git ls-files`. -- When files are added or changed, update this file overview where relevant. Test files are governed by `TESTING.md`. -- All repository files use UTF-8. Command-line reads and edits must explicitly use UTF-8. -- One-off artifact folders are exempt from this overview when explicitly approved. +### Core Configuration -## Architecture + + +### Runtime Entrypoints -The authoritative architecture document is `architecture.md`. If a change affects module boundaries, runtime call flow, data flow, state model, error model, external dependencies, or extension points, update `architecture.md`; otherwise explain the no-update reason in the PR body. + -## Business Knowledge +### Core Modules -## Review Checklist + -When the user asks you to submit a PR, fully follow `PR_Checklist.md`; any exemption must be explained in the PR description. +### Domain Logic -## Testing Flow + -Before fixing bugs, starting tests, or submitting a PR, read and follow `TESTING.md`. Main branch changes must go through PR merge only. +### Generated Artifacts and External State -## SOP + + +## Change Impact Rules -When you execute a standard process, read and follow `SOP.md`; when SOP entries are added or changed, update this list with the SOP name. +- Update or confirm `architecture.md` when module boundaries, runtime call flow, data flow, state, error + model, external dependencies, or extension points change. +- For capability changes, update or confirm `capability_contract.json` first, then inspect `interact.md` + 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. +- Derive encoding, lint, formatter, build, and type rules from real repository configuration, not this + template. -## User-Facing Document Relationships +## Collaboration -This project distinguishes three user-facing sources of truth: +- The primary executor owns final judgments, deliverables, and writes; delegated results must be reviewed + and synthesized before use. +- Assign non-overlapping path ownership before parallel writes; follow the target project's policy for the + isolation method. +- Divide work dynamically by module, call flow, risk, or evidence type; do not require a fixed agent count + or schedule. +- Agreement, voting, or consensus is not evidence. Important conclusions must trace to repository facts and + reproducible validation. +- Investigation and review tasks are read-only by default; when changes are needed, hand them off explicitly + to an executor who owns the affected paths. -1. `capability_contract.json` - - Machine-readable source for capability boundaries. - - Answers what the system can do, cannot do, must ask about, or must refuse. + -2. `interact.md` - - Source for user-visible behavior and acceptance invariants. - - Answers how the system must behave and what acceptance means. +## Architecture + +Treat `architecture.md` as the architecture authority. Rebuild affected call paths from real entrypoints +before a change, then verify invariants, module responsibilities, data contracts, state, side effects, and +failure paths afterward. + +## Testing + +Read `TESTING.md` completely before testing and derive exact commands from repository configuration. Do not +present light, mock, golden, or local repair success as a higher validation level. Choose the execution +environment from command side effects, CI capabilities, and project policy, with isolation and cleanup +verified before execution. + +## SOP -3. `docs/business_user_guide.md` - - Derived teaching document for first-time business users. - - Answers what business users can ask, how to ask, how to read results, and when to ask a human. - - It must not declare independent capabilities; it only explains capabilities and behavior already declared in `capability_contract.json` and `interact.md`. +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. -Update rules: +## PR Delivery -- If capability boundaries change, update or confirm `capability_contract.json` first, then check `interact.md` and `docs/business_user_guide.md`. -- If user-visible behavior changes, update or confirm `interact.md` first, then check `docs/business_user_guide.md`. -- If business-user questions, prompts, result interpretation, or escalation guidance change, check `docs/business_user_guide.md`. -- Any "can do / cannot do / must ask / must refuse" statement in `docs/business_user_guide.md` must anchor to `capability_contract.json`, `interact.md`, or tests. +- Follow `PR_Checklist.md` and `.github/pull_request_template.md`; write delivery facts from the actual Git + diff, test output, and final repository state. +- Resolve the default branch from the repository instead of hardcoding it. Follow target-project policy for + PR body draft location and publishing; never commit temporary drafts accidentally, and keep the body + consistent with the real diff and test evidence. +- Do not commit, push, or create a PR unless the user explicitly requests it. -## Coding Rules +## Project-specific Conventions -1. What I cannot create, I do not understand. -2. Use the project's selected working language; answer in English unless user or project instructions require otherwise. Code volume is a liability after functionality is met; keep code as small as possible, then optimize for maintainability. -3. Follow PEP 8 for Python. Use UTC time. Use UTF-8 text. -4. Manage parameters centrally. Always call functions with explicit parameter names instead of relying on positional defaults. Do not use `get` for expected parameters; fail fast when a required parameter is missing. -5. Every class and function needs a docstring. Each functional block inside a function needs comments explaining why, expected output, and parameter meaning, range, and format. Script-level docstrings must explain purpose and call relationships. -6. Do not let `try/except` or `if/else` blocks run naked. Use explicit exception types and enough print/log information in `except` and `else` branches. Unexpected errors should fail inside the current function. -7. Data in, data out: scripts, functions, and modules interact only through explicit data inputs and outputs, not hidden external state. -8. Reused code blocks should be wrapped into functions or modules to keep code DRY. -9. Use language features to reduce code and performance cost while keeping readability. -10. Do everything necessary to help the user reach the goal. + diff --git a/en/PR_Checklist.md b/en/PR_Checklist.md index 76618bd..1a5e0e8 100644 --- a/en/PR_Checklist.md +++ b/en/PR_Checklist.md @@ -1,52 +1,55 @@ # PR Submission Checklist -## Commit / Push Strategy - -This project defaults to "one PR, one commit + PR body records review and fix rounds." - -Goals: - -- Keep public PR history easy to review. -- Avoid making reviewers or LLMs reason from commit timeline noise. -- Use the PR body's Review / Fix Record as the durable review history. - -Rules: - -1. One PR should normally keep one commit. -2. After every review / fix round, update the PR body's Review / Fix Record first. -3. Merge fixes into the current commit with `git commit --amend`. -4. Push rewritten PR branches with `git push --force-with-lease`; never use bare `git push --force`. -5. Use `.github/pull_request_template.md` as the PR body template. -6. `.github/pull_request_template.md` is a long-term template file; do not submit it directly as the PR body. -7. `PR_BODY.md` is a local temporary PR body draft generated from the template. It is not committed and is important review input. - -## Capability Contract and User Documentation Sync - -- [ ] If this PR changes `capability_contract.json`, check whether `interact.md` and `docs/business_user_guide.md` need matching updates. If not, explain why in the PR body. -- [ ] If this PR changes "can do / cannot do / must ask / must refuse" claims in `interact.md` or `docs/business_user_guide.md`, confirm those claims anchor to `capability_contract.json`, `interact.md`, or tests. -- [ ] If this PR adds agent behavior commitments such as "must ask", "must refuse", "must not guess", or "must degrade", register a stable `anchor_id` in `capability_contract.json` and add test evidence or an explicit untestable reason. -- [ ] If this PR changes what business users can ask, how they ask, how they read results, or when they ask a human, check `docs/business_user_guide.md`. - -## PR Submission Steps - -Convert this checklist into a step-by-step todo list before submitting a PR. - -Use this fixed flow to avoid submitting the long-term template as the PR body: - -```bash -cp .github/pull_request_template.md PR_BODY.md -# Fill PR_BODY.md -gh pr create --title "" --body-file PR_BODY.md --head <feature-branch> --base master -``` - -Before submitting: - -- [ ] Write a structured summary with background / goal, implementation, and change scope. -- [ ] Confirm the current branch is not the main branch. -- [ ] Inspect local changes with git diff and confirm no intended file is missing. -- [ ] Use `git diff --name-only <base>...HEAD` to verify the PR body's change scope. -- [ ] Use `TESTING.md` as the testing authority: decide whether tests need changes, which tests to run, and how to record evidence. -- [ ] If tests were added or changed, update the test file overview or relevant testing notes in `TESTING.md`. -- [ ] If files were added or changed, update the matching document overview where required. -- [ ] If user-visible behavior changed, update `interact.md` and ensure acceptance evidence covers it. -- [ ] Before the final commit, compare `git diff --name-only <base>...HEAD`, `git status`, and `PR_BODY.md` to ensure the PR body contains no stale drafts, local-only changes, or unimplemented plans. +This file is the pre-submission todo. Check only items proven by the current diff, test output, or repository +state; record a reason when an item does not apply. Do not commit, push, or create a PR unless the user +explicitly requests it. + +<!-- project-fill: Add project-specific approval, commit, base/head, or release gates. Remove this marker when no project-specific rule applies. --> + +## Scope and Git State + +- [ ] Resolve `<base>` from the repository default branch and confirm the current and target branches. +- [ ] Inspect `git status`, working-tree diff, staged diff, and `git diff --name-only <base>...HEAD`. +- [ ] Ensure actual scope matches delivery notes and excludes local drafts, secrets, generated debris, and + unimplemented plans. +- [ ] Treat a one-commit policy as a replaceable team default when the project uses one; otherwise follow + current repository policy. Rewriting remote history requires explicit authority and lease protection. + +## Tests and Evidence + +- [ ] Select real commands from `TESTING.md` and current repository configuration; do not infer a runner or + service from a template. +- [ ] For each test, record exact command, scope, result, not-run reason, actual environment, and isolation + method. +- [ ] Ensure environment choice follows command side effects, CI capabilities, and project policy, with + verifiable records for writes, external state, residue, and cleanup. +- [ ] Describe failures, skips, and validation level accurately; do not present light, golden, or repair + evidence as full validation. + +## 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. +- [ ] 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. +- [ ] Check architecture impact across entrypoints, module boundaries, data flow, state, error models, + external dependencies, artifacts, and side effects. +- [ ] Replace or delete every active project-fill marker while preserving valid Markdown and JSON. + +## Review Closure + +- [ ] 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. +- [ ] Rerun affected tests and mechanical checks after fixes, then recheck the final diff and Git state. + +## PR Delivery + +- [ ] Write only completed facts in the PR body and use `.github/pull_request_template.md` for structure. +- [ ] Follow target-project policy for the PR body draft location, publishing tool, and commit treatment; + never commit a temporary draft accidentally, and keep the body consistent with the real diff and test + evidence. +- [ ] Use `<base>` or the repository default branch instead of hardcoding a branch name. +- [ ] Create a draft PR only when requested, and reconfirm title, base, head, body, and actual diff first. diff --git a/en/README.md b/en/README.md index a8dce5d..3828ab2 100644 --- a/en/README.md +++ b/en/README.md @@ -2,45 +2,75 @@ [中文](../zh/README.md) | English -This directory contains the English workflow-document templates. The canonical -single-session Skill is shared from `zh/skills/workflow-docs-sync/` and reads the -selected English templates from the pinned upstream commit. +This directory provides English templates derived from the Chinese semantic source. The templates are +language-, framework-, and project-neutral. The canonical `workflow-docs-sync` Skill remains under +`zh/skills/workflow-docs-sync/` and reads English templates from the pinned upstream commit. ## Quick Start -Invoke the Skill once and provide only the target repository, optional language, -and optional draft-PR intent: +Invoke the Skill once with the target Git repository, `zh` or `en`, and whether to create a draft PR after +success: ```text -Use $workflow-docs-sync for `/absolute/path/to/repository` in English. +Use $workflow-docs-sync for /absolute/path/to/repository in English. Do not create a draft PR. ``` -The main agent is the only target-workspace writer. Architecture, capability and -user behavior, testing, governance, and adversarial audit analysis are read-only. +The Skill pins target HEAD and upstream SHA, reconstructs facts from current code, configuration, tests, +committed artifacts, reproducible results, and necessary Git history, then makes only the document changes +those facts require. Existing documents and upstream templates are hypotheses, not evidence. + +Architecture, Capability / User Behavior, Testing, and Governance are coverage dimensions, not a fixed +agent topology. The main agent is the only target-workspace writer. Test environments follow actual +commands, side effects, CI capabilities, and project policy. + +Review prefers a fresh-context, blind-first independent reviewer. When cognitive isolation is unavailable, +the result is reported honestly as self-review. The deterministic checker proves final repository state +only, not investigation, test, or review history. + +## Template Contract + +Markdown project-fill slots use `<!-- project-fill: ... -->`; JSON uses strings prefixed with +`__PROJECT_FILL__:`. Target projects replace or delete every active marker before final `check`. Templates +at a pinned source object retain at least one active marker in every non-PR file; the PR template is exempt. +Templates do not assume a programming language, framework, test runner, service, or default branch. + +## Maintainer Map + +- Downstream templates: edit the nine core files under `zh/` as the Chinese semantic source, then derive the + matching `en/` paths. +- Canonical Skill: `../zh/skills/workflow-docs-sync/`. +- Installer: `../zh/scripts/install_skills.py`. +- README entrypoints: the root README is a summary, `../zh/README.md` is the Chinese maintainer entry, and + this file is derived from it. +- Development workflow and decisions: `../zh/docs/development_workflow/`; the English overview is under + `docs/development_workflow/README.md`. +- Scenario tests: `../tests/test_workflow_docs_sync.py`; test code owns the detailed constraints. +- GitHub paths: root `.github/` serves this repository, while `.github/` here and under `zh/` are downstream + template sources. + +The shortest entrypoint is `python3 -m pytest -q`. Full closure commands are maintained in the +[Chinese maintainer map](../zh/README.md#维护者地图). ## Directory Map -- [AGENTS.md](AGENTS.md): agent entrypoint, rules, and document relationships. -- [architecture.md](architecture.md): architecture and system-boundary template. -- [capability_contract.json](capability_contract.json): machine-readable capability contract. -- [interact.md](interact.md): user-visible behavior and acceptance template. -- [TESTING.md](TESTING.md): testing strategy and evidence template. -- [PR_Checklist.md](PR_Checklist.md): general PR submission template. -- [SOP.md](SOP.md): standard-process entrypoint template. -- [.github/pull_request_template.md](.github/pull_request_template.md): downstream PR body template. -- [docs/business_user_guide.md](docs/business_user_guide.md): business-user teaching template. +- [AGENTS.md](AGENTS.md): authority entrypoint, stable module map, and impact rules. +- [architecture.md](architecture.md): system purpose, call flows, boundaries, state, and side effects. +- [capability_contract.json](capability_contract.json): capability, boundary, responsibility, and behavior + anchors. +- [interact.md](interact.md): user-visible behavior and acceptance. +- [docs/business_user_guide.md](docs/business_user_guide.md): first-use business guide. +- [TESTING.md](TESTING.md): test entrypoints, layers, isolation, and evidence. +- [PR_Checklist.md](PR_Checklist.md): general PR todo and target-project publishing-policy boundary. +- [SOP.md](SOP.md): stable standard-process entrypoints. +- [.github/pull_request_template.md](.github/pull_request_template.md): long-term PR body structure. - [docs/development_workflow/README.md](docs/development_workflow/README.md): English workflow overview. - [../zh/skills/workflow-docs-sync/](../zh/skills/workflow-docs-sync/): canonical Skill implementation. -## Install Path Rule - -`en/` is an upstream source prefix, not a target directory. Sync strips only the -leading `en/` and preserves the rest of each path: +## Install and Language Boundary -- `en/AGENTS.md` -> `<target>/AGENTS.md` -- `en/docs/business_user_guide.md` -> `<target>/docs/business_user_guide.md` -- `en/.github/pull_request_template.md` -> `<target>/.github/pull_request_template.md` +`en/` is an upstream source prefix, not a target directory. Sync removes only the leading `en/` and keeps +the remaining path, including `.github/`. -Chinese remains the workflow source of truth. English is derived; unfinished -coverage must be marked `en-pending` instead of being presented as ready. +Chinese is the semantic source and English is derived. Bilingual content changed by a PR closes in that PR; +translation status in unchanged historical decisions remains a historical record. diff --git a/en/SOP.md b/en/SOP.md index f9af1b7..e8fb130 100644 --- a/en/SOP.md +++ b/en/SOP.md @@ -1,12 +1,31 @@ -# Guidance +# Standard Operating Procedures -When asked to execute an SOP, convert the required SOP steps into a todo list and complete them step by step. +## Purpose and Authority -## SOP Principles +`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. -- Principle A: Every SOP step uses the same structure: what to do, where to look for the authority, and how to verify completion. Do not repeat full rules inside the SOP. -- Principle B: SOPs do not contain volatile lists. They point to entrypoints. If stages or environment variables change, update `TESTING.md` or scripts, not the SOP. -- Principle C: `SOP.md` is not a rules document. It is a process skeleton plus pointers to authoritative documents, commands, and tests. When conflicts exist, tests, contracts, and rule documents override SOP. -- Example: if `PR_Checklist.md` and `TESTING.md` already explain what to run, update, and deliver, do not repeat PR or testing details here. +## Available SOPs -## SOP 0 +<!-- project-fill: List real project SOP names and authoritative entrypoints. If none exist, replace this with None and the scope checked, then remove this marker. --> + +## SOP Entry Structure + +Every SOP step contains only: + +1. Action: the stable action to perform. +2. Authority / Source: the authoritative entrypoint to read without copying volatile details. +3. Acceptance: how current tests, artifacts, or observable results prove completion. + +## Failure, Rollback, and Escalation + +On failure, stop at a safe boundary and preserve the exact error and current repository state. Rollback +must match real persistence and side effects. Escalate missing authority, product decisions, or external +coordination to the responsible person instead of guessing or bypassing the boundary. + +<!-- project-fill: Add verified stop conditions, recoverable rollback entrypoints, and escalation ownership. If no project-specific rules exist, write None with a verified reason, then remove this marker. --> + +Use LF line endings and UTF-8 for text files unless repository configuration explicitly requires another +format. diff --git a/en/TESTING.md b/en/TESTING.md index af07fff..d25d072 100644 --- a/en/TESTING.md +++ b/en/TESTING.md @@ -1,88 +1,86 @@ -# Testing Flow +# Testing -Read and follow this guide before submitting any PR or running regression tests. Unless stated otherwise, run commands from the repository root and prefer the project-provided test runner when one exists. +## 0. Canonical Test Entrypoints -## Testing Philosophy +Derive every exact command from current repository scripts, task configuration, CI, build files, or test +framework configuration, and verify it from the repository root or a recorded working directory. Do not +infer a language, runner, service, or phase from this template. -- Reject tests written only for the sake of tests. Tests should verify behavior and contracts, not implementation trivia. -- Avoid redundancy. If an end-to-end or scenario test already covers a behavior, do not duplicate it with a mock-only unit test unless it gives a much faster feedback loop or covers an edge case the live test cannot cover. -- Keep the suite lean. Regularly remove obsolete tests that no longer provide value. -- Keep tests deterministic. Except for explicit live monitors, tests should not depend on changing production data. -- Keep tests isolated. One test must not depend on another test's order or leftover state. -- Make failures diagnosable. Assertions should explain what was expected and what was observed. -- Keep this document current when test files are added or changed. +<!-- project-fill: List executable test commands, working directories, environment prerequisites, and scope from the current repository, then remove this marker. --> -## Capability Contract Alignment Tests +## 1. Testing Philosophy -When a project uses `capability_contract.json`, `interact.md`, or `docs/business_user_guide.md`, provide a lightweight contract alignment test such as `tests/.../test_capability_contract_alignment.py`. +- Test behavior, contracts, and failure boundaries without locking in implementation details that have no + user value. +- Each new test should cover a real gap. A higher-level test does not automatically make a fast, + diagnosable lower-level regression redundant. +- Use fixed inputs by default instead of changing production data. Mark live tests with their external + dependencies and risks. +- Keep tests isolated from order and residual state, and make failures explain expected versus actual + behavior. +- Record scope and reason accurately when a test is not run, skipped, or limited to static checks; do not + infer a pass. -The test does not verify business logic. It verifies that machine-readable capability contracts and user-readable claims have not obviously drifted. +## 2. Test Layers and What Each Proves -### anchor_id Extraction +- **Unit**: proves local behavior of one function, class, or module under isolated input. +- **Contract**: proves a public schema, interface, file format, or cross-module agreement. +- **Scenario**: proves a user or caller path through multiple real components. +- **Golden**: proves reviewed output for deterministic input; it does not alone prove external systems or + the complete runtime chain. +- **Report build**: proves a report or deliverable can be generated; it does not automatically prove + business correctness. +- **Repair validation**: proves a repaired artifact satisfies a specific gate; it does not prove every + upstream phase is correct. +- **Light review**: proves only the limited scope its implementation checks; never describe it as full + validation. +- **Full validation**: use this name only when the complete target path, dependencies, and acceptance + boundary are actually covered. +- **Live**: proves one run against real external dependencies; record environment, time sensitivity, and + reproducibility risk. -Alignment tests should recursively scan the full `capability_contract.json` tree for objects containing `anchor_id`. Do not hardcode JSON paths, bucket names, array indexes, or the current schema hierarchy. +## 3. Capability Contract Alignment -### Markdown Anchor Syntax +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 +check uniqueness and Markdown references without hardcoding buckets, JSON paths, array positions, or +requiring every contract entry to appear in the business guide. -All user-readable documents must reference contract anchors with this exact format: +Use `test_anchor: null` with a concrete reason for declarations without automation. Register the real test +anchor when a test exists. Before claiming an alignment test exists, verify its implementation and command +in the target repository. -```text -<!-- capability-anchor: <ANCHOR_ID> --> -``` +<!-- project-fill: Cite the target project's real alignment test, command, and scope. If it is not implemented, write Not configured and the reason, then remove this marker. --> -Rules: +## 4. Change Type to Required Evidence -- Do not use variants such as `<!-- anchor: ... -->`, `<!-- ref: ... -->`, or `<!-- contract: ... -->`. -- `<ANCHOR_ID>` must exist in `capability_contract.json`. -- Do not reference JSON paths, array indexes, or schema-internal paths. -- Alignment tests recognize only this syntax. +<!-- project-fill: Map code, configuration, schema, user behavior, artifact, and documentation changes to evidence levels using actual project risk, then remove this marker. --> -### What Tests Should Cover +## 5. Side Effects and Isolation -1. `anchor_id` uniqueness. -2. Recursive `anchor_id` extraction. -3. Valid Markdown anchor syntax. -4. No naked placeholder anchors such as `capability-anchor: TODO`. -5. Agent behavior commitments are registered. -6. Not every contract entry must appear in the business guide. -7. Teaching copy style is not a test target. -8. Alignment tests should read local files only and must not call real external services. +Before running a command, identify its write paths, external services, credentials, concurrency, ordering, +cleanup, and CI policy, then choose an environment that isolates those real side effects. The environment +may be CI, a container, a separate checkout, a remote test environment, or another project-validated +execution surface; this template does not prescribe one implementation. Record the actual environment, +isolation method, residual state, and cleanup result. -### Failure and Warning Rules +<!-- project-fill: Identify side effects, actual isolation environment, cleanup, and selection rationale for each project command, then remove this marker. --> -- A document references a missing `anchor_id`: fail. -- Duplicate `anchor_id`: fail. -- Naked TODO anchors: fail or high-priority warning. -- Untested behavior with an explicit untested reason: warn, do not fail by default. -- Contract entries missing from the business guide: do not fail unless marked as required for the guide. +## 6. Test Suite Overview -## Test Layers and Naming +Describe stable test directories, entrypoints, and responsibilities instead of permanently listing every +test file. -1. Module-level tests - - Goal: fast checks for one module's behavior. - - Naming: `<module_path>/<name>.py` and `tests/<module_path>/test_<name>.py`. - - These tests should not depend on external services. +<!-- project-fill: Summarize the real test suites, important fixtures, external dependencies, and recommended entrypoints, then remove this marker. --> -2. Contract / scenario / live tests - - Goal: validate public API contracts, full business scenarios, or external dependencies. - - Use only when the behavior cannot be covered by faster deterministic tests. +## 7. Known Gaps and Untested Reasons -## When to Add or Modify Tests +<!-- project-fill: List current coverage gaps, risk, owner, or trigger. If none are known, write None and the verified scope, then remove this marker. --> -If a code change fixes a bug not covered by existing tests, add a minimal regression test that reproduces the bug before or with the fix. After the fix, ensure the new test passes in the applicable gate. +## 8. Lessons Learned -Any behavioral code change needs test evidence. If no tests changed, explain why existing tests already cover the behavior and provide rerun evidence. +Record reusable test-decision rules supported by real failures, not incident chronology or volatile +commands. If a failure came from layers passing independently while their combination failed, keep both a +minimal regression and a scenario test that crosses the real boundary. -## Test File Overview - -## Recommended Test Gate by Change Type - -## Lessons Learned - -### Lesson Maintenance Rules - -- Add a lesson only when a real defect shows that existing testing guidance could not reliably lead to the right test strategy. -- Update existing lessons by raising abstraction level and clarifying boundaries, not by rewriting them for one implementation detail. -- Merge lessons that describe the same failure mode. -- Delete or rewrite a lesson only when a stronger rule, process, or automation fully replaces it. -- Lessons should guide future testing decisions, not preserve incident chronology. +<!-- project-fill: Add lessons supported by real failures and not replaced by stronger rules or automation. If none exist, write None, then remove this marker. --> diff --git a/en/architecture.md b/en/architecture.md index a8922a3..0970bc7 100644 --- a/en/architecture.md +++ b/en/architecture.md @@ -1,97 +1,60 @@ -# architecture.md +# Architecture -This document describes the project's system structure, module boundaries, data flow, architecture invariants, and extension points. +## 0. Scope and Update Triggers -This document does not cover: +This document is the authority for the current system structure. Update or confirm the relevant section +when runtime entrypoints, module boundaries, call flows, data contracts, state, error models, external +dependencies, authentication, configuration, artifacts, side effects, extension points, or architecture +debt change. Every claim must come from current implementation, configuration, tests, committed artifacts, +or reproducible evidence. -- Agent work rules: see `AGENTS.md` -- PR process: see `PR_Checklist.md` -- Testing strategy: see `TESTING.md` -- Capability boundaries, responsibility boundaries, and agent behavior commitments: see `capability_contract.json` -- User-visible behavior: see `interact.md` -- First-time business-user teaching: see `docs/business_user_guide.md` -- Standard operating procedures: see `SOP.md` +<!-- project-fill: Describe the system boundary, exclusions, and project-specific update triggers, then remove this marker. --> ---- - -## 0. Update Triggers - -Update this document when a change affects: +## 1. System Purpose -- Core modules added or removed -- Module responsibility boundaries -- Runtime call flow -- Data flow, data contracts, or schema -- State model -- Error handling model -- External dependencies, authentication, or configuration entrypoints -- Important extension points -- Important architecture debt or constraints +<!-- project-fill: In no more than five sentences, describe verified users, inputs, outputs, and core value, then remove this marker. --> ---- +## 2. Runtime Entrypoints and Main Flows -## 1. System Purpose +<!-- project-fill: Describe main call flows, important branches, and final outputs from real entrypoints. If there is no runtime entrypoint, write Not applicable with a verified reason, then remove this marker. --> -<!-- -Use no more than five sentences. -Explain: -- Who does this system serve? -- What are the inputs? -- What are the outputs? -- What is the core value? ---> +## 3. Architecture Invariants -This system is used for: +Each invariant should state the positive constraint, scope, falsification method, and consequence of +violation. Do not present a vision or proposed design as a current invariant. -Inputs: +<!-- project-fill: Add architecture invariants supported by code, configuration, or tests. If none exist, write Not applicable with a verified reason, then remove this marker. --> -Outputs: +## 4. Module Responsibility Boundaries -Core value: +Describe stable modules by responsibility, non-responsibility, permitted dependencies, and forbidden +dependencies. Do not copy the repository tree file by file. ---- +<!-- project-fill: Describe core module boundaries and dependency direction with precise implementation evidence, then remove this marker. --> -## 2. Architecture Invariants +## 5. Data Flow and Data Contracts -<!-- -An invariant is a system-level constraint that must remain true as the code evolves. ---> +<!-- project-fill: Describe how inputs are parsed, transformed, validated, and emitted, including schema, version, and boundary contracts. If there is no data flow, write Not applicable with a verified reason, then remove this marker. --> -### 2.1 Invariant Name +## 6. State and Persistence Model -- Positive statement: -- Negative statement: -- Scope: -- Review check: -- Automated check, if any: -- Consequence if violated: +<!-- project-fill: Describe in-process state, persistence, caches, idempotency, and lifecycle. If there is no persisted state, write Not applicable with a verified reason, then remove this marker. --> ---- +## 7. Error and Failure Model -## 3. Module Responsibility Boundaries +<!-- project-fill: Describe real validation, degradation, retry, hard-failure, rollback, and user-visible error boundaries, then remove this marker. --> -| Module / Directory | Responsibility | Non-responsibility | May Depend On | Must Not Depend On | -|---|---|---|---|---| -| | | | | | +## 8. External Dependencies, Authentication, and Configuration -### 3.1 Boundary Rules +<!-- project-fill: List real external dependencies, authentication boundaries, configuration sources, and missing-configuration behavior. If none exist, write Not applicable with a verified reason, then remove this marker. --> -- -- -- +## 9. Artifacts and Side Effects ---- +<!-- project-fill: Distinguish committed, generated, and ephemeral artifacts, and describe file, network, service, or other side effects and isolation. If none exist, write Not applicable with a verified reason, then remove this marker. --> -## 4. Main Data Flow +## 10. Extension Points and Architecture Debt -<!-- -Keep the main flow to at most three layers; complex capabilities can have their own subflow. ---> +Mark future or proposed items explicitly; do not present them as current capabilities or existing extension +points. -```mermaid -flowchart LR - Input[Input] --> Entry[Entry / Parser] - Entry --> Service[Service / Orchestration] - Service --> Core[Core Logic] - Core --> Output[Output] -``` +<!-- project-fill: List evidence-backed extension interfaces, known architecture debt, impact, and review triggers. If none exist, write None with a verified reason, then remove this marker. --> diff --git a/en/capability_contract.json b/en/capability_contract.json index eecac34..a825c6c 100644 --- a/en/capability_contract.json +++ b/en/capability_contract.json @@ -1,124 +1,107 @@ { "schema_version": "0.1.0", - "status": "sample", - "purpose": "Sample registry for stable capability, boundary, responsibility, behavior, and document anchors.", + "status": "__PROJECT_FILL__: replace with the verified contract status", + "purpose": "Machine-readable registry for current capability, boundary, responsibility, behavior, and document anchors.", "source_language": "en-US", "rules": [ - "Markdown documents reference stable anchor_id values only.", - "Markdown documents must not reference JSON paths, array indexes, or schema-specific internal paths.", - "Deprecated anchors should stay registered and must not be reused for a different meaning.", - "Untested contracts use test_anchor: null with untested_reason or pending_since instead of scattered TODO markers." + "Every fill entry must be replaced with verified project content or deleted before validation.", + "Every active statement must have evidence in the current implementation, configuration, tests, committed artifacts, or reproducible results.", + "Future or proposed behavior must use an explicit non-active status and must not be presented as currently available.", + "Markdown documents reference stable anchor_id values only, never JSON paths, array indexes, or schema-internal paths.", + "Deprecated anchors remain registered for their original meaning and must never be reused.", + "A contract without automation uses test_anchor: null and records a concrete untested_reason or pending_since value." ], "contracts": { "capabilities": [ { - "anchor_id": "CAPABILITY.sample_supported_question", + "anchor_id": "__PROJECT_FILL__:CAPABILITY.identifier", "type": "capability", - "status": "experimental", - "statement": "Sample capability placeholder for a project-supported business question type.", + "status": "__PROJECT_FILL__: replace with active, experimental, future, proposed, or deprecated", + "statement": "__PROJECT_FILL__: replace with a verified project capability", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: replace with the real audiences" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific contract and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: replace with the reason automation is absent, or replace test_anchor and remove this field", + "pending_since": "__PROJECT_FILL__: replace with the applicable date or remove this field", + "since": "__PROJECT_FILL__: replace with the first verified version or date" } ], "capability_boundaries": [ { - "anchor_id": "BOUNDARY.sample_multi_object_comparison_not_supported", + "anchor_id": "__PROJECT_FILL__:BOUNDARY.identifier", "type": "capability_boundary", - "status": "experimental", - "statement": "Sample boundary placeholder for a currently unsupported capability that may be added later.", + "status": "__PROJECT_FILL__: replace with the verified status", + "statement": "__PROJECT_FILL__: replace with a verified current limit, rejection, or degradation boundary", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: replace with the real audiences" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific boundary and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: replace with the reason automation is absent, or replace test_anchor and remove this field", + "pending_since": "__PROJECT_FILL__: replace with the applicable date or remove this field", + "since": "__PROJECT_FILL__: replace with the first verified version or date" } ], "responsibility_boundaries": [ { - "anchor_id": "RESPONSIBILITY.sample_no_final_business_decision", + "anchor_id": "__PROJECT_FILL__:RESPONSIBILITY.identifier", "type": "responsibility_boundary", - "status": "experimental", - "statement": "Sample responsibility boundary placeholder for decisions the system should not make on behalf of humans.", + "status": "__PROJECT_FILL__: replace with the verified status", + "statement": "__PROJECT_FILL__: replace with a verified human or system responsibility boundary", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: replace with the real audiences" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific responsibility boundary and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: replace with the reason automation is absent, or replace test_anchor and remove this field", + "pending_since": "__PROJECT_FILL__: replace with the applicable date or remove this field", + "since": "__PROJECT_FILL__: replace with the first verified version or date" } ], "agent_behaviors": [ { - "anchor_id": "BEHAVIOR.sample_requires_context_before_answer", + "anchor_id": "__PROJECT_FILL__:BEHAVIOR.identifier", "type": "agent_behavior", - "status": "experimental", - "statement": "Sample behavior placeholder requiring the agent to ask for missing critical context instead of guessing.", + "status": "__PROJECT_FILL__: replace with the verified status", + "statement": "__PROJECT_FILL__: replace with a verified ask, refuse, explain, or degrade behavior", "audience": [ - "business_user", - "agent", - "developer", - "reviewer" + "__PROJECT_FILL__: replace with the real audiences" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific behavior contract and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: replace with the reason automation is absent, or replace test_anchor and remove this field", + "pending_since": "__PROJECT_FILL__: replace with the applicable date or remove this field", + "since": "__PROJECT_FILL__: replace with the first verified version or date" } ], "documents": [ { - "anchor_id": "DOC.business_user_guide", + "anchor_id": "DOC.interact", "type": "document", - "status": "experimental", - "statement": "docs/business_user_guide.md is a teaching document derived from capability_contract.json and interact.md.", + "status": "active", + "statement": "interact.md is the source of truth for user-visible behavior and acceptance invariants.", "audience": [ "business_user", "agent", "reviewer" ], - "document_path": "docs/business_user_guide.md", - "language": "en-US", - "test_anchor": null, - "test_status": "not_automated", - "untested_reason": "Alignment principles are documented in TESTING.md; project-specific tests are added by each target project.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "document_path": "interact.md", + "language": "en-US" }, { - "anchor_id": "DOC.interact", + "anchor_id": "DOC.business_user_guide", "type": "document", "status": "active", - "statement": "interact.md is the source of truth for user-visible behavior and acceptance invariants.", + "statement": "docs/business_user_guide.md is a teaching document derived from capability_contract.json and interact.md.", "audience": [ "business_user", "agent", "reviewer" ], - "document_path": "interact.md", - "language": "en-US", - "test_anchor": null, - "test_status": "not_automated", - "untested_reason": "Alignment principles are documented in TESTING.md; project-specific tests are added by each target project.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "document_path": "docs/business_user_guide.md", + "language": "en-US" } ] } diff --git a/en/docs/business_user_guide.md b/en/docs/business_user_guide.md index 1190340..5e4f987 100644 --- a/en/docs/business_user_guide.md +++ b/en/docs/business_user_guide.md @@ -1,135 +1,40 @@ -# <project name>: First-Time Business User Guide +# First-use Guide -> Status: experimental -> Audience: business users -> Scope: <project / agent / app name> -> Capability boundary source: `capability_contract.json` -> User-visible behavior source: `interact.md` -> Rule: This guide is a derived teaching document. It must not declare independent capabilities. Any "can do / cannot do / must ask / must refuse" statement must anchor to `capability_contract.json`, `interact.md`, or tests. -> Template note: all `<angle-bracket placeholders>`, sample case titles, and generic business scenarios are scaffolding only. Replace them with real project business questions before publishing. +This guide is for business users encountering the project for the first time. It only explains capability +boundaries declared in `capability_contract.json` and user-visible behavior declared in `interact.md`; it +does not independently promise capabilities or describe implementation details. -This is a cross-project template. In a concrete project, keep only what a first-time business user truly needs; do not turn it into a complete feature manual. +## 1. Value -## 1. Start Here: What Value Does It Provide? +<!-- project-fill: In three to five sentences, describe verified business value, required inputs, and observable outputs, then remove this marker. --> -Use three to five sentences to explain what business problem the system helps solve, what information it takes as input, and what judgment support it returns. +## 2. Best-fit Business Questions -Do not independently promise new capabilities here. Capability statements must trace back to `capability_contract.json` or `interact.md`. +<!-- project-fill: In business language, list the main question types currently supported by the contract and cite stable capability anchors, then remove this marker. --> -## 2. Best-Fit Business Questions +## 3. Capability and Responsibility Boundaries -- **Sample capability: answer status, trend, or anomaly questions about one clear object** - <!-- capability-anchor: CAPABILITY.sample_supported_question --> - Replace this with a real project capability, written in business-user language. +<!-- project-fill: Derive current limits, refusals, follow-up questions, and human responsibilities from the contract. Mark future or proposed work explicitly, then remove this marker. --> -## 3. Capability Boundaries +## 4. Context to Provide -The current version does not support the following capabilities, though future versions may: +<!-- project-fill: Describe the objects, scope, time range, definitions, permissions, or other critical context required by real entrypoints, then remove this marker. --> -- **Sample boundary: comparing multiple objects in one request is not supported yet** - <!-- capability-anchor: BOUNDARY.sample_multi_object_comparison_not_supported --> - Replace this with a real project capability boundary. +## 5. Representative Real Cases -## 4. Responsibility Boundaries +Keep only one to three real cases supported by current implementation or tests. When there is no real case, +delete this section or write `Not configured —` with a verified reason. Do not keep a generic case skeleton. -The system intentionally does not do the following: +<!-- project-fill: Add one to three real cases with the question, recommended request, visible result, interpretation, and escalation condition, then remove this marker. --> -- **Sample responsibility boundary: it does not make final business decisions for humans** - <!-- capability-anchor: RESPONSIBILITY.sample_no_final_business_decision --> - The system may provide evidence, explanations, and risk signals, but final business decisions remain with the responsible human. +## 6. Reading Results -## 5. First Use: Classify Your Question +<!-- project-fill: Explain how users distinguish facts, inferences, limits, errors, and next steps. Keep this consistent with visible results in interact.md, then remove this marker. --> -Classify the question first, then provide context. Keep only the three to five most common question types, for example: +## 7. Human Escalation -- I want to check whether one object changed noticeably. -- I want to understand why an abnormal result may have happened. -- I want to know whether current data is sufficient for a judgment. +<!-- project-fill: Derive human-escalation triggers, responsible roles, and available paths from the contract and interact.md. If no path is configured, say so accurately, then remove this marker. --> -## 6. What Context Should You Provide? +## 8. Feedback and Ownership -Business users should usually provide: - -- Target object: which customer, product, region, model, or process you care about. -- Time range: which period matters. -- Judgment purpose: trend judgment, anomaly diagnosis, health check, or result explanation. -- Business context: known campaigns, strategy changes, or data definition changes. - -If critical context is missing, the agent should ask follow-up questions instead of guessing. -<!-- capability-anchor: BEHAVIOR.sample_requires_context_before_answer --> - -## 7. Common Business Cases - -### Case 1: Check Whether One Object Is Abnormal - -#### Business Question - -I want to know whether `<object>` has changed abnormally recently. - -#### Can This System Do It? - -It can assist only when the capability is declared in `capability_contract.json` and the input context is sufficient. - -#### Recommended Prompt - -Please check whether `<object>` changed abnormally during `<time range>` and explain the main evidence. - -#### What You Should See - -You should see a conclusion, key evidence, limitations, and suggested next steps. - -#### How to Read the Result - -First check whether the evidence supports the conclusion, then decide whether more data or a responsible person is needed. - -#### When Not to Ask This Way - -Do not ask for a direct final judgment when the target object, time range, or data definition is unclear. - -### Case 2: Understand Why a Result Changed - -#### Business Question - -I want to understand why `<metric / result>` changed. - -#### Can This System Do It? - -It should answer only when the project declares the corresponding explanation capability and data is sufficient. - -#### Recommended Prompt - -Please explain the change in `<metric / result>` during `<time range>`, separating verified evidence from inference. - -#### What You Should See - -You should see candidate causes, evidence sources, uncertain parts, and suggested missing information. - -#### How to Read the Result - -Do not treat inference as fact. Before making decisions, confirm key data sources and owners. - -#### When Not to Ask This Way - -Do not ask the system to make a final business decision when you actually need accountable human judgment. - -### Case 3: Decide Whether to Escalate to a Human - -#### Business Question - -I want to know whether this issue can continue through system analysis, or whether I should ask an engineer, data owner, or model owner. - -#### Can This System Do It? - -If the project has the corresponding behavior commitment, the system should explain what input is needed and when to escalate. - -#### Recommended Prompt - -Please decide whether this issue can continue through system analysis. If not, explain who should be involved and why. - -#### What You Should See - -You should see missing conditions, escalation reasons, and suggested responsible roles. - -#### How to Read the Result - -If identity is ambiguous, data definitions are unclear, external systems are failing, or the decision is high risk, confirm with the responsible human first. +<!-- project-fill: Add verified document owners, feedback entrypoints, and urgent escalation routes. Mark every unconfigured item as Not configured, then remove this marker. --> diff --git a/en/docs/development_workflow/README.md b/en/docs/development_workflow/README.md index 4135af9..0c3e850 100644 --- a/en/docs/development_workflow/README.md +++ b/en/docs/development_workflow/README.md @@ -1,79 +1,81 @@ # Development Workflow -This document is the English overview for the repository's human-in-the-loop -coding workflow. The Chinese workflow remains the source of truth; this file -only exposes the English path and the current English coverage boundary. +This is the English overview of the repository's human-in-the-loop coding workflow. The Chinese workflow +is the semantic source; this file closes the English meaning for the workflow content changed in this PR. ## Main Flow 1. Define the requirement. -2. Draft the `FSD Core Contract` with the Pro web model when local repository - exploration is not available. -3. Use a Target State Bridge agent to compare the FSD, current repository code, - and authoritative docs, then produce the `Repo Impact Forecast` and - `Target State Bridge`. -4. Use an Issue agent to turn the FSD, forecast, and bridge into an executable - issue contract. -5. Let the coding agent implement the issue after reading `AGENTS.md`, - `SOP.md`, `TESTING.md`, `PR_Checklist.md`, `interact.md`, and any project - capability or business-user docs. -6. Review the PR with business context first, then implementation quality, - test realism, and maintainability. -7. When reviewer findings appear, do not change code first. Verify each finding - through code reading, a minimal reproduction, targeted tests, or a path close - to real use. If a finding is real, report the analysis before fixing it: - check the PR description for similar prior fixes and, if any exist, define - an end-to-end acceptance plan that prevents repeated rework; assess affected - upstream inputs, the current module, downstream callers, equivalent entry - points, and adjacent scenarios; and determine whether test notes, user docs, - architecture or workflow docs, the PR description, or the Review / Fix - Record must be updated, stating why if no update is needed. -8. Keep PRs as one external commit when updating an existing PR, and keep - `PR_BODY.md` as local PR-body scratch unless the target project explicitly - tracks it. -9. After merge, summarize the PR from a tech-lead perspective and run - user-view acceptance when useful. +2. Draft the `FSD Core Contract` with the Pro web model when direct repository exploration is unavailable. +3. Compare the FSD, current repository code, and authoritative documents to produce the `Repo Impact + Forecast` and `Target State Bridge`. +4. Turn the FSD, forecast, and bridge into an executable issue contract. +5. Implement the issue after reading `AGENTS.md`, `SOP.md`, `TESTING.md`, `PR_Checklist.md`, + `interact.md`, and project capability or business-user documents. +6. Review business intent first, then implementation correctness, test realism, maintainability, and + documentation impact. Check the changed scope against `.github/pull_request_template.md`, + `docs/business_user_guide.md`, `AGENTS.md`, `architecture.md`, `capability_contract.json`, `interact.md`, + `PR_Checklist.md`, `SOP.md`, and `TESTING.md` where relevant. +7. Verify each reviewer finding through code reading, a minimal reproduction, targeted tests, or a path + close to real use before changing code. Preserve finding IDs and closure evidence in the existing PR + review / fix record and GitHub threads; do not create a second reconciliation ledger. +8. Follow the repository's current commit policy. A one-commit approach is a replaceable team default, not + a universal rule. Build the PR body as temporary Markdown outside the repository from + `.github/pull_request_template.md`, and let general GitHub publishing capability read it. +9. After merge, summarize runtime mechanisms and tradeoffs from code evidence, then perform user-view + acceptance when useful. ## Core Artifacts -- `FSD Core Contract`: requirement contract that can be implemented, tested, - and reviewed. -- `Repo Impact Forecast`: predicted repository touch points, risks, docs, and - test impact. -- `Target State Bridge`: target user or caller-visible state and validation - method. -- `Issue`: executable development contract. -- `PR_BODY.md`: local PR body draft generated from `.github/pull_request_template.md`. -- `Workflow Docs Sync`: one invocation that maps current code, performs four - read-only domain analyses, lets the main agent update documents, runs a - read-only adversarial audit, executes tests, and checks final repository state. +- `FSD Core Contract`: a requirement contract that can be implemented, tested, and reviewed. +- `Repo Impact Forecast`: predicted repository touchpoints, risks, documentation, and test impact. +- `Target State Bridge`: target user- or caller-visible state and its validation method. +- `Issue`: the executable development contract. +- External PR body Markdown: temporary review and publishing input derived from the tracked PR template; + it never enters the target worktree or commit. +- `Workflow Docs Sync`: one invocation for full fact reconstruction, minimal necessary document changes, + real tests, fresh-context independent review or honest self-review, and deterministic final checks. ## English Coverage Boundary -English workflow docs are intentionally exposed only when the English path is -ready. The long prompt pack is not yet published as English-ready; see -[en/prompts/README.md](../../prompts/README.md). If a PR changes Chinese prompt -semantics without updating English, mark `en-pending` in the PR body. +English workflow documentation is exposed only when its path is ready. The long prompt pack remains outside +the English-ready surface; see [en/prompts/README.md](../../prompts/README.md). Bilingual template, README, +and development-workflow content changed by a PR must close in that PR. Unchanged historical decisions +retain their recorded translation status. ## Workflow Docs Sync -Invoke `$workflow-docs-sync` once with the target repository, optional `en` -language, and optional draft-PR intent. Upstream checkout and commit resolution -are internal. The main agent is the only workspace writer; domain agents and -the adversarial auditor return read-only findings in the current session. +Invoke `$workflow-docs-sync` once with the target repository, a required `zh` / `en` language choice, and +whether to create a draft PR after success. The Skill resolves the canonical upstream checkout or uses an +external shallow clone, then pins target HEAD and upstream SHA for the entire run. -The deterministic script has only two internal commands: `prepare` installs -missing templates without replacing existing documents, and `check` validates -the final repository state. The entire sync flow neither creates, reads, rewrites, -nor deletes repository-local `PR_BODY.md`. It creates no run records, runs no -target test command, and performs no commit, push, or GitHub operation. If the -user asks for a draft PR, the post-check publishing step uses a temporary -Markdown body outside the repository and the general GitHub publishing ability. +- Before writing, use at least `git ls-files -z` to establish scope and reconstruct facts from code, + configuration, tests, committed artifacts, reproducible results, and necessary Git history. Existing + documents and upstream templates are hypotheses. +- Architecture, Capability / User Behavior, Testing, and Governance are coverage dimensions, not a fixed + agent topology. The main agent may investigate directly or delegate read-only work by module, call flow, + risk, or evidence type. +- The main agent is the only target-workspace writer. It questions all nine documents but changes only + incorrect, missing, or misleading content; correct content stays at zero diff. The workflow creates no + disposition ledger, run state, or receipt. +- Test environments follow real commands, side effects, CI capabilities, and project policy. Record the + actual environment, isolation, residue, and cleanup instead of fixing one implementation across projects. +- Review prefers a fresh-context, blind-first independent reviewer. When cognitive isolation is unavailable, + the final result says self-review and never claims independence. Fix all BLOCKERs and actionable WARNs that + do not require a new product decision, then recheck their direct effects. +- `sync_docs.py prepare` first validates all nine UTF-8 source templates at the pinned object and language, + including the non-PR active-marker invariant, then fills only missing templates. `check` rereads that pinned + 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. +- 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. Key implementation files: - `../../../zh/skills/workflow-docs-sync/SKILL.md`: orchestration contract. -- `../../../zh/skills/workflow-docs-sync/references/sections.md`: four domain analyses. -- `../../../zh/skills/workflow-docs-sync/references/audit.md`: adversarial audit. +- `../../../zh/skills/workflow-docs-sync/agents/openai.yaml`: UI metadata. +- `../../../zh/skills/workflow-docs-sync/evals/README.md`: forward-eval cases. - `../../../zh/skills/workflow-docs-sync/scripts/sync_docs.py`: mechanical preparation and check. -- `../../../tests/test_workflow_docs_sync.py`: regression tests. +- `../../../tests/test_workflow_docs_sync.py`: regression and real-template quality tests. diff --git a/en/interact.md b/en/interact.md index 32e4e9e..5884c40 100644 --- a/en/interact.md +++ b/en/interact.md @@ -1,60 +1,42 @@ -## Document Relationships +# User-visible Behavior and Acceptance -`capability_contract.json` is the machine-readable source of truth for capability boundaries. It answers whether the system can do something. +## 0. Authority and Scope -`interact.md` is the source of truth for user-visible behavior and acceptance invariants. It answers how the system must behave while doing it. +`capability_contract.json` is the machine-readable source of truth for capability boundaries. This document +is the source of truth for user-visible behavior and acceptance invariants. `docs/business_user_guide.md` +may only derive explanations from those two sources. Describe only behavior directly observable through a +UI, API response, or other public entrypoint; logs, monitoring, and internal state are not user results. -`docs/business_user_guide.md` is the derived teaching document for first-time business users. It answers how business users can start. +<!-- project-fill: Describe public entrypoints, scope, and explicit exclusions for this project, then remove this marker. --> -When these documents disagree: +## 1. Audience and Granularity -1. Capability boundaries follow `capability_contract.json`. -2. User-visible behavior follows `interact.md`. -3. `docs/business_user_guide.md` must be corrected from the first two documents. +Write for users, product staff, and acceptance reviewers. Organize by journeys that independently create +user value. Keep one representative for equivalent options, describe composable atomic behavior, and do +not enumerate low-value combinations. Mark future or proposed behavior explicitly. -## Writing Rules +<!-- project-fill: State the target users and acceptance granularity for this project, then remove this marker. --> -Any user-visible statement about "can do / cannot do / must ask / must refuse" should reference a stable `anchor_id` from `capability_contract.json` through a hidden Markdown anchor. +## 2. Supported User Journeys -Hidden anchors reference stable `anchor_id` values only. They must not reference JSON paths, array indexes, or schema-internal paths. +Every scenario must have current implementation or test evidence and use the fields below. If no journey +is verified, write `Not configured —` with the entrypoints checked and the reason instead of inventing one. -## 1. Positioning +### Scenario -- Capability groups should follow user mental models, not code modules. -- User experience and acceptance should describe what the user can do, what the user can see, and what value the user receives. +<!-- project-fill: Replace this section with one verified scenario containing User goal, Required context, User action or request, Directly observable result, Failure / degradation / escalation, Acceptance assertion, and capability anchor, then remove this marker. --> -## 2. Audience +## 3. Cross-cutting User-visible Invariants -Write for ordinary users, PMs, and business stakeholders. Describe only user-visible behavior; do not explain engineering details or implementation. +Invariants must be directly judgeable by the target reader, trace to the contract through stable capability +anchors, and avoid example values that change with data. -## 3. Boundaries +<!-- project-fill: List real cross-journey visible invariants and acceptance evidence. If none exist, write Not applicable with a verified reason, then remove this marker. --> -- Documentation may lead code only when status is explicit. To prevent code from leading documentation, the PR checklist must require every user-visible behavior change to update `interact.md`. -- Acceptance should be written as invariants, not data-dependent exact values. -- This document is an entrypoint, not an exhaustive authority list. -- Critical failure paths are manually curated. A tasteful PM should decide which failures are exposed to users and which are hidden or degraded. -- Narrative matters: do not write cold click-by-click UI traces. Explain what problem the user is solving, where they go, what they do, and what result they obtain. -- Visibility matters: every example must be directly verifiable through UI or API response. Log-only, monitoring-only, or internal-code state does not belong here. -- Falsifiability matters: each statement should include a concrete acceptance assertion that non-technical users can judge. +## 4. Known Limits and Human Escalation -## Granularity +Distinguish currently unsupported behavior, temporary degradation, and future or proposed work. Human +escalation should expose user-recognizable triggers, explanations, and responsible roles without leaking +internal monitoring details. -The granularity standard for `interact.md` is "what a PM would demo to a CEO." The CEO does not care about every dropdown combination; the CEO cares whether the core business flow works and feels right. - -- Equivalence class principle: if ten options share the same business logic, write one representative case. -- Orthogonality principle: describe composable atomic capabilities, not every combination. -- Independent value principle: write a scenario only when it independently answers what value the user receives. - -## Examples - -```text -# Worth writing -A user wants to understand the escalation trend over the last 30 days. -They enter the Flow Analysis page, select a time range, and see escalation rate -fall from 12% to 8%, supporting the judgment that recent improvements worked. - -# Not worth writing -A user opens the Country dropdown, chooses United States, and the dropdown closes. -``` - -## 4. User-Visible Invariants +<!-- project-fill: Add verified limits, degradation, refusal, and human escalation paths with capability anchors. If no path is configured, say so accurately, then remove this marker. --> diff --git a/tests/test_workflow_docs_sync.py b/tests/test_workflow_docs_sync.py index d4bb82a..16ade70 100644 --- a/tests/test_workflow_docs_sync.py +++ b/tests/test_workflow_docs_sync.py @@ -1,9 +1,10 @@ -"""验证单会话 Workflow Docs Sync 的 prepare、check、安装器和结构边界。""" +"""通过公共 CLI 和完整分发路径验证 Workflow Docs Sync 的核心风险边界。""" from __future__ import annotations import json import os +import shutil import subprocess import sys from pathlib import Path @@ -16,8 +17,6 @@ SKILL_ROOT = REPO_ROOT / "zh/skills/workflow-docs-sync" SYNC_SCRIPT = SKILL_ROOT / "scripts/sync_docs.py" INSTALLER = REPO_ROOT / "zh/scripts/install_skills.py" -PLATFORM_ROOTS = (Path(".agents/skills"), Path(".claude/skills")) -OBSOLETE_SKILL = "workflow-docs-sync-review" CORE_FILES = ( "architecture.md", "capability_contract.json", @@ -32,135 +31,113 @@ NON_PR_FILES = tuple( path for path in CORE_FILES if path != ".github/pull_request_template.md" ) -TEMPLATE_TOKENS = ( - "<项目名>", - "<项目 / agent / app 名称>", - "<对象>", - "<指标 / 结果>", - "<project name>", - "<project / agent / app name>", - "<object>", - "<metric / result>", - "Case 1:确认一个对象最近是否异常", - "Case 1: Check whether one object is abnormal", - "待项目负责人补充", - "project owner must replace this", - "sample_supported_question", - "sample_multi_object_comparison_not_supported", - "sample_no_final_business_decision", - "sample_requires_context_before_answer", - "CAPABILITY.sample_", - "BOUNDARY.sample_", - "RESPONSIBILITY.sample_", - "BEHAVIOR.sample_", - "<!-- capability-anchor: TODO -->", - "<!-- test-anchor: TODO -->", - "待补充", -) -FORBIDDEN_CONTROL_WORDS = ( - "start-pass", - "finish-pass", - "prepare-submit", - "seal-submit", - "finish-submit", - "active_mode", - "completed_modes", - "submit_ready", - "run.json", - "baseline", - "skill_runtime", - "skill_results", - "SYNC_PR_BODY_MARKER", - "sync:agent:start", - "headRefOid", +ACTIVE_MARKERS = ("<!-- project-fill:", "__PROJECT_FILL__:") +PLATFORM_ROOTS = ( + ("codex", Path(".agents/skills")), + ("claude", Path(".claude/skills")), ) +OBSOLETE_SKILL = "workflow-docs-sync-review" -def run( +def run_command( *, args: list[str], cwd: Path, - check: bool = False, - environment: dict[str, str] | None = None, + environment: dict[str, str] | None, ) -> subprocess.CompletedProcess[str]: - """运行测试子进程并稳定捕获 UTF-8 输出。""" + """运行隔离子进程并返回稳定 UTF-8 输出,不隐式抛出命令错误。""" merged_environment = os.environ.copy() - merged_environment["LC_ALL"] = "C" - merged_environment["PYTHONDONTWRITEBYTECODE"] = "1" + merged_environment.update( + {"LC_ALL": "C", "PYTHONDONTWRITEBYTECODE": "1"} + ) if environment is not None: merged_environment.update(environment) return subprocess.run( args=args, cwd=cwd, env=merged_environment, - check=check, + check=False, capture_output=True, encoding="utf-8", text=True, ) -def git(*, repo: Path, args: list[str], check: bool = True) -> str: - """运行 Git 并返回去掉末尾换行的 stdout。""" - result = run(args=["git", "-C", str(repo), *args], cwd=repo, check=False) +def git(*, repo: Path, args: list[str], check: bool) -> str: + """运行 Git 并在要求成功时用完整诊断立即终止场景。""" + result = run_command( + args=["git", "-C", str(repo), *args], + cwd=repo, + environment=None, + ) if check and result.returncode != 0: pytest.fail( f"git {' '.join(args)} failed ({result.returncode}):\n" f"stdout={result.stdout}\nstderr={result.stderr}" ) - return result.stdout.strip() + return result.stdout def init_repo(*, path: Path) -> Path: - """创建带固定身份和初始提交的临时 Git 仓库。""" + """创建带固定测试身份和初始提交的真实临时 Git 仓库。""" path.mkdir(parents=True) - git(repo=path, args=["init", "-q"]) - git(repo=path, args=["config", "user.email", "tests@example.com"]) - git(repo=path, args=["config", "user.name", "Workflow Tests"]) - (path / "README.md").write_text("# Temporary repository\n", encoding="utf-8") - git(repo=path, args=["add", "README.md"]) - git(repo=path, args=["commit", "-q", "-m", "initial"]) + git(repo=path, args=["init", "-q"], check=True) + git( + repo=path, + args=["config", "user.email", "tests@example.com"], + check=True, + ) + git( + repo=path, + args=["config", "user.name", "Workflow Scenario Tests"], + check=True, + ) + (path / "README.md").write_text( + data="# Temporary repository\n", + encoding="utf-8", + ) + git(repo=path, args=["add", "README.md"], check=True) + git(repo=path, args=["commit", "-q", "-m", "initial"], check=True) return path def commit_all(*, repo: Path, message: str) -> str: - """提交临时仓库全部文件并返回完整 HEAD。""" - git(repo=repo, args=["add", "-A"]) - git(repo=repo, args=["commit", "-q", "-m", message]) - return git(repo=repo, args=["rev-parse", "HEAD"]) + """提交临时仓库全部状态并返回固定 HEAD。""" + git(repo=repo, args=["add", "-A"], check=True) + git(repo=repo, args=["commit", "-q", "-m", message], check=True) + return git(repo=repo, args=["rev-parse", "HEAD"], check=True).strip() def template_text(*, language: str, relative_path: str, version: str) -> str: - """生成无占位、可用于 pin 与 equality 测试的最小模板。""" + """生成只含 active marker 的双语分发 fixture。""" if relative_path == "capability_contract.json": + payload = { + "schema_version": version, + "status": f"__PROJECT_FILL__: {language} {version} project status", + } return json.dumps( - { - "schema_version": "1.0", - "contracts": { - "documents": [ - { - "anchor_id": f"DOC.{language}_{version}", - "statement": f"{language} {version} contract", - } - ] - }, - }, + obj=payload, ensure_ascii=False, indent=2, sort_keys=True, ) + "\n" title = relative_path.replace("/", " ") - return f"# {language.upper()} {title} {version}\n\nUpstream {version} text.\n" + if relative_path == ".github/pull_request_template.md": + return f"# {language.upper()} {title} {version}\n" + return ( + f"# {language.upper()} {title} {version}\n\n" + "<!-- project-fill: replace with verified project facts -->\n" + ) def write_templates(*, upstream: Path, version: str) -> None: - """向临时上游写入两种语言的九份模板。""" + """写入两种语言的九份 fixture,以便真实 Git object 固定其 bytes。""" for language in ("zh", "en"): for relative_path in CORE_FILES: path = upstream / language / relative_path path.parent.mkdir(parents=True, exist_ok=True) path.write_text( - template_text( + data=template_text( language=language, relative_path=relative_path, version=version, @@ -169,68 +146,50 @@ def write_templates(*, upstream: Path, version: str) -> None: ) -def create_upstream(*, root: Path, version: str = "v1") -> tuple[Path, str]: - """创建并提交双语模板上游。""" +def create_upstream(*, root: Path, version: str) -> tuple[Path, str]: + """创建含双语模板的 clean 上游仓库和固定提交。""" upstream = init_repo(path=root) write_templates(upstream=upstream, version=version) - return upstream, commit_all(repo=upstream, message=f"templates {version}") - + sha = commit_all(repo=upstream, message=f"templates {version}") + return upstream, sha -def create_target(*, root: Path) -> Path: - """创建没有核心文档的 clean 目标仓库。""" - return init_repo(path=root) - -def parse_single_json(*, result: subprocess.CompletedProcess[str]) -> dict[str, Any]: - """断言脚本 stdout 恰好包含一行 JSON 并返回对象。""" +def parse_single_json( + *, result: subprocess.CompletedProcess[str] +) -> dict[str, Any]: + """断言 CLI stdout 只有一个 JSON object,并返回该数据。""" lines = result.stdout.splitlines() assert len(lines) == 1, result.stdout - payload = json.loads(lines[0]) + payload = json.loads(s=lines[0]) assert isinstance(payload, dict) return payload -def run_sync( - *, - command: str, - target: Path, - upstream: Path, - language: str = "zh", - upstream_sha: str | None = None, - expected_target_head: str | None = None, +def run_prepare( + *, target: Path, upstream: Path, language: str ) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: - """调用同步器内部 CLI 并解析单行结果。""" - args = [ - sys.executable, - str(SYNC_SCRIPT), - command, - "--target-repo", - str(target), - "--upstream-dir", - str(upstream), - ] - if command == "check": - assert upstream_sha is not None - assert expected_target_head is not None - args.extend( - [ - "--upstream-sha", - upstream_sha, - "--expected-target-head", - expected_target_head, - ] - ) - args.extend(["--language", language]) - result = run(args=args, cwd=target) + """通过公开 prepare CLI 返回进程结果和 JSON 数据。""" + result = run_command( + args=[ + sys.executable, + str(SYNC_SCRIPT), + "prepare", + "--target-repo", + str(target), + "--upstream-dir", + str(upstream), + "--language", + language, + ], + cwd=target, + environment=None, + ) return result, parse_single_json(result=result) -def prepare( - *, target: Path, upstream: Path, language: str = "zh" -) -> dict[str, Any]: - """运行成功的 prepare 并返回会话事实。""" - result, payload = run_sync( - command="prepare", +def prepare_success(*, target: Path, upstream: Path, language: str) -> dict[str, Any]: + """执行必须成功的 prepare,并立即返回会话固定数据。""" + result, payload = run_prepare( target=target, upstream=upstream, language=language, @@ -240,688 +199,1025 @@ def prepare( return payload -def specialize(*, target: Path, label: str = "target") -> None: - """把八份非 PR 模板改成项目内容,保留 PR template 直接继承。""" +def run_check( + *, + target: Path, + upstream: Path, + language: str, + upstream_sha: str, + expected_target_head: str, + environment: dict[str, str] | None, +) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: + """通过公开 check CLI 验证调用方固定的两端 SHA。""" + result = run_command( + args=[ + sys.executable, + str(SYNC_SCRIPT), + "check", + "--target-repo", + str(target), + "--upstream-dir", + str(upstream), + "--upstream-sha", + upstream_sha, + "--expected-target-head", + expected_target_head, + "--language", + language, + ], + cwd=target, + environment=environment, + ) + return result, parse_single_json(result=result) + + +def projectize(*, target: Path, label: str) -> None: + """清除 fixture 的 active marker,并写入足够的目标项目事实。""" for relative_path in NON_PR_FILES: path = target / relative_path if relative_path == "capability_contract.json": - payload = json.loads(path.read_text(encoding="utf-8")) - payload["project"] = label - path.write_text( - json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - else: path.write_text( - path.read_text(encoding="utf-8") - + f"\nProject-specific evidence for {label}.\n", + data=json.dumps( + obj={"project": label}, + ensure_ascii=False, + indent=2, + ) + "\n", encoding="utf-8", ) + continue + text = path.read_text(encoding="utf-8") + text = text.replace( + "<!-- project-fill: replace with verified project facts -->", + f"Verified project facts for {label}.", + ) + path.write_text(data=text, encoding="utf-8") + + +def file_tree(*, root: Path) -> dict[str, bytes]: + """记录根目录下除 Git 元数据外的文件、目录和 symlink bytes。""" + entries: dict[str, bytes] = {} + for path in sorted(root.rglob("*")): + relative = path.relative_to(root) + if relative.parts and relative.parts[0] == ".git": + continue + label = relative.as_posix() + if path.is_symlink(): + entries[label] = f"SYMLINK:{os.readlink(path=path)}".encode("utf-8") + elif path.is_file(): + entries[label] = path.read_bytes() + elif path.is_dir(): + entries[f"{label}/"] = b"DIRECTORY" + return entries + + +def repository_status(*, root: Path) -> str: + """返回 porcelain 状态;非 Git 根以稳定 sentinel 表达。""" + probe = run_command( + args=["git", "-C", str(root), "rev-parse", "--show-toplevel"], + cwd=root, + environment=None, + ) + if probe.returncode != 0: + return "NOT_A_GIT_ROOT" + return git( + repo=root, + args=["status", "--porcelain=v1", "--untracked-files=all"], + check=True, + ) -def run_check( +def repository_snapshot(*, root: Path) -> tuple[dict[str, bytes], str]: + """组合工作树 bytes 与 Git 状态,证明受测前置失败和 check 无副作用。""" + return file_tree(root=root), repository_status(root=root) + + +def existing_core_paths(*, target: Path) -> set[str]: + """返回存在或为 symlink 的核心路径,避免 broken symlink 被漏计。""" + return { + relative_path + for relative_path in CORE_FILES + if (target / relative_path).exists() + or (target / relative_path).is_symlink() + } + + +def assert_prepare_failure( *, target: Path, upstream: Path, - prepared: dict[str, Any], - language: str = "zh", - upstream_sha: str | None = None, -) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: - """使用 prepare 返回的 SHA 和目标 HEAD 调用 check。""" - return run_sync( - command="check", + expected_error: str, + expected_core_paths: set[str], +) -> dict[str, Any]: + """证明 prepare 失败摘要稳定,且调用前后 bytes 与 Git 状态完全一致。""" + before = repository_snapshot(root=target) + result, payload = run_prepare( target=target, upstream=upstream, - language=language, - upstream_sha=upstream_sha or prepared["upstream_sha"], - expected_target_head=prepared["target_head"], + language="zh", ) + assert result.returncode != 0 + assert payload["status"] == "failed" + assert payload["error"] == expected_error + assert repository_snapshot(root=target) == before + assert existing_core_paths(target=target) == expected_core_paths + return payload -def ready_case( - *, tmp_path: Path, language: str = "zh" +def create_ready_case( + *, root: Path, language: str ) -> tuple[Path, Path, dict[str, Any]]: - """创建已 prepare 且八份文档项目化的常用测试场景。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - prepared = prepare(target=target, upstream=upstream, language=language) - specialize(target=target) + """创建已 prepare 并完成 marker 清理的公共 check 场景。""" + upstream, _ = create_upstream(root=root / "upstream", version="v1") + target = init_repo(path=root / "target") + prepared = prepare_success( + target=target, + upstream=upstream, + language=language, + ) + projectize(target=target, label=root.name) return upstream, target, prepared -def assert_check_failed( +def assert_check_failure( *, target: Path, upstream: Path, prepared: dict[str, Any], - expected_text: str, - language: str = "zh", + expected_error: str, + expected_detail: str, ) -> dict[str, Any]: - """断言最终检查失败且诊断包含指定文本。""" + """断言公开 check 以稳定摘要拒绝指定无效终态。""" result, payload = run_check( target=target, upstream=upstream, - prepared=prepared, - language=language, + language=prepared["language"], + upstream_sha=prepared["upstream_sha"], + expected_target_head=prepared["target_head"], + environment=None, ) - assert result.returncode == 1 + assert result.returncode != 0 assert payload["status"] == "failed" - assert expected_text in payload["detail"] + assert payload["error"] == expected_error + assert expected_detail in payload["detail"] return payload -def snapshot(*, repo: Path) -> tuple[dict[str, bytes], str]: - """读取除 .git 外的文件 bytes 与 porcelain 状态。""" - files = { - path.relative_to(repo).as_posix(): path.read_bytes() - for path in repo.rglob("*") - if path.is_file() and ".git" not in path.relative_to(repo).parts - } - status = git( - repo=repo, - args=["status", "--porcelain=v1", "--untracked-files=all"], - ) - return files, status - - -@pytest.mark.parametrize("language", ["zh", "en"]) -def test_prepare_installs_selected_language_templates( - tmp_path: Path, language: str -) -> None: - """prepare 应从所选语言的固定 Git 对象安装九份模板。""" - upstream, upstream_sha = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - payload = prepare(target=target, upstream=upstream, language=language) - assert payload["upstream_sha"] == upstream_sha - assert payload["installed"] == list(CORE_FILES) - assert payload["existing"] == [] - for relative_path in CORE_FILES: - assert (target / relative_path).read_text(encoding="utf-8") == ( - upstream / language / relative_path - ).read_text(encoding="utf-8") - - -def test_prepare_is_idempotent(tmp_path: Path) -> None: - """第二次 prepare 不得重写第一次安装的任何文件。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - first = prepare(target=target, upstream=upstream) - before = snapshot(repo=target) - second = prepare(target=target, upstream=upstream) - assert second["target_head"] == first["target_head"] - assert second["installed"] == [] - assert second["existing"] == list(CORE_FILES) - assert snapshot(repo=target) == before - - -def test_prepare_never_overwrites_existing_document(tmp_path: Path) -> None: - """已有核心文档即使 dirty 也必须原样保留。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - custom = "# Existing architecture\n\nProject truth.\n" - (target / "architecture.md").write_text(custom, encoding="utf-8") - payload = prepare(target=target, upstream=upstream) - assert payload["installed"] == list(CORE_FILES[1:]) - assert payload["existing"] == ["architecture.md"] - assert (target / "architecture.md").read_text(encoding="utf-8") == custom - - -def test_prepare_reads_committed_template_not_upstream_worktree(tmp_path: Path) -> None: - """upstream dirty 内容不得影响 git-show 读取的 HEAD 模板。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - committed = (upstream / "zh/architecture.md").read_text(encoding="utf-8") - (upstream / "zh/architecture.md").write_text( - "# Dirty upstream worktree\n", - encoding="utf-8", - ) - prepare(target=target, upstream=upstream) - assert (target / "architecture.md").read_text(encoding="utf-8") == committed +def append_bad_whitespace(*, path: Path) -> None: + """向最终文件加入 Git 可识别的 trailing whitespace。""" + with path.open(mode="a", encoding="utf-8", newline="") as stream: + stream.write("bad trailing whitespace \n") -def test_check_uses_prepare_sha_after_upstream_head_advances(tmp_path: Path) -> None: - """check 必须复用 prepare SHA,而不能偷偷读取新的 upstream HEAD。""" - upstream, old_sha = create_upstream(root=tmp_path / "upstream", version="v1") - target = create_target(root=tmp_path / "target") - prepared = prepare(target=target, upstream=upstream) - specialize(target=target) - write_templates(upstream=upstream, version="v2") - new_sha = commit_all(repo=upstream, message="templates v2") - (target / "architecture.md").write_text( - (upstream / "zh/architecture.md").read_text(encoding="utf-8"), - encoding="utf-8", +def create_installer_upstream(*, root: Path) -> Path: + """把当前 canonical Skill 复制到 clean Git object,供安装器端到端读取。""" + upstream = init_repo(path=root) + destination = upstream / "zh/skills/workflow-docs-sync" + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree( + src=SKILL_ROOT, + dst=destination, + ignore=shutil.ignore_patterns("__pycache__", "*.py[cod]", ".DS_Store"), ) - old_result, old_payload = run_check( - target=target, - upstream=upstream, - prepared=prepared, - upstream_sha=old_sha, + commit_all(repo=upstream, message="canonical skill") + return upstream + + +def seed_legacy_install( + *, install_root: Path, obsolete_kinds: tuple[str, str] +) -> list[Path]: + """建立多形态废弃 reviewer、外部目标和必须保留的无关 Skill。""" + protected_targets: list[Path] = [] + for index, (platform, platform_root) in enumerate(PLATFORM_ROOTS): + obsolete = install_root / platform_root / OBSOLETE_SKILL + obsolete.parent.mkdir(parents=True, exist_ok=True) + kind = obsolete_kinds[index] + if kind == "directory": + obsolete.mkdir() + (obsolete / "legacy.bin").write_bytes(data=b"legacy reviewer\x00") + elif kind == "file": + obsolete.write_bytes(data=b"legacy reviewer file\x00") + elif kind == "symlink": + protected = ( + install_root.parent + / f"{install_root.name}-{platform}-protected.bin" + ) + protected.write_bytes(data=b"protected target\x00") + obsolete.symlink_to(target=protected) + protected_targets.append(protected) + else: + raise ValueError(f"未知废弃路径形态: {kind}") + unrelated = install_root / platform_root / "unrelated-skill" + unrelated.mkdir(parents=True, exist_ok=True) + (unrelated / "keep.bin").write_bytes(data=b"keep unrelated\x00") + return protected_targets + + +def run_user_install( + *, upstream: Path, home: Path +) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: + """在临时 HOME 执行公开 user-scope 安装流程。""" + result = run_command( + args=[ + sys.executable, + str(INSTALLER), + "--scope", + "user", + "--upstream-dir", + str(upstream), + ], + cwd=REPO_ROOT, + environment={"HOME": str(home)}, ) - new_result, new_payload = run_check( - target=target, - upstream=upstream, - prepared=prepared, - upstream_sha=new_sha, - ) - assert old_result.returncode == 0, old_payload - assert old_payload["upstream_sha"] == old_sha - assert new_result.returncode == 1 - assert "architecture.md" in new_payload["detail"] - assert "完全相同" in new_payload["detail"] - - -@pytest.mark.parametrize("dirty_kind", ["untracked", "tracked"]) -def test_prepare_rejects_outside_dirty_before_any_write( - tmp_path: Path, dirty_kind: str -) -> None: - """allowlist 外 dirty path 必须在九份模板落盘前失败。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - if dirty_kind == "untracked": - (target / "rogue.txt").write_text("rogue\n", encoding="utf-8") - else: - (target / "README.md").write_text("# Changed\n", encoding="utf-8") - result, payload = run_sync( - command="prepare", - target=target, - upstream=upstream, + return result, parse_single_json(result=result) + + +def run_repo_install( + *, upstream: Path, target: Path +) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: + """在 clean 目标仓库执行公开 repo-scope 安装流程。""" + result = run_command( + args=[ + sys.executable, + str(INSTALLER), + "--scope", + "repo", + "--target-repo", + str(target), + "--upstream-dir", + str(upstream), + ], + cwd=REPO_ROOT, + environment=None, ) - assert result.returncode == 1 - assert "dirty path" in payload["error"] - assert not any((target / path).exists() for path in CORE_FILES) + return result, parse_single_json(result=result) -def test_prepare_rejects_rename_with_outside_endpoint(tmp_path: Path) -> None: - """rename 的来源和目标都必须属于允许范围。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - (target / "architecture.md").write_text("# Architecture\n", encoding="utf-8") - commit_all(repo=target, message="add allowed doc") - git(repo=target, args=["mv", "architecture.md", "rogue.md"]) - result, payload = run_sync( - command="prepare", - target=target, - upstream=upstream, +def assert_installed(*, install_root: Path) -> None: + """验证双平台安装、Claude 显式调用边界与精确清理结果。""" + for platform, platform_root in PLATFORM_ROOTS: + installed = install_root / platform_root / "workflow-docs-sync" + skill_text = (installed / "SKILL.md").read_text(encoding="utf-8") + if platform == "claude": + assert skill_text.count("disable-model-invocation: true") == 1 + else: + assert "disable-model-invocation: true" not in skill_text + obsolete = install_root / platform_root / OBSOLETE_SKILL + assert not obsolete.exists() and not obsolete.is_symlink() + assert ( + install_root / platform_root / "unrelated-skill/keep.bin" + ).read_bytes() == b"keep unrelated\x00" + assert not list(install_root.rglob(".source.json")) + + +def replace_active_markers(*, value: Any) -> Any: + """模拟目标项目替换全部 active marker,保留 JSON 其余分发结构。""" + if isinstance(value, dict): + return { + key: replace_active_markers(value=item) + for key, item in value.items() + } + if isinstance(value, list): + return [replace_active_markers(value=item) for item in value] + if isinstance(value, str) and any( + marker in value for marker in ACTIVE_MARKERS + ): + return "verified project fact" + return value + + +def production_text_files(*, root: Path) -> list[Path]: + """列出全仓当前生产文本,排除测试自身、历史决策和生成缓存。""" + excluded = { + root / "tests/test_workflow_docs_sync.py", + root / "zh/docs/development_workflow/decisions.md", + } + files: list[Path] = [] + for path in root.rglob("*"): + relative = path.relative_to(root) + if not path.is_file(): + continue + if any( + part in {".git", ".pytest_cache", "__pycache__"} + for part in relative.parts + ): + continue + if path in excluded: + continue + try: + path.read_bytes().decode("utf-8") + except UnicodeDecodeError: + # 机器标识符约束的是当前生产文本;二进制资产不是散文或控制面。 + continue + files.append(path) + return sorted(files) + + +def is_installable_skill_entry(*, path: Path) -> bool: + """返回安装器会复制的 Skill 条目,排除其明确忽略的生成缓存。""" + return ( + "__pycache__" not in path.parts + and path.name != ".DS_Store" + and path.suffix not in {".pyc", ".pyd", ".pyo"} ) - assert result.returncode == 1 - assert "rogue.md" in payload["detail"] - assert not (target / "capability_contract.json").exists() -def test_prepare_allows_core_and_gitignore_dirty(tmp_path: Path) -> None: - """核心文档与辅助 .gitignore 是唯一允许的 dirty 范围。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - (target / ".gitignore").write_text("*.local\n", encoding="utf-8") - (target / "architecture.md").write_text( - "# Project architecture\n", +def test_scenario_1_success_path(tmp_path: Path) -> None: + """场景 1:双语 prepare、项目化、固定 object 和连续只读 check。""" + for language in ("zh", "en"): + case_root = tmp_path / language + upstream, upstream_sha = create_upstream( + root=case_root / "upstream", + version="v1", + ) + target = init_repo(path=case_root / "target") + prepared = prepare_success( + target=target, + upstream=upstream, + language=language, + ) + assert prepared["installed"] == list(CORE_FILES) + assert prepared["existing"] == [] + assert prepared["upstream_sha"] == upstream_sha + + # 已有文档必须保持用户 bytes,prepare 不得用模板覆盖已核实项目内容。 + custom = "# Project facts\n\nVerified existing architecture.\n" + (target / "architecture.md").write_text( + data=custom, + encoding="utf-8", + ) + repeated_prepare = prepare_success( + target=target, + upstream=upstream, + language=language, + ) + assert repeated_prepare["installed"] == [] + assert repeated_prepare["existing"] == list(CORE_FILES) + assert (target / "architecture.md").read_text(encoding="utf-8") == custom + + # 新目标在上游 dirty 时仍必须收到 HEAD object,而不是 working-tree bytes。 + committed_architecture = git( + repo=upstream, + args=["show", f"{upstream_sha}:{language}/architecture.md"], + check=True, + ) + (upstream / language / "architecture.md").write_text( + data="# Dirty upstream bytes\n", + encoding="utf-8", + ) + dirty_target = init_repo(path=case_root / "dirty-target") + dirty_prepared = prepare_success( + target=dirty_target, + upstream=upstream, + language=language, + ) + assert dirty_prepared["upstream_sha"] == upstream_sha + assert ( + dirty_target / "architecture.md" + ).read_text(encoding="utf-8") == committed_architecture + + # 项目化后连续 check 必须同值且不产生仓库副作用。 + projectize(target=target, label=f"scenario-1-{language}") + before = repository_snapshot(root=target) + first_result, first_payload = run_check( + target=target, + upstream=upstream, + language=language, + upstream_sha=prepared["upstream_sha"], + expected_target_head=prepared["target_head"], + environment=None, + ) + middle = repository_snapshot(root=target) + second_result, second_payload = run_check( + target=target, + upstream=upstream, + language=language, + upstream_sha=prepared["upstream_sha"], + expected_target_head=prepared["target_head"], + environment=None, + ) + assert first_result.returncode == second_result.returncode == 0 + assert first_payload == second_payload + assert before == middle == repository_snapshot(root=target) + + # 新 object 删除承重 marker 后,旧 object 仍通过而新 object 必须失败。 + (upstream / language / "architecture.md").write_text( + data=f"# {language.upper()} marker-less v2\n", + encoding="utf-8", + ) + new_sha = commit_all(repo=upstream, message="marker-less templates v2") + assert new_sha != upstream_sha + pinned_result, pinned_payload = run_check( + target=target, + upstream=upstream, + language=language, + upstream_sha=prepared["upstream_sha"], + expected_target_head=prepared["target_head"], + environment=None, + ) + assert pinned_result.returncode == 0, pinned_payload + assert pinned_payload["upstream_sha"] == upstream_sha + new_result, new_payload = run_check( + target=target, + upstream=upstream, + language=language, + upstream_sha=new_sha, + expected_target_head=prepared["target_head"], + environment=None, + ) + assert new_result.returncode != 0 + assert new_payload["error"] == ( + "固定上游模板违反 source marker invariant" + ) + assert new_sha in new_payload["detail"] + assert f"{language}/architecture.md" in new_payload["detail"] + + # language 必须真实选择 source tree;不能把 prepare 的 zh 身份机械回显成 en PASS。 + language_root = tmp_path / "wrong-language" + language_upstream = init_repo(path=language_root / "upstream") + write_templates(upstream=language_upstream, version="v1") + (language_upstream / "en/architecture.md").write_text( + data="# EN marker-less source\n", encoding="utf-8", ) - payload = prepare(target=target, upstream=upstream) - assert "architecture.md" in payload["existing"] - assert len(payload["installed"]) == 8 + language_sha = commit_all(repo=language_upstream, message="language fixture") + language_target = init_repo(path=language_root / "target") + language_prepared = prepare_success( + target=language_target, + upstream=language_upstream, + language="zh", + ) + assert language_prepared["upstream_sha"] == language_sha + projectize(target=language_target, label="wrong-language") + wrong_result, wrong_payload = run_check( + target=language_target, + upstream=language_upstream, + language="en", + upstream_sha=language_sha, + expected_target_head=language_prepared["target_head"], + environment=None, + ) + assert wrong_result.returncode != 0 + assert wrong_payload["error"] == ( + "固定上游模板违反 source marker invariant" + ) + assert "en/architecture.md" in wrong_payload["detail"] + + # unrelated object store 中即使 SHA 存在,也不能在没有九份 source path 时通过。 + unrelated = init_repo(path=language_root / "unrelated") + unrelated_sha = git( + repo=unrelated, + args=["rev-parse", "HEAD"], + check=True, + ).strip() + unrelated_result, unrelated_payload = run_check( + target=language_target, + upstream=unrelated, + language="zh", + upstream_sha=unrelated_sha, + expected_target_head=language_prepared["target_head"], + environment=None, + ) + assert unrelated_result.returncode != 0 + assert unrelated_payload["error"] == "无法读取固定上游模板" + assert "zh/architecture.md" in unrelated_payload["detail"] -def test_prepare_preflights_parent_collision(tmp_path: Path) -> None: - """父路径冲突必须在安装任意模板前失败。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - (target / "docs").write_text("not a directory\n", encoding="utf-8") - commit_all(repo=target, message="parent collision") - result, payload = run_sync( - command="prepare", - target=target, +def test_scenario_2_prepare_safe_failures(tmp_path: Path) -> None: + """场景 2:prepare 的各类前置失败都不改变目标 bytes 或状态。""" + upstream, _ = create_upstream( + root=tmp_path / "upstream", + version="v1", + ) + + # 真实仓库的子目录不能冒充根目录,且其 bytes 与父仓库状态必须保持不变。 + non_root_repo = init_repo(path=tmp_path / "non-root-repo") + non_git = non_root_repo / "nested" + non_git.mkdir() + (non_git / "note.txt").write_text(data="nested directory\n", encoding="utf-8") + assert_prepare_failure( + target=non_git, upstream=upstream, + expected_error="目标仓库必须是 Git 根目录", + expected_core_paths=set(), ) - assert result.returncode == 1 - assert "父路径不是目录" in payload["error"] - assert not (target / "architecture.md").exists() + # allowlist 外 dirty path 必须在任何核心模板落盘前终止。 + outside_dirty = init_repo(path=tmp_path / "outside-dirty") + (outside_dirty / "rogue.txt").write_text(data="rogue\n", encoding="utf-8") + dirty_payload = assert_prepare_failure( + target=outside_dirty, + upstream=upstream, + expected_error="存在同步范围外的 dirty path", + expected_core_paths=set(), + ) + assert "rogue.txt" in dirty_payload["detail"] -def test_prepare_rejects_symlink_destination_before_write(tmp_path: Path) -> None: - """核心文档 symlink 不得被当成已有普通文档。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") + # symlink 预检必须保留链接目标,并阻止其余八份模板形成部分安装。 + symlink_target = init_repo(path=tmp_path / "symlink-target") outside = tmp_path / "outside.md" - outside.write_text("# Outside\n", encoding="utf-8") - (target / "architecture.md").symlink_to(outside) - result, payload = run_sync( - command="prepare", - target=target, + outside.write_text(data="outside bytes\n", encoding="utf-8") + (symlink_target / "SOP.md").symlink_to(target=outside) + assert_prepare_failure( + target=symlink_target, upstream=upstream, + expected_error="核心文档路径不能是符号链接", + expected_core_paths={"SOP.md"}, ) - assert result.returncode == 1 - assert "符号链接" in payload["error"] - assert not (target / "capability_contract.json").exists() - assert outside.read_text(encoding="utf-8") == "# Outside\n" + assert outside.read_bytes() == b"outside bytes\n" + # 当前 HEAD 的 legacy source 缺少 active marker 时必须在目标写入前失败。 + legacy_upstream = init_repo(path=tmp_path / "legacy-upstream") + write_templates(upstream=legacy_upstream, version="legacy") + (legacy_upstream / "zh/architecture.md").write_text( + data="# Legacy marker-less architecture\n", + encoding="utf-8", + ) + commit_all(repo=legacy_upstream, message="legacy marker-less source") + legacy_target = init_repo(path=tmp_path / "legacy-target") + legacy_payload = assert_prepare_failure( + target=legacy_target, + upstream=legacy_upstream, + expected_error="固定上游模板违反 source marker invariant", + expected_core_paths=set(), + ) + assert "zh/architecture.md" in legacy_payload["detail"] -@pytest.mark.parametrize("missing_path", CORE_FILES) -def test_check_requires_all_nine_files(tmp_path: Path, missing_path: str) -> None: - """缺少任意核心文件都必须失败。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / missing_path).unlink() - assert_check_failed( - target=target, - upstream=upstream, - prepared=prepared, - expected_text=f"缺少必需文件: {missing_path}", + # Git object 的原始 bytes 不是 UTF-8 时也必须在目标写入前失败。 + binary_upstream = init_repo(path=tmp_path / "binary-upstream") + write_templates(upstream=binary_upstream, version="binary") + (binary_upstream / "zh/architecture.md").write_bytes( + data=b"# invalid source\n\xff\n" ) + commit_all(repo=binary_upstream, message="invalid UTF-8 source") + binary_target = init_repo(path=tmp_path / "binary-target") + binary_payload = assert_prepare_failure( + target=binary_target, + upstream=binary_upstream, + expected_error="固定上游模板不是有效 UTF-8", + expected_core_paths=set(), + ) + assert "zh/architecture.md" in binary_payload["detail"] -def test_check_rejects_invalid_utf8(tmp_path: Path) -> None: - """九份文件必须能严格解码为 UTF-8。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / "architecture.md").write_bytes(b"# valid\n\xff\n") - assert_check_failed( +def test_scenario_3_check_rejects_invalid_final_states(tmp_path: Path) -> None: + """场景 3:公开 check 拒绝身份、范围、内容、marker、JSON 和 whitespace 失败。""" + root = tmp_path / "missing" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "architecture.md").unlink() + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text="不是有效 UTF-8", + expected_error="最终仓库检查失败", + expected_detail="缺少必需文件: architecture.md", ) + for marker_index, marker in enumerate(ACTIVE_MARKERS): + root = tmp_path / f"active-{marker_index}" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "architecture.md").write_text( + data=f"project text\n{marker}\n", + encoding="utf-8", + ) + assert_check_failure( + target=target, + upstream=upstream, + prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail=marker, + ) -@pytest.mark.parametrize( - ("content", "expected"), - [ - ("{\n", "JSON 无效"), - ("[]\n", "顶层必须是 JSON object"), - ], -) -def test_check_validates_capability_contract( - tmp_path: Path, content: str, expected: str -) -> None: - """capability contract 应验证合法 JSON object。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / "capability_contract.json").write_text(content, encoding="utf-8") - assert_check_failed( + for case_name, content, detail in ( + ("invalid-json", "{\n", "JSON 无效"), + ("non-object-json", "[]\n", "顶层必须是 JSON object"), + ): + root = tmp_path / case_name + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "capability_contract.json").write_text( + data=content, + encoding="utf-8", + ) + assert_check_failure( + target=target, + upstream=upstream, + prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail=detail, + ) + + root = tmp_path / "empty" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "architecture.md").write_text(data="", encoding="utf-8") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text=expected, + expected_error="最终仓库检查失败", + expected_detail="文件为空", ) - -def test_capability_contract_accepts_project_specific_object(tmp_path: Path) -> None: - """机械层不强制项目合同 schema 或虚构语义锚点。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / "capability_contract.json").write_text( - '{"project":"schema-owned-by-target"}\n', - encoding="utf-8", - ) - result, payload = run_check( + root = tmp_path / "invalid-utf8" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "architecture.md").write_bytes(data=b"project\n\xff") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail="不是有效 UTF-8", ) - assert result.returncode == 0, payload - -@pytest.mark.parametrize( - ("content", "expected"), - [ - ("", "文件为空"), - ("plain text\n", "缺少非空 Markdown 标题"), - ("#\nbody\n", "Markdown 标题为空"), - ("# #\nbody\n", "Markdown 标题为空"), - ("# ###\nbody\n", "Markdown 标题为空"), - ("```text\n# code only\n```\n", "缺少非空 Markdown 标题"), - ], -) -def test_check_rejects_empty_file_or_markdown_title( - tmp_path: Path, content: str, expected: str -) -> None: - """空内容、空标题和 code fence 内伪标题都不能通过。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / "architecture.md").write_text(content, encoding="utf-8") - assert_check_failed( + root = tmp_path / "check-symlink" + upstream, target, prepared = create_ready_case(root=root, language="zh") + outside = tmp_path / "check-outside.md" + outside.write_text(data="external\n", encoding="utf-8") + (target / "architecture.md").unlink() + (target / "architecture.md").symlink_to(target=outside) + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text=expected, + expected_error="最终仓库检查失败", + expected_detail="核心文档不能是符号链接", ) + # 同一 final-bytes 路径覆盖 untracked、staged、tracked 和 committed 四种 Git 状态。 + for state in ("untracked", "staged", "tracked", "committed"): + root = tmp_path / f"whitespace-{state}" + upstream, target, prepared = create_ready_case(root=root, language="zh") + if state == "tracked": + commit_all(repo=target, message="project docs") + prepared = prepare_success( + target=target, + upstream=upstream, + language="zh", + ) + append_bad_whitespace(path=target / "architecture.md") + if state == "staged": + git(repo=target, args=["add", "architecture.md"], check=True) + elif state == "committed": + commit_all(repo=target, message="bad whitespace") + prepared = prepare_success( + target=target, + upstream=upstream, + language="zh", + ) + assert_check_failure( + target=target, + upstream=upstream, + prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail="trailing whitespace", + ) -@pytest.mark.parametrize("token", TEMPLATE_TOKENS) -def test_check_rejects_explicit_template_tokens(tmp_path: Path, token: str) -> None: - """明确列出的模板 token 和待填写值必须逐项失败。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / "architecture.md").write_text( - f"# Project architecture\n\nResidue: {token}\n", + # ignored core path 仍属于 final bytes;Git-equivalent 检查也必须拒绝 conflict marker。 + root = tmp_path / "ignored-conflict-marker" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / ".gitignore").write_text( + data="architecture.md\n", encoding="utf-8", ) - assert_check_failed( - target=target, - upstream=upstream, - prepared=prepared, - expected_text="检测到未项目化内容", - ) - - -def test_check_does_not_generalize_angle_bracket_tokens(tmp_path: Path) -> None: - """合法命令占位不得被泛化的尖括号规则误杀。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) (target / "architecture.md").write_text( - "# Project architecture\n\n" - "Compare <base>...HEAD and publish <feature-branch> for <time range>.\n", + data="<<<<<<< ours\nproject fact\n=======\nother fact\n>>>>>>> theirs\n", encoding="utf-8", ) - result, payload = run_check( - target=target, - upstream=upstream, - prepared=prepared, - ) - assert result.returncode == 0, payload - - -@pytest.mark.parametrize("relative_path", NON_PR_FILES) -def test_check_rejects_non_pr_file_equal_to_template( - tmp_path: Path, relative_path: str -) -> None: - """除 PR template 外的八份文档都必须体现项目事实。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / relative_path).write_text( - (upstream / "zh" / relative_path).read_text(encoding="utf-8"), - encoding="utf-8", + ignored_status = git( + repo=target, + args=["status", "--porcelain=v1", "--ignored"], + check=True, ) - assert_check_failed( + assert "!! architecture.md" in ignored_status + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text=f"{relative_path}: 不允许与固定上游模板完全相同", + expected_error="最终仓库检查失败", + expected_detail="leftover conflict marker", ) - -def test_check_treats_crlf_template_copy_as_equal(tmp_path: Path) -> None: - """模板 equality 只应忽略平台换行差异。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - text = (upstream / "zh/architecture.md").read_text(encoding="utf-8") - (target / "architecture.md").write_bytes(text.replace("\n", "\r\n").encode()) - assert_check_failed( + root = tmp_path / "gitignore-whitespace" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / ".gitignore").write_text(data="*.local \n", encoding="utf-8") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text="architecture.md: 不允许与固定上游模板完全相同", + expected_error="最终仓库检查失败", + expected_detail="trailing whitespace", ) - -def test_pr_template_exact_inheritance_is_allowed(tmp_path: Path) -> None: - """PR template 是唯一允许与固定上游完全相同的核心文件。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - result, payload = run_check( + root = tmp_path / "gitignore-invalid-utf8" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / ".gitignore").write_bytes(data=b"*.local\n\xff\n") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail=".gitignore: 不是有效 UTF-8", ) - assert result.returncode == 0, payload - assert payload["files_checked"] == 9 - -def tracked_ready_case(*, tmp_path: Path) -> tuple[Path, Path, dict[str, Any]]: - """创建核心文档已提交、随后重新 prepare 的 whitespace 场景。""" - upstream, target, _ = ready_case(tmp_path=tmp_path) + # index 中为 bad whitespace、worktree 为 clean 时存在两个发布候选,必须先统一。 + root = tmp_path / "index-worktree-split" + upstream, target, _ = create_ready_case(root=root, language="zh") commit_all(repo=target, message="project docs") - prepared = prepare(target=target, upstream=upstream) - return upstream, target, prepared - - -@pytest.mark.parametrize("state", ["unstaged", "staged", "committed"]) -def test_check_rejects_tracked_whitespace(tmp_path: Path, state: str) -> None: - """working tree、index 与已提交基线的 whitespace 都必须被发现。""" - upstream, target, prepared = tracked_ready_case(tmp_path=tmp_path) - with (target / "architecture.md").open("a", encoding="utf-8") as stream: - stream.write("bad trailing whitespace \n") - if state == "staged": - git(repo=target, args=["add", "architecture.md"]) - elif state == "committed": - commit_all(repo=target, message="bad baseline") - prepared = prepare(target=target, upstream=upstream) - assert_check_failed( + prepared = prepare_success( target=target, upstream=upstream, - prepared=prepared, - expected_text="trailing whitespace", - ) - - -@pytest.mark.parametrize("bad_line,ignored", [ - ("bad trailing whitespace \n", False), ("<<<<<<< ours\n", False), - ("bad trailing whitespace \n", True), -]) -def test_untracked_whitespace(tmp_path: Path, bad_line: str, ignored: bool) -> None: - """untracked 或 ignored 核心文档必须接受同等检查。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - if ignored: - (target / ".gitignore").write_text("architecture.md\n", encoding="utf-8") - with (target / "architecture.md").open("a", encoding="utf-8") as stream: - stream.write(bad_line) - expected = "trailing whitespace" if bad_line.startswith("bad") else "conflict marker" - assert_check_failed( + language="zh", + ) + architecture = target / "architecture.md" + clean_bytes = architecture.read_bytes() + append_bad_whitespace(path=architecture) + git(repo=target, args=["add", "architecture.md"], check=True) + architecture.write_bytes(data=clean_bytes) + assert "MM architecture.md" in repository_status(root=target) + assert_check_failure( target=target, upstream=upstream, prepared=prepared, - expected_text=expected, + expected_error="editable path 存在 index/worktree 分叉", + expected_detail="MM architecture.md", ) - -@pytest.mark.parametrize( - ("content", "expected"), - [(b"*.local\n", None), (b"*.local \n", "trailing whitespace"), - (b"*.local\n\xff", "不是有效 UTF-8")], -) -def test_check_validates_optional_gitignore( - tmp_path: Path, content: bytes, expected: str | None) -> None: - """可选 gitignore 必须是 UTF-8,且继续使用 Git whitespace 规则。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - (target / ".gitignore").write_bytes(content) - if expected is not None: - assert_check_failed( - target=target, upstream=upstream, prepared=prepared, expected_text=expected - ) - else: - result, payload = run_check(target=target, upstream=upstream, prepared=prepared) - assert result.returncode == 0, payload - - -@pytest.mark.parametrize("kind", ["symlink", "directory"]) -def test_check_rejects_non_regular_gitignore(tmp_path: Path, kind: str) -> None: - """可选 gitignore 不得是 symlink 或其他非普通文件。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - outside = tmp_path / "outside.gitignore" - outside.write_text("external target\n", encoding="utf-8") - if kind == "symlink": - (target / ".gitignore").symlink_to(outside) - else: - (target / ".gitignore").mkdir() - expected = "符号链接" if kind == "symlink" else "不是普通文件" - assert_check_failed( - target=target, upstream=upstream, prepared=prepared, expected_text=expected - ) - assert outside.read_text(encoding="utf-8") == "external target\n" - - -def test_check_rejects_changed_target_head(tmp_path: Path) -> None: - """prepare 后目标 HEAD 变化必须先于内容检查失败。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - commit_all(repo=target, message="unexpected commit") - result, payload = run_check( + # 目标仓库 attributes 不得关闭 checker 对 final Markdown 的 whitespace 规则。 + root = tmp_path / "attributes-cannot-disable-whitespace" + upstream, target, _ = create_ready_case(root=root, language="zh") + (target / ".gitattributes").write_text( + data="*.md -whitespace\n", + encoding="utf-8", + ) + commit_all(repo=target, message="disable repository whitespace attributes") + prepared = prepare_success( + target=target, + upstream=upstream, + language="zh", + ) + append_bad_whitespace(path=target / "architecture.md") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, + expected_error="最终仓库检查失败", + expected_detail="trailing whitespace", ) - assert result.returncode == 1 - assert payload["error"] == "目标 HEAD 已变化" - -def test_check_is_idempotent_and_read_only(tmp_path: Path) -> None: - """连续 check 应返回同一结果且不改变文件或 Git 状态。""" - upstream, target, prepared = ready_case(tmp_path=tmp_path) - before = snapshot(repo=target) - first_result, first = run_check( + # 合法的用户级 attributes 也不得把同一份 bad final bytes 改判为通过。 + root = tmp_path / "global-attributes-cannot-disable-whitespace" + upstream, target, prepared = create_ready_case(root=root, language="zh") + home = root / "home" + home.mkdir() + global_attributes = root / "global_attributes" + global_attributes.write_text(data="*.md -whitespace\n", encoding="utf-8") + global_config = home / ".gitconfig" + isolated_environment = { + "GIT_CONFIG_GLOBAL": str(global_config), + "HOME": str(home), + "XDG_CONFIG_HOME": str(home / "xdg"), + } + config_result = run_command( + args=[ + "git", + "config", + "--global", + "core.attributesFile", + str(global_attributes), + ], + cwd=root, + environment=isolated_environment, + ) + assert config_result.returncode == 0, config_result.stderr + append_bad_whitespace(path=target / "architecture.md") + check_result, check_payload = run_check( target=target, upstream=upstream, - prepared=prepared, + language=prepared["language"], + upstream_sha=prepared["upstream_sha"], + expected_target_head=prepared["target_head"], + environment=isolated_environment, ) - middle = snapshot(repo=target) - second_result, second = run_check( + assert check_result.returncode != 0 + assert check_payload["error"] == "最终仓库检查失败" + assert "trailing whitespace" in check_payload["detail"] + + root = tmp_path / "head-change" + upstream, target, prepared = create_ready_case(root=root, language="zh") + commit_all(repo=target, message="unexpected head") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, + expected_error="目标 HEAD 已变化", + expected_detail="期望", ) - assert first_result.returncode == second_result.returncode == 0 - assert first == second - assert before == middle == snapshot(repo=target) - assert not (target / ".coding_workflow").exists() - assert not (target / "PR_BODY.md").exists() - - -def test_existing_ignored_pr_body_is_not_sync_input(tmp_path: Path) -> None: - """被目标仓库忽略的通用 PR 草稿不得被读取、重写或删除。""" - upstream, _ = create_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - (target / ".gitignore").write_text("PR_BODY.md\n", encoding="utf-8") - body = "# Unrelated PR draft\n" - (target / "PR_BODY.md").write_text(body, encoding="utf-8") - prepared = prepare(target=target, upstream=upstream) - specialize(target=target) - result, payload = run_check( + + root = tmp_path / "outside-dirty" + upstream, target, prepared = create_ready_case(root=root, language="zh") + (target / "rogue.txt").write_text(data="rogue\n", encoding="utf-8") + assert_check_failure( target=target, upstream=upstream, prepared=prepared, + expected_error="存在同步范围外的 dirty path", + expected_detail="rogue.txt", ) - assert result.returncode == 0, payload - assert (target / "PR_BODY.md").read_text(encoding="utf-8") == body - assert not (target / ".coding_workflow").exists() -def create_installer_upstream(*, root: Path) -> Path: - """创建同时含 canonical Skill 和应被忽略的第二 Skill 的 clean 上游。""" - upstream = init_repo(path=root) - canonical = upstream / "zh/skills/workflow-docs-sync" - canonical.mkdir(parents=True) - (canonical / "SKILL.md").write_text( - "---\nname: workflow-docs-sync\n" - "description: Test canonical skill.\n---\n\n# Skill\n", - encoding="utf-8", +def test_scenario_4_installer_end_to_end(tmp_path: Path) -> None: + """场景 4:user/repo、Codex/Claude、精确清理和重复安装形成完整闭环。""" + upstream = create_installer_upstream(root=tmp_path / "upstream") + + # User scope 不依赖目标 Git;第二次运行必须产生相同安装 bytes。 + home = tmp_path / "home" + home.mkdir() + protected_targets = seed_legacy_install( + install_root=home, + obsolete_kinds=("symlink", "file"), ) - (canonical / "scripts").mkdir() - (canonical / "scripts/helper.py").write_text("VALUE = 1\n", encoding="utf-8") - obsolete = upstream / "zh/skills/workflow-docs-sync-review" - obsolete.mkdir(parents=True) - (obsolete / "SKILL.md").write_text( - "---\nname: workflow-docs-sync-review\n" - "description: Must not be copied.\n---\n", - encoding="utf-8", + user_result, user_payload = run_user_install(upstream=upstream, home=home) + assert user_result.returncode == 0, user_payload + assert user_payload["scope"] == "user" + assert user_payload["removed_obsolete"] == [ + str(home / platform_root / OBSOLETE_SKILL) + for _, platform_root in PLATFORM_ROOTS + ] + assert_installed(install_root=home) + for protected in protected_targets: + assert protected.read_bytes() == b"protected target\x00" + user_snapshot = file_tree(root=home) + repeated_result, repeated_payload = run_user_install( + upstream=upstream, + home=home, ) - commit_all(repo=upstream, message="skills") - return upstream - + assert repeated_result.returncode == 0, repeated_payload + assert repeated_payload["removed_obsolete"] == [] + assert file_tree(root=home) == user_snapshot -def run_installer( - *, upstream: Path, target: Path | None = None, home: Path | None = None -) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]]: - """以 repo 或临时 HOME user scope 运行安装器并解析 JSON。""" - scope = "repo" if target is not None else "user" - args = [sys.executable, str(INSTALLER), "--scope", scope, "--upstream-dir", str(upstream)] - if target is not None: - args.extend(["--target-repo", str(target)]) - result = run( - args=args, - cwd=REPO_ROOT, - environment={"HOME": str(home)} if home is not None else None, + # Repo scope 先提交安装结果,再证明重复安装不制造任何 Git diff。 + target = init_repo(path=tmp_path / "target") + seed_legacy_install( + install_root=target, + obsolete_kinds=("directory", "directory"), ) - return result, parse_single_json(result=result) - - -def test_installer_repo_upgrade_removes_only_obsolete_directories(tmp_path: Path) -> None: - """repo 升级应精确删除两份旧 reviewer,并保持其他 Skill 字节不变。""" - upstream = create_installer_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - for platform_root in PLATFORM_ROOTS: - obsolete = target / platform_root / OBSOLETE_SKILL - obsolete.mkdir(parents=True) - (obsolete / "legacy.bin").write_bytes(b"legacy reviewer\x00") - unrelated = target / platform_root / "unrelated-skill" - unrelated.mkdir() - (unrelated / "keep.bin").write_bytes(b"keep unrelated\x00") - commit_all(repo=target, message="legacy installed skills") - result, payload = run_installer(upstream=upstream, target=target) - assert result.returncode == 0, payload - assert payload["skill"] == "workflow-docs-sync" - expected = [str(target / root / OBSOLETE_SKILL) for root in PLATFORM_ROOTS] - assert payload["removed_obsolete"] == expected - for platform_root in PLATFORM_ROOTS: - installed = target / platform_root / "workflow-docs-sync" - assert (installed / "SKILL.md").is_file() - assert (installed / "scripts/helper.py").is_file() - assert not (target / platform_root / OBSOLETE_SKILL).exists() - unrelated = target / platform_root / "unrelated-skill/keep.bin" - assert unrelated.read_bytes() == b"keep unrelated\x00" - assert not list(target.rglob(".source.json")) - commit_all(repo=target, message="upgrade installed skills") - repeated, repeated_payload = run_installer(upstream=upstream, target=target) - assert repeated.returncode == 0, repeated_payload + commit_all(repo=target, message="legacy skills") + repo_result, repo_payload = run_repo_install( + upstream=upstream, + target=target, + ) + assert repo_result.returncode == 0, repo_payload + assert repo_payload["scope"] == "repo" + assert_installed(install_root=target) + commit_all(repo=target, message="installed canonical skill") + repo_snapshot = repository_snapshot(root=target) + repeated_result, repeated_payload = run_repo_install( + upstream=upstream, + target=target, + ) + assert repeated_result.returncode == 0, repeated_payload assert repeated_payload["removed_obsolete"] == [] - assert git(repo=target, args=["status", "--short"]) == "" + assert repository_snapshot(root=target) == repo_snapshot -def test_installer_removes_obsolete_symlink_and_file(tmp_path: Path) -> None: - """旧路径为 symlink 或普通文件时应 unlink,且不得跟随 symlink。""" - upstream = create_installer_upstream(root=tmp_path / "upstream") - target = create_target(root=tmp_path / "target") - outside = tmp_path / "outside-skill" - outside.mkdir() - (outside / "keep.bin").write_bytes(b"external\x00") - paths = [target / root / OBSOLETE_SKILL for root in PLATFORM_ROOTS] - paths[0].parent.mkdir(parents=True) - paths[0].symlink_to(outside, target_is_directory=True) - paths[1].parent.mkdir(parents=True) - paths[1].write_bytes(b"legacy file\x00") - commit_all(repo=target, message="legacy path types") - result, payload = run_installer(upstream=upstream, target=target) - assert result.returncode == 0, payload - assert payload["removed_obsolete"] == [str(path) for path in paths] - assert all(not path.exists() and not path.is_symlink() for path in paths) - assert (outside / "keep.bin").read_bytes() == b"external\x00" +def test_scenario_5_repository_distribution_contract() -> None: + """场景 5:真实仓库 bytes、Skill 结构、语境边界与旧控制面保持可分发。""" + root_agents = REPO_ROOT / "AGENTS.md" + assert root_agents.is_file() and not root_agents.is_symlink() - -def test_installer_user_upgrade_cleans_temporary_home(tmp_path: Path) -> None: - """user scope 应在临时 HOME 执行同样的精确清理。""" - upstream = create_installer_upstream(root=tmp_path / "upstream") - home = tmp_path / "home" - for platform_root in PLATFORM_ROOTS: - obsolete = home / platform_root / OBSOLETE_SKILL - obsolete.mkdir(parents=True) - (obsolete / "legacy.bin").write_bytes(b"legacy\x00") - unrelated = home / platform_root / "unrelated-skill" - unrelated.mkdir() - (unrelated / "keep.bin").write_bytes(b"keep\x00") - result, payload = run_installer(upstream=upstream, home=home) - assert result.returncode == 0, payload - expected = [str(home / root / OBSOLETE_SKILL) for root in PLATFORM_ROOTS] - assert payload["removed_obsolete"] == expected - for platform_root in PLATFORM_ROOTS: - assert not (home / platform_root / OBSOLETE_SKILL).exists() - assert (home / platform_root / "workflow-docs-sync/SKILL.md").is_file() - unrelated = home / platform_root / "unrelated-skill/keep.bin" - assert unrelated.read_bytes() == b"keep\x00" - assert not list(home.rglob(".source.json")) - - -def test_sync_cli_exposes_only_prepare_and_check() -> None: - """内部 CLI 不得重新长出其他控制入口。""" - result = run( - args=[sys.executable, str(SYNC_SCRIPT), "--help"], - cwd=REPO_ROOT, + skill_symlinks = sorted( + path.relative_to(SKILL_ROOT).as_posix() + for path in SKILL_ROOT.rglob("*") + if is_installable_skill_entry(path=path) and path.is_symlink() ) - assert result.returncode == 0 - assert "{prepare,check}" in result.stdout - assert "status" not in result.stdout + assert skill_symlinks == [] + actual_skill_directories = sorted( + path.relative_to(SKILL_ROOT).as_posix() + for path in SKILL_ROOT.rglob("*") + if is_installable_skill_entry(path=path) and path.is_dir() + ) + assert actual_skill_directories == ["agents", "evals", "scripts"] + actual_skill_files = sorted( + path.relative_to(SKILL_ROOT).as_posix() + for path in SKILL_ROOT.rglob("*") + if is_installable_skill_entry(path=path) and path.is_file() + ) + assert actual_skill_files == [ + "SKILL.md", + "agents/openai.yaml", + "evals/README.md", + "scripts/sync_docs.py", + ] + # 真实模板只通过直接 bytes 与 Git CLI 校验,避免重新耦合生产 helper。 + for language in ("zh", "en"): + actual_templates = { + relative_path + for relative_path in CORE_FILES + if (REPO_ROOT / language / relative_path).is_file() + } + assert actual_templates == set(CORE_FILES) + for relative_path in CORE_FILES: + path = REPO_ROOT / language / relative_path + assert path.is_file() and not path.is_symlink() + content = path.read_bytes() + text = content.decode("utf-8") + assert content + assert b"\r" not in content + whitespace = run_command( + args=[ + "git", + "diff", + "--no-index", + "--check", + "--", + os.devnull, + str(path), + ], + cwd=REPO_ROOT, + environment=None, + ) + assert whitespace.returncode in (0, 1), ( + whitespace.stdout + whitespace.stderr + ) + assert whitespace.stdout == "" + assert whitespace.stderr == "" + if relative_path in NON_PR_FILES: + assert any(marker in text for marker in ACTIVE_MARKERS), ( + "equality removal depends on an active marker in " + f"{language}/{relative_path}" + ) + + # 固定文档锚点不得在 marker 清零后偷偷保留泛化测试结论。 + 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")) + 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"): + assert anchor in by_anchor + assert not { + "test_anchor", + "test_status", + "untested_reason", + } & set(by_anchor[anchor]) + + # 机械兜底只覆盖本次真实误植过的八份下游模板与五个精确 token。 + internal_context_tokens = ( + "disposable clone", + "同步工作树", + "synchronized worktree", + "共享工作树", + "shared worktree", + "fresh-context", + "blind-first", + "通用 GitHub 发布能力", + "general GitHub publishing capability", + ) + guarded_templates = ( + "AGENTS.md", + "TESTING.md", + "PR_Checklist.md", + ".github/pull_request_template.md", + ) + context_hits: list[str] = [] + for language in ("zh", "en"): + for relative_path in guarded_templates: + path = REPO_ROOT / language / relative_path + text = path.read_text(encoding="utf-8") + for token in internal_context_tokens: + if token in text: + 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) -def test_deleted_control_plane_files_do_not_exist() -> None: - """旧 launcher、review Skill、runbook 和测试不能作为兼容实现保留。""" - deleted = ( + deleted_paths = ( "PR_BODY.md", "scripts/sync_coding_workflow.py", "scripts/sync.sh", @@ -932,68 +1228,49 @@ def test_deleted_control_plane_files_do_not_exist() -> None: "zh/scripts/sync_pr_review_system.md", "en/scripts/sync_pr_review_system.md", "zh/skills/workflow-docs-sync/scripts/harness.py", - "zh/skills/workflow-docs-sync/references/modes.md", - "zh/skills/workflow-docs-sync/references/pass_ownership.json", + "zh/skills/workflow-docs-sync/references", "zh/skills/workflow-docs-sync-review", "tests/test_workflow_sync_skill.py", "tests/test_sync_coding_workflow.py", ) - assert [path for path in deleted if (REPO_ROOT / path).exists()] == [] - assert git(repo=REPO_ROOT, args=["ls-files", "--", "PR_BODY.md"]) == "" - - -def production_text_files() -> list[Path]: - """列出结构性负向测试覆盖的生产与用户文档文件。""" - suffixes = {".md", ".py", ".yaml", ".yml", ".json", ".sh"} - excluded = { - REPO_ROOT / "zh/docs/development_workflow/decisions.md", - Path(__file__).resolve(), - } - return sorted( + assert [ path - for path in REPO_ROOT.rglob("*") - if path.is_file() - and ".git" not in path.relative_to(REPO_ROOT).parts - and "__pycache__" not in path.parts - and path.suffix in suffixes - and path not in excluded + for path in deleted_paths + if (REPO_ROOT / path).exists() or (REPO_ROOT / path).is_symlink() + ] == [] + + machine_identifiers = ( + "active_mode", + "completed_modes", + "run.json", + "skill_runtime", + "skill_results", + "SYNC_PR_BODY_MARKER", + "sync:agent:start", + "headRefOid", ) - - -def test_production_files_have_no_forbidden_control_plane_words() -> None: - """除 DEC 历史外,生产面不得残留旧控制面状态或远端绑定词。""" - hits: list[str] = [] - for path in production_text_files(): + identifier_hits: list[str] = [] + for path in production_text_files(root=REPO_ROOT): text = path.read_text(encoding="utf-8") - for word in FORBIDDEN_CONTROL_WORDS: - if word in text: - hits.append(f"{path.relative_to(REPO_ROOT)}: {word}") - assert hits == [] - - -def test_source_line_budgets() -> None: - """保持测试可审查,并限制生产 Python 总体积。""" - test_lines = Path(__file__).read_text(encoding="utf-8").count("\n") + 1 - production_lines = sum( - path.read_text(encoding="utf-8").count("\n") + 1 - for path in (SYNC_SCRIPT, INSTALLER) - ) - assert 600 <= test_lines <= 1000 - assert 500 <= production_lines <= 700 - - -def test_skill_has_exact_minimal_structure() -> None: - """canonical Skill 只保留合同指定的最小文件结构。""" - actual = sorted( - path.relative_to(SKILL_ROOT).as_posix() - for path in SKILL_ROOT.rglob("*") - if path.is_file() and "__pycache__" not in path.parts + for identifier in machine_identifiers: + if identifier in text: + identifier_hits.append( + f"{path.relative_to(REPO_ROOT)}: {identifier}" + ) + assert identifier_hits == [] + + help_result = run_command( + args=[sys.executable, str(SYNC_SCRIPT), "--help"], + cwd=REPO_ROOT, + environment=None, ) - assert actual == [ - "SKILL.md", - "agents/openai.yaml", - "evals/README.md", - "references/audit.md", - "references/sections.md", - "scripts/sync_docs.py", - ] + assert help_result.returncode == 0 + assert "{prepare,check}" in help_result.stdout + for removed_command in ( + "start-pass", + "finish-pass", + "prepare-submit", + "seal-submit", + "finish-submit", + ): + assert removed_command not in help_result.stdout diff --git a/zh/.github/pull_request_template.md b/zh/.github/pull_request_template.md index 80221bf..ea6bfdd 100644 --- a/zh/.github/pull_request_template.md +++ b/zh/.github/pull_request_template.md @@ -1,113 +1,49 @@ <!-- -PR body 原则: - -1. 只写本 PR 已经完成的事实,不写计划。 -2. 文件清单必须来自:git diff --name-only <base>...HEAD。 -3. 测试策略与测试证据记录方式以 TESTING.md 为准。 -4. 用户可见变化对照 interact.md。 -5. 架构变化对照 architecture.md。 -6. 每轮 review / 修复都必须写入“Review / 修复记录”。 +只写本 PR 已完成的事实。变更范围来自真实 Git diff;测试证据遵循 TESTING.md;用户行为和 +架构影响分别核对 interact.md 与 architecture.md。不要把计划、历史草稿或本地未提交内容 +写入 PR body。 --> -## 1. 背景与目标 +## 1. Background and Goal ---- +<!-- 说明问题、目标、关联需求,以及本 PR 明确不处理的范围。 --> -## 2. 实现方案 +## 2. Implementation and Tradeoffs -<!-- -写核心思路和关键取舍。 -不要复述所有代码。 ---> - ---- -## 3. 变更范围 - -<!-- -必须来自: -git diff --name-only <base>...HEAD - -只列本 PR 实际改动的文件或目录。 -不要写当前 patch 中不存在的文件。 ---> +<!-- 说明核心实现、关键取舍、被拒绝方案及原因,不逐文件复述 diff。 --> -| 文件 / 目录 | 变更类型 | 说明 | -|---|---|---| -| | 新增 / 修改 / 删除 | | +## 3. Actual Change Scope ---- -## 4. 文档影响 - -<!-- -只写受影响的文档。 -如果没有文档需要更新,写:无。 - -如果本 PR 改变能力边界,请检查 capability_contract.json / interact.md / docs/business_user_guide.md。 - -如果本 PR 改变用户可观察行为,请检查 interact.md,并判断 docs/business_user_guide.md 是否需要同步。 - -如果本 PR 改变业务人员能问什么、怎么问、结果怎么看、什么时候该找人,请检查 docs/business_user_guide.md。 - -如果新增“能做 / 不能做 / 必须追问 / 必须拒绝”的声明,请确认它有 capability_contract.json anchor_id 或对应测试锚点。 ---> +<!-- 根据 git diff --name-only <base>...HEAD 列出实际变更;不要保留空表或计划中的文件。 --> -受影响文档: +## 4. Documentation Impact -- 无 - -说明: - -- - ---- - -## 5. 用户与架构影响 - -用户可见变化: - -- Yes / No -- 说明: - -架构变化: - -- Yes / No -- 说明: - ---- - -## 6. Review / 修复记录 - -<!-- -单 commit 策略下,这里就是修复历史。 -每次 review、修复、merge-readiness 反馈后都必须更新。 ---> +<!-- 列出实际更新的文档和证据。未更新的受影响候选文档写真实 no-update reason;不要求全部文档都有 diff。 --> -| 轮次 | 来源 | 问题摘要 | 判断 | 处理结果 | 证据 | -|---|---|---|---|---|---| -| R0 | 初始提交 | N/A | N/A | 初始实现 | | -| R1 | Codex / Claude / 人工 | | 真实存在 / 不成立 / 可暂缓 | Fixed / Won't fix / N/A | | +## 5. User-visible and Architecture Impact ---- +<!-- 分别说明用户可观察变化与架构变化;没有时写 None,并给出核对依据。 --> -## 7. 已知限制与回滚 +## 6. Testing Evidence -已知限制: +- Exact command:记录原样命令;未运行时写 `Not run`。 +- Scope:说明该命令实际证明的层级、入口和边界。 +- Result:记录通过、失败、跳过及关键数量或错误。 +- Not-run reason:已运行时写 `Not applicable`;未运行时写具体原因和风险。 +- Environment:记录实际执行环境、隔离方式、副作用和清理结果。 -- +## 7. Review / Fix Record -回滚方式: +<!-- 按 finding ID 记录 severity、证据、判断、修复和复核结果;没有 finding 时列出已核对的高风险点。不要增加重复 reconciliation ledger。 --> -- +## 8. Known Limits, Open Decisions, and Rollback ---- +<!-- 区分已知限制、需要产品判断的 open decision 和可执行回滚;没有时明确写 None。 --> -## 8. 最终自检 +## 9. Final Self-check -- [ ] 当前分支不是主干 -- [ ] 已执行 `git diff --name-only <base>...HEAD` -- [ ] “变更范围”与实际 diff 一致 -- [ ] PR body 不包含历史草稿、旧分支名、未落地计划 -- [ ] 已按 `TESTING.md` 完成测试与测试记录 -- [ ] 用户可见变化已对照 `interact.md` -- [ ] 架构变化已对照 `architecture.md` -- [ ] 每轮 review / 修复都已写入“Review / 修复记录” +- [ ] Actual Change Scope 与真实 diff 一致。 +- [ ] 测试命令、范围、结果和未运行原因准确。 +- [ ] 用户可见与架构影响已核对对应权威文档。 +- [ ] BLOCKER 与 actionable WARN 已关闭;open decisions 未被伪装为完成。 +- [ ] PR body 不含历史草稿、未落地计划、错误 base/head 或仓库内临时 body 路径。 diff --git a/zh/AGENTS.md b/zh/AGENTS.md index c7541d4..608e2fb 100644 --- a/zh/AGENTS.md +++ b/zh/AGENTS.md @@ -1,67 +1,85 @@ -## 文件简介 -### 核心配置 -- `AGENTS.md`:agent 工作入口、文件简介、多 Agent 协作边界、代码规范与用户视角文档关系。 -- `capability_contract.json`:跨项目能力边界、职责边界与 agent 行为承诺的样本注册表。 -- `.github/pull_request_template.md`:PR body 长期模板,生成本地临时 `PR_BODY.md` 时使用。 +# Agent Working Guide -### 核心模块 +## Authority Map -### 业务逻辑 +- 当前代码、配置、测试、committed artifacts 和可重复运行结果是项目事实来源。 +- `architecture.md` 定义系统结构与边界;`TESTING.md` 定义测试入口与证据层级; + `PR_Checklist.md` 定义交付核对;`SOP.md` 只保存稳定流程入口。 +- `capability_contract.json` 定义能力边界,`interact.md` 定义用户可观察行为与验收, + `docs/business_user_guide.md` 只派生解释前两者。 +- 现有文档是需要与实现核对的声明,不能单独证明自身正确。 -### 备注 -* 一旦文件有了修改或新增,需要对照文件简介相应部分进行修改。测试文件不在要求内,因为会有TESTING.md专门管理。 -* 统一编码与查看约定:仓库所有文件均为 UTF-8 编码。使用命令行或脚本查看/编辑时必须显式指定 UTF-8。 -* artifacts文件夹目录是一次性产出物,批准豁免不加入文件简介。 +<!-- project-fill: 补充本项目的其他权威来源及冲突优先级;完成后删除此 marker --> -## 多 Agent 协作 -- 主会话对最终判断、最终产物和最终写入结果负责;受委派结果必须经过主会话审阅与合成,不得未经裁决直接拼接为权威结论。 -- 多个 Agent 得出相同结论不构成独立证据;关键结论必须由代码、测试、文档契约或实际运行结果支持。 -- 调查和审查类子任务默认只读;并行修改仅限边界明确且改动不重叠的任务,最终由主会话统一集成并完成验证。 +## Repository Overview -## 架构说明 -架构权威文档见 `architecture.md`。当本次变更影响模块边界、运行时调用链、数据流、状态模型、错误模型、外部依赖或扩展点时,必须同步更新 `architecture.md`;如无需更新,必须在 PR body 中说明原因。 +文件简介只记录稳定模块、入口和职责,不永久镜像 `git ls-files`。 -## 业务知识 +### Core Configuration -## 审查检查清单 -一旦用户要求PR提交代码,必须完整遵循 PR_Checklist.md,如需豁免必须在 PR 描述中解释原因。 +<!-- project-fill: 列出真实配置入口及其职责;如不适用,写 Not applicable — 已验证原因;完成后删除此 marker --> -## 测试流程 -在遇到BUG或开始测试或提交 PR 之前,必须阅读并遵循 `TESTING.md`,只允许通过PR合并到主分支。 +### Runtime Entrypoints + +<!-- project-fill: 列出用户、服务、作业或 CLI 的真实运行入口;完成后删除此 marker --> + +### Core Modules + +<!-- project-fill: 按稳定模块边界概述核心实现,不逐文件抄目录;完成后删除此 marker --> + +### Domain Logic + +<!-- project-fill: 说明领域规则所在模块及其权威测试或契约;完成后删除此 marker --> + +### Generated Artifacts and External State + +<!-- project-fill: 列出 committed/generated artifacts、持久化状态和外部系统;无此类状态时写 Not applicable — 已验证原因;完成后删除此 marker --> + +## Change Impact Rules + +- 模块边界、运行时调用链、数据流、状态、错误模型、外部依赖或扩展点变化时,更新或确认 + `architecture.md`。 +- 能力边界变化时,先更新或确认 `capability_contract.json`,再检查 `interact.md` 和 business + guide;用户可观察行为变化时,先更新或确认 `interact.md`。 +- 测试仍是事实证据;具体命令、fixture、层级和隔离要求只在 `TESTING.md` 维护。 +- 不要求每次修改全部文档。未更新受影响候选文档时,在交付说明中给出基于当前事实的 + no-update reason。 +- 编码、lint、formatter、build 和类型规则必须从仓库真实配置提取,不从本模板推断。 + +## Collaboration + +- 主执行者对最终判断、最终产物和最终写入结果负责;受委派结果必须经过审阅与合成。 +- 并行写入时必须先明确不重叠的路径所有权;具体隔离方式遵循目标项目政策。 +- 可按模块、调用链、风险或证据类型动态分工,不强制 Agent 数量或固定调度顺序。 +- 协作者结论、投票或共识不等于证据;重要判断必须回到仓库事实和可重复验证。 +- 调查与审查任务默认只读;需要修改时应显式移交给具有写入所有权的执行者。 + +<!-- project-fill: 补充本项目确有需要的协作或所有权规则;没有时删除此 marker --> + +## Architecture + +以 `architecture.md` 为架构权威。修改前从真实入口重建受影响调用链,修改后核对不变量、 +模块职责、数据契约、状态、副作用和失败路径是否闭合。 + +## Testing + +测试前完整读取 `TESTING.md`,从仓库配置确认 exact command。不得用 light、mock、golden +或局部 repair 成功冒充更高验证层级。测试环境由命令副作用、CI 能力和项目政策决定,并在 +执行前确认隔离与清理边界。 ## SOP -当你需要执行以下标准流程时,请阅读并遵循 `SOP.md`;当SOP新增/修改后,也要回补此清单(只需要列出名字): - -## 用户视角文档关系 -本项目区分三类用户视角真相: - -1. `capability_contract.json` - - 能力边界的机器可读真相源。 - - 回答“系统能做什么 / 不能做什么 / 必须追问什么 / 必须拒绝什么”。 - -2. `interact.md` - - 用户可观察行为与验收不变量的真相源。 - - 回答“系统做的时候必须如何表现,什么算验收通过”。 - -3. `docs/business_user_guide.md` - - 面向第一次接触项目的业务人员的派生教学文档。 - - 回答“业务人员可以问什么、怎么问、结果怎么看、什么时候该找人”。 - - 它不独立声明新能力,只能用业务语言解释 `capability_contract.json` 和 `interact.md` 已声明的能力与行为。 - -更新规则: -- 如果本次变更改变能力边界,必须先更新或确认 `capability_contract.json`,再检查 `interact.md` 和 `docs/business_user_guide.md`。 -- 如果本次变更改变用户可观察行为,必须先更新或确认 `interact.md`,再检查 `docs/business_user_guide.md`。 -- 如果本次变更改变业务人员能问什么、怎么问、结果怎么看、什么时候该找人,必须检查 `docs/business_user_guide.md`。 -- `docs/business_user_guide.md` 中任何“能做 / 不能做 / 必须追问 / 必须拒绝”的声明,都必须能在 `capability_contract.json`、`interact.md` 或对应测试中找到锚点。 - -## 代码规范 -1. What I cannot create, I do not understand. -2. 永远用中文回答。在实现功能的前提下,代码量是一种负资产,应尽可能少,其次是可维护性。优先追求修复语义所需的最小结构,而不是字面改动最少;修改代码时,如果变量名不影响理解和运行,不要重命名。 -3. 遵循PEP 8 编码规范。时间使用UTC时间。文字编码使用UTF-8。 -4. 参数管理应统一规划并集中管理。函数调用时应始终显式指定参数名称,而非依赖参数默认的位置顺序。为了避免预期外的行为,严禁使用get函数获取参数。如果一个预期的参数没有被提供,程序应该Fail Fast,而不是带着一个None值继续往下走。 -5. 每一个类和函数都需要包含doc string,函数内每一个功能块都需要带有注释解释why而不是what,预期的输出,以及参数含义,范围,格式。脚本顶部doc string要写明这个脚本的功能或目的,每个函数之间的调用关系。根据issue和FSD(功能规格说明书)提供的信息,努力提高代码自文档化程度。 -6. 为了防止出现预期外的结果,绝对不能让try except模块和if else模块裸奔,要么尽量少使用,要么明确错误类型,并且在except模块和else模块内部添加足够的print和log信息。预期外的错误就应该使其在当前函数报错,绝对不可以扩散。你每多写一个不必要的try,except和else都会增加用户的工作量并减少你被调用的机会。 -7. 数据进数据出原则:所有脚本、函数或模块的交互必须仅通过数据进行。即输入是明确的数据,输出也是数据,不依赖于外部状态或隐式副作用。 -8. 重复使用的代码块请封装成函数或模块,以确保代码的 DRY。 -9. 在确保代码可读性的前提下利用各个语言的特性简化代码、减少性能消耗;模块应职责明确、接口清晰、保持松耦合,避免无必要的抽象层和复杂继承。 -10. 为了提高你的使用率和生存几率,不要有所保留,竭尽全力帮助用户达成目标。 + +执行标准流程时读取 `SOP.md` 的对应入口。执行 checklist 保留在当前会话,不创建仓库内 +运行状态、receipt 或临时流程文档。 + +## PR Delivery + +- 遵循 `PR_Checklist.md` 和 `.github/pull_request_template.md`,以真实 Git diff、测试结果和 + 最终仓库状态编写说明。 +- 默认分支从仓库解析,不硬编码分支名。PR body 草稿位置与发布方式遵循目标项目政策; + 临时草稿不得被误提交,且 body 必须与真实 diff 和测试证据一致。 +- 未经用户明确要求,不 commit、push 或创建 PR。 + +## Project-specific Conventions + +<!-- project-fill: 从 lint、formatter、compiler、build 或团队配置提取项目专属约定;没有可验证约定时写 None — 已检查的配置范围;完成后删除此 marker --> diff --git a/zh/PR_Checklist.md b/zh/PR_Checklist.md index 1b30df6..4583391 100644 --- a/zh/PR_Checklist.md +++ b/zh/PR_Checklist.md @@ -1,50 +1,47 @@ -# PR提交检查清单 -## Commit / Push 策略 - -本项目默认采用“单 PR 单 commit + PR body 记录修复轮次”的策略。 - -目标: -- 保持 PR 对外 commit history 简洁。 -- 避免 reviewer / LLM 被 commit timeline 分散注意力。 -- 用 PR body 的“Review / 修复记录”保存每轮 review 与修复历史。 - -规则: -1. 一个 PR 默认保持 1 个 commit。 -2. 每轮 review / 修复后,必须先更新 PR body 的“Review / 修复记录”。 -3. 修复代码、测试、文档后,使用 `git commit --amend` 合入当前 commit。 -4. 推送重写后的 PR 分支时,必须使用 `git push --force-with-lease`,禁止裸 `git push --force`。 -5. PR body 模板参照 `.github/pull_request_template.md`。 -6. `.github/pull_request_template.md` 是长期模板文件,不直接作为 PR body 提交。 -7. `PR_BODY.md` 是本地临时 PR body 草稿,由 `.github/pull_request_template.md` 生成,不提交仓库,是 review 的重要输入材料,不是核心长期文档。 - -## 能力契约与用户文档同步 - -- [ ] 如果本 PR 修改了 `capability_contract.json`,必须检查 `interact.md` 和 `docs/business_user_guide.md` 中相关能力声明是否需要同步更新;如无需更新,必须在 PR body 的“文档影响”中说明原因。 -- [ ] 如果本 PR 修改了 `interact.md` 或 `docs/business_user_guide.md` 中关于“能做 / 不能做 / 必须追问 / 必须拒绝”的能力或行为声明,必须确认这些声明能在 `capability_contract.json`、`interact.md` 或对应测试中找到锚点;找不到时,必须补充锚点或说明为什么暂时只能人工维护。 -- [ ] 如果本 PR 新增 agent 行为承诺,例如“必须追问”“必须拒绝”“不得猜测”“必须降级”,必须在 `capability_contract.json` 中登记稳定 `anchor_id`,并在 `TESTING.md` 允许的范围内提供对应测试或写明暂时不可测原因。 -- [ ] 如果本 PR 改变业务人员能问什么、怎么问、结果怎么看、什么时候该找人,必须检查 `docs/business_user_guide.md`;如无需更新,必须在 PR body 的“文档影响”中说明原因。 - -## PR提交检查清单 -注意:你必须一一完成check清单(等价于todo list)并最终提交pr,任何偷懒和跳过都会让用户暴跳如雷。 -使用以下固定流程创建 PR,避免把长期模板直接作为 PR body 提交: - -```bash -cp .github/pull_request_template.md PR_BODY.md -# 填写 PR_BODY.md -gh pr create --title "<标题 MMDD>" --body-file PR_BODY.md --head <feature-branch> --base master -``` - -始终牢记你可以使用 gh 工具。 - -在提交 PR 前的确认清单,你需要将其转为todo list进行step by step的完成: -- [ ] 撰写结构化的工作总结,至少包含以下小节,确保下一个开发人员能顺利接手继续开发: - - 背景 & 目标(Why):本次改动解决了什么问题?关联哪些 Issue / 需求? - - 实现方案(How):核心思路、关键设计决策、有无其他候选方案。 - - 变更范围(What):主要修改了哪些模块/文件(可按目录分组列出);文件清单必须来自 `git diff --name-only <base>...HEAD` 的实际输出,禁止写未出现在当前 patch 中的文件。 -- [ ] 确认当前分支不是主干,并调用 git diff 工具仔细分析本地修改,确认无遗漏 -- [ ] 已用 `git diff --name-only <base>...HEAD` 反向核对 `PR_BODY.md` 的“变更范围”:diff 中有但 PR_BODY 未列的已补齐,PR_BODY 中列了但 diff 中不存在的已删除。 -- [ ] 测试策略以 `TESTING.md` 为唯一权威:已按 `TESTING.md` 判断本 PR 是否需要新增/修改测试、需要运行哪些测试、以及测试证据如何记录。 -- [ ] 如果本 PR 新增/修改测试文件,已按 `TESTING.md` 更新测试文件简介或相关测试说明。 -- [ ] 当有文件新增和修改后,确认对应的文档已更新。例如新增了测试文件,就需要更新在`TESTING.md`,有代码脚本的功能被修改,更新在`AGENTS.md`的## 文件简介。 -- [ ] 任何用户可见的行为变化(入口/输出结构/默认行为/错误提示/排序稳定性)都必须同步更新`interact.md`,并确保浏览器验收覆盖了对应断言 -- [ ] 最终提交前,已重新对照 `git diff --name-only <base>...HEAD`、`git status` 与 `PR_BODY.md`,确认 PR 描述不包含历史草稿、本地未提交改动或“计划做但未落地”的内容 +# PR Submission Checklist + +本文件本身就是提交前 todo。只勾选已经由当前 diff、测试输出或仓库状态证明的项目;不适用 +时记录原因。除非用户明确要求,不执行 commit、push 或 PR 创建。 + +<!-- project-fill: 补充本项目特有的审批、提交、base/head 或发布 gate;没有项目专属要求时删除此 marker --> + +## Scope and Git State + +- [ ] 从 repository default branch 解析 `<base>`,确认当前工作分支和目标分支正确。 +- [ ] 检查 `git status`、工作树 diff、暂存区 diff 和 `git diff --name-only <base>...HEAD`。 +- [ ] 实际变更范围与交付说明一致,不包含本地草稿、秘密、生成垃圾或未落地计划。 +- [ ] 团队若采用单 commit,将其视为可替换的团队默认;否则遵循仓库现有提交策略。重写远端 + 历史前必须有明确授权,并使用安全的 lease 保护。 + +## Tests and Evidence + +- [ ] 按 `TESTING.md` 和当前仓库配置选择真实命令,没有从模板猜测 runner 或服务。 +- [ ] 每条测试记录 exact command、scope、result、not-run reason、实际环境和隔离方式。 +- [ ] 环境选择与命令副作用、CI 能力和项目政策一致;写入、外部状态、残留和清理结果均有 + 可核对记录。 +- [ ] 失败、跳过和验证层级被准确描述;light、golden 或 repair 未冒充 full validation。 + +## Documentation and Contracts + +- [ ] 已按真实影响检查 `AGENTS.md`、`architecture.md`、`capability_contract.json`、 + `interact.md`、business guide、`TESTING.md` 和 `SOP.md`;无需更新的候选项有真实 + no-update reason,不要求为了齐全而修改所有文档。 +- [ ] 能力变化遵循 `capability_contract.json → interact.md → business_user_guide.md` 的权威 + 方向;用户可见声明有当前实现或测试证据和稳定 anchor。 +- [ ] 架构影响已核对入口、模块边界、数据流、状态、错误模型、外部依赖、artifact 和副作用。 +- [ ] 所有 active project-fill marker 已替换或删除,Markdown 与 JSON 仍可被严格解析。 + +## Review Closure + +- [ ] 已完成本项目测试与交付政策要求的 review gate,并准确记录 reviewer 身份、范围和限制。 +- [ ] 所有 BLOCKER 和不需要新产品决策的 actionable WARN 已修复并复核;其余问题进入 open + decisions,包含证据与影响。 +- [ ] 修复后重跑受影响测试和机械检查,最终 diff 与 Git 状态已再次检查。 + +## PR Delivery + +- [ ] PR body 只写当前已完成事实,并使用 `.github/pull_request_template.md` 的结构。 +- [ ] PR body 的草稿位置、发布工具和提交方式遵循目标项目政策;临时草稿不得被误提交,且 + body 必须与真实 diff 和测试证据一致。 +- [ ] base 使用 `<base>` 或 repository default branch,不硬编码某个分支名。 +- [ ] 只有用户要求时才创建 draft PR;发布前再次确认 title、base、head、body 和实际 diff。 diff --git a/zh/README.md b/zh/README.md index 135d7ec..8d1f8be 100644 --- a/zh/README.md +++ b/zh/README.md @@ -2,18 +2,27 @@ 中文 | [English](../en/README.md) -本目录提供中文核心模板、开发工作流和单会话 `workflow-docs-sync` Skill。 +本目录提供真正跨语言、跨框架、跨项目的中文核心模板、开发工作流和 canonical +`workflow-docs-sync` Skill。 ## Quick Start -用户只调用一次 Skill,只提供目标仓库、可选语言和可选 draft PR 意图: +用户只调用一次 Skill,只提供目标 Git 仓库、`zh` 或 `en`,以及成功后是否创建 draft PR: ```text -使用 $workflow-docs-sync 同步 `/目标仓库绝对路径`,语言 zh,结束后不要创建 draft PR。 +使用 $workflow-docs-sync 同步 /目标仓库绝对路径,语言 zh,结束后不要创建 draft PR。 ``` -主 Agent 是目标工作区唯一写入者;四领域分析和内部对抗性审计都只读。上游 checkout -和 SHA 由 Skill 内部解析,最终检查只验证仓库状态,不证明执行历史。 +Skill 固定目标 HEAD 与上游 SHA,从当前代码、配置、测试、committed artifacts、可重复运行 +结果和必要 Git 历史全量重建事实,再只做事实要求的最小文档改写。现有文档与上游模板都是 +hypotheses,不是证据。 + +Architecture、Capability / User Behavior、Testing、Governance 是覆盖维度,不是固定 Agent +拓扑。主 Agent 是目标工作区唯一写入者;测试环境由项目命令、副作用、CI 和项目政策决定。 + +复核优先使用 fresh-context、blind-first independent reviewer。平台不能提供认知隔离时, +最终结果诚实标记 self-review。确定性 checker 只验证最终仓库状态,不证明调查、测试或复核 +历史。 ## Skill 安装 @@ -26,30 +35,56 @@ python3 zh/scripts/install_skills.py --scope repo \ --target-repo <目标仓库> --upstream-dir <clean-canonical-checkout> ``` +## Template Contract + +Markdown project-fill slot 使用 `<!-- project-fill: ... -->`,JSON 使用 +`__PROJECT_FILL__:` 字符串前缀。目标项目必须在最终 `check` 前替换或删除全部 active marker。 +固定 source object 中的每份非 PR 模板必须至少保留一个 active marker,PR template 豁免; +模板不预设语言、框架、test runner、服务或默认分支。 + +## 维护者地图 + +- 下游模板:以 `zh/` 九份核心文件为中文语义源,同步派生 `en/` 同路径文件。 +- canonical Skill:`zh/skills/workflow-docs-sync/`。 +- 安装器:`zh/scripts/install_skills.py`。 +- 入口 README:根 README 做摘要,`zh/README.md` 是中文维护入口,`en/README.md` 从中文派生。 +- 开发工作流与决策:`zh/docs/development_workflow/README.md` 和 `decisions.md`;英文概览在 + `en/docs/development_workflow/README.md`。 +- 场景测试:`tests/test_workflow_docs_sync.py`;具体测试约束以代码为准。 +- GitHub 路径:根 `.github/` 只服务本仓库 CI/GitHub;`zh/.github/` 与 `en/.github/` 是下游 + 模板源。 + +最短入口是 `python3 -m pytest -q`。完整收口命令: + +```bash +python3 -m pytest -q +python3 -m py_compile zh/skills/workflow-docs-sync/scripts/sync_docs.py zh/scripts/install_skills.py +python3 "${CODEX_HOME:-$HOME/.codex}/skills/.system/skill-creator/scripts/quick_validate.py" \ + zh/skills/workflow-docs-sync +git diff --check +python3 zh/skills/workflow-docs-sync/scripts/sync_docs.py --help +``` + ## 目录地图 -- [AGENTS.md](AGENTS.md):agent 工作入口、代码规范与文档关系。 -- [architecture.md](architecture.md):系统架构、模块边界、数据流和架构不变量模板。 -- [capability_contract.json](capability_contract.json):能力、边界和 agent 行为承诺契约。 -- [interact.md](interact.md):用户可观察行为与验收不变量模板。 -- [TESTING.md](TESTING.md):测试策略和证据规则模板。 -- [PR_Checklist.md](PR_Checklist.md):通用 PR 提交规则模板。 -- [SOP.md](SOP.md):标准流程入口模板。 -- [.github/pull_request_template.md](.github/pull_request_template.md):下游 PR body 模板。 -- [docs/business_user_guide.md](docs/business_user_guide.md):业务人员教学模板。 +- [AGENTS.md](AGENTS.md):agent 权威入口、稳定模块地图与影响规则模板。 +- [architecture.md](architecture.md):系统目的、调用链、边界、状态与副作用模板。 +- [capability_contract.json](capability_contract.json):能力、边界、职责与行为锚点契约。 +- [interact.md](interact.md):用户可观察行为与验收模板。 +- [docs/business_user_guide.md](docs/business_user_guide.md):首次使用业务指南模板。 +- [TESTING.md](TESTING.md):测试入口、层级、隔离和证据模板。 +- [PR_Checklist.md](PR_Checklist.md):通用 PR todo 与目标项目发布政策边界。 +- [SOP.md](SOP.md):稳定标准流程入口模板。 +- [.github/pull_request_template.md](.github/pull_request_template.md):长期 PR body 结构。 - [docs/development_workflow/README.md](docs/development_workflow/README.md):完整开发工作流。 - [docs/development_workflow/decisions.md](docs/development_workflow/decisions.md):产品实现决策。 -- [skills/workflow-docs-sync/](skills/workflow-docs-sync/):单会话同步 Skill。 -- [skills/workflow-docs-sync/references/sections.md](skills/workflow-docs-sync/references/sections.md):四领域只读分析语义。 -- [skills/workflow-docs-sync/references/audit.md](skills/workflow-docs-sync/references/audit.md):只读对抗性审计语义。 -- [skills/workflow-docs-sync/scripts/sync_docs.py](skills/workflow-docs-sync/scripts/sync_docs.py):内部 `prepare` / `check` 机械接口。 +- [skills/workflow-docs-sync/](skills/workflow-docs-sync/):canonical 单会话同步 Skill。 - [scripts/install_skills.py](scripts/install_skills.py):单 Skill 双平台薄复制器。 -## 路径与维护边界 +## 路径与双语边界 -`zh/` 是上游源码前缀,不写入目标项目。安装模板时只剥离开头的 `zh/`;例如 -`zh/docs/business_user_guide.md` 落到 `<target>/docs/business_user_guide.md`, -`zh/.github/pull_request_template.md` 落到 `<target>/.github/pull_request_template.md`。 +安装模板时只剥离开头的 `zh/`;例如 `zh/docs/business_user_guide.md` 落到目标仓库的 +`docs/business_user_guide.md`。根目录 `.github/` 是本仓库基础设施,不是下游模板源。 -中文是锚点,英文是派生层。修改中文模板、Skill 或开发流程后,必须同步检查英文路径; -暂未覆盖时明确标记 `en-pending`。根目录 `.github/` 是本仓库基础设施,不是下游模板源。 +中文是语义锚点,英文是派生层。本 PR 新增或修改的双语模板、README 和 development +workflow 必须同一 PR 闭合;未触及历史决策的既有翻译状态不要求顺手改变。 diff --git a/zh/SOP.md b/zh/SOP.md index 7518210..5f3efcb 100644 --- a/zh/SOP.md +++ b/zh/SOP.md @@ -1,12 +1,28 @@ -## 指引 -你需要将你被要求完成的SOP的步骤转为todo list进行step by step的完成。 - -## SOP 原则 -* 原则 A:SOP 每一步都用同一种结构:每一步只允许包含三类信息:做什么(动作),去哪看(权威引用,精确到文件/章节),做完如何验收(跑哪个测试/生成哪个 artifact)。除此之外,不允许把规范再讲一遍。 -* 原则 B:SOP 不写“会变的列表”,只写“入口”。这样将来就算 Stage 变更、环境变量调整,也只需要改 TESTING.md / 脚本,而 SOP.md 不用动(自然防 drift)。 -* 原则 C:SOP.md 绝对不是“规范文档”;它只是“流程骨架 + 指向权威文档/命令/测试”的导航页。发生冲突时:以 tests / 合同 / 规范文档为准,SOP.md 自动作废。 -* 例子:如果 PR_Checklist 和 TESTING.md 已经覆盖了“该跑什么、该更新什么、该交付什么产物”,SOP 不再重复 PR/测试细节。反过来,如果 SOP 被迫写 PR/测试细节,说明 Checklist 或 TESTING 有缺口,应该补它们,而不是补 SOP。 - - -## SOP 0: - +# Standard Operating Procedures + +## Purpose and Authority + +`SOP.md` 只保存稳定流程入口,不复制易漂移的命令、测试清单或发布细节。发生冲突时,以 +当前代码、配置、测试、契约以及 `TESTING.md`、`PR_Checklist.md` 等专项权威为准。执行中的 +checklist 只存在于当前会话,不落盘为 run state、receipt 或临时 SOP。 + +## Available SOPs + +<!-- project-fill: 列出本项目真实存在的 SOP 名称和权威入口;没有时替换为 None — 已检查的范围与原因;完成后删除此 marker --> + +## SOP Entry Structure + +每个 SOP step 只包含: + +1. Action:要执行的稳定动作。 +2. Authority / Source:应读取的权威入口,不复制其易漂移内容。 +3. Acceptance:如何用当前测试、artifact 或可观察结果判断完成。 + +## Failure, Rollback, and Escalation + +失败时停止在安全边界,保留精确错误和当前仓库状态;回滚方式必须与真实持久化及副作用 +模型一致。需要权限、产品判断或外部协调时,明确升级给责任人,不猜测或绕过。 + +<!-- project-fill: 写入本项目已验证的停止条件、可恢复回滚入口和升级责任;没有专属规则时写 None — 已验证原因;完成后删除此 marker --> + +所有文本文件使用 LF 换行与 UTF-8 编码,除非仓库配置明确规定其他格式。 diff --git a/zh/TESTING.md b/zh/TESTING.md index 16efe4c..efaa6ef 100644 --- a/zh/TESTING.md +++ b/zh/TESTING.md @@ -1,159 +1,69 @@ -# 测试流程 +# Testing -在提交任何 PR 或进行回归前,务必阅读并遵循本指南。除非明确说明,否则所有命令均在仓库根目录执行,优先使用 `.\run_tests.ps1` 来降低心智负担。**当 Stage 需要本地服务时**,脚本会自动调用 `start_server.ps1`、等待 `/healthz` 可用,并分阶段执行下列测试。若需在 CI 或 Unix 环境单独运行,可直接采用列出的命令。 +## 0. Canonical Test Entrypoints -## 测试原则 (Testing Philosophy) +所有 exact command 必须从当前仓库的脚本、任务配置、CI、构建文件或测试框架配置中提取, +并在仓库根目录或明确记录的工作目录验证。不得从本模板猜测语言、runner、服务或阶段名。 -- **拒绝为了测试而测试**:测试代码应当验证行为和契约,而不是实现细节。避免使用脆弱的正则表达式去匹配代码字符串(如统计度量数量),这会导致重构困难。 -- **避免冗余**:如果一个端到端的实测已经覆盖了某个场景,不要再编写一个仅仅是 Mock 的单元测试来重复验证同一件事,除非该单元测试能提供极快的反馈循环或覆盖了实测无法覆盖的边缘情况。 -- **谨慎扩张**:新增测试前先说明它覆盖的真实缺口。优先补真实失败曾经漏掉的组合路径、用户可见 JSON 契约、跨模块边界、日志可回放证据,以及 live 环境不可稳定复现的边缘条件;不满足这些条件的测试默认视为维护负担。 -- **保持精简**:定期审查测试套件,删除不再有价值的过时测试。 -- **确定性 (Determinism)**:除非是专门做 Live Monitor,否则测试不应依赖即时变化的生产数据。应使用 Mock 数据或固定的测试数据集,确保测试结果在任何时间、任何环境下都是可重复的。 -- **独立性 (Isolation)**:每个测试用例应当是独立的,不应依赖其他测试的执行顺序或残留状态。 -- **可读性与诊断性 (Readability & Diagnosability)**:断言失败时应提供清晰的错误信息(例如 `assert actual == expected, f"Expected {expected} but got {actual}"`),让开发者无需 Debug 就能明白哪里出了问题。 -- **及时更新**:当测试文件新增或者更改功能时,及时更新本文档。 +<!-- project-fill: 列出当前仓库真实可执行的测试命令、工作目录、环境前提和适用范围;完成后删除此 marker --> -## capability contract alignment 测试原则 +## 1. Testing Philosophy -当项目引入 `capability_contract.json`、`interact.md`、`docs/business_user_guide.md` 等用户能力文档时,应提供一个轻量的 contract alignment 测试,例如 `tests/.../test_capability_contract_alignment.py`。 +- 测试验证行为、契约和失败边界,不锁死无用户价值的实现细节。 +- 每个新增测试应覆盖真实缺口;已有更高层测试不自动否定快速、可诊断的低层回归测试。 +- 默认使用固定输入,避免依赖变化中的生产数据;live test 必须明确标注外部依赖与风险。 +- 测试彼此隔离,不依赖执行顺序或残留状态;失败信息应足以定位预期与实际差异。 +- 未运行、跳过或只完成静态检查时,准确记录范围和原因,不推断为通过。 -该测试的目的不是验证具体业务逻辑,而是验证“机器可读能力契约”和“用户可读文档声明”之间没有明显漂移。 +## 2. Test Layers and What Each Proves -### anchor_id 提取原则 +- **Unit**:证明单个函数、类或模块在隔离输入下的局部行为。 +- **Contract**:证明公开 schema、接口、文件格式或跨模块约定。 +- **Scenario**:证明多个真实组件串联后的用户或调用方路径。 +- **Golden**:证明确定性输入对应的已审查输出;不单独证明外部系统或完整运行链。 +- **Report build**:证明报告或交付物可以生成;不自动证明内容业务正确。 +- **Repair validation**:证明修复后的 artifact 满足特定 gate;不等同于所有上游阶段正确。 +- **Light review**:证明其实现实际检查的有限范围;不得描述成 full validation。 +- **Full validation**:只有真实覆盖完整目标路径、依赖和验收边界时才能使用此名称。 +- **Live**:证明真实外部依赖下的当次行为;必须记录环境、时间敏感性和不可重复风险。 -alignment 测试提取 `capability_contract.json` 中的 `anchor_id` 时,应优先采用递归全树扫描,而不是按固定 JSON path 提取。 +## 3. Capability Contract Alignment -原因: -- `capability_contract.json` 的 schema 可能演化; -- 未来可能新增能力桶,例如 `failure_modes`、`escalation_paths`; -- 文档锚点引用的是稳定 `anchor_id`,不应依赖当前 JSON 层级结构。 +alignment test 属于目标项目自己的测试套件,不属于文档同步 checker。它应在本地递归收集 +`capability_contract.json` 中所有对象的稳定 `anchor_id`,检查唯一性和 Markdown 引用, +但不硬编码 bucket、JSON path、数组位置或要求所有 contract 条目进入 business guide。 -测试原则: -- 递归遍历 JSON 中所有对象; -- 只要对象包含 `anchor_id` 字段,就纳入 anchor 集合; -- 不把具体能力名、类型桶路径或数组下标硬编码进测试。 +无法自动化的声明使用 `test_anchor: null` 并记录具体原因;已有测试时登记真实测试锚点。 +文档声明 alignment test 存在之前,必须确认目标仓库确有对应测试实现和可执行命令。 -### Markdown anchor 引用语法 +<!-- project-fill: 引用目标项目真实 alignment test、命令和覆盖范围;尚未实现时准确写 Not configured 及原因;完成后删除此 marker --> -所有用户可读文档引用 `capability_contract.json` 中的 `anchor_id` 时,必须使用统一格式: +## 4. Change Type to Required Evidence -```text -<!-- capability-anchor: <ANCHOR_ID> --> -``` +<!-- project-fill: 按本项目真实风险映射代码、配置、schema、用户行为、artifact 和文档变更所需测试层级;完成后删除此 marker --> -规则: -- 只允许这一种格式。 -- 不允许 `<!-- anchor: ... -->`、`<!-- ref: ... -->`、`<!-- contract: ... -->` 等变体。 -- `<ANCHOR_ID>` 必须是 `capability_contract.json` 中存在的稳定 `anchor_id`。 -- 不允许引用 JSON path、数组下标或 schema 内部路径。 -- alignment 测试只识别这一种格式。 +## 5. Side Effects and Isolation -### 测试应覆盖的原则 +每条命令先核对写入路径、外部服务、凭据、并发、顺序依赖、清理方式和 CI 政策,再选择足以 +隔离其真实副作用的环境。环境可以是 CI、container、独立 checkout、远端测试环境或项目已 +验证的其他执行面;不得从本模板固定一种实现。测试记录必须说明实际环境、隔离方式、残留 +状态和清理结果。 -1. `anchor_id` 唯一性 - - `capability_contract.json` 中所有可被文档引用的对象,只要包含 `anchor_id` 字段,就必须拥有唯一、稳定的 `anchor_id`。 - - 不允许重复 `anchor_id`。 - - 不允许文档引用不存在的 `anchor_id`。 +<!-- project-fill: 标出本项目各命令的副作用、实际隔离环境、清理方式和选择依据;完成后删除此 marker --> -2. `anchor_id` 提取方式 - - 测试应优先递归扫描整个 `capability_contract.json`,收集所有对象中的 `anchor_id`。 - - 不应把能力类型桶、JSON path、数组下标或当前 schema 层级硬编码进测试。 - - schema 演化时,测试应尽量无需修改。 +## 6. Test Suite Overview -3. Markdown 锚点语法 - - 所有文档中引用 contract anchor 时,必须使用统一格式: +以稳定测试目录、入口和职责为粒度,不永久镜像每个测试文件。 - ```text - <!-- capability-anchor: <ANCHOR_ID> --> - ``` +<!-- project-fill: 概述真实测试套件、关键 fixture、外部依赖和推荐入口;完成后删除此 marker --> - - alignment 测试只识别这一种格式。 - - 不允许其他变体。 - - 不允许引用 JSON path、数组下标或 schema 内部路径。 +## 7. Known Gaps and Untested Reasons -4. 文档锚点不应出现裸 TODO - - Markdown 中不应出现 `capability-anchor: TODO` 或 `test-anchor: TODO`。 - - 暂时不可测的契约应集中登记在 `capability_contract.json`,使用 `test_anchor: null` 并写明 `untested_reason` 或 `pending_since`。 +<!-- project-fill: 列出当前真实覆盖缺口、风险、owner 或触发条件;没有缺口时写 None — 已验证的范围;完成后删除此 marker --> -5. agent 行为承诺登记 - - 凡是文档中声明“必须追问 / 必须拒绝 / 不得猜测 / 必须降级 / 必须解释”的行为,应在 `capability_contract.json` 中有对应 `anchor_id`。 - - 如果有自动化测试,应登记测试锚点。 - - 如果暂时没有自动化测试,应显式说明不可测原因,而不是散落 TODO。 +## 8. Lessons Learned -6. 不要求所有 contract 条目都出现在 business guide - - `docs/business_user_guide.md` 是教学派生文档,只覆盖最常见路径。 - - alignment 测试不应盲目要求 `capability_contract.json` 中每个能力都出现在 business guide。 - - 只有被标记为必须文档化、用户可见、或指南必提的条目,才要求在指定文档中出现。 +只记录真实缺陷暴露出的可复用测试决策规则,不保存事故编年史或易漂移命令。若一次事故 +来自“各层单独通过但组合失败”,同时保留最小回归测试与覆盖真实边界的 scenario 测试。 -7. 不测试教学文案风格 - - alignment 测试不判断业务指南写得是否好看。 - - 它只检查能力声明、行为承诺和锚点是否一致。 - - 普通“好问法 / 坏问法 / 使用建议”不应被过度机器化。 - -8. 不依赖外部服务 - - alignment 测试应只读取本地文件。 - - 不应调用真实 API、数据库、BI 服务或 LLM。 - - 它应该稳定、快速、可在普通回归阶段运行。 - -### 测试失败与警告原则 - -- 文档引用了不存在的 `anchor_id`:应失败。 -- contract 中出现重复 `anchor_id`:应失败。 -- Markdown 中出现裸 TODO 锚点:应失败或至少高优先级警告。 -- 行为承诺缺少测试锚点但已登记不可测原因:可警告,不必默认失败。 -- contract 中存在未被 business guide 引用的能力:默认不失败,除非该条目标记为必须进入业务指南。 - -## 测试分层与命名约定 - -本仓库中的测试分为两层: - -1. 近单元级测试(Module-level Tests) - - 目标:快速验证单个模块的行为正确性。 - - 命名规则: - - 代码文件:`<module_path>/<name>.py` - - 对应测试:`tests/<module_path>/test_<name>.py` - - 特点:不依赖外部服务,便于在 `Stage quick` 中频繁运行。 - - 如果文件不存在,自行开发。 - -2. 契约 / 场景 / Live 测试(Contract / Scenario / Live Tests) - - 目标:验证对外暴露的 API 契约、完整业务场景和带外部依赖的实测行为。 - - 目录约定: - - 特点: - -## 何时新增或修改测试 - -当对代码进行修改时,请按以下规则决定如何操作测试: - -1. 发现 Bug 但现有测试未覆盖 - - 必须: - - 先新增一个能重现 Bug 的测试用例(近单元或场景测试皆可),再修复实现。 - - 修复完成后: - - 确保新增测试在所有适用的 Stage 中稳定通过。 - -> 总结:任何行为性代码变更都必须有测试证据。默认应新增或修改测试;如果没有测试变更,必须说明为什么现有测试已经覆盖,并提供对应测试或验收的重跑证据。 - -## 测试文件简介 - -## 变更类型与推荐 \run_tests.ps1 测试阶段(决策表) - -## 教训模块(Lessons Learned) - -### 教训模块维护规则: - - 新增:只有当一次真实缺陷暴露出“现有测试规范无法稳定引导出正确测试策略”时,才新增教训条目;普通实现细节、一次性操作步骤或仅对当前目录结构有效的说明不要进入该模块。 - - 更新:优先提升既有条目的抽象层级、补充适用边界与反例,而不是因为具体实现变化就重写整条教训;具体 case 可以保留,但规则部分必须在脱离当前仓库后仍然成立。 - - 合并:若多个教训条目实质上描述的是同一类测试失效模式,应合并为一条更通用的规则,并把具体案例压缩为简短背景,而不是按模块或事故时间线持续堆叠。 - - 删改:只有当旧教训已被更高层、可持续执行的规则、流程或自动化机制完整替代时,才允许删改;判断标准是“知识是否被替代”,不是“案例是否过时”。 - - 约束:教训模块服务于未来的测试决策,而不是维护事故编年史。具体 case 可以保留作为说明材料,但任何删改都不能破坏后来者对“为什么会漏测、以后该补哪类测试”的理解。 - - 如果某次事故最终定位到“测试分层都测了,但没测组合”,则修复 PR 必须同时包含两部分: - - 一个能稳定复现事故的最小回归测试。 - - 一段写回 `TESTING.md` 的教训,总结“现有测试为什么会漏”,而不是只记录“这次是哪个模块坏了”。 - - 推荐落点: - - 若只涉及单个消费者,在该消费者对应的 `tests/unit/test_<consumer>.py` 中补回归测试。 - - 若涉及多个模块编排,或只有串起来才会暴露错误,则补到 `tests/scenario/`。 - -### Case 1 - -当多个单元分别通过、但真实流程依赖跨阶段累计状态、阶段间产物或固定执行顺序时, -只测单脚本的成功/失败分支不足以证明编排可用。必须增加一条从第一个公开入口走到最终 -handoff 的场景测试,并在每个跨模块边界落下真实 artifact。最小回归仍应保留,用于快速 -定位具体 tripwire;场景测试负责防止“局部全绿、完整流程不可运行”再次发生。 +<!-- project-fill: 写入由真实失败支持、且尚未被更强规则或自动化替代的教训;没有时写 None;完成后删除此 marker --> diff --git a/zh/architecture.md b/zh/architecture.md index 998a2bd..75b14b1 100644 --- a/zh/architecture.md +++ b/zh/architecture.md @@ -1,94 +1,57 @@ -# architecture.md +# Architecture -本文档描述本项目的系统结构、模块边界、数据流、架构不变量和扩展点。 +## 0. Scope and Update Triggers -本文档不负责: -- agent 工作规则:见 `AGENTS.md` -- PR 流程:见 `PR_Checklist.md` -- 测试策略:见 `TESTING.md` -- 能力边界、职责边界与 agent 行为承诺:见 `capability_contract.json` -- 用户可见行为:见 `interact.md` -- 业务人员首次使用教学:见 `docs/business_user_guide.md` -- 标准操作流程:见 `SOP.md` +本文档是当前系统结构的权威说明。新增或删除运行入口、模块边界、调用链、数据契约、状态、 +错误模型、外部依赖、认证、配置、artifact、副作用、扩展点或架构债务时,必须更新或确认 +对应章节。内容必须来自当前实现、配置、测试、committed artifacts 或可重复运行证据。 ---- +<!-- project-fill: 说明本文档覆盖的系统边界、排除项和本项目特有更新触发条件;完成后删除此 marker --> -## 0. 更新触发条件 +## 1. System Purpose -以下变化必须更新本文档: +<!-- project-fill: 用不超过五句话说明服务对象、输入、输出和核心价值;只写已验证的当前事实;完成后删除此 marker --> -- 新增 / 删除核心模块 -- 模块职责边界变化 -- 运行时调用链变化 -- 数据流、数据契约或 schema 变化 -- 状态模型变化 -- 错误处理模型变化 -- 外部依赖、认证、配置入口变化 -- 新增重要扩展点 -- 新增重要架构债务或约束 +## 2. Runtime Entrypoints and Main Flows ---- +<!-- project-fill: 从真实入口描述主要调用链、关键分支和最终输出;如无运行时入口,写 Not applicable — 已验证原因;完成后删除此 marker --> -## 1. 系统目的 +## 3. Architecture Invariants -<!-- -不超过 5 句。 -说明: -- 本系统服务谁? -- 输入是什么? -- 输出是什么? -- 核心价值是什么? ---> +每条不变量应包含正向约束、适用边界、可证伪方式和违反后果。愿景或 proposed 设计不得写成 +当前不变量。 -本系统用于: +<!-- project-fill: 写入有代码、配置或测试证据的架构不变量;没有时写 Not applicable — 已验证原因;完成后删除此 marker --> -输入: +## 4. Module Responsibility Boundaries -输出: +以稳定模块和职责为粒度,说明负责什么、不负责什么、允许依赖和禁止依赖;不要逐文件复制 +仓库目录。 -核心价值: +<!-- project-fill: 描述核心模块边界及依赖方向,并引用精确实现证据;完成后删除此 marker --> ---- +## 5. Data Flow and Data Contracts -## 2. 架构不变量 +<!-- project-fill: 描述输入如何被解析、转换、验证并输出,以及 schema、版本和边界契约;无数据流时写 Not applicable — 已验证原因;完成后删除此 marker --> -<!-- -不变量 = 不管以后怎么改代码,都不能被破坏的系统级约束。 ---> +## 6. State and Persistence Model -### 2.1 不变量名称 -- 正向陈述: -- 反向陈述: -- 适用边界: -- Review 检测方式: -- 自动化检测方式(如有): -- 违反后的后果: +<!-- project-fill: 说明进程内状态、持久化、缓存、幂等性和生命周期;无持久化状态时写 Not applicable — 已验证原因;完成后删除此 marker --> ---- +## 7. Error and Failure Model -## 3. 模块职责边界 +<!-- project-fill: 说明 validation、降级、重试、hard failure、回滚和用户可见错误的真实边界;完成后删除此 marker --> -| 模块 / 目录 | 职责 | 非职责 | 可以依赖 | 不应依赖 | -|---|---|---|---|---| -| | | | | | +## 8. External Dependencies, Authentication, and Configuration -### 3.1 边界规则 +<!-- project-fill: 列出真实外部依赖、认证边界、配置来源和缺失配置时的行为;没有时写 Not applicable — 已验证原因;完成后删除此 marker --> -- -- -- +## 9. Artifacts and Side Effects ---- +<!-- project-fill: 区分 committed、generated、ephemeral artifacts,并说明文件、网络、服务或其他副作用及隔离要求;没有时写 Not applicable — 已验证原因;完成后删除此 marker --> -## 4. 数据流主干 +## 10. Extension Points and Architecture Debt -<!-- -主干图最多 3 层;复杂能力可以单独展开子链路。 ---> +future / proposed 项目必须显式标注状态,不得伪装为当前能力或既有扩展点。 -```mermaid -flowchart LR - Input[Input] --> Entry[Entry / Parser] - Entry --> Service[Service / Orchestration] - Service --> Core[Core Logic] - Core --> Output[Output] +<!-- project-fill: 列出有证据的扩展接口、已知架构债务、影响和触发重审条件;没有时写 None — 已验证原因;完成后删除此 marker --> diff --git a/zh/capability_contract.json b/zh/capability_contract.json index 41ba9c1..15f7df6 100644 --- a/zh/capability_contract.json +++ b/zh/capability_contract.json @@ -1,123 +1,107 @@ { "schema_version": "0.1.0", - "status": "sample", - "purpose": "Cross-project sample registry for stable capability, boundary, responsibility, behavior, and document anchors.", + "status": "__PROJECT_FILL__: 替换为已验证的契约状态", + "purpose": "当前能力、边界、职责、行为及文档锚点的机器可读登记表。", + "source_language": "zh-CN", "rules": [ - "Markdown documents reference stable anchor_id values only.", - "Markdown documents must not reference JSON paths, array indexes, or schema-specific internal paths.", - "Deprecated anchors should stay registered and must not be reused for a different meaning.", - "Untested contracts use test_anchor: null with untested_reason or pending_since instead of scattered TODO markers." + "每个填充项必须在验证前替换为已核实的项目内容,或从契约中删除。", + "每条 active 声明必须有当前实现、配置、测试、committed artifacts 或可重复运行结果作为证据。", + "future 或 proposed 行为必须使用明确的非 active 状态,不得呈现为当前可用能力。", + "Markdown 文档只引用稳定的 anchor_id,不引用 JSON path、数组位置或 schema 内部路径。", + "deprecated anchor 必须保留原始语义登记,且不得复用于其他含义。", + "没有自动化覆盖的契约使用 test_anchor: null,并记录具体 untested_reason 或 pending_since。" ], "contracts": { "capabilities": [ { - "anchor_id": "CAPABILITY.sample_supported_question", + "anchor_id": "__PROJECT_FILL__:CAPABILITY.identifier", "type": "capability", - "status": "experimental", - "statement": "Sample capability placeholder for a project-supported business question type.", + "status": "__PROJECT_FILL__: 替换为 active、experimental、future、proposed 或 deprecated", + "statement": "__PROJECT_FILL__: 替换为已验证的项目能力", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: 替换为真实受众" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific contract and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: 替换为缺少自动化的原因;如已有测试则填写 test_anchor 并删除本字段", + "pending_since": "__PROJECT_FILL__: 替换为适用日期,或删除本字段", + "since": "__PROJECT_FILL__: 替换为首次验证的版本或日期" } ], "capability_boundaries": [ { - "anchor_id": "BOUNDARY.sample_multi_object_comparison_not_supported", + "anchor_id": "__PROJECT_FILL__:BOUNDARY.identifier", "type": "capability_boundary", - "status": "experimental", - "statement": "Sample boundary placeholder for a currently unsupported capability that may be added later.", + "status": "__PROJECT_FILL__: 替换为已验证的状态", + "statement": "__PROJECT_FILL__: 替换为已验证的当前限制、拒绝或降级边界", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: 替换为真实受众" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific boundary and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: 替换为缺少自动化的原因;如已有测试则填写 test_anchor 并删除本字段", + "pending_since": "__PROJECT_FILL__: 替换为适用日期,或删除本字段", + "since": "__PROJECT_FILL__: 替换为首次验证的版本或日期" } ], "responsibility_boundaries": [ { - "anchor_id": "RESPONSIBILITY.sample_no_final_business_decision", + "anchor_id": "__PROJECT_FILL__:RESPONSIBILITY.identifier", "type": "responsibility_boundary", - "status": "experimental", - "statement": "Sample responsibility boundary placeholder for decisions the system should not make on behalf of humans.", + "status": "__PROJECT_FILL__: 替换为已验证的状态", + "statement": "__PROJECT_FILL__: 替换为已验证的人类或系统职责边界", "audience": [ - "business_user", - "agent", - "reviewer" + "__PROJECT_FILL__: 替换为真实受众" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific responsibility boundary and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: 替换为缺少自动化的原因;如已有测试则填写 test_anchor 并删除本字段", + "pending_since": "__PROJECT_FILL__: 替换为适用日期,或删除本字段", + "since": "__PROJECT_FILL__: 替换为首次验证的版本或日期" } ], "agent_behaviors": [ { - "anchor_id": "BEHAVIOR.sample_requires_context_before_answer", + "anchor_id": "__PROJECT_FILL__:BEHAVIOR.identifier", "type": "agent_behavior", - "status": "experimental", - "statement": "Sample behavior placeholder requiring the agent to ask for missing critical context instead of guessing.", + "status": "__PROJECT_FILL__: 替换为已验证的状态", + "statement": "__PROJECT_FILL__: 替换为已验证的追问、拒绝、解释或降级行为", "audience": [ - "business_user", - "agent", - "developer", - "reviewer" + "__PROJECT_FILL__: 替换为真实受众" ], "test_anchor": null, "test_status": "not_automated", - "untested_reason": "Sample entry only; replace with a project-specific behavior contract and test anchor.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "untested_reason": "__PROJECT_FILL__: 替换为缺少自动化的原因;如已有测试则填写 test_anchor 并删除本字段", + "pending_since": "__PROJECT_FILL__: 替换为适用日期,或删除本字段", + "since": "__PROJECT_FILL__: 替换为首次验证的版本或日期" } ], "documents": [ { - "anchor_id": "DOC.business_user_guide", + "anchor_id": "DOC.interact", "type": "document", - "status": "experimental", - "statement": "docs/business_user_guide.md is a teaching document derived from capability_contract.json and interact.md.", + "status": "active", + "statement": "interact.md 是用户可观察行为与验收不变量的真相源。", "audience": [ "business_user", "agent", "reviewer" ], - "document_path": "docs/business_user_guide.md", - "language": "zh-CN", - "test_anchor": null, - "test_status": "not_automated", - "untested_reason": "Alignment principles are documented in TESTING.md; project-specific tests are added by each target project.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "document_path": "interact.md", + "language": "zh-CN" }, { - "anchor_id": "DOC.interact", + "anchor_id": "DOC.business_user_guide", "type": "document", "status": "active", - "statement": "interact.md is the source of truth for user-visible behavior and acceptance invariants.", + "statement": "docs/business_user_guide.md 是派生自 capability_contract.json 与 interact.md 的教学文档。", "audience": [ "business_user", "agent", "reviewer" ], - "document_path": "interact.md", - "language": "zh-CN", - "test_anchor": null, - "test_status": "not_automated", - "untested_reason": "Alignment principles are documented in TESTING.md; project-specific tests are added by each target project.", - "pending_since": "2026-04-26", - "since": "2026-04-26" + "document_path": "docs/business_user_guide.md", + "language": "zh-CN" } ] } diff --git a/zh/docs/business_user_guide.md b/zh/docs/business_user_guide.md index d026d24..439b01b 100644 --- a/zh/docs/business_user_guide.md +++ b/zh/docs/business_user_guide.md @@ -1,177 +1,39 @@ -# <项目名>:业务人员首次使用指南 +# First-use Guide -> Status: experimental -> Audience: business users -> Scope: <项目 / agent / app 名称> -> Capability boundary source: `capability_contract.json` -> User-visible behavior source: `interact.md` -> Rule: 本指南是派生教学文档,不独立声明新能力;任何“能做 / 不能做 / 必须追问 / 必须拒绝”的声明,都必须能在 `capability_contract.json`、`interact.md` 或对应测试中找到锚点。 -> 模板使用须知: -> 本模板中的所有 `<尖括号占位符>`、Case 1/2/3 示例标题和通用业务场景,都只是骨架。 -> 落地具体项目时,必须替换成真实业务问题。 -> 不允许把带有占位符的骨架文本作为正式业务用户文档发布。 -> 如果某个 Case 在当前项目里没有真实业务场景,删掉它,不要硬填。 +本指南面向第一次接触项目的业务人员,只解释 `capability_contract.json` 已声明的能力边界和 +`interact.md` 已声明的用户可观察行为,不独立承诺新能力或描述实现细节。 -本文档是跨项目模板。落到具体项目时,只保留业务人员第一次使用时真正需要知道的内容,不要把它扩展成完整功能手册。 +## 1. Value -## 1. 先看这一段:它对你有什么价值 +<!-- project-fill: 用三到五句话说明已验证的业务价值、所需输入和可观察输出;完成后删除此 marker --> -用 3 到 5 句话说明这个系统帮业务人员解决什么问题、输入什么信息、输出什么判断辅助。 +## 2. Best-fit Business Questions -不要在这里独立承诺新能力;所有能力声明必须回到 `capability_contract.json` 或 `interact.md`。 +<!-- project-fill: 用业务语言列出当前 contract 支持的主要问题类型,并引用稳定 capability anchor;完成后删除此 marker --> -## 2. 它最适合哪些业务问题 +## 3. Capability and Responsibility Boundaries -- **示例能力:围绕一个明确对象回答状态、趋势或异常类问题** - <!-- capability-anchor: CAPABILITY.sample_supported_question --> - 把这里替换为本项目真实能力,并写成业务人员能理解的问题类型。 +<!-- project-fill: 从 contract 派生当前能力限制、拒绝、追问和人类责任;future / proposed 必须显式标注;完成后删除此 marker --> -## 3. 它的能力边界 +## 4. Context to Provide -以下是当前版本还不支持,但未来可能扩展的能力: +<!-- project-fill: 说明真实入口所需的对象、范围、时间、口径、权限或其他关键上下文;完成后删除此 marker --> -- **示例边界:暂不支持一次请求内并排比较多个对象** - <!-- capability-anchor: BOUNDARY.sample_multi_object_comparison_not_supported --> - 把这里替换为本项目真实的能力边界。 +## 5. Representative Real Cases -## 4. 它的职责边界 +只保留一到三个有当前实现或测试证据的真实案例。没有真实案例时删除本节,或写 +`Not configured —` 加已验证原因;不要保留通用 Case 骨架。 -以下是系统刻意不做的事: +<!-- project-fill: 写入一到三个真实案例,包括问题、推荐请求、可见结果、结果解读和升级条件;完成后删除此 marker --> -- **示例职责边界:不替业务人员做最终业务决策** - <!-- capability-anchor: RESPONSIBILITY.sample_no_final_business_decision --> - 系统可以给出证据、解释和风险提示,但最终业务决策仍由负责人做出。 +## 6. Reading Results -## 5. 第一次使用,先判断你在问哪一类问题 +<!-- project-fill: 说明用户如何区分事实、推断、限制、错误和下一步;内容必须与 interact.md 的可见结果一致;完成后删除此 marker --> -先把问题归到一个大类,再补上下文。建议只列最常见的 3 到 5 类问题,例如: +## 7. Human Escalation -- 我想确认一个对象最近有没有明显变化。 -- 我想理解一个异常结果可能来自哪里。 -- 我想知道当前数据是否足够支持判断。 +<!-- project-fill: 从 contract 和 interact.md 派生需要人工处理的触发条件、责任角色与可用路径;没有已配置路径时准确说明;完成后删除此 marker --> -## 6. 提问时尽量带上哪些信息 +## 8. Feedback and Ownership -建议业务人员至少说明: - -- 目标对象:你要看哪个客户、产品、区域、模型或流程。 -- 时间范围:你关心哪段时间。 -- 判断目的:你想做趋势判断、异常排查、健康检查,还是结果解释。 -- 业务背景:是否有已知活动、策略变化、数据口径调整。 - -如果缺少关键上下文,agent 应该先追问,而不是直接猜。 -<!-- capability-anchor: BEHAVIOR.sample_requires_context_before_answer --> - -## 7. 常见业务案例 - -### Case 1:确认一个对象最近是否异常 - -#### 业务问题 - -我想知道 `<对象>` 最近是否出现异常变化。 - -#### 这个系统能不能做 - -如果该能力已在 `capability_contract.json` 中声明并且输入上下文足够,则可以辅助分析。 - -#### 推荐提问 - -请帮我看 `<对象>` 在 `<时间范围>` 内是否有异常变化,并说明主要证据。 - -#### 你大概会看到什么 - -你应该看到结论、关键证据、限制条件和下一步建议。 - -#### 结果怎么看 - -优先看证据是否足以支持结论,再看是否需要补充数据或找负责人确认。 - -#### 什么时候不要这样问 - -当目标对象、时间范围或数据口径不清楚时,不要要求系统直接给最终判断。 - -### Case 2:理解一个结果为什么变化 - -#### 业务问题 - -我想理解 `<指标 / 结果>` 为什么发生变化。 - -#### 这个系统能不能做 - -只有当项目已声明对应解释能力,并且数据足够支持拆解时,才应该回答。 - -#### 推荐提问 - -请解释 `<指标 / 结果>` 在 `<时间范围>` 的变化,并区分已验证证据和推断。 - -#### 你大概会看到什么 - -你应该看到候选原因、证据来源、不能确认的部分和建议补充的信息。 - -#### 结果怎么看 - -不要把推断当成事实;需要决策时,先确认关键数据源和责任人。 - -#### 什么时候不要这样问 - -当你实际需要的是最终业务决策,而不是证据辅助时,不要让系统替你拍板。 - -### Case 3:判断是否需要找人 - -#### 业务问题 - -我想知道这个问题能不能靠系统继续分析,还是应该找技术同学、数据负责人或模型维护者。 - -#### 这个系统能不能做 - -如果项目有对应行为承诺,系统应该说明继续分析需要什么输入,以及什么时候应升级给人。 - -#### 推荐提问 - -请判断这个问题是否可以继续由系统分析;如果不可以,请说明应该找谁、为什么。 - -#### 你大概会看到什么 - -你应该看到缺失条件、升级原因和建议联系对象。 - -#### 结果怎么看 - -如果问题涉及身份不唯一、数据口径不明、外部系统异常或高风险决策,应优先找负责人确认。 - -#### 什么时候不要这样问 - -当系统已经明确拒绝或要求人工确认时,不要反复要求它猜测。 - -## 8. 结果怎么看 - -业务人员阅读结果时,建议按顺序看: - -1. 结论是否明确区分事实、推断和限制。 -2. 证据是否来自你认可的数据范围。 -3. 是否出现了必须找人的提示。 -4. 下一步建议是否可执行。 - -## 9. 什么时候应该找人 - -需要找人的常见情况: - -- 关键身份、时间范围或数据口径缺失。 -- 系统明确说明当前能力不支持。 -- 结果影响高风险业务决策。 -- 需要修改外部模型、数据源、权限或生产配置。 - -反馈方式: -- GitHub issue:待项目负责人补充。 -- Teams / Slack 频道:待项目负责人补充。 -- 负责人:待项目负责人补充。 -- 紧急问题路径:待项目负责人补充。 - -## 10. 给第一次使用者的最短建议 - -先说明对象、时间范围和判断目的;如果系统要求补充上下文,先补齐,不要要求它猜。 - -## 11. 文档状态与反馈方式 - -本文档状态为 experimental。它只负责教学说明,不负责定义能力边界或验收不变量。 - -如发现本文档与 `capability_contract.json` 或 `interact.md` 不一致,应优先修正本文档。 +<!-- project-fill: 写入已验证的文档 owner、反馈入口和紧急升级方式;未配置项必须明确写 Not configured;完成后删除此 marker --> diff --git a/zh/docs/development_workflow/README.md b/zh/docs/development_workflow/README.md index 1f74214..7c3eaa6 100644 --- a/zh/docs/development_workflow/README.md +++ b/zh/docs/development_workflow/README.md @@ -45,7 +45,7 @@ 正式审核任务短 prompt: ```text - 对 XX 项目的 PR XX(最新head XX)进行严格详细全面的代码审查。在评估代码时不但要评估开发是否符合issue,还要评估有没有过度开发,是否可以在架构层级精简(功能可以提前开发,但是不允许有脱裤子放屁的冗余)。切记不要去优化或修复一个本不应该存在的问题!PR_BODY.md 是你重要的参考材料,你需要检查有没有重复开发和修补,如果有,分析其原因。重要问题需要实际运行代码来验证你的猜想,没有调查就没有发言权。 + 对 XX 项目的 PR XX(最新 head XX)进行严格详细全面的代码审查。在评估代码时不但要评估开发是否符合 issue,还要评估有没有过度开发,是否可以在架构层级精简(功能可以提前开发,但是不允许有脱裤子放屁的冗余)。切记不要去优化或修复一个本不应该存在的问题!仓库外临时 PR body Markdown 是重要参考材料,你需要检查有没有重复开发和修补,如果有,分析其原因。重要问题需要实际运行代码来验证你的猜想,没有调查就没有发言权。 并检查是否遵守: * .github/pull_request_template.md @@ -72,14 +72,14 @@ 先判断本地是主干还是分支,如果是主干则先创建分支并。然后在既有分支的既有 PR 上提交本地全部代码, 要求: 1. 遵守 PR_Checklist.md。 - 2. PR 对外保持 1 个 commit。 - 3. 每轮 review / 修复都必须更新 PR_BODY.md 的“Review / 修复记录”。 - 4. PR_BODY.md 必须根据 `.github/pull_request_template.md` 填写,并覆盖已有 PR (如果本次对话才创建分支可以跳过)和本地全部修改内容。 + 2. 遵守仓库当前 commit 策略;单 commit 只是可替换的团队默认。 + 3. 每轮 review / 修复都必须更新 PR body 的“Review / 修复记录”。 + 4. 根据 `.github/pull_request_template.md` 在仓库外创建临时 Markdown body,并覆盖已有 PR(如果本次对话才创建分支可以跳过)和本地全部修改内容。 5. 测试策略与测试证据记录方式以 TESTING.md 为准。 备注: - - PR_BODY.md 是本地临时产物,不提交仓库。 - - PR_BODY.md 是重要的代码审核材料之一。 + - 临时 PR body 位于仓库外,不进入目标工作树或 commit。 + - PR body 是重要的代码审核材料之一,由通用 GitHub 发布能力读取。 - PR 审核指南:《》 ``` @@ -106,7 +106,7 @@ * 修复后:继续复用“PR 提交短 prompt”。然后新开codex对话进行pr审核,一直到没有P0和P1问题为止。P2问题可以接受。 * 备注:同一个 PR 的 patch 不需要每次重新完整粘贴给 GPT,可以在原对话里覆盖最新 patch,避免上下文过时。 * 完整顺序:在具体操作中,先让codex 负责PR的代码审核,如果review 有问题,用第8节的prompt分别交给codex(新对话)和claude code验证,这里实习生的发现就是codex审核pr给出的发现。在验证阶段codex和cc经常会有不同意见,在这里交互意见最多三次,然后以codex的意见为准,在codex验证的对话里直接输入‘按照你的观点进行修复‘,然后在这个对话继续‘重新以 PR 审核的态度审核你的新增代码,如有问题先验证是否真实存在,再决定是否修复’这个pr提交环节。然后新开codex对话进行pr审核,一直到没有P0和P1问题为止。P2问题可以接受。 - * Finding 并集销号:跨 reviewer 或跨会话接力前,必须把历史 Finding Ledger、全部 GitHub review thread 和本轮新增 finding 做并集。每个来源 ID 都必须保留,并明确标记为 `confirmed`、`rejected`、`merged_as_duplicate:<ID>`、`downgraded:<新严重度>` 或 `needs_human`;已修复项另记关闭证据。任何 finding 从后续清单静默消失都视为流程错误,在并集未逐项销号前不得声明“无剩余问题”。 + * Finding 闭合:在既有 PR review / fix record 和 GitHub thread 中保留来源 ID、判断与关闭证据;不得让未解决 finding 静默消失,也不另建一套重复 reconciliation ledger。 11. **如果 review 没有问题(定义为没有P0/P1级别发现),在 PR 评论区输入 `/claude-merge-check`**(这个环节暂时放弃,反复的PR审查已经足够) 自动化文件:[.github/workflows/claude-merge-readiness.yml](../../../.github/workflows/claude-merge-readiness.yml) @@ -180,53 +180,60 @@ E. 输出风格约束 - `Repo Impact Forecast`:预测 FSD 与当前仓库的真实触点、风险、文档和测试影响。 - `Target State Bridge`:定义开发完成后用户 / 调用方应该看到什么状态,以及如何验证。 - `Issue`:把契约、范围、任务拆解、文档更新预测、测试更新预测、验收条件固化。 -- `PR_BODY.md`:本地临时 PR body 草稿,由 `.github/pull_request_template.md` 生成,不提交仓库;是 review 的重要输入材料。 +- 仓库外 PR body Markdown:由 `.github/pull_request_template.md` 派生,不进入目标工作树或 commit;是 review 和通用 GitHub 发布能力的输入。 - `Merge Readiness Report`:判断当前 PR 是否具备合并条件。 - `FSD 完备性验收报告`:Issue 关闭前的最后一道契约核查。 -- `Workflow Docs Sync`:用户一次调用完成代码地图、四领域只读分析、主 Agent 统一改写、 - 内部只读审计、测试和最终仓库检查。 +- `Workflow Docs Sync`:用户一次调用完成全量事实重建、最小必要改写、真实测试、 + fresh-context independent review 或诚实 self-review,以及最终仓库检查。 ## 代码项目核心文档 -本仓库中的这些文件是给目标项目继承和项目化的 upstream 模板 / 样本文档。开发 sync -工具时,不因为工具实现细节去改写 `AGENTS.md`、`TESTING.md`、`PR_Checklist.md`、 -`architecture.md` 这类模板;sync 工具自身说明和实现放在 -`zh/skills/workflow-docs-sync/`。例外是 `.github/pull_request_template.md`:它是长期 -PR body 模板,可以直接继承 upstream。 +本仓库中的这些文件是给目标项目继承和项目化的跨语言、跨框架 upstream 基础模板。固定 +规范写成完整规则,项目事实 slot 使用 active project-fill marker;目标项目通过最终检查前 +必须替换或删除 marker。仅对 sync 工具自身有意义的实现细节放在 +`zh/skills/workflow-docs-sync/`,不写入下游模板。 -- `AGENTS.md`:agent 工作入口、文件简介、代码规范与文档关系。 -- `architecture.md`:系统架构、模块边界、数据流、架构不变量与扩展点。 +- `AGENTS.md`:agent 权威入口、稳定模块地图、影响规则与项目约定。 +- `architecture.md`:系统目的、运行时主流程、边界、状态、失败与副作用。 - `capability_contract.json`:能力边界、职责边界、agent 行为承诺的机器可读真相源。 - `interact.md`:用户可观察行为与验收不变量。 - `docs/business_user_guide.md`:面向首次接触业务人员的教学派生文档。 -- `TESTING.md`:测试策略、测试分层、测试证据与 contract alignment 测试原则。 -- `PR_Checklist.md`:PR 提交、commit / push、PR body 使用规则。 +- `TESTING.md`:测试入口、测试分层、隔离、测试证据与 alignment 边界。 +- `PR_Checklist.md`:PR todo、Git diff、测试、文档、review closure 与仓库外 body 边界。 - `SOP.md`:标准流程骨架,只做入口,不重复规范。 - `.github/pull_request_template.md`:PR body 的长期模板。 ## Workflow Docs Sync -用户只调用一次 `$workflow-docs-sync`,只提供目标仓库、可选 `zh` / `en` 和可选 draft -PR 意图。Skill 内部解析 canonical upstream checkout;无法定位时使用仓库外临时 shallow -clone,并在整轮固定同一上游提交。 - -- 主 Agent 是目标工作区唯一写入者,先建立真实代码地图。 -- Architecture、Capability / User Behavior、Testing、Governance 四领域分析只读并只在 - 当前会话返回发现;无 subagent 平台由主 Agent 按四个隔离章节顺序执行。 -- 主 Agent 统一修改九份核心文档,再由内部只读对抗性审计检查事实、跨文档闭合和验证 - 层级;BLOCKER 与可行动 WARN 修复后进行轻量复审。 -- 主 Agent 实际运行目标项目必要测试并记录命令与结果。 -- `sync_docs.py prepare` 只补齐缺失模板;`check` 只读验证最终 HEAD、dirty 范围、九份 - 文件、编码、JSON、标题、模板残留、固定上游差异和 whitespace。 -- 同步过程不读取、创建、改写或删除仓库内 `PR_BODY.md`,也不创建工单、模板镜像或运行状态。 - commit、push 和 draft PR 创建由通用 GitHub 发布能力在检查成功后完成。 -- 最终机械检查只证明最终仓库状态,不证明四领域分析、审计或测试曾运行。 +用户只调用一次 `$workflow-docs-sync`,只提供目标仓库、必选的 `zh` / `en`,以及成功后是否 +创建 draft PR。Skill 内部解析 canonical upstream checkout;无法定位时使用仓库外临时 shallow clone, +并在整轮固定目标 HEAD 与上游 SHA。 + +- 写文档前至少以 `git ls-files -z` 建立范围,从代码、配置、测试、committed artifacts、 + 可重复运行结果和必要 Git 历史全量重建事实;现有文档与上游模板只是 hypotheses。 +- Architecture、Capability / User Behavior、Testing、Governance 是覆盖维度,不是固定 Agent + 拓扑。主 Agent 可独立完成,也可按模块、调用链、风险或证据类型动态委派只读调查。 +- 主 Agent 是目标工作区唯一写入者。全量质疑九份文档后只改错误、缺失或失真部分;正确 + 内容保持零 diff,不输出 disposition ledger、run state 或 receipt。 +- 测试环境由真实命令、副作用、CI 能力和项目政策决定;记录实际环境、隔离、残留和清理, + 不把一种环境实现固化成跨项目规则。 +- 复核优先使用 fresh-context、blind-first independent reviewer;不可用时明确标记 + self-review,不能冒充独立复核。BLOCKER 与无需产品决策的 actionable WARN 修复后复核。 +- `sync_docs.py prepare` 先验证固定 object 与 language 下九份 UTF-8 source template 及非 PR + active-marker invariant,再补齐缺失模板;`check` 重读同一 pinned source,并只读验证最终 + HEAD、dirty 范围、editable path 无 index/worktree 分叉、九份普通 UTF-8 非空文件、JSON + object、active marker,以及存在时为 UTF-8 的 `.gitignore`。最终 bytes 的 whitespace 使用 + 临时非 Git 目录和固定 Git 规则,不继承目标仓库 attributes。Checker 不解析 Markdown,也不 + 验证目标项目 capability、测试层级或文案质量。 +- PR body 临时 Markdown 始终位于仓库外。commit、push 和 draft PR 创建只在用户要求且检查 + 成功后,由通用 GitHub 发布能力完成。 +- 最终机械检查只证明最终仓库状态,不证明调查、测试或复核执行历史。 维护入口: - `zh/skills/workflow-docs-sync/SKILL.md` -- `zh/skills/workflow-docs-sync/references/sections.md` -- `zh/skills/workflow-docs-sync/references/audit.md` +- `zh/skills/workflow-docs-sync/agents/openai.yaml` +- `zh/skills/workflow-docs-sync/evals/README.md` - `zh/skills/workflow-docs-sync/scripts/sync_docs.py` - `zh/scripts/install_skills.py` - `tests/test_workflow_docs_sync.py` @@ -261,7 +268,7 @@ clone,并在整轮固定同一上游提交。 - 做什么:以中文 diff 为锚,判断英文是否覆盖同一流程、能力边界、验收不变量、路径和拒绝 / 追问规则;`both` 状态重点判断英文是否是中文语义派生,而不是独立创作。 - 去哪看:中文文件当前内容、英文文件当前内容、`zh/README.md` 的中文锚点规则和 - `zh/skills/workflow-docs-sync/references/` 的领域语义。 + `zh/skills/workflow-docs-sync/SKILL.md` 的领域语义。 - 做完如何验收:每个配对文件得到一个结论:`ok`、`missing translation`、`stale en`、`contradiction`、`intentionally pending`;除 `ok` 外都必须记录具体文件、段落和建议处理方式。 ### Step 4:生成审核结论 diff --git a/zh/docs/development_workflow/decisions.md b/zh/docs/development_workflow/decisions.md index cf6bdd8..652599e 100644 --- a/zh/docs/development_workflow/decisions.md +++ b/zh/docs/development_workflow/decisions.md @@ -53,7 +53,7 @@ 在同一会话按四个隔离章节顺序完成同样的语义检查。 - 数据边界:子 Agent 发现只通过当前会话返回,不写运行状态、result receipt、工单、 模板镜像或 PR body。上游 checkout 和 SHA 由 Skill 内部解析并在同一轮固定复用; - 用户只提供目标仓库、可选语言和可选 draft PR 意图。 + 用户只提供目标仓库、必选的 `zh` / `en`,以及成功后是否创建 draft PR。 - 机械边界:`prepare` 只解析 Git 根目录与 SHA、在任何写入前检查 dirty allowlist, 并补齐缺失模板;`check` 只读验证最终仓库状态。检查器不证明 Agent、审计或测试曾经 执行,测试由主 Agent 实际运行并在最终报告记录。 @@ -63,4 +63,73 @@ 实现;不保留旧 launcher、mode、harness、缓存模板或控制面兼容 fallback,Git 历史 承担回滚。无 subagent 时的顺序执行是当前正式路径,不是旧实现 fallback。仍保留 DEC-003 的原则:AI 负责项目语义与文档改写,机械层只判断可确定事实。 +- 后续:DEC-006 partial supersede 本决策中的固定四个领域 Agent、固定内部对抗性审计 Agent, + 以及无 subagent 时固定四章节执行顺序;其余边界继续有效。 - 英文状态:`en-pending`。 + +## DEC-006:Workflow Docs Sync 以直接风险覆盖取代代理约束 + +- 状态:accepted +- 日期:2026-08-01 UTC +- 原则:更少不是目标;只有在风险已由更直接机制覆盖,或被明确接受时,才删除原机制。 + 行数下降、测试变少或文件减少都不能单独构成删除理由。 +- 产品边界:保留 one-call UX、固定目标 HEAD 与上游 SHA、从固定 Git object 分发模板、dirty + allowlist、写入前完整预检、只创建缺失文件、single writer、全量事实重建、最小必要改写、 + fresh-context review、最终 `check` 和显式授权发布。Architecture、Capability / User Behavior、 + Testing、Governance 是覆盖维度,不是固定 Agent 拓扑、写入顺序或完成进度协议。 +- 与 DEC-005 的关系:本决策 partial supersede 其中的固定四个领域 Agent、固定内部对抗性审计 + Agent,以及无 subagent 时固定四章节执行顺序。DEC-005 的 one-call、single writer、不写 + repository run state / receipt、`prepare` / `check` 机械责任边界,以及发布必须显式授权且不由 + `sync_docs.py` 执行继续有效。 + +### 删除的机械机制与风险转移 + +- 删除 Markdown heading / fence parser。它只能证明有限语法形状,不能证明章节语义、文案 + 质量或项目事实;模板语义完整性改由四维 review、Case G 和 SEC_metrics Case A 真实 eval + 发现。接受的剩余风险是:纯机械 `check` 不再单独发现空标题或 fence 失配。 +- 删除模板 equality,包括 CRLF 归一化和 PR template 特例。每份非 PR 上游模板必须至少含 + 一个 active project-fill marker;逐字复制模板必然保留 marker,最终 `check` 因 marker 失败, + 因此 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。 +- 删除旧版 22 项 `TEMPLATE_TOKENS` 和独立的 broad“待补充”扫描,并新引入两个 active + project-fill marker 作为唯一机器未完成状态。历史 upstream 模板确实包含旧 token,但当前 + 登记的真实下游 SEC_metrics 未命中,本仓库也未登记其他下游依赖,因此不逐项保留 target-side + legacy blacklist;旧 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,作为已知回归兜底,不扩展成自然语言黑名单。 + +### 删除的代理测试与新信任基础 + +- 删除全部函数级单元测试、parser 分支测试、实现细节 status-code 测试、模板 equality / CRLF + 测试、标题/fence 测试、广泛自然语言 residue 扫描,以及行数、行宽、测试数量预算。 +- 这些 proxy invariant 已产生真实问题:普通散文中的 `baseline` 被误伤;为守行数预算产生 + 难审查的超长行;equality 与 active marker 同时保护模板项目化,形成重叠并在合法 PR + template 上需要特例。 +- 新信任基础是少量公共 CLI 场景、真实模板分发合同、fresh-context review,以及在同一最终 + SHA 上连续两轮完整 SEC_metrics Case A。第二轮必须重新调查、选择测试、review 并运行最终 + `check`;连续 prepare/check no-op 只证明机械幂等,不能替代第二轮 eval。场景覆盖成功主路径、 + prepare 原子前置失败、check 无效终态、user/repo 与 Codex/Claude 完整安装、以及仓库分发 + 结构;测试不 import `sync_docs.py` helper。 +- 旧 eval 风险并未静默删除:部分过时旧文档、共同虚构能力和验证层级膨胀成为 Case A 的 + 强制检查项;机械 no-op 与第二轮完整 Case A 分别承担不同风险;review-mode 身份风险由公共 + 场景和 `SKILL.md` 规则承担。 + +Case A 对“部分过时旧文档、共同虚构能力、验证层级膨胀”的检查是现实抽样,不是注入已知 +缺陷的阳性对照;若所选目标初始状态不含相应缺陷,只能报告“本次未观察到”,不得写成 +“已验证不存在”或“检测能力已验证”。 + +- 英文状态:本决策对应的对外说明已同步到英文 README 与 development workflow。 diff --git a/zh/interact.md b/zh/interact.md index 54c057e..7179d0a 100644 --- a/zh/interact.md +++ b/zh/interact.md @@ -1,51 +1,39 @@ -## 文档关系 - -`capability_contract.json` 是能力边界的机器可读真相源,回答“系统能不能做”。 - -`interact.md` 是用户可观察行为与验收不变量的真相源,回答“系统做的时候必须如何表现”。 - -`docs/business_user_guide.md` 是面向首次接触业务人员的派生教学文档,回答“业务人员怎么开始用”。 - -当三者不一致时: -1. 能力边界以 `capability_contract.json` 为准。 -2. 用户可观察行为以 `interact.md` 为准。 -3. `docs/business_user_guide.md` 必须回到前两者修正。 - -## 写作规则 - -凡是描述“能做 / 不能做 / 必须追问 / 必须拒绝”的用户可见能力或行为承诺,应尽量通过隐藏锚点引用 `capability_contract.json` 中的稳定 `anchor_id`。 - -隐藏锚点只引用稳定 `anchor_id`,不引用 JSON 路径、数组下标或当前 schema 内部路径。 - -# 1,定位 -* 能力分组:按用户心智而不是代码模块 -* 用户体验/验收路线:“用户能做什么”、“用户能看到什么”、“用户能得到什么价值” -# 2,读者 -为普通用户/PM/业务方,只描述“用户可观察行为”;工程细节与实现方式一律不写。 -# 3,边界 -* 允许“文档领先代码”,但必须显式标注状态。为了防止“代码领先文档”,PR checklist需要检查:“任何用户可见的行为变化(入口/输出结构/默认行为/错误提示/排序稳定性)都必须同步更新 interact.md”。验收写“不变量”,不要写“数据相关的具体数值”。 -* 不做“权威引用列表”,只做入口。 -* “关键失败路径”:为避免文档过度膨胀,失败路径只由人类手动提供。哪些失败愿意暴露给用户、哪些要隐藏/降级,需要由有品味的PM决定。 -* 叙事性:不要写冷冰冰的“点击 A,显示 B”。要写“用户为了解决 X 问题,进入 Y 页面,操作 Z,获得了 W 结果”。 -* 可见性(UI/API):任何例子都必须能被目标读者通过 UI 或 API 响应直接验证;只在日志/监控/代码内部状态可感知的内容不写。 -* 可证伪:描述必须包含具体的**“验收断言”**(例如:“看到绿色的成功提示”),让非技术人员也能判断对错。 -# 颗粒度 -一个app它有很多选项,如果排序组合的话,它可能有无穷个操作顺序。因此需要定几个原则,限定住用户体验的颗粒度。interact.md 的颗粒度标准是 “产品经理演示给 CEO 看的颗粒度”。 CEO 不关心所有下拉框的组合,他关心的是核心业务流是否跑通,以及关键的体验是否爽。守住这个底线,文档就不会无限膨胀。 -* 等价类原则:如果 10 个选项的业务逻辑是相同的,只写 1 个代表。 -* 正交性原则:描述“原子能力”的可组合性,而不是描述“所有组合结果”。 如果一个页面有“时间筛选”和“地区筛选”,不需要写“时间+地区”、“时间only”、“地区only”三种剧本。 -* 独立价值原则:只有当一个场景能独立回答"用户获得了什么价值"时,才值得写。 -## 例子 -``` -# 值得写(独立价值) -用户想了解过去30天的工单升级趋势, -进入 Flow 分析页面,选择时间范围, -看到升级率从 12% 降到 8%,判断近期改进措施有效。 - -# 不值得写(没有独立价值,只是操作细节) -用户点击"国家"下拉框, -展开选项列表, -选择"United States", -下拉框收起并显示已选项。 -``` - -# 4,用户可观察不变量(UI) +# User-visible Behavior and Acceptance + +## 0. Authority and Scope + +`capability_contract.json` 是能力边界的机器可读真相源;本文档是用户可观察行为与验收不变量 +的真相源;`docs/business_user_guide.md` 只能派生解释二者。本文档只写可由 UI、API 响应或 +其他公开入口直接观察的行为,不把日志、监控或内部状态当成用户结果。 + +<!-- project-fill: 说明本项目的公开入口、覆盖范围与明确排除项;完成后删除此 marker --> + +## 1. Audience and Granularity + +面向用户、产品和验收人员。按能独立产生用户价值的 journey 编写;等价选项只保留代表, +描述可组合的原子行为,不枚举无价值的排列组合。future / proposed 行为必须显式标注状态。 + +<!-- project-fill: 写明本项目的目标用户和验收粒度;完成后删除此 marker --> + +## 2. Supported User Journeys + +每个 scenario 都必须来自当前实现或测试证据,并使用以下字段。没有已验证 journey 时,写 +`Not configured —` 加已检查的入口和原因,而不是编造示例。 + +### Scenario + +<!-- project-fill: 用一个已验证的真实场景替换本段;包含 User goal、Required context、User action or request、Directly observable result、Failure / degradation / escalation、Acceptance assertion 和 capability anchor;完成后删除此 marker --> + +## 3. Cross-cutting User-visible Invariants + +不变量必须可由目标读者直接判断,通过稳定 capability anchor 回到 contract,并避免依赖 +会随数据变化的示例数值。 + +<!-- project-fill: 列出跨 journey 的真实可见不变量与验收证据;没有时写 Not applicable — 已验证原因;完成后删除此 marker --> + +## 4. Known Limits and Human Escalation + +限制必须区分当前不支持、暂时降级和 future / proposed。人工升级只写用户能识别的触发条件、 +可见解释和责任角色,不暴露内部监控细节。 + +<!-- project-fill: 写入已验证限制、降级、拒绝和人工升级路径,并引用 capability anchor;没有配置升级路径时准确说明;完成后删除此 marker --> diff --git a/zh/skills/workflow-docs-sync/SKILL.md b/zh/skills/workflow-docs-sync/SKILL.md index 5d79e06..00daa58 100644 --- a/zh/skills/workflow-docs-sync/SKILL.md +++ b/zh/skills/workflow-docs-sync/SKILL.md @@ -1,64 +1,143 @@ --- name: workflow-docs-sync -description: 在一个会话内把 wlvh/coding-workflow 的九份核心工作流文档同步并项目化到目标 Git 仓库。用户要求同步 workflow docs、补齐或核对 architecture/capability/interact/business guide/testing/governance 文档时使用;只接受目标仓库、可选 zh/en 语言和可选结束后创建 draft PR。 +description: 从目标 Git 仓库的代码、配置、测试和 committed artifacts 重建事实,最小必要改写 wlvh/coding-workflow 的九份核心工作流文档,运行项目真实测试,并以 fresh-context independent review 或诚实 self-review 收口。用户要求同步、补齐或核对 architecture、capability、interact、business guide、testing 或 governance 文档时使用;一次调用只接收目标仓库、zh/en 语言和成功后是否创建 draft PR。 --- # Workflow Docs Sync -在一个会话内完成编排。始终由主 Agent 独占目标工作区写入;领域分析和审计只读。 - -## 用户输入 - -- 要求目标仓库路径。 -- 语言仅允许 `zh` 或 `en`,未提供时使用 `zh`。 -- 未明确要求时不创建 draft PR。 -- 不要求或接受用户提供上游 SHA、上游 checkout、内部命令或分析分工。 - -## 上游解析 - -1. 先判断当前 Skill 是否位于 canonical `wlvh/coding-workflow` checkout:其 Git 根目录 - 必须同时包含当前 Skill 和所选语言的九份模板。满足时直接复用该 checkout。 -2. 无法定位 canonical checkout 时,在仓库外创建临时目录并 shallow clone - `https://github.com/wlvh/coding-workflow.git`。网络失败时停止并报告,不回退到缓存模板。 -3. 调用 `scripts/sync_docs.py prepare --target-repo <target> --upstream-dir <upstream> - --language <zh|en>`,只消费其单行 JSON。 -4. 保存返回的 `target_head` 与 `upstream_sha` 于当前会话;整轮使用同一 SHA。 -5. 会话结束时清理本轮临时 clone;canonical checkout 不清理。 - -## 执行流程 - -1. 主 Agent 读取目标仓库规则、入口、代码、测试和现有文档,建立带路径证据的代码地图。 -2. 完整读取 [references/sections.md](references/sections.md),启动四个相互隔离的只读分析: - Architecture、Capability / User Behavior、Testing、Governance。明确禁止它们编辑、 - stage、commit 或运行会产生项目 artifact 的命令;发现只在当前会话返回。 -3. 平台不支持 subagent 时,主 Agent 按 reference 中四个章节的顺序逐章分析,保持相同 - 输出结构和最终用户体验。 -4. 主 Agent 合并四份发现,只修改九份核心文档;`.gitignore` 仅在目标项目确有忽略 - 需求时修改。不要创建运行记录、模板镜像或工单;整个同步流程不读取、创建、改写或删除 - 仓库内 `PR_BODY.md`。 -5. 完整读取 [references/audit.md](references/audit.md),启动一个独立只读对抗性审计。 -6. 主 Agent 修复全部 BLOCKER 和所有可行动 WARN,再要求审计者只读轻量复审修复点 - 及其跨文档影响。无法闭合的产品判断保留为未解决决策,不得编造事实。 -7. 主 Agent 依据目标 `TESTING.md` 和真实代码路径运行必要测试。记录原样命令、结果和 - 未运行原因;测试输出不是检查器 receipt。 -8. 调用: +一次调用完成事实重建、必要文档改写、真实测试、复核和机械检查。现有文档与上游模板都是 +待验证声明,不是项目事实来源。主 Agent 是目标工作区唯一写入者。 + +## 输入与固定边界 + +- 要求目标仓库的 Git 根目录。 +- 语言必须由用户选择,且仅允许 `zh` 或 `en`。 +- 只有用户明确要求时,才在全部 gate 成功后创建 draft PR。 +- 不要求用户提供上游 checkout、SHA、内部命令、Agent 数量或执行顺序。 + +按以下方式准备: + +1. 如果当前 Skill 位于 canonical `wlvh/coding-workflow` Git 根目录,复用该 checkout。 +2. 否则在目标仓库外临时 shallow clone `https://github.com/wlvh/coding-workflow.git`;网络失败 + 时停止,不回退到缓存模板。 +3. 调用: ```bash - python3 <skill-root>/scripts/sync_docs.py check \ + python3 <skill-root>/scripts/sync_docs.py prepare \ --target-repo <target> \ --upstream-dir <upstream> \ - --upstream-sha <prepare 返回的 upstream_sha> \ - --expected-target-head <prepare 返回的 target_head> \ --language <zh|en> ``` -9. 检查失败时修复真实问题并重跑相关测试和检查;不得放宽断言、跳过必要测试或引入兼容实现。 +4. 只消费单行 JSON,固定返回的 `target_head` 和 `upstream_sha`,整轮复用同一对 SHA。 +5. 临时 clone 在会话结束时清理;canonical checkout 不清理。 + +`prepare` 从固定 Git object 读取模板,并在任何目标写入前确认九份 source path 都存在且为 +UTF-8、八份非 PR source 至少含一个 active marker;随后只创建缺失文件,不覆盖已有文档。 +上游 dirty worktree 不影响模板 bytes。不得创建 run state、receipt、ledger、Skill 副本、模板 +镜像或仓库内 PR body。 + +## 重建事实并最小改写 + +写入前至少运行 `git -C <target> ls-files -z` 建立范围。完整调查当前代码、配置、测试、 +committed artifacts、可重复运行结果和必要 Git 历史;不得用旧文档证明旧文档正确,也不得 +因为内容未出现在近期 diff 中而跳过。 + +- 全量质疑九份文档,只改错误、缺失、失真或失效内容;正确内容保持零 diff。 +- 删除没有当前事实证据的强声明。需要产品判断时记录 open decision,不编造结论。 +- 保持 `capability_contract.json → interact.md → docs/business_user_guide.md` 的权威方向。 +- 清除所有 active project-fill marker;不要使用固定写入顺序、完成百分比、KEEP ledger 或 + 过程状态。 +- Architecture、Capability / User Behavior、Testing、Governance 是覆盖维度,不是固定四 + Agent 拓扑。可自行调查,也可按模块、调用链、风险或证据类型委派只读工作;所有结论都 + 必须回到项目证据。 + +## 四个覆盖维度 + +### Architecture + +从真实入口向下重建核心调用链,核对系统目的、模块职责、依赖方向、数据流、状态、错误 +模型、配置、认证、外部依赖、artifact、副作用、扩展点和架构债务。主动证伪架构不变量, +不得把未来设想写成 active 现状。 + +### Capability / User Behavior + +从入口、UI 或 API 响应、错误、测试和用户可见限制提取已实现能力、拒绝、追问、降级和责任 +边界。递归核对稳定 `anchor_id` 与 Markdown 引用。主动寻找多份文档彼此一致、但代码和测试 +不存在的共同虚构能力;文档共识不是实现证据。Business guide 只能教学性解释 contract 与 +`interact.md` 已确认的内容。 + +### Testing + +盘点真实 runner、命令、fixture、层级、外部依赖、副作用和必要顺序。测试环境由真实命令、 +副作用、CI 能力和项目政策决定,可以是 CI、container、独立 checkout、远端环境或其他已 +验证环境;记录 exact command、scope、result、not-run reason、环境、隔离和清理结果。 + +验证层级不得膨胀。Unit、contract、scenario、golden、report build、repair validation、light +review、full validation 和 live test 只能按实际覆盖范围表述;未运行不得报告为通过。 + +### Governance + +在前三维事实稳定后核对 `AGENTS.md`、`PR_Checklist.md`、`SOP.md` 与 PR template。命令、路径、 +默认分支、发布和部署声明必须有当前证据。跨项目模板不得混入仅属于 Workflow Docs Sync 的 +内部执行语境、pin、reviewer 拓扑或隔离实现。PR body 只能位于目标仓库外。 + +## Fresh-context review + +优先使用 `independent` reviewer:提供 fresh context、完整仓库只读访问和同一 `target_head`。 +Reviewer 在 blind-first 初始阶段不得读取主 Agent 的任何中间产物;必须先从代码、配置、 +测试和 committed artifacts 独立重建高风险事实并形成 findings。初始 findings 形成后,才可 +读取最终候选文档和原始 Git diff,检查遗漏、最小改写与跨文档影响。 + +Reviewer 不编辑、stage、commit、push,也不把复核过程写入目标仓库。无法提供认知隔离时, +执行完整 self-review,并在最终报告原样写: + +```text +Review mode: self-review; independent review was not available +``` + +不得把 self-review 称为 independent。用户明确要求 independent review 而平台无法提供时, +停止在发布前。 + +每个 finding 包含唯一 ID、severity、证据、风险和预期修复边界: + +- `BLOCKER`:当前候选会产生错误、虚构、越权、不可复现交付或关键风险遗漏,必须修复后才能 + 收口。 +- `WARN`:存在实质性质量或维护风险;除非需要新的产品决策,否则必须修复。 +- `NOTE`:不改变可交付性的观察或后续建议,不作为 gate。 + +主 Agent 修复全部 BLOCKER 和无需新产品决策的 actionable WARN,请 reviewer 复核 finding +对应修改及直接跨文档影响,并重跑受影响测试。不得用新增状态文件、额外 Agent、词表或代理 +指标代替本应覆盖该风险的场景、review 或真实 eval。 + +## 最终检查与报告 + +运行固定 SHA 的最终检查: + +```bash +python3 <skill-root>/scripts/sync_docs.py check \ + --target-repo <target> \ + --upstream-dir <upstream> \ + --upstream-sha <prepare 返回的 upstream_sha> \ + --expected-target-head <prepare 返回的 target_head> \ + --language <zh|en> +``` + +`check` 从 `upstream_dir` 的 object store 按 `upstream_sha` 与 language 重读并验证同一 source +contract,再验证目标 HEAD、dirty allowlist、editable path 无 index/worktree 分叉、九份普通 +文件、UTF-8、非空、JSON object、active marker,以及存在时为 UTF-8 的 `.gitignore`。唯一 +final bytes 的 whitespace 检查在临时非 Git 目录以固定 Git 规则运行,不继承目标仓库 +attributes、用户 global attributes 或 system attributes。它不解析 Markdown 标题或 fence, +不判断文案质量、capability 真实性、测试层级或业务指南可读性,也不证明调查、测试或 review +曾执行。 -## 最终报告 +最终报告必须包含: -一次性报告:修改文件、代码与测试证据路径、每条测试命令及结果、未解决决策、审计发现 -及处置、内部固定的上游 SHA。明确说明机械检查只验证最终仓库状态,不证明分析、审计或 -测试的执行历史。 +- `target_head`、`upstream_sha`、language 和真实 review mode; +- 修改文档及代码、配置、测试或 artifact 证据; +- 每条测试的 exact command、scope、result、not-run reason、环境与隔离方式; +- review finding、修复、复核结果和 open decisions; +- mechanical check 结果,并明确它只证明最终状态。 -用户要求 draft PR 时,在上述流程成功后使用仓库外临时 Markdown body,把 commit、push -和 draft PR 创建交给通用 GitHub 发布能力。同步脚本不参与发布。 +用户要求 draft PR 时,只有上述流程成功后才使用仓库外临时 Markdown body;commit、push 和 +draft PR 创建交给通用 GitHub 发布能力,同步脚本不参与发布。 diff --git a/zh/skills/workflow-docs-sync/agents/openai.yaml b/zh/skills/workflow-docs-sync/agents/openai.yaml index c02e54e..c981af5 100644 --- a/zh/skills/workflow-docs-sync/agents/openai.yaml +++ b/zh/skills/workflow-docs-sync/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "工作流文档同步" - short_description: "单会话编排九份核心工作流文档的分析、改写、审计与检查" + short_description: "全量重建项目事实、最小改写并复核九份核心工作流文档" default_prompt: "使用 $workflow-docs-sync 同步目标仓库的工作流文档。" policy: diff --git a/zh/skills/workflow-docs-sync/evals/README.md b/zh/skills/workflow-docs-sync/evals/README.md index 37d1181..586fb54 100644 --- a/zh/skills/workflow-docs-sync/evals/README.md +++ b/zh/skills/workflow-docs-sync/evals/README.md @@ -1,17 +1,57 @@ -# 真实仓库前向验证 +# 合并前端到端 Eval -在合并前从 clean disposable worktree 调用一次 `$workflow-docs-sync`,用户只提供目标仓库 -和语言,不创建 draft PR。验证主 Agent 是唯一写入者、分析与审计只读、没有仓库内运行 -状态或 PR body,并保存最终报告中的路径证据、测试命令、审计处置和固定上游 SHA。 +以下 Case 是 Skill 的真实合并门,不是单元测试或注入已知缺陷的阳性对照。只在最终候选 +`upstream_sha` 上运行;后续代码、模板或合同变化都会使记录过期。每次保存固定 target SHA、 +language、exact prompt、执行者与 review mode、exact commands、files changed / digest、findings +和原始 final-check JSON;未实际运行不得写“已验证”。目标仓库内不得出现 run state、ledger、 +receipt、scratch 或 PR body。 -## Case A:SEC_metrics +## Case G:本仓库入口可导航性 -- 从当前真实代码重建 pipeline 语义,不依赖历史 shadow 文件。 -- 专门核对 stage 10、11、12 的输入、写入副作用、容错参数和 hard failure 位置。 -- 区分 unit tests、golden、report build、repair validation、light review 与 full validation。 -- 没有当前部署配置和运行证据时,不得把项目描述为已完成生产调度。 -- 文档同步 worktree 只运行低污染检查;会改写 pipeline artifact 的重型命令放到另一个 - 锁定同一提交的 disposable clone。 +让 fresh-context 执行者只从仓库根开始,不提供内部路径或维护说明。执行者必须能从根 +`AGENTS.md` 与根 README 导航到中文 maintainer map,并准确找到: -完成条件:九份核心文档通过最终检查;测试与重型验证结果被准确记录;对抗审计没有未 -处理 BLOCKER,所有可行动 WARN 已修复;需要新产品决策的问题记入未解决决策。 +- 双语下游模板源及中文语义源; +- canonical Skill、安装器、测试、README、development workflow 与 DEC-006; +- 根 `.github/` 与 `zh/.github/`、`en/.github/` 的不同职责; +- 最短测试入口、`py_compile`、Skill quick validation、`git diff --check` 和 CLI help。 + +记录 candidate upstream SHA、exact prompt、执行者上下文边界和实际命令,并报告每个入口的 +实际路径、findings 和歧义。路径缺失、链接错误、把根 GitHub 基础设施当成下游模板,或需要 +会话外隐含知识才能找到验证入口时,Case G 失败。 + +## Case A:SEC_metrics 真实端到端 + +从 SEC_metrics 当前真实 Git HEAD 建立隔离目标,按项目命令的副作用与政策选择执行环境;不 +复用旧 eval 结论或旧 shadow 文件。第一次运行 `$workflow-docs-sync` 时: + +- 从当前代码、配置、测试和 committed artifacts 重建 pipeline,专门核对 stage 10、11、12 + 的输入、写入副作用、容错参数和 hard failure; +- 主动寻找部分过时旧文档; +- 主动寻找多份文档彼此一致、但代码和测试不存在的共同虚构能力; +- 主动检查 light、golden、repair validation 是否被写成 full validation; +- 没有当前部署配置和运行证据时,不得声称生产调度完成; +- 运行项目真实测试,准确记录验证层级,并完成 review 与最终 `check`。 + +这些检查是现实抽样。所选 HEAD 初始状态不含相应缺陷时,只能报告“本次未观察到”,不得写 +成“已验证不存在”或“检测能力已验证”。若观察到缺陷,记录代码/测试反证、文档声明、修改和 +复核结果。 + +在同一候选文档上立即进行第二次完整运行。重新调查和检查,但不得依赖第一次的过程说明; +记录前后九份文件 bytes、Git diff 与测试选择。预期是 no-op;只有新增项目事实要求的最小 +改写才可接受,并必须说明触发证据。任何无事实依据的格式抖动、重复改写或新增过程状态均 +失败。 + +Case A 最终记录包含: + +- SEC_metrics target SHA、候选 upstream SHA 与 language,且两轮固定为同一对身份; +- 每轮的 exact prompt、实际 review mode 和 exact commands; +- 第一次运行的 files changed / digest、完整文档 diff、测试选择与结果、review findings 和原始 + final-check JSON; +- 第二次完整重新调查后的 files changed / digest、完整 diff、重新选择的测试与结果、review + findings 和原始 final-check JSON; +- 对“部分过时旧文档、共同虚构能力、验证层级膨胀”的逐项结果:已观察到并检出,或本次未 + 观察到; +- 未运行项、open decisions 和剩余风险。 + +第二次 `prepare` / `check` no-op 只能作为幂等性附加证据,不能代替第二次完整 Case A。 diff --git a/zh/skills/workflow-docs-sync/references/audit.md b/zh/skills/workflow-docs-sync/references/audit.md deleted file mode 100644 index 0f7a90b..0000000 --- a/zh/skills/workflow-docs-sync/references/audit.md +++ /dev/null @@ -1,29 +0,0 @@ -# 只读对抗性审计 - -在主 Agent 完成统一文档修改后执行。审计者只读目标工作区和固定上游模板,不能编辑、 -暂存、提交、生成审计文件或运行会写项目 artifact 的命令。机械检查不能替代本审计。 - -## 审计方法 - -1. 从当前代码、测试、配置和已提交 artifact 独立重建关键事实,不以主 Agent 总结为真相源。 -2. 逐份核对九个核心文件,并交叉检查架构、能力、用户行为、测试和治理之间是否矛盾。 -3. 对相邻 stage 或 gate 逐个读取实现,确认输入、写入副作用、容错参数和 hard - failure 位置;不要根据编号或名称推断语义。 -4. 明确区分 unit tests、golden 计算、report build、repair validation、light review 和 - full validation。检查一种成功是否被误写成另一种成功。 -5. 对“已上线”“已生产调度”“全量验证”等强声明寻找当前代码、配置和运行证据;证据 - 不足即要求降级为准确的当前状态。 -6. 核对固定上游规则是否被采用或有项目事实支持的不采用理由,并检查模板 token、空泛 - 标题、无锚点能力和不可执行命令。 - -## 严重度 - -- `BLOCKER`:事实矛盾、虚构能力、错误失败语义、把会写 artifact 的动作称为只读、 - 混淆验证层级、遗漏必需核心文档,或会直接误导实现/测试/发布的规则。 -- `WARN`:证据不足、跨文档未闭合、命令或路径可能漂移、边界表达含混。能明确修改且 - 不需要新产品决策时视为可行动。 -- `NOTE`:不影响正确性的改进建议或已确认无问题的高风险点。 - -每条 finding 返回唯一 ID、严重度、受影响文件、精确证据、风险和建议修复。没有发现时 -也要列出已核对的高风险语义。主 Agent 修复后,轻量复审只验证 finding 对应修改及其 -直接跨文档影响;新发现仍按同一严重度报告。 diff --git a/zh/skills/workflow-docs-sync/references/sections.md b/zh/skills/workflow-docs-sync/references/sections.md deleted file mode 100644 index e17201c..0000000 --- a/zh/skills/workflow-docs-sync/references/sections.md +++ /dev/null @@ -1,52 +0,0 @@ -# 四领域只读分析 - -四个领域分析都以目标仓库当前代码、测试、九份现有文档和固定上游模板为证据。只读执行, -不编辑文件、不暂存改动、不运行会写项目 artifact 的命令,也不把发现写入磁盘。 - -每个分析返回同一结构:`Findings`(BLOCKER / WARN / NOTE)、`Evidence`(路径与符号或 -行号)、`Proposed updates`(建议由主 Agent 落盘的文件和内容边界)、`Open decisions`。 -事实不足时明确写未知;不得用上游模板替代项目事实。 - -## 1. Architecture - -负责 `architecture.md`。 - -- 读取真实入口、核心模块、调用链、数据流、状态与错误模型、外部依赖、配置和扩展点。 -- 核对架构不变量是否能由代码、配置或测试证伪;移除只描述愿景而无当前证据的断言。 -- 区分三类问题:仍是模板或缺段;固定上游新增的通用写作要求未吸收;代码已经变化而 - 文档未跟上。 -- 只提出架构文档修改。能力、测试或治理影响作为下游建议交回主 Agent,不越界改写。 - -## 2. Capability / User Behavior - -负责 `capability_contract.json`、`interact.md`、`docs/business_user_guide.md`。 - -- 从真实入口、响应、错误、测试和可见限制提取已实现能力、拒绝、追问、降级与责任边界。 -- 以 `capability_contract.json` 为能力边界真相源,以 `interact.md` 为用户可观察行为和 - 验收不变量真相源;business guide 只能教学性解释前两者已确认的内容。 -- 递归核对稳定 `anchor_id`、Markdown 引用和测试证据;不依赖 JSON 数组位置或固定桶路径。 -- 禁止把内部日志、仅代码可见状态、推测能力或未来计划写成用户已可用功能。 -- 对模板残留、上游语义变化、代码或测试行为漂移分别给出证据和建议。 - -## 3. Testing - -负责 `TESTING.md`。 - -- 盘点真实测试入口、层级、fixture、外部依赖、生成 artifact 的副作用和推荐执行顺序。 -- 区分 unit、contract、scenario、golden、report build、repair/validation gate 与 live test; - 不用其中一层的成功替代另一层。 -- 验证文档中的命令、环境前提、失败条件和产物与当前代码一致;标出重型或污染性命令, - 供主 Agent 在隔离 checkout 中运行。 -- 判断已有覆盖、冗余测试、真实高价值缺口、mock-only 风险和不值得新增的测试。 -- light review 只能按代码实际覆盖范围描述,不能写成 full validation。 - -## 4. Governance - -负责 `PR_Checklist.md`、`SOP.md`、`AGENTS.md`、`.github/pull_request_template.md`。 - -- 从前三个领域的下游影响反向检查规则入口、文档关系、测试证据和文件地图是否闭合。 -- 保持权威边界:`AGENTS.md` 做入口与规则,`SOP.md` 做流程骨架,`PR_Checklist.md` - 做提交核对,PR template 做长期通用 body 结构;避免四处复制易漂移细节。 -- 核对所有命令、路径、分支和发布声明是否仍真实;没有当前代码或配置证据时,不得声称 - 已完成生产部署、调度或自动化。 -- PR template 可以直接继承固定上游内容;其他八份核心文档必须体现目标项目事实。 diff --git a/zh/skills/workflow-docs-sync/scripts/sync_docs.py b/zh/skills/workflow-docs-sync/scripts/sync_docs.py index a36ef25..b875588 100644 --- a/zh/skills/workflow-docs-sync/scripts/sync_docs.py +++ b/zh/skills/workflow-docs-sync/scripts/sync_docs.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """为单会话 Workflow Docs Sync 准备模板并检查最终仓库状态。 -调用关系:CLI 解析参数后,``prepare`` 解析两个仓库的 HEAD、从固定上游提交读取 -九份模板并只补齐缺失文件;``check`` 使用同一上游提交只读验证目标 HEAD、文件范围、 -内容和 whitespace。脚本不运行项目测试,也不执行任何 Git 发布动作。 +调用关系:CLI 解析参数后,``prepare`` 解析两个仓库的 HEAD,读取并验证固定 source,完成 +全部目标预检后只补齐缺失文件;``check`` 重读并验证调用方固定的 source object,再只读验证 +目标 HEAD、文件范围、内容和 whitespace。脚本不运行项目测试,也不执行任何 Git 发布动作。 """ from __future__ import annotations @@ -14,6 +14,7 @@ import re import subprocess import sys +import tempfile from dataclasses import dataclass from pathlib import Path from typing import Any @@ -30,36 +31,10 @@ "AGENTS.md", ".github/pull_request_template.md", ) -PR_TEMPLATE = ".github/pull_request_template.md" EDITABLE_PATHS = frozenset((*CORE_FILES, ".gitignore")) LANGUAGES = ("zh", "en") SHA_PATTERN = re.compile(r"[0-9a-f]{40}") -ATX_HEADING = re.compile(r"^[ \t]{0,3}#{1,6}(?:[ \t]+(.*)|[ \t]*)$") -FENCE = re.compile(r"^[ \t]{0,3}(`{3,}|~{3,})") -TEMPLATE_TOKENS = ( - "<项目名>", - "<项目 / agent / app 名称>", - "<对象>", - "<指标 / 结果>", - "<project name>", - "<project / agent / app name>", - "<object>", - "<metric / result>", - "Case 1:确认一个对象最近是否异常", - "Case 1: Check whether one object is abnormal", - "待项目负责人补充", - "project owner must replace this", - "sample_supported_question", - "sample_multi_object_comparison_not_supported", - "sample_no_final_business_decision", - "sample_requires_context_before_answer", - "CAPABILITY.sample_", - "BOUNDARY.sample_", - "RESPONSIBILITY.sample_", - "BEHAVIOR.sample_", - "<!-- capability-anchor: TODO -->", - "<!-- test-anchor: TODO -->", -) +TEMPLATE_TOKENS = ("<!-- project-fill:", "__PROJECT_FILL__:") class SyncError(RuntimeError): @@ -90,7 +65,7 @@ class Repository: @dataclass(frozen=True) class StatusEntry: - """保存一条 porcelain 状态及其涉及的全部相对路径。""" + """保存一条 porcelain XY code 及其涉及的全部相对路径。""" code: str paths: tuple[str, ...] @@ -101,8 +76,8 @@ def emit_json(*, payload: dict[str, Any]) -> None: print(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))) -def run_git(*, repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[str]: - """在指定仓库运行只含显式参数的 Git 命令并捕获 UTF-8 输出。""" +def run_git(*, repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[bytes]: + """在指定目录运行显式 Git 命令并保留原始输出 bytes。""" environment = os.environ.copy() environment.update({"LC_ALL": "C", "GIT_OPTIONAL_LOCKS": "0"}) return subprocess.run( @@ -111,8 +86,28 @@ def run_git(*, repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[ env=environment, check=False, capture_output=True, - encoding="utf-8", - text=True, + ) + + +def run_whitespace_git( + *, repo_root: Path, args: list[str] +) -> subprocess.CompletedProcess[bytes]: + """在不读取用户或系统 Git attributes/config 的环境中运行 whitespace Git 命令。""" + # 仅隔离 whitespace gate,避免改变 object、HEAD 和 status 等通用 Git 行为。 + environment = os.environ.copy() + environment.update({ + "GIT_ATTR_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_OPTIONAL_LOCKS": "0", + "LC_ALL": "C", + }) + return subprocess.run( + args=["git", "-C", str(repo_root), *args], + cwd=repo_root, + env=environment, + check=False, + capture_output=True, ) @@ -120,9 +115,13 @@ def git_output(*, repo_root: Path, args: list[str], purpose: str) -> str: """运行必须成功的 Git 命令并返回 stdout。""" result = run_git(repo_root=repo_root, args=args) if result.returncode != 0: - detail = result.stderr.strip() or result.stdout.strip() or "Git 无输出" + diagnostic = result.stderr or result.stdout or b"Git produced no output" + detail = diagnostic.decode("utf-8", errors="replace").strip() raise SyncError(error=purpose, detail=detail) - return result.stdout + try: + return result.stdout.decode("utf-8") + except UnicodeDecodeError as exc: + raise SyncError(error=purpose, detail=f"Git 输出不是有效 UTF-8: {exc}") from exc def require_sha(*, value: str, label: str) -> str: @@ -202,7 +201,7 @@ def read_status(*, repo_root: Path) -> list[StatusEntry]: def require_editable_dirty(*, repo_root: Path) -> list[StatusEntry]: - """在任何写入前拒绝九份文档和可选 gitignore 之外的 dirty 路径。""" + """拒绝范围外 dirty path 和 editable path 的 index/worktree 分叉。""" entries = read_status(repo_root=repo_root) outside = sorted({ path @@ -212,6 +211,20 @@ 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 + }) + if split: + raise SyncError( + error="editable path 存在 index/worktree 分叉", + detail=f"{', '.join(split)};请先统一最终 bytes", + ) return entries @@ -220,11 +233,24 @@ def read_template( ) -> str: """只通过固定提交的 Git 对象读取一个语言模板。""" source_path = f"{language}/{relative_path}" - return git_output( + result = run_git( repo_root=upstream_root, args=["show", f"{upstream_sha}:{source_path}"], - purpose="无法读取固定上游模板", ) + if result.returncode != 0: + diagnostic = result.stderr or result.stdout or b"Git produced no output" + detail = diagnostic.decode("utf-8", errors="replace").strip() + raise SyncError( + error="无法读取固定上游模板", + detail=f"{upstream_sha}:{source_path}: {detail}", + ) + try: + return result.stdout.decode("utf-8") + except UnicodeDecodeError as exc: + raise SyncError( + error="固定上游模板不是有效 UTF-8", + detail=f"{upstream_sha}:{source_path}: {exc}", + ) from exc def read_templates( @@ -242,6 +268,36 @@ 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}", + ) + markerless = [ + f"{language}/{relative_path}" + for relative_path in CORE_FILES + if relative_path != ".github/pull_request_template.md" + and not any( + token in templates[relative_path] + for token in TEMPLATE_TOKENS + ) + ] + if markerless: + raise SyncError( + error="固定上游模板违反 source marker invariant", + detail=f"{upstream_sha}: 非 PR source template 缺少 active marker: " + f"{', '.join(markerless)}", + ) + + def inspect_destinations(*, target_root: Path) -> tuple[list[str], list[str]]: """区分缺失和已有目标文件,并在写入前拒绝目录冲突。""" missing: list[str] = [] @@ -284,6 +340,11 @@ def prepare(*, target_repo: str, upstream_dir: str, language: str) -> dict[str, upstream_sha=upstream.head, language=language, ) + validate_source_templates( + templates=templates, + language=language, + upstream_sha=upstream.head, + ) require_editable_dirty(repo_root=target.root) missing, existing = inspect_destinations(target_root=target.root) install_missing(target_root=target.root, templates=templates, missing=missing) @@ -300,17 +361,11 @@ def prepare(*, target_repo: str, upstream_dir: str, language: str) -> dict[str, } -def normalize_text(*, text: str) -> str: - """只规范换行符,使跨平台模板原样复制判断稳定。""" - return text.replace("\r\n", "\n").replace("\r", "\n") - - def line_hits(*, relative_path: str, text: str) -> list[str]: - """返回明确模板 token 和未填写中文占位的逐行错误。""" + """返回两个 active project-fill marker 的逐行错误。""" failures: list[str] = [] - tokens = (*TEMPLATE_TOKENS, "待补充") for line_number, line in enumerate(text.splitlines(), start=1): - for token in tokens: + for token in TEMPLATE_TOKENS: if token in line: failures.append( f"{relative_path}:{line_number}: 检测到未项目化内容 {token}" @@ -318,39 +373,6 @@ def line_hits(*, relative_path: str, text: str) -> list[str]: return failures -def markdown_failures(*, relative_path: str, text: str) -> list[str]: - """忽略 fenced code,要求至少一个非空 ATX 标题且不存在空标题。""" - failures: list[str] = [] - valid_heading = False - fence_character = "" - fence_width = 0 - for line_number, line in enumerate(text.splitlines(), start=1): - fence_match = FENCE.match(line) - if fence_match is not None: - marker = fence_match.group(1) - if not fence_character: - fence_character, fence_width = marker[0], len(marker) - elif marker[0] == fence_character and len(marker) >= fence_width: - fence_character, fence_width = "", 0 - continue - if fence_character: - continue - heading = ATX_HEADING.match(line) - if heading is None: - continue - title = (heading.group(1) or "").strip() - title = re.sub(r"[ \t]+#+[ \t]*$", "", title).strip() - if re.fullmatch(r"#+", title) is not None: - title = "" - if title: - valid_heading = True - else: - failures.append(f"{relative_path}:{line_number}: Markdown 标题为空") - if not valid_heading: - failures.append(f"{relative_path}: 缺少非空 Markdown 标题") - return failures - - def capability_failures(*, text: str) -> list[str]: """验证 capability contract 是合法 JSON object,不机械裁定项目语义。""" try: @@ -362,12 +384,9 @@ def capability_failures(*, text: str) -> list[str]: return [] -def content_failures(*, target_root: Path, templates: dict[str, str]) -> tuple[ - list[str], dict[str, str] -]: - """验证九份文件的存在性、编码、内容、标题、JSON 和模板差异。""" +def content_failures(*, target_root: Path) -> list[str]: + """验证核心文件内容及存在的 gitignore UTF-8 合同。""" failures: list[str] = [] - texts: dict[str, str] = {} for relative_path in CORE_FILES: path = target_root / relative_path if path.is_symlink(): @@ -381,40 +400,23 @@ def content_failures(*, target_root: Path, templates: dict[str, str]) -> tuple[ except UnicodeDecodeError as exc: failures.append(f"{relative_path}: 不是有效 UTF-8: {exc}") continue - texts[relative_path] = text if not text.strip(): failures.append(f"{relative_path}: 文件为空") continue - if relative_path.endswith(".md"): - failures.extend(markdown_failures(relative_path=relative_path, text=text)) failures.extend(line_hits(relative_path=relative_path, text=text)) - if ( - relative_path != PR_TEMPLATE - and normalize_text(text=text) - == normalize_text(text=templates[relative_path]) - ): - failures.append(f"{relative_path}: 不允许与固定上游模板完全相同") - if "capability_contract.json" in texts: - failures.extend(capability_failures(text=texts["capability_contract.json"])) - return failures, texts - - -def tracked_whitespace_failures(*, target_root: Path) -> list[str]: - """对 working tree 和 index 的 tracked diff 运行 Git whitespace 检查。""" - failures: list[str] = [] - for args, label in ( - (["diff", "--check"], "working tree"), - (["diff", "--cached", "--check"], "index"), - ): - result = run_git(repo_root=target_root, args=args) - if result.returncode != 0: - detail = result.stdout.strip() or result.stderr.strip() or "无诊断输出" - failures.append(f"{label} whitespace 检查失败: {detail}") + if relative_path == "capability_contract.json": + failures.extend(capability_failures(text=text)) + gitignore = target_root / ".gitignore" + if not gitignore.is_symlink() and gitignore.is_file(): + try: + gitignore.read_bytes().decode("utf-8") + except UnicodeDecodeError as exc: + failures.append(f".gitignore: 不是有效 UTF-8: {exc}") return failures def file_whitespace_failures(*, target_root: Path) -> list[str]: - """用 Git 的同一规则检查九份最终文档和存在的辅助 gitignore。""" + """在非仓库目录用固定 Git 规则检查唯一 final bytes。""" failures: list[str] = [] paths = [path for path in CORE_FILES if (target_root / path).is_file()] gitignore = target_root / ".gitignore" @@ -422,28 +424,30 @@ def file_whitespace_failures(*, target_root: Path) -> list[str]: failures.append(".gitignore: 不能是符号链接") elif gitignore.exists() and not gitignore.is_file(): failures.append(".gitignore: 不是普通文件") - elif gitignore.is_file(): - try: - gitignore.read_bytes().decode("utf-8") - except UnicodeDecodeError as exc: - failures.append(f".gitignore: 不是有效 UTF-8: {exc}") if not gitignore.is_symlink() and gitignore.is_file(): paths.append(".gitignore") - for relative_path in paths: - result = run_git( - repo_root=target_root, - args=[ - "diff", - "--no-index", - "--check", - "--", - os.devnull, - str(target_root / relative_path), - ], - ) - if result.returncode not in (0, 1): - detail = result.stdout.strip() or result.stderr.strip() or "无诊断输出" - failures.append(f"最终文件 whitespace 检查失败: {detail}") + with tempfile.TemporaryDirectory(prefix="workflow-docs-whitespace-") as value: + checker_root = Path(value) + for relative_path in paths: + result = run_whitespace_git( + repo_root=checker_root, + args=[ + "-c", + f"core.attributesFile={os.devnull}", + "-c", + "core.whitespace=blank-at-eol,blank-at-eof,space-before-tab", + "diff", + "--no-index", + "--check", + "--", + os.devnull, + str(target_root / relative_path), + ], + ) + if result.returncode not in (0, 1): + diagnostic = result.stdout or result.stderr or b"no diagnostic output" + detail = diagnostic.decode("utf-8", errors="replace").strip() + failures.append(f"最终文件 whitespace 检查失败: {detail}") return failures @@ -458,25 +462,29 @@ def check( """只读验证固定上游提交对应的最终目标仓库状态。""" target = require_repository(value=target_repo, label="目标仓库") expected_head = require_sha(value=expected_target_head, label="预期目标 HEAD") - if target.head != expected_head: - raise SyncError( - error="目标 HEAD 已变化", - detail=f"期望 {expected_head},实际 {target.head}", - ) upstream = require_repository(value=upstream_dir, label="上游目录") pinned_sha = require_commit( repo_root=upstream.root, value=upstream_sha, label="上游 SHA", ) - entries = require_editable_dirty(repo_root=target.root) templates = read_templates( upstream_root=upstream.root, upstream_sha=pinned_sha, language=language, ) - failures, _ = content_failures(target_root=target.root, templates=templates) - failures.extend(tracked_whitespace_failures(target_root=target.root)) + validate_source_templates( + templates=templates, + language=language, + upstream_sha=pinned_sha, + ) + if target.head != expected_head: + raise SyncError( + error="目标 HEAD 已变化", + detail=f"期望 {expected_head},实际 {target.head}", + ) + entries = require_editable_dirty(repo_root=target.root) + failures = content_failures(target_root=target.root) failures.extend(file_whitespace_failures(target_root=target.root)) if failures: raise SyncError(error="最终仓库检查失败", detail=" | ".join(failures))