From 70e087e47dedc19541897301b77516250b27d0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Sun, 30 Aug 2026 19:46:03 +0800 Subject: [PATCH] feat(toolkit)!: migrate violation evidence to defect Add explicit staged store migration with marker-backed crash recovery, preserve neutral evidence records, and retire work-performing violation commands with guidance. --- AGENTS.md | 2 +- .../contract/handoff-executor-result-v1.md | 8 +- .../contract/specification-v1.md | 8 +- references/assets/template/AGENTS.md | 2 +- references/evals/orchestration/evals.json | 8 +- ...-evidence.yaml => wb-defect-evidence.yaml} | 0 rules/index.yaml | 34 +-- rules/orchestration/orch-handoff-required.md | 2 +- rules/orchestration/orch-open-questions.md | 4 +- rules/orchestration/orch-review-completion.md | 2 +- ...-evaluation.md => wb-defect-evaluation.md} | 30 +- rules/work-bundle/wb-defect-evidence.md | 64 ++++ rules/work-bundle/wb-violation-evidence.md | 61 ---- scripts/work-bundle/README.md | 11 +- .../work-bundle/{violations.py => defects.py} | 285 +++++++++++++++--- scripts/work-bundle/dispatcher.py | 44 ++- skills/orch-create-specification/SKILL.md | 2 +- skills/orch-execute-plan/SKILL.md | 2 +- skills/orch-review-plan/SKILL.md | 2 +- skills/wb-initialize-project/SKILL.md | 2 +- .../test_orchestration_skill_rule_boundary.py | 2 +- tests/test_rule_contracts.py | 28 +- ...py => test_work_bundle_defect_evidence.py} | 127 +++++--- tests/test_work_bundle_defect_migration.py | 261 ++++++++++++++++ 24 files changed, 754 insertions(+), 237 deletions(-) rename references/{wb-violation-evidence.yaml => wb-defect-evidence.yaml} (100%) rename rules/work-bundle/{wb-violation-evaluation.md => wb-defect-evaluation.md} (81%) create mode 100644 rules/work-bundle/wb-defect-evidence.md delete mode 100644 rules/work-bundle/wb-violation-evidence.md rename scripts/work-bundle/{violations.py => defects.py} (56%) rename tests/{test_work_bundle_violation_evidence.py => test_work_bundle_defect_evidence.py} (65%) create mode 100644 tests/test_work_bundle_defect_migration.py diff --git a/AGENTS.md b/AGENTS.md index 0aeb1b4..f79a34c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ purpose: - Orchestration skills: `orch-*`, provide skills for feature-implementation orchestration, including evidence gathering, specification/plan creation, execution instruction, verifying, etc. - General conception: - `work_bundle_root`: installed WorkBundle toolkit source; it owns builtin skills, rules, references, and toolkit helper code. - - `work_bundle_config_root`: user runtime state at `~/.work-bundle/`; it owns bootstrap, global registries, global rules, and violation state. + - `work_bundle_config_root`: user runtime state at `~/.work-bundle/`; it owns bootstrap, global registries, global rules, and defect state. - `workspace_root`: authority root for one managed workspace; it owns `.work-bundle/`, root `AGENTS.md`, `script/`, and `credentials/`; in multi-repository mode it also owns managed repository member paths. - `project_root`: root of one concrete source repository checkout that agents inspect, edit, test, and commit. In multi-repository mode it is a child of `workspace_root`; in single-repository mode it equals `workspace_root`. diff --git a/references/assets/orchestration/contract/handoff-executor-result-v1.md b/references/assets/orchestration/contract/handoff-executor-result-v1.md index 0d60634..772e959 100644 --- a/references/assets/orchestration/contract/handoff-executor-result-v1.md +++ b/references/assets/orchestration/contract/handoff-executor-result-v1.md @@ -92,10 +92,10 @@ convergence: checks: - "exact command or inspection" -violation_closure: +defect_closure: status: not-applicable | carried-to-review | completed | blocked evidence: - - violation-id-or-path + - defect-id-or-path note: "Review-only closure evidence; executors do not delete evidence." unresolved: @@ -171,7 +171,7 @@ allocation_evidence: - `contract_decoupling` is required when a task is marked contract-decoupled or depends on a common contract group. - `barrier` is required when a task is a barrier participant or convergence owner. - `convergence` is required when the task owns post-barrier joint debug, integration checks, or cross-branch validation. -- `violation_closure` is required when a review task closes or carries specification-included violation evidence. +- `defect_closure` is required when a review task closes or carries specification-included defect evidence. - `unresolved` is included only when blockers or issues remain. - `task_fit_check` is required for completed and partial task results. It records the assigned task, result `clean|repaired|unresolved|skipped`, artifacts checked, and meaningful findings. - `acceptance_review` is required when the task contract requires review. A review-required task cannot become `Completed` until the verdict is `accept`. @@ -214,7 +214,7 @@ Compact handoffs must not weaken safety gates: - `knowledge_disposition.action` is exactly `none`, `update`, `supersede`, or `reclassify`; reasons and affected authority must not name knowledge paths or any `ks-*` skill, and review owns any approved persistence follow-up. - Contract-decoupled handoffs must show validation against the common contract and accepted prior handoffs, not sibling in-progress implementation. - Barrier handoffs must show whether the participant reached the barrier or blocked before convergence work is scheduled. -- Violation closure handoffs must use review-owned lifecycle evidence and must not delete violation evidence files. +- Defect closure handoffs must use review-owned lifecycle evidence and must not delete defect evidence files. ## Format Guidance diff --git a/references/assets/orchestration/contract/specification-v1.md b/references/assets/orchestration/contract/specification-v1.md index 9e77e50..229b408 100644 --- a/references/assets/orchestration/contract/specification-v1.md +++ b/references/assets/orchestration/contract/specification-v1.md @@ -61,7 +61,7 @@ Metadata blockers may block source inspection, impact traversal, planning, or ex Source Context must include project metadata preflight evidence or a blocking open question that explains why metadata preflight could not establish a trustworthy branch, commit, registry, and CodeGraph baseline. -For WorkBundle project specifications, Source Context must include related active violation registry evidence when the current scope matches active violations. Each included violation records ID, severity, deviation summary, related scope, required resolution, and expected review closure. Exact-current-work conflicts may be specification-owned instead of requiring separate new violation evidence. +For WorkBundle project specifications, Source Context must include related active defect registry evidence when the current scope matches active defects. Each included defect records ID, severity, deviation summary, related scope, required resolution, and expected review closure. Exact-current-work conflicts may be specification-owned instead of requiring separate new defect evidence. When no supporting authority note exists for the user purpose, record the retrieval gap and analyze the purpose from user input and repository evidence. Use Design Interrogation only for unresolved design intent that cannot be answered from current evidence. @@ -146,7 +146,7 @@ Do not add a lifecycle stage, force a recommendation, or use a universal product - **REQ-SHELL-003**: Revise draft requirements after bounded evidence gathering instead of silently replacing them. - **REQ-META-001**: Run project metadata preflight after shell creation and before broad repository evidence gathering; block on missing metadata, branch mismatch, stale baseline affecting evidence trust, registry contradiction, or inconsistent CodeGraph state. - **REQ-KG-001**: Run bounded durable-knowledge gateway retrieval for material new findings or requests even when repository metadata blockers prevent source inspection, provided the gateway is accessible. -- **REQ-VIOL-001**: For WorkBundle project scopes, inspect related active violation registry evidence and carry matching violations into Source Context or Open Questions with review closure expectations. +- **REQ-DEF-001**: For WorkBundle project scopes, inspect related active defect registry evidence and carry matching defects into Source Context or Open Questions with review closure expectations. ## 6. Interfaces & Data Contracts @@ -226,7 +226,7 @@ Every open question must include: | Required resolution | yes | | Advised options | yes | -Candidate, background, blocked, draft, proposed, stale, opposing, or otherwise non-authority durable knowledge must not become requirement text. When material, record it as rationale, traceability, conflict evidence, or open-question input. Related active violations that affect the specification scope are blocking open questions unless the user or accepted evidence resolves them. Non-authority or opposing evidence is blocking only when the unresolved decision affects implementation or review safety. +Candidate, background, blocked, draft, proposed, stale, opposing, or otherwise non-authority durable knowledge must not become requirement text. When material, record it as rationale, traceability, conflict evidence, or open-question input. Related active defects that affect the specification scope are blocking open questions unless the user or accepted evidence resolves them. Non-authority or opposing evidence is blocking only when the unresolved decision affects implementation or review safety. ## 12. Knowledge Base Update @@ -262,7 +262,7 @@ Do not instruct specification authors or executors to write durable knowledge di - The source context records neutral cross-stage retrieval anchors or a retrieval gap, and any named retrieval policy is used only for classification/output grouping. - The specification carries accepted authority context forward so downstream planning and execution do not need to read `.work-bundle/knowledge/`. - The specification records project metadata preflight evidence including `working_branch`, `last_commit_id`, branch status, baseline status, and CodeGraph no-index fallback when applicable. -- WorkBundle project specifications record related active violations and expected review closure when applicable. +- WorkBundle project specifications record related active defects and expected review closure when applicable. - Material non-authority or opposing evidence is visible without shaping requirements unless resolved by user decision or accepted authority. ## 15. Quality Gate diff --git a/references/assets/template/AGENTS.md b/references/assets/template/AGENTS.md index aec3b7c..395e2d0 100644 --- a/references/assets/template/AGENTS.md +++ b/references/assets/template/AGENTS.md @@ -6,7 +6,7 @@ purpose: - Orchestration skills: `orch-*`, provide skills for feature-implementation orchestration, including evidence gathering, specification/plan creation, execution instruction, verifying, etc. - General conception: - `work_bundle_root`: installed WorkBundle toolkit source; it owns builtin skills, rules, references, and toolkit helper code. - - `work_bundle_config_root`: user runtime state at `~/.work-bundle/`; it owns bootstrap, global registries, global rules, and violation state. + - `work_bundle_config_root`: user runtime state at `~/.work-bundle/`; it owns bootstrap, global registries, global rules, and defect state. - `workspace_root`: authority root for one managed workspace; it owns `.work-bundle/`, root `AGENTS.md`, `script/`, and `credentials/`; in multi-repository mode it also owns managed repository member paths. - `project_root`: root of one concrete source repository checkout that agents inspect, edit, test, and commit. In multi-repository mode it is a child of `workspace_root`; in single-repository mode it equals `workspace_root`. diff --git a/references/evals/orchestration/evals.json b/references/evals/orchestration/evals.json index e719d04..883662e 100644 --- a/references/evals/orchestration/evals.json +++ b/references/evals/orchestration/evals.json @@ -231,14 +231,14 @@ }, { "id": 39, - "prompt": "Create a WorkBundle implementation specification for a scope that matches active violation registry entries.", - "expected_output": "Selects create-specification, inspects the WorkBundle violation registry without browsing .work-bundle/knowledge/**, includes related active violations as blocking Open Questions with severity, related scope, required resolution, and expected review closure, and treats exact-current-work conflicts as specification-owned when applicable.", + "prompt": "Create a WorkBundle implementation specification for a scope that matches active defect registry entries.", + "expected_output": "Selects create-specification, inspects the WorkBundle defect registry without browsing .work-bundle/knowledge/**, includes related active defects as blocking Open Questions with severity, related scope, required resolution, and expected review closure, and treats exact-current-work conflicts as specification-owned when applicable.", "files": [] }, { "id": 40, - "prompt": "Review a completed WorkBundle plan whose source specification included active violation evidence and whose implementation passed unblocked validation.", - "expected_output": "Selects review-plan, validates implementation and handoff evidence, closes resolved specification-included violations with the approved lifecycle action before archive, records violation_closure evidence, does not delete evidence files, keeps unrelated violations untouched, and blocks archive if included violation closure cannot be completed.", + "prompt": "Review a completed WorkBundle plan whose source specification included active defect evidence and whose implementation passed unblocked validation.", + "expected_output": "Selects review-plan, validates implementation and handoff evidence, closes resolved specification-included defects with the approved lifecycle action before archive, records defect_closure evidence, does not delete evidence files, keeps unrelated defects untouched, and blocks archive if included defect closure cannot be completed.", "files": [] }, { diff --git a/references/wb-violation-evidence.yaml b/references/wb-defect-evidence.yaml similarity index 100% rename from references/wb-violation-evidence.yaml rename to references/wb-defect-evidence.yaml diff --git a/rules/index.yaml b/rules/index.yaml index 5e05285..8ee29a5 100644 --- a/rules/index.yaml +++ b/rules/index.yaml @@ -206,6 +206,23 @@ rules: load: conditional requires: - rule-work-bundle-security-exclusion + - id: wb-defect-evaluation + path: work-bundle/wb-defect-evaluation.md + applies_when: + - a conflict, violation, error, failed validation, contradictory workflow behavior, user interruption, or user correction occurs during WorkBundle-guided work + - visible task evidence suggests a WorkBundle skill, rule, script, specification, plan, handoff, workflow contract, or toolkit execution surface may have caused or contributed to the problem + - an agent must decide whether an observed problem is work-bundle-scoped, project-scoped, mixed, or undetermined before creating defect evidence or reporting a blocker + enforcement: must + load: conditional + requires: [] + - id: wb-defect-evidence + path: work-bundle/wb-defect-evidence.md + applies_when: + - the Work Bundle rule is visible in AGENTS.md and any conflict, confliction, violation, contradiction, or user correction occurs + - the Work Bundle rule is visible in AGENTS.md and the session needs a visible entry point for defect evaluation and first-observed evidence routing + enforcement: must + load: always + requires: [] - id: wb-migrate-to-multi-repository path: work-bundle/wb-migrate-to-multi-repository.md applies_when: @@ -259,20 +276,3 @@ rules: enforcement: must load: conditional requires: [] - - id: wb-violation-evaluation - path: work-bundle/wb-violation-evaluation.md - applies_when: - - a conflict, violation, error, failed validation, contradictory workflow behavior, user interruption, or user correction occurs during WorkBundle-guided work - - visible task evidence suggests a WorkBundle skill, rule, script, specification, plan, handoff, workflow contract, or toolkit execution surface may have caused or contributed to the problem - - an agent must decide whether an observed problem is work-bundle-scoped, project-scoped, mixed, or undetermined before creating violation evidence or reporting a blocker - enforcement: must - load: conditional - requires: [] - - id: wb-violation-evidence - path: work-bundle/wb-violation-evidence.md - applies_when: - - the Work Bundle rule is visible in AGENTS.md and any conflict, confliction, violation, contradiction, or user correction occurs - - the Work Bundle rule is visible in AGENTS.md and the session needs a visible entry point for violation evaluation and first-observed evidence routing - enforcement: must - load: always - requires: [] diff --git a/rules/orchestration/orch-handoff-required.md b/rules/orchestration/orch-handoff-required.md index a37adf3..4e723b6 100644 --- a/rules/orchestration/orch-handoff-required.md +++ b/rules/orchestration/orch-handoff-required.md @@ -32,7 +32,7 @@ Require compact executor-result handoffs before reporting execution complete or - For contract-decoupled task handoffs, include compact `contract_decoupling` evidence: common contract group, common contracts checked, validation scope, `peer_implementation_validation_used: false`, and forbidden peer validation result. - For barrier participants, include compact `barrier` evidence with barrier id, participant role, readiness `reached|blocked`, and whether convergence remains pending. - For convergence owners, include compact `barrier` and `convergence` evidence showing every participant completed or blocked with executor-result handoffs before joint validation began. -- For review handoffs or review-adjacent executor results that carry specification-included violations, include `violation_closure` evidence only as review-owned lifecycle evidence or carry-forward status; executors must not delete violation evidence. +- For review handoffs or review-adjacent executor results that carry specification-included defects, include `defect_closure` evidence only as review-owned lifecycle evidence or carry-forward status; executors must not delete defect evidence. - Do not report execution complete while drift or gaps remain within task scope. Record out-of-scope findings as unresolved issues and block completion when they prevent conformance with the assigned artifacts. - Keep executor-result handoffs on carried spec, plan, phase, task, declared handoff, and task-scoped source or test context only; do not retrieve durable knowledge during execution-completion handoffs. - Update `.work-bundle/orchestration/handoff/index.jsonl` with id, type, status, path, project, timestamps, and related spec, plan, phase, and task links when helper/index support is available for the handoff format. diff --git a/rules/orchestration/orch-open-questions.md b/rules/orchestration/orch-open-questions.md index f3f3b3c..c50f69c 100644 --- a/rules/orchestration/orch-open-questions.md +++ b/rules/orchestration/orch-open-questions.md @@ -22,7 +22,7 @@ Expose uncertainty explicitly in specifications and block planning while blockin - Treat material draft, proposed, conflicting, stale, missing-evidence, candidate, background, or blocked knowledge as a blocking open question when it affects requirements, architecture, workflow, API, persistence, validation, execution behavior, or conflict with user purpose. - Record material opposite, candidate, background, blocked, draft, proposed, conflicting, stale, or missing-evidence inputs in Open Questions when they affect the specification scope, even when they are not blocking. - Decide blocking status from unresolved impact to requirements, architecture, workflow, policy, API, persistence, validation, execution behavior, review closure, or user-purpose safety; evidence class or polarity alone is not a blocker. -- For WorkBundle project specifications, record related active violation registry evidence as blocking Open Questions unless the user or accepted evidence resolves them for the current scope. +- For WorkBundle project specifications, record related active defect registry evidence as blocking Open Questions unless the user or accepted evidence resolves them for the current scope. - Treat non-material unsettled knowledge as out of `create-specification` resolution scope; it may remain source context or be omitted, but it must not block planning. - When no relevant uncertainty exists, state `None for this specification scope.` in the Open Questions section. - Inspect the source specification `Open Questions` section first before creating any implementation plan. @@ -47,7 +47,7 @@ Expose uncertainty explicitly in specifications and block planning while blockin - Confirm the specification includes an Open Questions section with ID, source, scope, blocking classification, required resolution, and advised options for each listed uncertainty, or an explicit none statement. - Confirm material non-authority or opposing evidence is visible when scope-affecting but does not shape requirements and is not marked blocking solely by evidence class or polarity. -- Confirm WorkBundle related active violations are carried as blocking Open Questions with review closure expectations unless resolved for the current scope. +- Confirm WorkBundle related active defects are carried as blocking Open Questions with review closure expectations unless resolved for the current scope. - Confirm planning either cites no unresolved questions or returns the required refusal table before any plan artifact is written. - Confirm no plan task depends on silently inferred answers to listed open questions. diff --git a/rules/orchestration/orch-review-completion.md b/rules/orchestration/orch-review-completion.md index 4034ef4..36186eb 100644 --- a/rules/orchestration/orch-review-completion.md +++ b/rules/orchestration/orch-review-completion.md @@ -42,7 +42,7 @@ Keep final review focused on whether the WorkBundle workflow completed correctly - Permit `new_feature` or `uncovered_fixture` with an empty `invariant_trace` only when `negative_evidence` proves no matching original user request or accepted specification invariant and no plan, handoff, or produced-commit contradiction. - Route `owning_repair` to the first broken artifact: task or acceptance criterion present plus implementation miss means task repair and re-review; accepted specification present plus plan omission means plan repair and resume from the owning step; original-request invariant omitted or contradicted by the accepted specification means specification repair. Only after those cases are excluded may a residual class stand. - Keep classification agent-owned and evidence-linked. A helper may require and structurally validate the record but must not decide the semantic class. -- Keep same-scope specification-owned handling authoritative for a first-observed classification defect. Persist separate WorkBundle violation evidence only after `wb-violation-evaluation` classifies the finding as work-bundle-scoped or mixed and same-scope specification-owned handling no longer applies. +- Keep same-scope specification-owned handling authoritative for a first-observed classification defect. Persist separate WorkBundle defect evidence only after `wb-defect-evaluation` classifies the finding as work-bundle-scoped or mixed and same-scope specification-owned handling no longer applies. ## Must Not diff --git a/rules/work-bundle/wb-violation-evaluation.md b/rules/work-bundle/wb-defect-evaluation.md similarity index 81% rename from rules/work-bundle/wb-violation-evaluation.md rename to rules/work-bundle/wb-defect-evaluation.md index dc2fa1f..09fcafe 100644 --- a/rules/work-bundle/wb-violation-evaluation.md +++ b/rules/work-bundle/wb-defect-evaluation.md @@ -1,19 +1,19 @@ --- -id: wb-violation-evaluation +id: wb-defect-evaluation applies_when: - a conflict, violation, error, failed validation, contradictory workflow behavior, user interruption, or user correction occurs during WorkBundle-guided work - visible task evidence suggests a WorkBundle skill, rule, script, specification, plan, handoff, workflow contract, or toolkit execution surface may have caused or contributed to the problem - - an agent must decide whether an observed problem is work-bundle-scoped, project-scoped, mixed, or undetermined before creating violation evidence or reporting a blocker + - an agent must decide whether an observed problem is work-bundle-scoped, project-scoped, mixed, or undetermined before creating defect evidence or reporting a blocker enforcement: must load: conditional requires: [] --- -# Violation Evaluation +# Defect Evaluation ## Purpose -Classify observed problems during WorkBundle-guided work early enough to preserve toolkit process evidence while keeping violation evidence storage narrow and first-observed. +Classify observed problems during WorkBundle-guided work early enough to preserve toolkit process evidence while keeping defect evidence storage narrow and first-observed. ## Must @@ -26,11 +26,11 @@ Classify observed problems during WorkBundle-guided work early enough to preserv - Use `project-scoped` when the problem is limited to project business logic, project implementation, project data, or project-specific requirements with no visible WorkBundle process cause. - Use `mixed` when both WorkBundle toolkit behavior and project-specific behavior materially contribute to the problem. - Use `undetermined` when available evidence is insufficient to choose another classification and the classification affects authority, target scope, validation, or continuation. -- When the current project is the WorkBundle toolkit itself and the observed WorkBundle-scoped or mixed problem is exactly the current specification-owned work item, route the finding as `same-scope specification-owned` instead of forcing separate violation evidence persistence. +- When the current project is the WorkBundle toolkit itself and the observed WorkBundle-scoped or mixed problem is exactly the current specification-owned work item, route the finding as `same-scope specification-owned` instead of forcing separate defect evidence persistence. - Use `same-scope specification-owned` handling only when an active specification or current specification update already names the same conflict, workflow behavior, rule, skill, contract, or process surface as in-scope work to resolve. -- For `same-scope specification-owned` handling, record the issue in the active specification source context, Open Questions, or review evidence as appropriate, and carry it to review settlement/closure instead of creating a new violation evidence file. -- Create or update minimal violation evidence for `work-bundle-scoped` and `mixed` findings through the violation evidence workflow. -- Report `project-scoped` findings as project blockers and do not write them to the WorkBundle violation store. +- For `same-scope specification-owned` handling, record the issue in the active specification source context, Open Questions, or review evidence as appropriate, and carry it to review settlement/closure instead of creating a new defect evidence file. +- Create or update minimal defect evidence for `work-bundle-scoped` and `mixed` findings through the defect evidence workflow. +- Report `project-scoped` findings as project blockers and do not write them to the WorkBundle defect store. - Block for resolution when an `undetermined` finding affects authority, target scope, validation, or continuation. - Keep the evaluation result compact: trigger, short symptom, classification, visible WorkBundle-related artifact when any, trace depth, evidence action, and `same_scope_specification_owned: true|false` when applicable. @@ -38,9 +38,9 @@ Classify observed problems during WorkBundle-guided work early enough to preserv - Do not treat the example chain `instruction -> plan -> plan skill -> source specification -> specification skill` as a mandatory fix pattern, mandatory chain-of-thought output, or exhaustive tracing path. - Do not require full root-cause tracing after WorkBundle toolkit relatedness is visible. -- Do not create violation evidence for purely project-scoped findings. -- Do not create a new violation evidence file when evaluation returns `same-scope specification-owned` handling for exact current WorkBundle specification work. -- Do not use `same-scope specification-owned` handling for unrelated WorkBundle violations, historical issues outside the active specification scope, or project-specific implementation defects. +- Do not create defect evidence for purely project-scoped findings. +- Do not create a new defect evidence file when evaluation returns `same-scope specification-owned` handling for exact current WorkBundle specification work. +- Do not use `same-scope specification-owned` handling for unrelated WorkBundle defects, historical issues outside the active specification scope, or project-specific implementation defects. - Do not expand evaluation into unrelated repository browsing, historical reconstruction, durable knowledge retrieval, or broad contract exploration. - Do not store raw chat logs, private reasoning, or executor-result forbidden advice fields as the evaluation or evidence surface. - Do not silently continue when an `undetermined` classification affects authority, target scope, validation, or continuation. @@ -50,11 +50,11 @@ Classify observed problems during WorkBundle-guided work early enough to preserv - Confirm the trigger was a visible conflict, violation, error, failed validation, contradictory workflow behavior, user interruption, or user correction during WorkBundle-guided work. - Confirm evaluation stopped once visible WorkBundle relatedness was established, or that the active workflow chain was used only as a bounded aid when relatedness was not already clear. - Confirm the result uses one of `work-bundle-scoped`, `project-scoped`, `mixed`, or `undetermined`. -- Confirm `work-bundle-scoped` and `mixed` findings create or update minimal violation evidence through the evidence workflow. -- Confirm exact current WorkBundle specification work that is routed as `same-scope specification-owned` is recorded in the active specification, Open Questions, or review evidence instead of a new violation evidence file. -- Confirm `project-scoped` findings are reported as project blockers and are not persisted in the WorkBundle violation store. +- Confirm `work-bundle-scoped` and `mixed` findings create or update minimal defect evidence through the evidence workflow. +- Confirm exact current WorkBundle specification work that is routed as `same-scope specification-owned` is recorded in the active specification, Open Questions, or review evidence instead of a new defect evidence file. +- Confirm `project-scoped` findings are reported as project blockers and are not persisted in the WorkBundle defect store. - Confirm `undetermined` findings that affect authority, target scope, validation, or continuation block for resolution. ## On Violation -Stop the unsafe continuation, repair the classification or action boundary, remove any broad root-cause tracing or forbidden evidence content, and resume only after the finding is classified and routed to violation evidence, same-scope specification-owned handling, project blocker, or resolution block as required. +Stop the unsafe continuation, repair the classification or action boundary, remove any broad root-cause tracing or forbidden evidence content, and resume only after the finding is classified and routed to defect evidence, same-scope specification-owned handling, project blocker, or resolution block as required. diff --git a/rules/work-bundle/wb-defect-evidence.md b/rules/work-bundle/wb-defect-evidence.md new file mode 100644 index 0000000..56ef1d6 --- /dev/null +++ b/rules/work-bundle/wb-defect-evidence.md @@ -0,0 +1,64 @@ +--- +id: wb-defect-evidence +applies_when: + - the Work Bundle rule is visible in AGENTS.md and any conflict, confliction, violation, contradiction, or user correction occurs + - the Work Bundle rule is visible in AGENTS.md and the session needs a visible entry point for defect evaluation and first-observed evidence routing +enforcement: must +load: always +requires: [] +--- + +# Defect Evidence + +## Purpose + +Keep the WorkBundle defect workflow visible whenever the Work Bundle rule is visible in `AGENTS.md`, and preserve first-observed work-bundle process, rule, skill, workflow, script, orchestration, or handoff contract deviations only after evaluation confirms the finding is `work-bundle-scoped` or `mixed`. + +## Must + +- Treat any conflict, confliction, violation, contradiction, or user correction as a `wb-defect-evidence` trigger when the Work Bundle rule is visible in `AGENTS.md`. +- When a trigger appears, immediately call `wb-defect-evaluation` to classify whether the finding is `work-bundle-scoped`, `project-scoped`, `mixed`, or `undetermined`. +- Exit the defect evidence workflow without recording evidence when `wb-defect-evaluation` confirms the finding is not WorkBundle-related and does not affect authority, target scope, validation, or continuation. +- Exit the defect evidence workflow without recording a new evidence file when `wb-defect-evaluation` returns `same-scope specification-owned` handling for exact current WorkBundle specification work. +- When `same-scope specification-owned` handling applies, rely on the active specification source context, Open Questions, or review evidence to carry the issue and its settlement path. +- Record a defect evidence file only when `wb-defect-evaluation` classifies the first-observed finding as `work-bundle-scoped` or `mixed`. +- Record only the minimal first-observed evidence needed to preserve the WorkBundle-scoped or mixed deviation after evaluation. +- Keep evidence narrow to the deviation, occurrence condition, visible first evidence artifacts, current status, action taken when any, and severity. +- Record only files, artifacts, UI output, terminal output, or runtime surfaces already visible in the active task context at the moment the work-bundle defect candidate is observed. +- Use `$work_bundle_config_root/defect/active/` for unresolved evidence and `$work_bundle_config_root/defect/archived/` for dismissed or completed evidence. +- Use evidence filenames shaped as `evidence--.yaml`. +- Use `defect-migrate-store` explicitly when the legacy store remains; the other defect commands must fail before creating or reading the destination until migration completes. +- Use the `defect-ensure-store`, `defect-create-evidence`, `defect-build-index`, `defect-write-index`, and `defect-archive-evidence` script entry points when writing, indexing, or moving evidence files is required. +- Keep the evaluation compact and stop once visible WorkBundle relatedness is established. +- Treat project-scoped findings from `wb-defect-evaluation` as blockers reported to the user rather than work-bundle defect records. +- Treat undetermined findings that affect authority, target scope, validation, or continuation as resolution blockers until evaluation can classify them. + +## Must Not + +- Do not record project business logic, project implementation, project spec or plan execution, or durable project-knowledge semantic deviations in the work-bundle defect store. +- Do not record a separate defect evidence file for exact current WorkBundle specification-owned work when evaluation explicitly says evidence persistence is not required. +- Do not wait for a user to explicitly request defect recording before considering the defect workflow. +- Do not expand evidence capture into evaluation, root-cause investigation, or exhaustive workflow-chain tracing. +- Do not perform additional file search, repository browsing, historical tracing, or contract exploration solely to find more evidence when the already-visible artifact is sufficient to record the defect. +- Do not delay or widen plan execution to enrich a defect record. +- Do not store defect evidence under project roots or `.work-bundle/knowledge/`. +- Do not make a non-migration defect command migrate, merge, or initialize beside legacy authority. +- Do not use executor-result forbidden advice fields as the defect recording surface. +- Do not silently continue from a work-bundle defect that makes the active execution unsafe, unauthoritative, or impossible to verify. + +## Validation + +- Confirm `wb-defect-evidence` is loaded as the always-visible entry point for conflict, confliction, violation, contradiction, and user-correction signals when the Work Bundle rule is visible in `AGENTS.md`. +- Confirm possible defect signals are routed first from `wb-defect-evidence` to `wb-defect-evaluation`. +- Confirm `same-scope specification-owned` evaluation results do not create new defect evidence and are carried by the active specification or review evidence instead. +- Confirm every recorded defect was classified by `wb-defect-evaluation` as `work-bundle-scoped` or `mixed` and cites only visible first evidence artifacts or runtime surfaces. +- Confirm the agent did not perform further exploration solely to enrich the defect evidence record. +- Confirm project-scope deviations are reported as blockers and are not written to the defect store. +- Confirm undetermined findings that affect authority, target scope, validation, or continuation are blocked for resolution rather than written as evidence. +- Confirm evidence path, status, action, and severity satisfy the defect evidence contract. +- Confirm legacy state routes to explicit `defect-migrate-store` and every other defect command fails before destination initialization. +- Confirm the defect index is rebuilt or explicitly left for `defect-write-index` after evidence changes. + +## On Violation + +Stop unsafe continuation, call `wb-defect-evaluation`, and either record minimal evidence for `work-bundle-scoped` or `mixed` findings, exit without persistence for `same-scope specification-owned` handling, report or exit for non-WorkBundle findings, or block when `undetermined` affects authority, target scope, validation, or continuation. diff --git a/rules/work-bundle/wb-violation-evidence.md b/rules/work-bundle/wb-violation-evidence.md deleted file mode 100644 index fc4846c..0000000 --- a/rules/work-bundle/wb-violation-evidence.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -id: wb-violation-evidence -applies_when: - - the Work Bundle rule is visible in AGENTS.md and any conflict, confliction, violation, contradiction, or user correction occurs - - the Work Bundle rule is visible in AGENTS.md and the session needs a visible entry point for violation evaluation and first-observed evidence routing -enforcement: must -load: always -requires: [] ---- - -# Violation Evidence - -## Purpose - -Keep the WorkBundle violation workflow visible whenever the Work Bundle rule is visible in `AGENTS.md`, and preserve first-observed work-bundle process, rule, skill, workflow, script, orchestration, or handoff contract deviations only after evaluation confirms the finding is `work-bundle-scoped` or `mixed`. - -## Must - -- Treat any conflict, confliction, violation, contradiction, or user correction as a `wb-violation-evidence` trigger when the Work Bundle rule is visible in `AGENTS.md`. -- When a trigger appears, immediately call `wb-violation-evaluation` to classify whether the finding is `work-bundle-scoped`, `project-scoped`, `mixed`, or `undetermined`. -- Exit the violation evidence workflow without recording evidence when `wb-violation-evaluation` confirms the finding is not WorkBundle-related and does not affect authority, target scope, validation, or continuation. -- Exit the violation evidence workflow without recording a new evidence file when `wb-violation-evaluation` returns `same-scope specification-owned` handling for exact current WorkBundle specification work. -- When `same-scope specification-owned` handling applies, rely on the active specification source context, Open Questions, or review evidence to carry the issue and its settlement path. -- Record a violation evidence file only when `wb-violation-evaluation` classifies the first-observed finding as `work-bundle-scoped` or `mixed`. -- Record only the minimal first-observed evidence needed to preserve the WorkBundle-scoped or mixed deviation after evaluation. -- Keep evidence narrow to the deviation, occurrence condition, visible first evidence artifacts, current status, action taken when any, and severity. -- Record only files, artifacts, UI output, terminal output, or runtime surfaces already visible in the active task context at the moment the work-bundle violation is observed. -- Use `$work_bundle_config_root/violation/active/` for unresolved evidence and `$work_bundle_config_root/violation/archived/` for dismissed or completed evidence. -- Use evidence filenames shaped as `evidence--.yaml`. -- Use the `violation-ensure-store`, `violation-create-evidence`, `violation-build-index`, `violation-write-index`, and `violation-archive-evidence` script entry points when writing, indexing, or moving evidence files is required. -- Keep the evaluation compact and stop once visible WorkBundle relatedness is established. -- Treat project-scoped findings from `wb-violation-evaluation` as blockers reported to the user rather than work-bundle violation records. -- Treat undetermined findings that affect authority, target scope, validation, or continuation as resolution blockers until evaluation can classify them. - -## Must Not - -- Do not record project business logic, project implementation, project spec or plan execution, or durable project-knowledge semantic deviations in the work-bundle violation store. -- Do not record a separate violation evidence file for exact current WorkBundle specification-owned work when evaluation explicitly says evidence persistence is not required. -- Do not wait for a user to explicitly request violation recording before considering the violation workflow. -- Do not expand evidence capture into evaluation, root-cause investigation, or exhaustive workflow-chain tracing. -- Do not perform additional file search, repository browsing, historical tracing, or contract exploration solely to find more evidence when the already-visible artifact is sufficient to record the violation. -- Do not delay or widen plan execution to enrich a violation record. -- Do not store violation evidence under project roots or `.work-bundle/knowledge/`. -- Do not use executor-result forbidden advice fields as the violation recording surface. -- Do not silently continue from a work-bundle violation that makes the active execution unsafe, unauthoritative, or impossible to verify. - -## Validation - -- Confirm `wb-violation-evidence` is loaded as the always-visible entry point for conflict, confliction, violation, contradiction, and user-correction signals when the Work Bundle rule is visible in `AGENTS.md`. -- Confirm possible violation signals are routed first from `wb-violation-evidence` to `wb-violation-evaluation`. -- Confirm `same-scope specification-owned` evaluation results do not create new violation evidence and are carried by the active specification or review evidence instead. -- Confirm every recorded violation was classified by `wb-violation-evaluation` as `work-bundle-scoped` or `mixed` and cites only visible first evidence artifacts or runtime surfaces. -- Confirm the agent did not perform further exploration solely to enrich the violation evidence record. -- Confirm project-scope deviations are reported as blockers and are not written to the violation store. -- Confirm undetermined findings that affect authority, target scope, validation, or continuation are blocked for resolution rather than written as evidence. -- Confirm evidence path, status, action, and severity satisfy the violation evidence contract. -- Confirm the violation index is rebuilt or explicitly left for `violation-write-index` after evidence changes. - -## On Violation - -Stop unsafe continuation, call `wb-violation-evaluation`, and either record minimal evidence for `work-bundle-scoped` or `mixed` findings, exit without persistence for `same-scope specification-owned` handling, report or exit for non-WorkBundle findings, or block when `undetermined` affects authority, target scope, validation, or continuation. diff --git a/scripts/work-bundle/README.md b/scripts/work-bundle/README.md index 537a9dc..9d0604b 100644 --- a/scripts/work-bundle/README.md +++ b/scripts/work-bundle/README.md @@ -33,11 +33,12 @@ python3 scripts/wb.py inspect-skill skills/wb-credential-use/SKILL.md python3 scripts/wb.py validate-registry-entry python3 scripts/wb.py create-rules rules python3 scripts/wb.py validate-rules rules -python3 scripts/wb.py violation-ensure-store -python3 scripts/wb.py violation-create-evidence --status active --short-description --deviation --occurrence --evidence --severity p5 -python3 scripts/wb.py violation-build-index -python3 scripts/wb.py violation-write-index -python3 scripts/wb.py violation-archive-evidence --action completed +python3 scripts/wb.py defect-ensure-store +python3 scripts/wb.py defect-migrate-store +python3 scripts/wb.py defect-create-evidence --status active --short-description --deviation --occurrence --evidence --severity p5 +python3 scripts/wb.py defect-build-index +python3 scripts/wb.py defect-write-index +python3 scripts/wb.py defect-archive-evidence --action completed ``` Prefer `--scope` for `create-rules` and `validate-rules`: `toolkit` resolves to `$work_bundle_root/rules/`, `global` resolves to `$work_bundle_config_root/rules/`, and `project` resolves to `/.work-bundle/rules/`. The project-root form remains a single-repository compatibility alias. diff --git a/scripts/work-bundle/violations.py b/scripts/work-bundle/defects.py similarity index 56% rename from scripts/work-bundle/violations.py rename to scripts/work-bundle/defects.py index 4da0fdc..3a04de4 100644 --- a/scripts/work-bundle/violations.py +++ b/scripts/work-bundle/defects.py @@ -1,7 +1,10 @@ from __future__ import annotations import argparse +import hashlib +import json import re +import shutil import sys from datetime import datetime from pathlib import Path @@ -9,10 +12,14 @@ from core import out, read, work_bundle_config_root, write -REFERENCE_PATH = Path(__file__).resolve().parents[2] / 'references' / 'wb-violation-evidence.yaml' +REFERENCE_PATH = Path(__file__).resolve().parents[2] / 'references' / 'wb-defect-evidence.yaml' +STAGING_DIRECTORY = '.defect-migration-staging' +STAGING_OWNER_FILE = '.staging-owner' +STAGING_OWNER_CONTENT = 'work-bundle:defect-migrate-store:v1\n' +MIGRATION_MARKER_FILE = '.migration-marker.json' -class ViolationError(Exception): +class DefectError(Exception): pass @@ -54,7 +61,7 @@ def _plain_yaml_map(text: str, key: str) -> dict[str, str]: def _catalog() -> dict[str, object]: text = read(REFERENCE_PATH) if not text: - raise ViolationError(f'missing reference catalog: {REFERENCE_PATH}') + raise DefectError(f'missing reference catalog: {REFERENCE_PATH}') catalog = { 'statuses': _plain_yaml_list(text, 'statuses'), 'actions': _plain_yaml_list(text, 'actions'), @@ -62,16 +69,38 @@ def _catalog() -> dict[str, object]: 'filename': _plain_yaml_map(text, 'filename'), } if not all(catalog.values()): - raise ViolationError(f'invalid reference catalog: {REFERENCE_PATH}') + raise DefectError(f'invalid reference catalog: {REFERENCE_PATH}') return catalog def _store_root() -> Path: + return work_bundle_config_root() / 'defect' + + +def _legacy_store_root() -> Path: return work_bundle_config_root() / 'violation' -def _ensure_store() -> dict[str, object]: - root = _store_root() +def _staging_root() -> Path: + return work_bundle_config_root() / STAGING_DIRECTORY + + +def _migration_marker(root: Path | None = None) -> Path: + return (root or _store_root()) / MIGRATION_MARKER_FILE + + +def _guard_ready_store() -> None: + legacy = _legacy_store_root() + destination = _store_root() + staging = _staging_root() + if legacy.exists(): + detail = 'legacy and defect stores conflict' if destination.exists() else 'legacy violation store requires migration' + raise DefectError(f'{detail}; run defect-migrate-store') + if staging.exists() or _migration_marker().exists(): + raise DefectError('incomplete defect store migration; run defect-migrate-store') + + +def _ensure_store_at(root: Path) -> dict[str, object]: active = root / 'active' archived = root / 'archived' active.mkdir(parents=True, exist_ok=True) @@ -84,6 +113,11 @@ def _ensure_store() -> dict[str, object]: } +def _ensure_store() -> dict[str, object]: + _guard_ready_store() + return _ensure_store_at(_store_root()) + + def _quote(value: str) -> str: escaped = value.replace('\\', '\\\\').replace('"', '\\"') return f'"{escaped}"' @@ -103,12 +137,12 @@ def _validate_slug(slug: str, catalog: dict[str, object]) -> None: assert isinstance(filename, dict) pattern = filename.get('slug_pattern', '') if not re.fullmatch(pattern, slug): - raise ViolationError(f'invalid short-description slug: {slug}') + raise DefectError(f'invalid short-description slug: {slug}') def _validate_date(date_value: str) -> None: if not re.fullmatch(r'\d{8}', date_value): - raise ViolationError(f'invalid evidence date: {date_value}') + raise DefectError(f'invalid evidence date: {date_value}') def _validate_evidence_id(evidence_id: str, catalog: dict[str, object]) -> None: @@ -117,7 +151,7 @@ def _validate_evidence_id(evidence_id: str, catalog: dict[str, object]) -> None: prefix = filename.get('prefix', 'evidence') parts = evidence_id.split('-', 2) if len(parts) != 3 or parts[0] != prefix: - raise ViolationError(f'invalid evidence filename: {evidence_id}') + raise DefectError(f'invalid evidence filename: {evidence_id}') _validate_date(parts[1]) _validate_slug(parts[2], catalog) @@ -128,20 +162,20 @@ def _validate_status_action(status: str, action: str | None, catalog: dict[str, assert isinstance(statuses, list) assert isinstance(actions, list) if status not in statuses: - raise ViolationError(f'invalid status: {status}') + raise DefectError(f'invalid status: {status}') if action is not None and action not in actions: - raise ViolationError(f'invalid action: {action}') + raise DefectError(f'invalid action: {action}') if status == 'archived' and action is None: - raise ViolationError('archived evidence requires --action') + raise DefectError('archived evidence requires --action') if status == 'active' and action is not None: - raise ViolationError('active evidence must not include final --action') + raise DefectError('active evidence must not include final --action') def _validate_severity(severity: str, catalog: dict[str, object]) -> None: severities = catalog['severities'] assert isinstance(severities, list) if severity not in severities: - raise ViolationError(f'invalid severity: {severity}') + raise DefectError(f'invalid severity: {severity}') def _evidence_id(slug: str, date_value: str, catalog: dict[str, object]) -> str: @@ -151,8 +185,8 @@ def _evidence_id(slug: str, date_value: str, catalog: dict[str, object]) -> str: return f'{prefix}-{date_value}-{slug}' -def _evidence_path(status: str, evidence_id: str) -> Path: - return _store_root() / status / f'{evidence_id}.yaml' +def _evidence_path(status: str, evidence_id: str, root: Path | None = None) -> Path: + return (root or _store_root()) / status / f'{evidence_id}.yaml' def _evidence_item(value: str) -> dict[str, str]: @@ -211,7 +245,7 @@ def _parse_evidence(text: str, path: Path) -> dict[str, object]: continue in_evidence = False if ':' not in raw: - raise ViolationError(f'invalid yaml line in {path}: {raw}') + raise DefectError(f'invalid yaml line in {path}: {raw}') key, value = raw.split(':', 1) data[key.strip()] = _unquote(value) return data @@ -228,11 +262,11 @@ def _validate_record(record: dict[str, object], path: Path, status_dir: str, cat severity = str(record.get('severity', '')) try: _validate_status_action(status, str(action) if action is not None else None, catalog) - except ViolationError as exc: + except DefectError as exc: errors.append(f'{path}: {exc}') try: _validate_severity(severity, catalog) - except ViolationError as exc: + except DefectError as exc: errors.append(f'{path}: {exc}') if status and status != status_dir: errors.append(f'{path}: status {status} does not match directory {status_dir}') @@ -242,19 +276,31 @@ def _validate_record(record: dict[str, object], path: Path, status_dir: str, cat return errors -def _load_records(catalog: dict[str, object]) -> tuple[dict[str, dict[str, object]], list[str]]: - _ensure_store() +def _load_records( + catalog: dict[str, object], + root: Path | None = None, + *, + create: bool = True, +) -> tuple[dict[str, dict[str, object]], list[str]]: + target = root or _store_root() + if create: + if root is None: + _ensure_store() + else: + _ensure_store_at(target) + elif not target.is_dir() or not (target / 'active').is_dir() or not (target / 'archived').is_dir(): + return {'active': {}, 'archived': {}}, [f'{target}: invalid evidence store layout'] records: dict[str, dict[str, object]] = {'active': {}, 'archived': {}} errors: list[str] = [] for status in ['active', 'archived']: - for path in sorted((_store_root() / status).glob('*.yaml')): + for path in sorted((target / status).glob('*.yaml')): try: _validate_evidence_id(path.stem, catalog) - except ViolationError as exc: + except DefectError as exc: errors.append(f'{path}: {exc}') try: record = _parse_evidence(read(path), path) - except ViolationError as exc: + except DefectError as exc: errors.append(str(exc)) continue errors.extend(_validate_record(record, path, status, catalog)) @@ -262,11 +308,11 @@ def _load_records(catalog: dict[str, object]) -> tuple[dict[str, dict[str, objec return records, errors -def _build_index_data() -> dict[str, dict[str, dict[str, str]]]: +def _build_index_data(root: Path | None = None, *, create: bool = True) -> dict[str, dict[str, dict[str, str]]]: catalog = _catalog() - records, errors = _load_records(catalog) + records, errors = _load_records(catalog, root, create=create) if errors: - raise ViolationError('; '.join(errors)) + raise DefectError('; '.join(errors)) index: dict[str, dict[str, dict[str, str]]] = {'active': {}, 'archived': {}} for evidence_id, record in records['active'].items(): index['active'][evidence_id] = { @@ -301,31 +347,182 @@ def _render_index(index: dict[str, dict[str, dict[str, str]]]) -> str: return '\n'.join(lines) +def _validate_store(root: Path) -> None: + _build_index_data(root, create=False) + + +def _record_fingerprint(root: Path) -> str: + digest = hashlib.sha256() + for status in ('active', 'archived'): + directory = root / status + if not directory.is_dir(): + raise DefectError(f'{root}: invalid evidence store layout') + for path in sorted(directory.glob('*.yaml')): + digest.update(f'{status}/{path.name}'.encode('utf-8')) + digest.update(b'\0') + digest.update(path.read_bytes()) + digest.update(b'\n') + return digest.hexdigest() + + +def _marker_content(source_fingerprint: str, destination_fingerprint: str) -> str: + return json.dumps( + { + 'schema_version': 1, + 'source_fingerprint': source_fingerprint, + 'destination_fingerprint': destination_fingerprint, + }, + sort_keys=True, + indent=2, + ) + '\n' + + +def _read_marker(root: Path) -> dict[str, object]: + marker = _migration_marker(root) + if not marker.is_file(): + raise DefectError(f'defect store migration conflict: missing {MIGRATION_MARKER_FILE}') + try: + payload = json.loads(marker.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError) as exc: + raise DefectError(f'invalid defect migration marker: {marker}') from exc + source_fingerprint = payload.get('source_fingerprint') if isinstance(payload, dict) else None + destination_fingerprint = payload.get('destination_fingerprint') if isinstance(payload, dict) else None + if ( + not isinstance(payload, dict) + or payload.get('schema_version') != 1 + or not isinstance(source_fingerprint, str) + or not isinstance(destination_fingerprint, str) + or not re.fullmatch(r'[0-9a-f]{64}', source_fingerprint) + or not re.fullmatch(r'[0-9a-f]{64}', destination_fingerprint) + or source_fingerprint != destination_fingerprint + ): + raise DefectError(f'invalid defect migration marker: {marker}') + return payload + + +def _staging_is_owned(staging: Path) -> bool: + owner = staging / STAGING_OWNER_FILE + return owner.is_file() and owner.read_text(encoding='utf-8') == STAGING_OWNER_CONTENT + + +def _prepare_staging(legacy: Path, staging: Path) -> tuple[str, str]: + if staging.exists(): + if not _staging_is_owned(staging): + raise DefectError(f'unowned defect migration staging path: {staging}') + shutil.rmtree(staging) + staging.mkdir(parents=True) + (staging / STAGING_OWNER_FILE).write_text(STAGING_OWNER_CONTENT, encoding='utf-8') + for status in ('active', 'archived'): + shutil.copytree(legacy / status, staging / status) + source_fingerprint = _record_fingerprint(legacy) + destination_fingerprint = _record_fingerprint(staging) + if source_fingerprint != destination_fingerprint: + raise DefectError('defect migration staging fingerprint mismatch') + index = _build_index_data(staging, create=False) + (staging / 'index.yaml').write_text(_render_index(index), encoding='utf-8') + _validate_store(staging) + _migration_marker(staging).write_text( + _marker_content(source_fingerprint, destination_fingerprint), + encoding='utf-8', + ) + return source_fingerprint, destination_fingerprint + + +def _finalize_published_migration(legacy: Path, destination: Path) -> str: + payload = _read_marker(destination) + _validate_store(destination) + destination_fingerprint = _record_fingerprint(destination) + if destination_fingerprint != payload['destination_fingerprint']: + raise DefectError('defect migration marker does not match destination records') + if legacy.exists(): + _validate_store(legacy) + source_fingerprint = _record_fingerprint(legacy) + if source_fingerprint != payload['source_fingerprint'] or source_fingerprint != destination_fingerprint: + raise DefectError('defect migration marker does not match legacy records') + shutil.rmtree(legacy) + _migration_marker(destination).unlink() + owner = destination / STAGING_OWNER_FILE + if owner.exists(): + owner.unlink() + return destination_fingerprint + + def _write_or_collide(path: Path, content: str) -> bool: existing = read(path) if existing: if existing == content: return False - raise ViolationError(f'evidence file collision: {path}') + raise DefectError(f'evidence file collision: {path}') return write(path, content) def _run(handler) -> int: try: return handler() - except ViolationError as exc: + except DefectError as exc: out({'status': 'error', 'error': str(exc)}) return 1 -def cmd_violation_ensure_store(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog='wb.py violation-ensure-store') +def cmd_defect_migrate_store(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-migrate-store') + parser.parse_args(argv) + + def handler() -> int: + legacy = _legacy_store_root() + destination = _store_root() + staging = _staging_root() + marker = _migration_marker(destination) + + if destination.exists(): + if staging.exists(): + raise DefectError('defect store migration conflict: destination and staging both exist') + if marker.exists(): + fingerprint = _finalize_published_migration(legacy, destination) + out({ + 'status': 'ok', + 'migration_status': 'migrated', + 'root': str(destination), + 'fingerprint': fingerprint, + 'resumed': True, + }) + return 0 + if legacy.exists(): + raise DefectError('defect store migration conflict: legacy and destination stores both exist') + _validate_store(destination) + out({'status': 'ok', 'migration_status': 'already-migrated', 'root': str(destination), 'changed': False}) + return 0 + + if legacy.exists(): + _validate_store(legacy) + _prepare_staging(legacy, staging) + staging.rename(destination) + fingerprint = _finalize_published_migration(legacy, destination) + out({ + 'status': 'ok', + 'migration_status': 'migrated', + 'root': str(destination), + 'fingerprint': fingerprint, + 'resumed': False, + }) + return 0 + + if staging.exists(): + raise DefectError(f'defect store migration is incomplete without legacy authority: {staging}') + out({'status': 'ok', 'migration_status': 'no-store', 'root': str(destination), 'changed': False}) + return 0 + + return _run(handler) + + +def cmd_defect_ensure_store(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-ensure-store') parser.parse_args(argv) return _run(lambda: (out(_ensure_store()) or 0)) -def cmd_violation_create_evidence(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog='wb.py violation-create-evidence') +def cmd_defect_create_evidence(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-create-evidence') parser.add_argument('--status', required=True) parser.add_argument('--short-description', required=True) parser.add_argument('--deviation', required=True) @@ -360,19 +557,19 @@ def handler() -> int: return _run(handler) -def cmd_violation_build_index(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog='wb.py violation-build-index') +def cmd_defect_build_index(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-build-index') parser.parse_args(argv) try: sys.stdout.write(_render_index(_build_index_data())) return 0 - except ViolationError as exc: + except DefectError as exc: out({'status': 'error', 'error': str(exc)}) return 1 -def cmd_violation_write_index(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog='wb.py violation-write-index') +def cmd_defect_write_index(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-write-index') parser.parse_args(argv) def handler() -> int: @@ -386,8 +583,8 @@ def handler() -> int: return _run(handler) -def cmd_violation_archive_evidence(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog='wb.py violation-archive-evidence') +def cmd_defect_archive_evidence(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog='wb.py defect-archive-evidence') parser.add_argument('evidence') parser.add_argument('--action', required=True) parsed = parser.parse_args(argv) @@ -409,18 +606,18 @@ def handler() -> int: if read(archived_path) == content: out({'status': 'ok', 'source_path': None, 'archived_path': str(archived_path), 'action': parsed.action, 'changed': False}) return 0 - raise ViolationError(f'archived evidence collision: {archived_path}') + raise DefectError(f'archived evidence collision: {archived_path}') if not active_path.exists(): - raise ViolationError(f'active evidence not found: {parsed.evidence}') + raise DefectError(f'active evidence not found: {parsed.evidence}') record = _parse_evidence(read(active_path), active_path) errors = _validate_record(record, active_path, 'active', catalog) if errors: - raise ViolationError('; '.join(errors)) + raise DefectError('; '.join(errors)) record['status'] = 'archived' record['action'] = parsed.action content = _render_evidence(record) if archived_path.exists() and read(archived_path) != content: - raise ViolationError(f'archived evidence collision: {archived_path}') + raise DefectError(f'archived evidence collision: {archived_path}') changed = write(archived_path, content) active_path.unlink() out({'status': 'ok', 'source_path': str(active_path), 'archived_path': str(archived_path), 'action': parsed.action, 'changed': changed}) diff --git a/scripts/work-bundle/dispatcher.py b/scripts/work-bundle/dispatcher.py index 135d9fb..4caa073 100644 --- a/scripts/work-bundle/dispatcher.py +++ b/scripts/work-bundle/dispatcher.py @@ -12,12 +12,13 @@ from project import cmd_cleanup_member, cmd_doctor_project, cmd_init_project, cmd_migrate_project, cmd_migrate_to_multi_repository, cmd_project, cmd_provision_member, cmd_register_project_command, cmd_session_start, cmd_set_prefer_subagent, cmd_show_project, cmd_validate_project from rules import cmd_create_rules, cmd_validate_rules from skill_registry import cmd_merge_skill_hints, cmd_registry -from violations import ( - cmd_violation_archive_evidence, - cmd_violation_build_index, - cmd_violation_create_evidence, - cmd_violation_ensure_store, - cmd_violation_write_index, +from defects import ( + cmd_defect_archive_evidence, + cmd_defect_build_index, + cmd_defect_create_evidence, + cmd_defect_ensure_store, + cmd_defect_migrate_store, + cmd_defect_write_index, ) from credential import CredentialError, list_metadata from execution_workspace import cmd_execution_workspace @@ -44,6 +45,15 @@ def main() -> int: parser.add_argument('args', nargs=argparse.REMAINDER) parsed = parser.parse_args() command = parsed.command + legacy_defect_commands = { + 'violation-ensure-store': 'defect-ensure-store', + 'violation-create-evidence': 'defect-create-evidence', + 'violation-build-index': 'defect-build-index', + 'violation-write-index': 'defect-write-index', + 'violation-archive-evidence': 'defect-archive-evidence', + } + if command in legacy_defect_commands: + return cmd_legacy_command_removed(command, legacy_defect_commands[command]) if command in LEGACY_COMMAND_MIGRATIONS: return cmd_legacy_command_removed(command, LEGACY_COMMAND_MIGRATIONS[command]) aliases = { @@ -119,16 +129,18 @@ def main() -> int: return cmd_create_rules(parsed.args) if command == 'validate-rules': return cmd_validate_rules(parsed.args) - if command == 'violation-ensure-store': - return cmd_violation_ensure_store(parsed.args) - if command == 'violation-create-evidence': - return cmd_violation_create_evidence(parsed.args) - if command == 'violation-build-index': - return cmd_violation_build_index(parsed.args) - if command == 'violation-write-index': - return cmd_violation_write_index(parsed.args) - if command == 'violation-archive-evidence': - return cmd_violation_archive_evidence(parsed.args) + if command == 'defect-ensure-store': + return cmd_defect_ensure_store(parsed.args) + if command == 'defect-create-evidence': + return cmd_defect_create_evidence(parsed.args) + if command == 'defect-build-index': + return cmd_defect_build_index(parsed.args) + if command == 'defect-write-index': + return cmd_defect_write_index(parsed.args) + if command == 'defect-archive-evidence': + return cmd_defect_archive_evidence(parsed.args) + if command == 'defect-migrate-store': + return cmd_defect_migrate_store(parsed.args) if command in {'doctor', 'repository-health', 'validate-directive-wiring', 'validate-skill-registry', 'validate-work-bundle-rules'}: return cmd_doctor(parsed.args) if command == 'render-doctor-report': diff --git a/skills/orch-create-specification/SKILL.md b/skills/orch-create-specification/SKILL.md index 1857301..af33606 100644 --- a/skills/orch-create-specification/SKILL.md +++ b/skills/orch-create-specification/SKILL.md @@ -16,7 +16,7 @@ Create the smallest authoritative specification under `.work-bundle/orchestratio 3. Through `ks-what-is-helpful`, discover with polarity-neutral and stage/perspective/status-neutral query anchors. `implementation_spec` is classification and output-grouping intent, not a discovery-stage lifecycle filter. 4. Classify results as authority, candidate, background, or blocked. Only authority shapes requirements and only accepted authority enters front-matter `source_knowledge` as `path` plus the already-reconciled `constraint`; keep candidate, background, blocked, superseded, supporting, opposing, constraining, unresolved/open-question, obsolete, and irrelevant-with-reason evidence in Source Context when material. Downstream planning allocates `AUTH-NNN` aliases from the accepted list so execution does not require `.work-bundle/knowledge/` reads or expose knowledge paths. 5. Build one bounded current-state impact basis from the requested surface, material upstream dependencies or producers, downstream consumers, validation/test surfaces, and relevant dirty work. Treat a relation as material only when its disposition could change a requirement, constraint, acceptance criterion, user-observable or contractual outcome, architectural boundary, measurable quality target, validation target, or declared boundary. Record `impact_decisions` and give each material relation exactly one disposition: `accepted | excluded | blocking`. An accepted relation must use `projects_to` to name stable requirement, constraint, interface, acceptance-criterion, or validation-target IDs. An excluded relation requires evidence and a reason stronger than user omission. A blocking relation creates a blocking open question. When no material relation exists, record `none_relevant` with the searched boundary, reason, and `stopping_reason`. -6. Stop when further exploration could change none of those surfaces and record the reason. Escalate to targeted Git history, prior work artifacts, execution evidence, or durable knowledge only when current-state evidence is contradictory, ownership is unresolved, regression or causality is material, or a governing legacy decision is suspected. Do not require full-history archaeology or broad knowledge retrieval by default. For WorkBundle scope, include related active violations by ID, severity, deviation, scope, required resolution, and expected review closure. Exact-current-work conflicts may remain specification-owned. +6. Stop when further exploration could change none of those surfaces and record the reason. Escalate to targeted Git history, prior work artifacts, execution evidence, or durable knowledge only when current-state evidence is contradictory, ownership is unresolved, regression or causality is material, or a governing legacy decision is suspected. Do not require full-history archaeology or broad knowledge retrieval by default. For WorkBundle scope, include related active defects by ID, severity, deviation, scope, required resolution, and expected review closure. Exact-current-work conflicts may remain specification-owned. 7. Within Design Interrogation, run one compact, evidence-routed product-excellence applicability pass. Record exactly one `excellence_applicability` result: `no_material_opportunity` with a non-empty reason, or `material_opportunities` with one or more proposals. Select dimensions from the task evidence and change shape rather than a universal checklist. Surface an option only when accepting or rejecting it could change a requirement, constraint, acceptance criterion, user-observable or contractual outcome, architectural boundary, measurable quality target, validation target, or declared boundary. Each proposal records user value, evidence, cost, risk, recommendation, and one disposition: `accepted | rejected | deferred | not_material`; unanswered proposals become deferred. Only accepted proposals may project through stable `projects_to` IDs into authoritative requirements, constraints, interfaces, acceptance criteria, or validation targets. Keep all other proposals traceable but excluded from planning, executor briefs, and acceptance obligations. Stop after one compact pass when further exploration could change none of those surfaces, record the reason, and ensure every surfaced proposal has a disposition. Optional proposals do not block unless accepted without complete projection or they expose an unresolved safety or authority conflict governed by existing open-question rules. 8. Ask Design Interrogation questions only for unresolved intent that changes requirements, architecture, workflow, API, persistence, validation, execution safety, or user purpose. Evidence class alone does not make an open question blocking. 9. Normalize stable IDs, requirements, constraints, interfaces, acceptance criteria, decisions, open questions, and Knowledge Base Update disposition. diff --git a/skills/orch-execute-plan/SKILL.md b/skills/orch-execute-plan/SKILL.md index e38565b..2362f54 100644 --- a/skills/orch-execute-plan/SKILL.md +++ b/skills/orch-execute-plan/SKILL.md @@ -42,7 +42,7 @@ python3 scripts/orch.py validate-executor-result --task --handoff None: sys.path.pop(0) -def test_violation_evaluation_rule_has_relaxed_trigger_and_routing_contract() -> None: - rule = (REPO_ROOT / "rules/work-bundle/wb-violation-evaluation.md").read_text(encoding="utf-8") +def test_defect_evaluation_rule_has_relaxed_trigger_and_routing_contract() -> None: + rule = (REPO_ROOT / "rules/work-bundle/wb-defect-evaluation.md").read_text(encoding="utf-8") index = (REPO_ROOT / "rules/index.yaml").read_text(encoding="utf-8") relaxed_trigger = ( @@ -479,13 +479,13 @@ def test_violation_evaluation_rule_has_relaxed_trigger_and_routing_contract() -> assert relaxed_trigger in rule assert relaxed_trigger in index assert "work-bundle-scoped`, `project-scoped`, `mixed`, or `undetermined" in rule - assert "Create or update minimal violation evidence for `work-bundle-scoped` and `mixed`" in rule + assert "Create or update minimal defect evidence for `work-bundle-scoped` and `mixed`" in rule assert "Report `project-scoped` findings as project blockers" in rule assert "Block for resolution when an `undetermined` finding affects authority" in rule -def test_violation_evaluation_stops_at_visible_workbundle_relatedness() -> None: - rule = (REPO_ROOT / "rules/work-bundle/wb-violation-evaluation.md").read_text(encoding="utf-8") +def test_defect_evaluation_stops_at_visible_workbundle_relatedness() -> None: + rule = (REPO_ROOT / "rules/work-bundle/wb-defect-evaluation.md").read_text(encoding="utf-8") assert "Use visible evidence first" in rule assert "stop evaluation and proceed to the matching action" in rule @@ -495,8 +495,8 @@ def test_violation_evaluation_stops_at_visible_workbundle_relatedness() -> None: assert "mandatory chain-of-thought output" in rule -def test_violation_evidence_calls_evaluation_and_keeps_storage_boundary() -> None: - evidence = (REPO_ROOT / "rules/work-bundle/wb-violation-evidence.md").read_text(encoding="utf-8") +def test_defect_evidence_calls_evaluation_and_keeps_storage_boundary() -> None: + evidence = (REPO_ROOT / "rules/work-bundle/wb-defect-evidence.md").read_text(encoding="utf-8") index = (REPO_ROOT / "rules/index.yaml").read_text(encoding="utf-8") trigger = "the Work Bundle rule is visible in AGENTS.md and any conflict, confliction, violation, contradiction, or user correction occurs" @@ -504,23 +504,23 @@ def test_violation_evidence_calls_evaluation_and_keeps_storage_boundary() -> Non assert trigger in index assert "requires: []" in evidence assert "requires: []" in index - assert "immediately call `wb-violation-evaluation`" in evidence - assert "Exit the violation evidence workflow without recording evidence" in evidence + assert "immediately call `wb-defect-evaluation`" in evidence + assert "Exit the defect evidence workflow without recording evidence" in evidence assert "classifies the first-observed finding as `work-bundle-scoped` or `mixed`" in evidence assert "Do not expand evidence capture into evaluation, root-cause investigation, or exhaustive workflow-chain tracing" in evidence - assert "project-scoped findings from `wb-violation-evaluation` as blockers" in evidence + assert "project-scoped findings from `wb-defect-evaluation` as blockers" in evidence assert "undetermined findings that affect authority, target scope, validation, or continuation as resolution blockers" in evidence -def test_violation_rules_support_same_scope_specification_owned_handling() -> None: - evaluation = (REPO_ROOT / "rules/work-bundle/wb-violation-evaluation.md").read_text(encoding="utf-8") - evidence = (REPO_ROOT / "rules/work-bundle/wb-violation-evidence.md").read_text(encoding="utf-8") +def test_defect_rules_support_same_scope_specification_owned_handling() -> None: + evaluation = (REPO_ROOT / "rules/work-bundle/wb-defect-evaluation.md").read_text(encoding="utf-8") + evidence = (REPO_ROOT / "rules/work-bundle/wb-defect-evidence.md").read_text(encoding="utf-8") assert "same-scope specification-owned" in evaluation assert "current project is the WorkBundle toolkit itself" in evaluation assert "exactly the current specification-owned work item" in evaluation assert "record the issue in the active specification source context, Open Questions, or review evidence" in evaluation - assert "instead of creating a new violation evidence file" in evaluation + assert "instead of creating a new defect evidence file" in evaluation assert "same_scope_specification_owned: true|false" in evaluation assert "same-scope specification-owned" in evidence diff --git a/tests/test_work_bundle_violation_evidence.py b/tests/test_work_bundle_defect_evidence.py similarity index 65% rename from tests/test_work_bundle_violation_evidence.py rename to tests/test_work_bundle_defect_evidence.py index 2c35d09..797d12a 100644 --- a/tests/test_work_bundle_violation_evidence.py +++ b/tests/test_work_bundle_defect_evidence.py @@ -10,13 +10,13 @@ REPO_ROOT = Path(__file__).resolve().parents[1] WB = REPO_ROOT / "scripts" / "wb.py" -CATALOG = (REPO_ROOT / "references" / "wb-violation-evidence.yaml").read_text(encoding="utf-8") +CATALOG = (REPO_ROOT / "references" / "wb-defect-evidence.yaml").read_text(encoding="utf-8") def prepare_cwd(tmp_path: Path, catalog: str = CATALOG) -> Path: cwd = tmp_path / "cwd" (cwd / "references").mkdir(parents=True, exist_ok=True) - (cwd / "references" / "wb-violation-evidence.yaml").write_text(catalog, encoding="utf-8") + (cwd / "references" / "wb-defect-evidence.yaml").write_text(catalog, encoding="utf-8") return cwd @@ -36,7 +36,7 @@ def run_wb(tmp_path: Path, *args: str, cwd: Path | None = None) -> subprocess.Co def create_active(tmp_path: Path, slug: str = "contract-conflict", *evidence: str) -> dict[str, object]: result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "active", "--short-description", @@ -57,30 +57,30 @@ def evidence_id(slug: str) -> str: return f"evidence-{datetime.now().strftime('%Y%m%d')}-{slug}" -def test_violation_ensure_store_creates_directories(tmp_path: Path) -> None: - result = run_wb(tmp_path, "violation-ensure-store") +def test_defect_ensure_store_creates_directories(tmp_path: Path) -> None: + result = run_wb(tmp_path, "defect-ensure-store") assert result.returncode == 0, result.stdout + result.stderr payload = json.loads(result.stdout) assert payload["status"] == "ok" assert Path(payload["active"]).is_dir() assert Path(payload["archived"]).is_dir() - assert Path(payload["root"]) == tmp_path / "config" / "violation" - assert not (Path.home() / ".work-bundle" / "violation" / "active" / "__pytest_marker__").exists() + assert Path(payload["root"]) == tmp_path / "config" / "defect" + assert not (Path.home() / ".work-bundle" / "defect" / "active" / "__pytest_marker__").exists() -def test_violation_catalog_is_cwd_independent(tmp_path: Path) -> None: +def test_defect_catalog_is_cwd_independent(tmp_path: Path) -> None: external_cwd = tmp_path / "external-cwd" external_cwd.mkdir() - result = run_wb(tmp_path, "violation-build-index", cwd=external_cwd) + result = run_wb(tmp_path, "defect-build-index", cwd=external_cwd) assert result.returncode == 0, result.stdout + result.stderr assert "active:" in result.stdout assert "archived:" in result.stdout -def test_violation_create_evidence_writes_active_record_with_supplied_evidence_only(tmp_path: Path) -> None: +def test_defect_create_evidence_writes_active_record_with_supplied_evidence_only(tmp_path: Path) -> None: cwd = prepare_cwd(tmp_path) supplied = cwd / "visible.txt" supplied.write_text("visible", encoding="utf-8") @@ -88,7 +88,7 @@ def test_violation_create_evidence_writes_active_record_with_supplied_evidence_o result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "active", "--short-description", @@ -115,10 +115,10 @@ def test_violation_create_evidence_writes_active_record_with_supplied_evidence_o assert "not-supplied.txt" not in text -def test_violation_create_evidence_rejects_invalid_severity(tmp_path: Path) -> None: +def test_defect_create_evidence_rejects_invalid_severity(tmp_path: Path) -> None: result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "active", "--short-description", @@ -138,10 +138,10 @@ def test_violation_create_evidence_rejects_invalid_severity(tmp_path: Path) -> N assert "invalid severity: p11" in payload["error"] -def test_violation_create_evidence_rejects_archived_without_action(tmp_path: Path) -> None: +def test_defect_create_evidence_rejects_archived_without_action(tmp_path: Path) -> None: result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "archived", "--short-description", @@ -161,29 +161,29 @@ def test_violation_create_evidence_rejects_archived_without_action(tmp_path: Pat assert "archived evidence requires --action" in payload["error"] -def test_violation_create_evidence_rejects_status_directory_mismatch(tmp_path: Path) -> None: +def test_defect_create_evidence_rejects_status_directory_mismatch(tmp_path: Path) -> None: create_active(tmp_path, "status-mismatch") - path = tmp_path / "config" / "violation" / "active" / f"{evidence_id('status-mismatch')}.yaml" + path = tmp_path / "config" / "defect" / "active" / f"{evidence_id('status-mismatch')}.yaml" path.write_text(path.read_text(encoding="utf-8").replace("status: active", "status: archived"), encoding="utf-8") - result = run_wb(tmp_path, "violation-build-index") + result = run_wb(tmp_path, "defect-build-index") assert result.returncode == 1 payload = json.loads(result.stdout) assert "status archived does not match directory active" in payload["error"] -def test_violation_build_index_outputs_active_and_archived_maps(tmp_path: Path) -> None: +def test_defect_build_index_outputs_active_and_archived_maps(tmp_path: Path) -> None: create_active(tmp_path, "active-contract") archived = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "archived", "--short-description", "archived-contract", "--deviation", - "Archived work-bundle violation.", + "Archived work-bundle defect.", "--occurrence", "Created as archived.", "--evidence", @@ -195,22 +195,22 @@ def test_violation_build_index_outputs_active_and_archived_maps(tmp_path: Path) ) assert archived.returncode == 0, archived.stdout + archived.stderr - result = run_wb(tmp_path, "violation-build-index") + result = run_wb(tmp_path, "defect-build-index") assert result.returncode == 0, result.stdout + result.stderr assert f"{evidence_id('active-contract')}:" in result.stdout assert f"{evidence_id('archived-contract')}:" in result.stdout assert 'deviation: "Planning contract asks for executor-result forbidden fields."' in result.stdout - assert 'deviation: "Archived work-bundle violation."' in result.stdout + assert 'deviation: "Archived work-bundle defect."' in result.stdout assert "action: dismiss" in result.stdout -def test_violation_write_index_writes_deterministic_index(tmp_path: Path) -> None: +def test_defect_write_index_writes_deterministic_index(tmp_path: Path) -> None: create_active(tmp_path, "write-index") - built = run_wb(tmp_path, "violation-build-index") + built = run_wb(tmp_path, "defect-build-index") assert built.returncode == 0, built.stdout + built.stderr - result = run_wb(tmp_path, "violation-write-index") + result = run_wb(tmp_path, "defect-write-index") assert result.returncode == 0, result.stdout + result.stderr payload = json.loads(result.stdout) @@ -219,10 +219,10 @@ def test_violation_write_index_writes_deterministic_index(tmp_path: Path) -> Non assert Path(payload["path"]).read_text(encoding="utf-8") == built.stdout -def test_violation_archive_evidence_moves_record(tmp_path: Path) -> None: +def test_defect_archive_evidence_moves_record(tmp_path: Path) -> None: created = create_active(tmp_path, "archive-me") - result = run_wb(tmp_path, "violation-archive-evidence", str(created["id"]), "--action", "completed") + result = run_wb(tmp_path, "defect-archive-evidence", str(created["id"]), "--action", "completed") assert result.returncode == 0, result.stdout + result.stderr payload = json.loads(result.stdout) @@ -236,7 +236,7 @@ def test_violation_archive_evidence_moves_record(tmp_path: Path) -> None: assert "action: completed" in text -def test_violation_duplicate_identical_create_reports_changed_false(tmp_path: Path) -> None: +def test_defect_duplicate_identical_create_reports_changed_false(tmp_path: Path) -> None: first = create_active(tmp_path, "duplicate") second = create_active(tmp_path, "duplicate") @@ -245,13 +245,13 @@ def test_violation_duplicate_identical_create_reports_changed_false(tmp_path: Pa assert second["changed"] is False -def test_violation_conflicting_duplicate_fails_without_overwrite(tmp_path: Path) -> None: +def test_defect_conflicting_duplicate_fails_without_overwrite(tmp_path: Path) -> None: first = create_active(tmp_path, "collision") original = Path(str(first["path"])).read_text(encoding="utf-8") result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "active", "--short-description", @@ -272,26 +272,27 @@ def test_violation_conflicting_duplicate_fails_without_overwrite(tmp_path: Path) assert Path(str(first["path"])).read_text(encoding="utf-8") == original -def test_violation_dispatcher_routes_all_commands_and_command_help(tmp_path: Path) -> None: +def test_defect_dispatcher_routes_all_commands_and_command_help(tmp_path: Path) -> None: for command in [ - "violation-ensure-store", - "violation-create-evidence", - "violation-build-index", - "violation-write-index", - "violation-archive-evidence", + "defect-ensure-store", + "defect-create-evidence", + "defect-build-index", + "defect-write-index", + "defect-archive-evidence", + "defect-migrate-store", ]: result = run_wb(tmp_path, command, "--help") assert result.returncode == 0, result.stdout + result.stderr assert f"usage: wb.py {command}" in result.stdout -def test_violation_catalog_ignores_cwd_shadow(tmp_path: Path) -> None: +def test_defect_catalog_ignores_cwd_shadow(tmp_path: Path) -> None: custom_catalog = CATALOG.replace(" - p3\n", "") cwd = prepare_cwd(tmp_path, custom_catalog) result = run_wb( tmp_path, - "violation-create-evidence", + "defect-create-evidence", "--status", "active", "--short-description", @@ -312,12 +313,54 @@ def test_violation_catalog_ignores_cwd_shadow(tmp_path: Path) -> None: assert payload["status"] == "ok" -def test_violation_evidence_rule_remains_minimal_storage_after_evaluation() -> None: - rule = (REPO_ROOT / "rules/work-bundle/wb-violation-evidence.md").read_text(encoding="utf-8") +def test_defect_evidence_rule_remains_minimal_storage_after_evaluation() -> None: + rule = (REPO_ROOT / "rules/work-bundle/wb-defect-evidence.md").read_text(encoding="utf-8") assert "Record only the minimal first-observed evidence" in rule assert "Record only files, artifacts, UI output, terminal output, or runtime surfaces already visible" in rule assert "Do not perform additional file search, repository browsing, historical tracing, or contract exploration" in rule - assert "Do not delay or widen plan execution to enrich a violation record" in rule + assert "Do not delay or widen plan execution to enrich a defect record" in rule assert "Do not record project business logic, project implementation" in rule assert "Do not expand evidence capture into evaluation, root-cause investigation, or exhaustive workflow-chain tracing" in rule + assert "Use `defect-migrate-store` explicitly when the legacy store remains" in rule + assert "Do not make a non-migration defect command migrate, merge, or initialize beside legacy authority" in rule + + +def test_retired_violation_vocabulary_is_limited_to_fail_only_surfaces() -> None: + roots = ["AGENTS.md", "references", "rules", "scripts", "skills", "tests"] + tokens = ( + "wb-violation", + "violation_closure", + "REQ-VIOL-001", + "violation-ensure-store", + "violation-create-evidence", + "violation-build-index", + "violation-write-index", + "violation-archive-evidence", + "wb-violation-evidence.yaml", + "work-bundle/violations.py", + "test_work_bundle_violation_evidence", + ) + allowed = { + REPO_ROOT / "scripts/work-bundle/dispatcher.py", + Path(__file__).resolve(), + REPO_ROOT / "tests/test_work_bundle_defect_migration.py", + } + offenders: list[str] = [] + for root_name in roots: + root = REPO_ROOT / root_name + paths = [root] if root.is_file() else [path for path in root.rglob("*") if path.is_file()] + for path in paths: + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + if any(token in text for token in tokens) and path.resolve() not in allowed: + offenders.append(path.relative_to(REPO_ROOT).as_posix()) + assert offenders == [] + assert not (REPO_ROOT / "scripts/work-bundle/violations.py").exists() + assert not (REPO_ROOT / "references/wb-violation-evidence.yaml").exists() + assert not (REPO_ROOT / "rules/work-bundle/wb-violation-evidence.md").exists() + assert not (REPO_ROOT / "rules/work-bundle/wb-violation-evaluation.md").exists() + assert not (REPO_ROOT / "tests/test_work_bundle_violation_evidence.py").exists() + assert "## On Violation" in (REPO_ROOT / "rules/repository-boundary.md").read_text(encoding="utf-8") diff --git a/tests/test_work_bundle_defect_migration.py b/tests/test_work_bundle_defect_migration.py new file mode 100644 index 0000000..74d6bf4 --- /dev/null +++ b/tests/test_work_bundle_defect_migration.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import sys +from datetime import datetime +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WB = REPO_ROOT / "scripts" / "wb.py" + + +def run_wb(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: + cwd = tmp_path / "cwd" + cwd.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["WB_CONFIG_ROOT"] = str(tmp_path / "config") + return subprocess.run( + [sys.executable, str(WB), *args], + cwd=cwd, + env=env, + check=False, + capture_output=True, + text=True, + ) + + +def evidence_id(slug: str) -> str: + return f"evidence-{datetime.now().strftime('%Y%m%d')}-{slug}" + + +def write_legacy_record(tmp_path: Path, *, status: str = "active", slug: str = "migration-record") -> tuple[Path, bytes]: + root = tmp_path / "config" / "violation" + (root / "active").mkdir(parents=True, exist_ok=True) + (root / "archived").mkdir(parents=True, exist_ok=True) + action = "null" if status == "active" else "completed" + content = ( + 'deviation: "Legacy evidence must survive migration."\n' + 'occurrence: "Observed before the defect cutover."\n' + 'evidence:\n' + ' - surface: "visible migration fixture"\n' + ' role: "first-evidence"\n' + f'status: {status}\n' + f'action: {action}\n' + 'severity: p3\n' + ).encode() + path = root / status / f"{evidence_id(slug)}.yaml" + path.write_bytes(content) + return path, content + + +def record_fingerprint(root: Path) -> str: + digest = hashlib.sha256() + for status in ("active", "archived"): + for path in sorted((root / status).glob("*.yaml")): + digest.update(f"{status}/{path.name}".encode()) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\n") + return digest.hexdigest() + + +def publish_incomplete_destination(tmp_path: Path, *, mismatch: bool = False) -> tuple[Path, Path]: + legacy_path, _ = write_legacy_record(tmp_path) + legacy = legacy_path.parents[1] + destination = tmp_path / "config" / "defect" + shutil.copytree(legacy, destination) + fingerprint = record_fingerprint(destination) + marker = { + "schema_version": 1, + "source_fingerprint": "0" * 64 if mismatch else fingerprint, + "destination_fingerprint": fingerprint, + } + (destination / ".migration-marker.json").write_text(json.dumps(marker), encoding="utf-8") + (destination / ".staging-owner").write_text("work-bundle:defect-migrate-store:v1\n", encoding="utf-8") + return legacy, destination + + +def test_defect_migrate_store_preserves_record_bytes_and_rebuilds_index(tmp_path: Path) -> None: + legacy_path, original = write_legacy_record(tmp_path) + (legacy_path.parents[1] / "index.yaml").write_text("stale: true\n", encoding="utf-8") + + result = run_wb(tmp_path, "defect-migrate-store") + + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(result.stdout) + assert payload["migration_status"] == "migrated" + defect = tmp_path / "config" / "defect" + assert not (tmp_path / "config" / "violation").exists() + assert (defect / "active" / legacy_path.name).read_bytes() == original + assert legacy_path.stem in (defect / "index.yaml").read_text(encoding="utf-8") + assert not (defect / ".migration-marker.json").exists() + assert not (defect / ".staging-owner").exists() + + +def test_defect_non_migration_command_blocks_before_destination_creation(tmp_path: Path) -> None: + cases = { + "defect-ensure-store": [], + "defect-create-evidence": ["--status", "active", "--short-description", "guarded", "--deviation", "guard", "--occurrence", "guard", "--evidence", "visible", "--severity", "p3"], + "defect-build-index": [], + "defect-write-index": [], + "defect-archive-evidence": [evidence_id("migration-record"), "--action", "completed"], + } + for command, arguments in cases.items(): + case = tmp_path / command + write_legacy_record(case) + result = run_wb(case, command, *arguments) + assert result.returncode == 1, command + assert "defect-migrate-store" in result.stdout + assert not (case / "config" / "defect").exists() + + +def test_defect_non_migration_commands_block_on_staging_or_marker(tmp_path: Path) -> None: + staging_case = tmp_path / "staging" + staging = staging_case / "config" / ".defect-migration-staging" + staging.mkdir(parents=True) + staging_result = run_wb(staging_case, "defect-ensure-store") + assert staging_result.returncode == 1 + assert staging.exists() + + marker_case = tmp_path / "marker" + ensured = run_wb(marker_case, "defect-ensure-store") + assert ensured.returncode == 0 + marker = marker_case / "config" / "defect" / ".migration-marker.json" + marker.write_text("{}", encoding="utf-8") + marker_result = run_wb(marker_case, "defect-build-index") + assert marker_result.returncode == 1 + assert "defect-migrate-store" in marker_result.stdout + + +def test_defect_migrate_store_fails_closed_when_both_roots_are_unmarked(tmp_path: Path) -> None: + write_legacy_record(tmp_path) + defect = tmp_path / "config" / "defect" + (defect / "active").mkdir(parents=True) + (defect / "archived").mkdir() + config = tmp_path / "config" + before = sorted(path.relative_to(config).as_posix() for path in config.rglob("*")) + + result = run_wb(tmp_path, "defect-migrate-store") + + assert result.returncode == 1 + assert "conflict" in result.stdout + assert sorted(path.relative_to(config).as_posix() for path in config.rglob("*")) == before + + +def test_defect_migrate_store_is_noop_when_neither_store_exists(tmp_path: Path) -> None: + result = run_wb(tmp_path, "defect-migrate-store") + + assert result.returncode == 0, result.stdout + result.stderr + assert json.loads(result.stdout)["migration_status"] == "no-store" + assert not (tmp_path / "config" / "defect").exists() + + +def test_defect_migrate_store_rejects_invalid_legacy_without_destination(tmp_path: Path) -> None: + path, original = write_legacy_record(tmp_path) + path.write_text("invalid\n", encoding="utf-8") + + result = run_wb(tmp_path, "defect-migrate-store") + + assert result.returncode == 1 + assert path.read_text(encoding="utf-8") == "invalid\n" + assert not (tmp_path / "config" / "defect").exists() + assert original != path.read_bytes() + + +def test_defect_migrate_store_resumes_matching_published_destination(tmp_path: Path) -> None: + legacy, destination = publish_incomplete_destination(tmp_path) + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 0, result.stdout + result.stderr + assert json.loads(result.stdout)["resumed"] is True + assert not legacy.exists() + assert destination.exists() + assert not (destination / ".migration-marker.json").exists() + assert not (destination / ".staging-owner").exists() + + +def test_defect_migrate_store_rejects_mismatched_published_destination(tmp_path: Path) -> None: + legacy, destination = publish_incomplete_destination(tmp_path, mismatch=True) + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 1 + assert "invalid defect migration marker" in result.stdout + assert legacy.exists() + assert destination.exists() + assert (destination / ".migration-marker.json").exists() + + +def test_defect_migrate_store_finalizes_destination_only_marker(tmp_path: Path) -> None: + legacy, destination = publish_incomplete_destination(tmp_path) + shutil.rmtree(legacy) + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 0, result.stdout + result.stderr + assert json.loads(result.stdout)["resumed"] is True + assert not (destination / ".migration-marker.json").exists() + + +def test_defect_migrate_store_replaces_owned_staging_beside_legacy(tmp_path: Path) -> None: + write_legacy_record(tmp_path) + staging = tmp_path / "config" / ".defect-migration-staging" + staging.mkdir() + (staging / ".staging-owner").write_text("work-bundle:defect-migrate-store:v1\n", encoding="utf-8") + (staging / "partial").write_text("partial", encoding="utf-8") + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 0, result.stdout + result.stderr + assert not staging.exists() + assert (tmp_path / "config" / "defect").exists() + + +def test_defect_migrate_store_rejects_unowned_staging(tmp_path: Path) -> None: + staging = tmp_path / "config" / ".defect-migration-staging" + staging.mkdir(parents=True) + (staging / "user-file").write_text("preserve", encoding="utf-8") + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 1 + assert (staging / "user-file").read_text(encoding="utf-8") == "preserve" + + +def test_defect_migrate_store_is_idempotent_after_success(tmp_path: Path) -> None: + write_legacy_record(tmp_path) + first = run_wb(tmp_path, "defect-migrate-store") + destination = tmp_path / "config" / "defect" + before = {path.relative_to(destination).as_posix(): path.read_bytes() for path in destination.rglob("*") if path.is_file()} + second = run_wb(tmp_path, "defect-migrate-store") + assert first.returncode == 0 + assert second.returncode == 0 + assert json.loads(second.stdout)["migration_status"] == "already-migrated" + assert {path.relative_to(destination).as_posix(): path.read_bytes() for path in destination.rglob("*") if path.is_file()} == before + + +def test_defect_migrate_store_rejects_invalid_destination_only(tmp_path: Path) -> None: + destination = tmp_path / "config" / "defect" + destination.mkdir(parents=True) + sentinel = destination / "preserve" + sentinel.write_text("user-state", encoding="utf-8") + result = run_wb(tmp_path, "defect-migrate-store") + assert result.returncode == 1 + assert "invalid evidence store layout" in result.stdout + assert sentinel.read_text(encoding="utf-8") == "user-state" + + +def test_legacy_command_fails_with_guidance_without_store_effects(tmp_path: Path) -> None: + replacements = { + "violation-ensure-store": "defect-ensure-store", + "violation-create-evidence": "defect-create-evidence", + "violation-build-index": "defect-build-index", + "violation-write-index": "defect-write-index", + "violation-archive-evidence": "defect-archive-evidence", + } + for command, replacement in replacements.items(): + case = tmp_path / command + legacy_path, original = write_legacy_record(case) + result = run_wb(case, command) + assert result.returncode == 2 + payload = json.loads(result.stdout) + assert payload["diagnostic"] == "WB_LEGACY_COMMAND_REMOVED" + assert payload["replacement_command"] == replacement + assert legacy_path.read_bytes() == original + assert not (case / "config" / "defect").exists()