diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml index 17cd6da1..b2b3a917 100644 --- a/.github/workflows/ci-gate.yml +++ b/.github/workflows/ci-gate.yml @@ -143,6 +143,16 @@ jobs: - name: Check governance evidence-first contract (structure + semantics) run: ./scripts/ci/check_governance_evidence_truth.sh + governance-traceability: + name: governance-traceability + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check governance traceability contract (template + indexes + TODO linkage) + run: ./scripts/ci/check_governance_traceability.sh + p0-gate: name: p0-gate runs-on: ubuntu-latest diff --git a/docs/README.md b/docs/README.md index 57b08ef8..6695ac54 100644 --- a/docs/README.md +++ b/docs/README.md @@ -103,6 +103,8 @@ | `features/README.md` | 特性聚合文档规范:单一状态源、证据回写与归档迁移 | | `guides/Team_Agent_Collab_Playbook.md` | 团队并行开发协作手册,含 spec-driven 认领粒度与 execution board 规则 | | `guides/Tool_Approval_Memory.md` | 工具审批记忆使用指南:pending/grant/deny/revoke、scope/matcher、持久化与接线方式 | +| `features/templates/feature_aggregation_template.md` | 特性聚合模板:用于新建 `docs/features/.md` | +| `features/archive/README.md` | 特性聚合归档索引:记录已归档 change 的入口与迁移规则 | --- diff --git a/docs/features/README.md b/docs/features/README.md index cc4e96f8..cb33119a 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -25,7 +25,32 @@ - `### Behavior Verification` - `### Risks and Rollback` - `### Review and Merge Gate Links` +- CI gate command: `./scripts/ci/check_governance_traceability.sh` - CI gate command: `./scripts/ci/check_governance_evidence_truth.sh` 5. Archive - Move completed docs to `docs/features/archive/` after closeout. + +## Template + +- Canonical template: `docs/features/templates/feature_aggregation_template.md` + +## Active Entries + +- `docs/features/agentscope-d2-d4-thinking-transport.md` +- `docs/features/agentscope-d5-safe-compression.md` +- `docs/features/agentscope-d7-plan-state-tools.md` +- `docs/features/enhance-doc-governance-traceability.md` +- `docs/features/p0-conformance-gate.md` +- `docs/features/p0-default-eventlog.md` +- `docs/features/p0-step-driven-execution.md` + +## Archive Index + +- `docs/features/archive/README.md` + +## Migration Rules + +- Active feature docs live in `docs/features/` until completion-archive. +- Closeout must update this active index and the archive index in the same change. +- Archived feature docs move to `docs/features/archive/` and keep stable evidence links. diff --git a/docs/features/agentscope-d2-d4-thinking-transport.md b/docs/features/agentscope-d2-d4-thinking-transport.md index 76c4f778..386de938 100644 --- a/docs/features/agentscope-d2-d4-thinking-transport.md +++ b/docs/features/agentscope-d2-d4-thinking-transport.md @@ -2,8 +2,9 @@ change_ids: ["agentscope-d2-d4-thinking-transport"] doc_kind: feature topics: ["agentscope", "transport", "thinking", "tool-events", "model-response"] +todo_ids: ["D2-1", "D2-2", "D2-3", "D2-4", "D4-1", "D4-2", "D4-3", "D4-4"] created: 2026-03-02 -updated: 2026-03-02 +updated: 2026-03-03 status: draft mode: openspec --- diff --git a/docs/features/archive/README.md b/docs/features/archive/README.md new file mode 100644 index 00000000..7f06be38 --- /dev/null +++ b/docs/features/archive/README.md @@ -0,0 +1,17 @@ +# Feature Aggregation Archive + +`docs/features/archive/` stores completed change aggregation docs after closeout. + +## Archived Entries + +- `docs/features/archive/client-capability-discovery-and-host-tests.md` +- `docs/features/archive/client-external-control-plane-v1.md` +- `docs/features/archive/client-headless-event-envelope-v1.md` +- `docs/features/archive/client-host-orchestration-doc-baseline.md` +- `docs/features/archive/refactor-dare-agent-structure-split.md` + +## Archive Migration Rules + +- Move a feature doc here only after the corresponding change is archived or explicitly closed. +- Archive moves must preserve evidence links and OpenSpec artifact paths. +- The active index at `docs/features/README.md` must be updated in the same change that performs the archive move. diff --git a/docs/features/enhance-doc-governance-traceability.md b/docs/features/enhance-doc-governance-traceability.md index 99af844d..bd661441 100644 --- a/docs/features/enhance-doc-governance-traceability.md +++ b/docs/features/enhance-doc-governance-traceability.md @@ -3,7 +3,7 @@ change_ids: ["enhance-doc-governance-traceability"] doc_kind: feature topics: ["documentation-governance", "traceability", "skills"] created: 2026-02-28 -updated: 2026-03-02 +updated: 2026-03-04 status: active mode: openspec --- @@ -30,60 +30,77 @@ Unify documentation management structure, lifecycle governance, and SOP-to-skill ## Evidence ### Commands +- `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py tests/unit/test_governance_evidence_truth_gate.py` +- `./scripts/ci/check_governance_traceability.sh` - `./scripts/ci/check_governance_evidence_truth.sh` -- `openspec validate --changes enhance-doc-governance-traceability` -- `openspec status --change enhance-doc-governance-traceability --json` +- `openspec validate enhance-doc-governance-traceability --type change --strict --json --no-interactive` ### Results -- `check_governance_evidence_truth.sh`: pass. -- `openspec validate`: pass. -- `openspec status`: pass, `isComplete: true`. +- `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py tests/unit/test_governance_evidence_truth_gate.py`: passed (`50 passed`) after extending the traceability gate regression suite to also cover stale active index entries, `Active Entries`-only membership checks, explicit checkpoint-to-skill pair rows, active/archive index path-family enforcement, README index-file exclusion for both active and archived entries, discrete `todo_ids` token matching, Claim Ledger-only TODO/change validation, same-record TODO/change validation, claim-scope range resolution, range-only claim-scope resolution without explicit todo tokens, full lifecycle checkpoint coverage, and date-prefixed archived change task discovery. +- `./scripts/ci/check_governance_traceability.sh`: passed against the real repository tree after tightening active/archive index membership to canonical sections, rejecting index entries outside the correct feature-doc path family, excluding `docs/features/README.md` and `docs/features/archive/README.md` from valid feature-entry targets, requiring explicit checkpoint-to-skill pair rows in Section 7, and resolving pilot `todo_ids` only through Claim Ledger records, including same-claim scope ranges where the TODO id is only implied by the claim range. +- `./scripts/ci/check_governance_evidence_truth.sh`: passed, confirming the new traceability assets do not break the existing evidence-first contract. +- `openspec validate enhance-doc-governance-traceability --type change --strict --json --no-interactive`: passed (`1/1` change valid, `0` issues). ### Contract Delta -- `schema`: evidence contract now requires a full acceptance-pack layout in every active feature doc (`Contract Delta`, `Golden Cases`, `Regression Summary`, `Observability and Failure Localization`, `Structured Review Report`). -- `error semantics`: no runtime API `error_code` enum change; this gate now accepts framework-native error semantics (`error_code`/`error_type`/`exception_class`/`ToolResult.error`) and blocks missing declarations. -- `retry`: CI retry does not bypass policy checks; rerun only after evidence/doc fixes, with no semantic downgrade on retry. +- `schema`: added a canonical feature aggregation template, active/archive feature indexes, and a machine-checkable backlog-id pilot linkage for one active change. +- `error semantics`: no runtime API change; the new traceability gate fails structurally on missing template/index/mapping assets with deterministic file-scoped messages. +- `retry`: no retry semantic change; this slice is docs/CI only, and reruns remain deterministic after fixing the flagged governance asset. ### Golden Cases -- Updated evidence contract baseline: `docs/guides/Evidence_Truth_Implementation_Strategy.md`. -- Added acceptance-pack canonical spec: `docs/governance/Acceptance_Pack_Spec.md`. -- Updated PR authoring baseline: `.github/pull_request_template.md`. +- `docs/features/templates/feature_aggregation_template.md` +- `docs/features/README.md` +- `docs/features/archive/README.md` +- `scripts/ci/check_governance_traceability.sh` +- `tests/unit/test_governance_traceability_gate.py` +- `docs/features/agentscope-d2-d4-thinking-transport.md` ### Regression Summary - Runner commands: + - `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py tests/unit/test_governance_evidence_truth_gate.py` + - `./scripts/ci/check_governance_traceability.sh` - `./scripts/ci/check_governance_evidence_truth.sh` - - `openspec validate --changes enhance-doc-governance-traceability` - - `openspec status --change enhance-doc-governance-traceability --json` -- Summary: pass 3, fail 0, skip 0. + - `openspec validate enhance-doc-governance-traceability --type change --strict --json --no-interactive` +- Summary: pass 4, fail 0, skip 0. ### Observability and Failure Localization -- Event chain coverage includes `start`, `tool_call`, `end`, and `fail` events for traceable execution lifecycle. -- Failure localization fields required for triage and review are: `run_id`, `tool_call_id`, `capability_id`, `attempt`, `trace_id`, plus at least one error locator (`error_code`/`error_type`/`exception_class`/`ToolResult.error`). -- Gate failures must emit enough context to locate the exact document/section mismatch without full code deep-dive. +- N/A for runtime event chain in this docs/CI governance slice. +- Reason: this slice only adds document topology and traceability checks; it does not modify runtime event emission. +- Fallback evidence: unit tests and both governance gate commands above exercise the failing/passing paths for template/index/mapping localization. ### Structured Review Report - Changed Module Boundaries / Public API: governance scope only; no new runtime public API added. -- New State: no new cache/global/singleton runtime state; only documentation governance state tightened. -- Concurrency / Timeout / Retry: no new concurrent runtime path; retry policy is documentation gate rerun after fixes, with unchanged timeout semantics. -- Side Effects and Idempotency: side effects are limited to docs/CI gate outputs; idempotency relies on deterministic section checks and repeatable command outputs. -- Coverage and Residual Risk: governance evidence and OpenSpec validation are covered; residual risk is false positives from regex-based checks when section names drift from canonical wording. +- New State: adds one new repository gate script, one new gate test file, and canonical docs/index/template assets under `docs/features/`. +- Concurrency / Timeout / Retry: no concurrency change; gate runs are single-process document scans with deterministic rerun behavior after fixes. +- Side Effects and Idempotency: side effects are limited to CI/log output; repeated runs are idempotent against unchanged docs. +- Coverage and Residual Risk: template/index/skill-mapping/TODO-linkage checks are covered; residual risk is that broader frontmatter enforcement across `docs/guides/**` and `docs/design/**` is still pending. ### Behavior Verification -- Happy path: governance flow remains `analysis -> master TODO -> OpenSpec slice execution` with docs as canonical source. -- Error/fallback path: TODO fallback metadata now requires `mode: todo_fallback` + `topic_slug`, with explicit migration back to OpenSpec. +- Happy path: the repository now has a canonical feature aggregation template, explicit active/archive feature indexes, and a green traceability gate that resolves a pilot feature doc back to its TODO ledger and owning change-id through Claim Ledger records, including scope ranges such as `D2-1~D2-4, D4-1~D4-4`, even when the concrete TODO id does not appear elsewhere in the file. +- Error/fallback path: the new gate fails deterministically when a feature doc is missing from the `## Active Entries` section, when an active/archive index entry points at the wrong doc family, when Section 7 keeps checkpoint names but drops the actual `checkpoint -> skill` mapping rows, or when `todo_ids` and `change_ids` only co-occur in detail-board/prose lines without a matching Claim Ledger record. ### Risks and Rollback -- Risk: CI checks not yet fully implemented as scripts may leave policy drift windows. -- Rollback: keep contract wording changes, temporarily downgrade new CI gate checks to warning if false positives block delivery. +- Risk: `3.2-3.4` are still open, so the new gate does not yet enforce full frontmatter coverage for every governance-tracked doc family or full master-TODO/task completeness. +- Risk: active/archive indexes are now explicit manual ledgers, so closeout changes that forget to update them will fail the new gate. +- Rollback: remove `governance-traceability` from `.github/workflows/ci-gate.yml` and revert the template/index additions if the new gate produces unexpected false positives. ### Review and Merge Gate Links - Intent PR: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/126` - Implementation PR: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/137` +- Current implementation PR: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175` - Review request: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/126#issuecomment-3976690386` - Key owner feedback: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/126#issuecomment-3976707233` - Active fix threads: - - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/126#discussion_r2867257929` - - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/126#discussion_r2867257932` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878449796` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878449803` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878551469` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878551474` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878634816` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878634820` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878773226` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2878885299` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2881384029` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2881384034` + - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2881421738` ## Next Milestone -Implement tasks group 1-2 (taxonomy contract + standards alignment). +Implement the remaining CI depth tasks: widen frontmatter enforcement beyond feature docs and add machine-checkable TODO/task and master-TODO/change-slice consistency checks before closeout. diff --git a/docs/features/templates/feature_aggregation_template.md b/docs/features/templates/feature_aggregation_template.md new file mode 100644 index 00000000..dbfaf318 --- /dev/null +++ b/docs/features/templates/feature_aggregation_template.md @@ -0,0 +1,56 @@ +# Feature Aggregation Template + +Use this template for new `docs/features/.md` entries in OpenSpec mode. + +```yaml +--- +change_ids: [""] +doc_kind: feature +topics: ["topic-a", "topic-b"] +todo_ids: ["OPTIONAL-TODO-ID"] +created: YYYY-MM-DD +updated: YYYY-MM-DD +status: draft +mode: openspec +--- +``` + +```md +# Feature: + +## Scope +Describe the slice boundary and the user-visible or contract-visible outcome. + +## OpenSpec Artifacts +- Proposal: `openspec/changes//proposal.md` +- Design: `openspec/changes//design.md` +- Specs: + - `openspec/changes//specs//spec.md` +- Tasks: `openspec/changes//tasks.md` + +## Governance Anchors +- `docs/guides/Development_Constraints.md` +- `docs/guides/Documentation_First_Development_SOP.md` +- Add the canonical design or governance docs changed by this slice. + +## Evidence + +### Commands +- `exact command` + +### Results +- `pass/fail + key summary` + +### Behavior Verification +- Happy path: +- Error/fallback path: + +### Risks and Rollback +- Risk: +- Rollback: + +### Review and Merge Gate Links +- Intent PR: +- Implementation PR: +- Review thread: +``` diff --git a/docs/governance/Documentation_Management_Model.md b/docs/governance/Documentation_Management_Model.md index 43161906..afe10ef8 100644 --- a/docs/governance/Documentation_Management_Model.md +++ b/docs/governance/Documentation_Management_Model.md @@ -144,8 +144,12 @@ CI MUST validate: - required skill files exist, - checkpoint mapping is declared, - governance frontmatter required keys by mode (OpenSpec vs TODO fallback) are present, +- feature aggregation template and active/archive feature indexes are present, +- active feature index entries resolve to real feature docs and archive index entries resolve to archived docs, +- declared `todo_ids` can be resolved back to a TODO ledger that also names the owning change-id, - feature aggregation links to real OpenSpec artifact paths, - evidence block required fields (commands/results/behavior checks/risks/rollback/review links) are present for active items, +- traceability gate command `./scripts/ci/check_governance_traceability.sh` passes, - evidence-truth gate command `./scripts/ci/check_governance_evidence_truth.sh` passes, - required assets and linkages are present. diff --git a/docs/plans/2026-03-03-enhance-doc-governance-traceability-implementation.md b/docs/plans/2026-03-03-enhance-doc-governance-traceability-implementation.md new file mode 100644 index 00000000..21702dad --- /dev/null +++ b/docs/plans/2026-03-03-enhance-doc-governance-traceability-implementation.md @@ -0,0 +1,125 @@ +# Enhance Doc Governance Traceability Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Close the next real gaps in `enhance-doc-governance-traceability` by adding a reusable feature aggregation template, explicit active/archive feature indexes, a machine-checkable governance traceability gate, and one active pilot backfill. + +**Architecture:** Keep the existing evidence-truth gate focused on feature evidence semantics, and add a separate traceability gate for document topology, metadata, and TODO/change linkage. Use one active feature doc as the pilot so the new checks prove the contract without forcing a full historical backfill. + +**Tech Stack:** Markdown governance docs, shell CI scripts, Python unittest subprocess-based gate tests, GitHub Actions. + +--- + +### Task 1: Define the docs/features governance surface + +**Files:** +- Create: `docs/features/templates/feature_aggregation_template.md` +- Create: `docs/features/archive/README.md` +- Modify: `docs/features/README.md` +- Modify: `docs/README.md` + +**Step 1: Write the failing test** + +Add a gate test that expects: +- `docs/features/templates/feature_aggregation_template.md` to exist +- `docs/features/README.md` to reference the template and active entries section +- `docs/features/archive/README.md` to exist as the archive index + +**Step 2: Run test to verify it fails** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py -k template` +Expected: FAIL because the template/archive index gate does not exist yet. + +**Step 3: Write minimal implementation** + +Create the template and archive index, then update `docs/features/README.md` and `docs/README.md` so the feature-doc lifecycle is discoverable from the docs navigation. + +**Step 4: Run test to verify it passes** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py -k template` +Expected: PASS + +### Task 2: Add the governance traceability CI gate + +**Files:** +- Create: `scripts/ci/check_governance_traceability.sh` +- Create: `tests/unit/test_governance_traceability_gate.py` +- Modify: `.github/workflows/ci-gate.yml` + +**Step 1: Write the failing test** + +Add subprocess-based tests that create a temp docs tree and assert the new gate fails when: +- the template or archive index is missing +- active feature docs are not listed in the active index +- checkpoint-to-skill mapping is missing +- a declared `todo_ids` entry cannot be found in any TODO ledger for the same change + +**Step 2: Run test to verify it fails** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py` +Expected: FAIL with missing gate/script behavior. + +**Step 3: Write minimal implementation** + +Implement `scripts/ci/check_governance_traceability.sh` with repository-root override support and wire it into `ci-gate` as a dedicated job. + +**Step 4: Run test to verify it passes** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py` +Expected: PASS + +### Task 3: Backfill one active pilot change + +**Files:** +- Modify: `docs/features/agentscope-d2-d4-thinking-transport.md` + +**Step 1: Write the failing test** + +Extend the gate test so a pilot feature doc declaring `todo_ids` must be backed by a TODO ledger that contains both the same TODO ids and the owning change-id. + +**Step 2: Run test to verify it fails** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py -k todo` +Expected: FAIL until the pilot doc and gate are aligned. + +**Step 3: Write minimal implementation** + +Add `todo_ids` to `docs/features/agentscope-d2-d4-thinking-transport.md` using the already-declared `D2-*` / `D4-*` mapping in `docs/todos/agentscope_domain_execution_todos.md`. + +**Step 4: Run test to verify it passes** + +Run: `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py -k todo` +Expected: PASS + +### Task 4: Sync the active governance change evidence + +**Files:** +- Modify: `openspec/changes/enhance-doc-governance-traceability/tasks.md` +- Modify: `docs/features/enhance-doc-governance-traceability.md` + +**Step 1: Write the failing test** + +No new automated test. This step is evidence synchronization after the new gate is green. + +**Step 2: Run verification** + +Run: +- `../../.venv/bin/python -m pytest -q tests/unit/test_governance_traceability_gate.py tests/unit/test_governance_evidence_truth_gate.py` +- `./scripts/ci/check_governance_traceability.sh` +- `./scripts/ci/check_governance_evidence_truth.sh` +- `openspec validate enhance-doc-governance-traceability --type change --strict --json --no-interactive` + +Expected: PASS + +**Step 3: Write minimal implementation** + +Mark only the tasks actually completed by this slice, and replace stale `openspec status` claims in the feature doc with fresh command results from this work. + +**Step 4: Commit** + +```bash +git add docs/features docs/README.md docs/plans/2026-03-03-enhance-doc-governance-traceability-implementation.md \ + scripts/ci/check_governance_traceability.sh tests/unit/test_governance_traceability_gate.py \ + .github/workflows/ci-gate.yml openspec/changes/enhance-doc-governance-traceability/tasks.md +git commit -m "feat(governance): add traceability gate baseline" -m "Add a feature aggregation template, active/archive feature indexes, a new governance traceability CI gate, and a pilot TODO-to-change backfill for an active change. This closes the next real gaps in enhance-doc-governance-traceability without forcing a broad historical docs rewrite." +``` diff --git a/openspec/changes/enhance-doc-governance-traceability/tasks.md b/openspec/changes/enhance-doc-governance-traceability/tasks.md index 7dac94b7..85dd3b6f 100644 --- a/openspec/changes/enhance-doc-governance-traceability/tasks.md +++ b/openspec/changes/enhance-doc-governance-traceability/tasks.md @@ -1,36 +1,53 @@ ## 1. Governance contract and template baseline -- [ ] 1.0 Publish unified docs directory taxonomy and type-to-path mapping in governance model + docs navigation. -- [ ] 1.1 Define and publish the frontmatter field contract for governance-tracked documents (required vs optional fields). -- [ ] 1.2 Create a governance aggregation template keyed by OpenSpec change-id and add one example document. -- [ ] 1.3 Add an active governance index and archive index format with explicit migration rules. +- [x] 1.0 Publish unified docs directory taxonomy and type-to-path mapping in governance model + docs navigation. + Evidence: `docs/governance/Documentation_Management_Model.md` and `docs/README.md` define the docs directory taxonomy and governance navigation contract. +- [x] 1.1 Define and publish the frontmatter field contract for governance-tracked documents (required vs optional fields). + Evidence: `docs/governance/Documentation_Management_Model.md` Section 6 defines the frontmatter contract by mode. +- [x] 1.2 Create a governance aggregation template keyed by OpenSpec change-id and add one example document. + Evidence: `docs/features/templates/feature_aggregation_template.md` plus active feature docs under `docs/features/`. +- [x] 1.3 Add an active governance index and archive index format with explicit migration rules. + Evidence: `docs/features/README.md` and `docs/features/archive/README.md`. ## 2. Documentation alignment updates -- [ ] 2.1 Update `docs/guides/Documentation_First_Development_SOP.md` with aggregation/frontmatter/checkpoint execution order. -- [ ] 2.2 Update `docs/guides/Development_Constraints.md` to require governance aggregation entry + machine-checkable mapping. -- [ ] 2.3 Update `docs/design/Design_Reconstructability_Traceability_Matrix.md` to include links to governance aggregation entries. -- [ ] 2.4 Update standards to explicitly define OpenSpec default collaboration and TODO-driven fallback collaboration. -- [ ] 2.5 Update standards to explicitly define `docs/**` as canonical full record and `openspec/**` as execution trace record. -- [ ] 2.6 Define analysis-first + master-TODO-first workflow and multi-change OpenSpec slicing policy. +- [x] 2.1 Update `docs/guides/Documentation_First_Development_SOP.md` with aggregation/frontmatter/checkpoint execution order. + Evidence: `docs/guides/Documentation_First_Development_SOP.md` Sections 1, 2, 7, and 8. +- [x] 2.2 Update `docs/guides/Development_Constraints.md` to require governance aggregation entry + machine-checkable mapping. + Evidence: `docs/guides/Development_Constraints.md` hard-gate bullets under 文档先行硬门禁. +- [x] 2.3 Update `docs/design/Design_Reconstructability_Traceability_Matrix.md` to include links to governance aggregation entries. + Evidence: `docs/design/Design_Reconstructability_Traceability_Matrix.md` Governance 聚合锚点 table. +- [x] 2.4 Update standards to explicitly define OpenSpec default collaboration and TODO-driven fallback collaboration. + Evidence: `docs/guides/Documentation_First_Development_SOP.md` Section 7 and `docs/governance/Documentation_Management_Model.md` Section 5. +- [x] 2.5 Update standards to explicitly define `docs/**` as canonical full record and `openspec/**` as execution trace record. + Evidence: `docs/guides/Documentation_First_Development_SOP.md` Section 1 and `docs/governance/Documentation_Management_Model.md` Section 1.1. +- [x] 2.6 Define analysis-first + master-TODO-first workflow and multi-change OpenSpec slicing policy. + Evidence: `docs/guides/Documentation_First_Development_SOP.md`, `docs/todos/README.md`, and `docs/governance/Documentation_Management_Model.md`. ## 3. Automation and CI checkpoint implementation -- [ ] 3.1 Implement or extend CI checks to validate aggregation entry existence when governance-scoped files change. +- [x] 3.1 Implement or extend CI checks to validate aggregation entry existence when governance-scoped files change. + Evidence: `scripts/ci/check_governance_traceability.sh`, `tests/unit/test_governance_traceability_gate.py`, and `.github/workflows/ci-gate.yml`. - [ ] 3.2 Implement or extend CI checks to validate required frontmatter fields for governance-tracked docs. - [ ] 3.3 Implement or extend CI checks to validate gap/TODO -> OpenSpec task mapping completeness. - [ ] 3.4 Implement or extend CI checks to validate master TODO -> OpenSpec change-slice mapping consistency. -- [ ] 3.5 Implement evidence truth structural gate (`scripts/ci/check_governance_evidence_truth.sh`) and wire it into `ci-gate`. +- [x] 3.5 Implement evidence truth structural gate (`scripts/ci/check_governance_evidence_truth.sh`) and wire it into `ci-gate`. + Evidence: `scripts/ci/check_governance_evidence_truth.sh`, `tests/unit/test_governance_evidence_truth_gate.py`, and `.github/workflows/ci-gate.yml`. ## 4. SOP skillization implementation -- [ ] 4.1 Define and publish a checkpoint-to-skill mapping document for governance lifecycle stages. -- [ ] 4.2 Add or update at least two governance skills under repository-managed skills: `documentation-management` and `development-workflow`. -- [ ] 4.3 Add CI validation to ensure required governance checkpoint-skill mappings are present and non-stale. -- [ ] 4.4 Define reuse contract so `documentation-management` and `development-workflow` can be reused by both OpenSpec mode and TODO fallback mode. +- [x] 4.1 Define and publish a checkpoint-to-skill mapping document for governance lifecycle stages. + Evidence: `docs/governance/Documentation_Management_Model.md` Section 7 and `docs/guides/Documentation_First_Development_SOP.md` Section 8. +- [x] 4.2 Add or update at least two governance skills under repository-managed skills: `documentation-management` and `development-workflow`. + Evidence: `.codex/skills/documentation-management/SKILL.md` and `.codex/skills/development-workflow/SKILL.md`. +- [x] 4.3 Add CI validation to ensure required governance checkpoint-skill mappings are present and non-stale. + Evidence: `scripts/ci/check_governance_traceability.sh` validates the mapping section and required skill paths. +- [x] 4.4 Define reuse contract so `documentation-management` and `development-workflow` can be reused by both OpenSpec mode and TODO fallback mode. + Evidence: `docs/governance/Documentation_Management_Model.md`, `docs/guides/Documentation_First_Development_SOP.md`, and `.codex/skills/development-workflow/SKILL.md`. ## 5. Pilot backfill and closure evidence -- [ ] 5.1 Backfill one active governance change using the new aggregation + frontmatter + skill mapping contract as pilot evidence. -- [ ] 5.2 Run governance check scripts and capture passing command output in PR evidence. +- [x] 5.1 Backfill one active governance change using the new aggregation + frontmatter + skill mapping contract as pilot evidence. + Evidence: `docs/features/agentscope-d2-d4-thinking-transport.md` now declares `todo_ids` that resolve back to `docs/todos/agentscope_domain_execution_todos.md`. +- [x] 5.2 Run governance check scripts and capture passing command output in PR evidence. - [ ] 5.3 Update TODO/archive records and mark this OpenSpec change as complete with evidence links. diff --git a/scripts/ci/check_governance_traceability.sh b/scripts/ci/check_governance_traceability.sh new file mode 100755 index 00000000..6da5b8c6 --- /dev/null +++ b/scripts/ci/check_governance_traceability.sh @@ -0,0 +1,474 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="${GOVERNANCE_TRACEABILITY_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +cd "$ROOT_DIR" + +failures=0 + +log() { + echo "[governance-traceability] $*" +} + +extract_frontmatter() { + local file="$1" + awk ' + NR == 1 && $0 == "---" {in_fm=1; next} + in_fm && $0 == "---" {exit} + in_fm {print} + ' "$file" +} + +frontmatter_scalar() { + local block="$1" + local key="$2" + awk -v key="$key" ' + $0 ~ "^[[:space:]]*" key ":[[:space:]]*" { + sub("^[[:space:]]*" key ":[[:space:]]*", "", $0) + print + exit + } + ' <<<"$block" +} + +trim_quotes() { + local value="$1" + value="${value#\"}" + value="${value%\"}" + value="${value#\'}" + value="${value%\'}" + echo "$value" +} + +frontmatter_list_items() { + local block="$1" + local key="$2" + awk -v key="$key" ' + function emit_inline(value) { + gsub(/^[[:space:]]*\[/, "", value) + gsub(/\][[:space:]]*$/, "", value) + n = split(value, parts, ",") + for (i = 1; i <= n; i++) { + item = parts[i] + gsub(/^[[:space:]]+|[[:space:]]+$/, "", item) + gsub(/^"/, "", item) + gsub(/"$/, "", item) + gsub(/^'\''/, "", item) + gsub(/'\''$/, "", item) + if (item != "") { + print item + } + } + } + + $0 ~ "^[[:space:]]*" key ":[[:space:]]*\\[[^]]*\\][[:space:]]*$" { + sub("^[[:space:]]*" key ":[[:space:]]*", "", $0) + emit_inline($0) + exit + } + + $0 ~ "^[[:space:]]*" key ":[[:space:]]*$" {in_list=1; next} + in_list && $0 ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*:[[:space:]]*/ {exit} + in_list && $0 ~ /^[[:space:]]*-[[:space:]]*/ { + sub(/^[[:space:]]*-[[:space:]]*/, "", $0) + gsub(/^"/, "", $0) + gsub(/"$/, "", $0) + gsub(/^'\''/, "", $0) + gsub(/'\''$/, "", $0) + print + } + ' <<<"$block" +} + +require_file() { + local path="$1" + local label="$2" + if [[ ! -f "$path" ]]; then + log "missing $label: $path" + failures=$((failures + 1)) + fi +} + +require_line() { + local needle="$1" + local file="$2" + local label="$3" + if ! grep -Fq -- "$needle" "$file"; then + log "missing $label in $file" + failures=$((failures + 1)) + fi +} + +require_section_line() { + local needle="$1" + local file="$2" + local heading="$3" + local label="$4" + if ! grep -Fq -- "$needle" < <(extract_markdown_section "$file" "$heading"); then + log "missing $label in $file" + failures=$((failures + 1)) + fi +} + +require_checkpoint_skill_pair() { + local checkpoint="$1" + local file="$2" + local heading="$3" + local skills_raw + shift 3 + local IFS=$'\034' + skills_raw="$*" + + if ! awk -v checkpoint="$checkpoint" -v skills="$skills_raw" -v heading="$heading" ' + function line_has_all_skills(line, skills_raw, n, i, arr) { + n = split(skills_raw, arr, "\034") + for (i = 1; i <= n; i++) { + if (arr[i] != "" && index(line, arr[i]) == 0) { + return 0 + } + } + return 1 + } + + $0 == heading {in_section=1; next} + in_section && /^##[[:space:]]+/ {exit} + in_section && index($0, "- " checkpoint " ->") == 1 { + if (line_has_all_skills($0, skills)) { + found=1 + exit + } + } + END {exit found ? 0 : 1} + ' "$file"; then + log "missing checkpoint-to-skill pair in $file" + failures=$((failures + 1)) + fi +} + +extract_markdown_section() { + local file="$1" + local heading="$2" + awk -v heading="$heading" ' + $0 == heading {in_section=1; next} + in_section && /^##[[:space:]]+/ {exit} + in_section {print} + ' "$file" +} + +extract_markdown_section_matching() { + local file="$1" + local pattern="$2" + awk -v pattern="$pattern" ' + $0 ~ /^##[[:space:]]+/ && $0 ~ pattern {in_section=1; next} + in_section && /^##[[:space:]]+/ {exit} + in_section {print} + ' "$file" +} + +escape_extended_regex() { + printf '%s' "$1" | sed -E 's/[][(){}.^$*+?|\\/]/\\&/g' +} + +file_has_discrete_token() { + local file="$1" + local token="$2" + local escaped + escaped="$(escape_extended_regex "$token")" + grep -Eq "(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)" "$file" +} + +text_has_discrete_token() { + local text="$1" + local token="$2" + local escaped + escaped="$(escape_extended_regex "$token")" + grep -Eq "(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)" <<<"$text" +} + +trim_whitespace() { + sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' <<<"$1" +} + +normalize_scope_segment() { + local segment="$1" + segment="$(trim_whitespace "$segment")" + sed -E 's/[[:space:]]*[((].*$//' <<<"$segment" +} + +todo_id_within_range() { + local todo_id="$1" + local range_start="$2" + local range_end="$3" + local todo_prefix todo_number start_prefix start_number end_prefix end_number + + if [[ ! "$todo_id" =~ ^([A-Za-z0-9]+)-([0-9]+)$ ]]; then + return 1 + fi + todo_prefix="${BASH_REMATCH[1]}" + todo_number="${BASH_REMATCH[2]}" + + if [[ ! "$range_start" =~ ^([A-Za-z0-9]+)-([0-9]+)$ ]]; then + return 1 + fi + start_prefix="${BASH_REMATCH[1]}" + start_number="${BASH_REMATCH[2]}" + + if [[ ! "$range_end" =~ ^([A-Za-z0-9]+)-([0-9]+)$ ]]; then + return 1 + fi + end_prefix="${BASH_REMATCH[1]}" + end_number="${BASH_REMATCH[2]}" + + if [[ "$todo_prefix" != "$start_prefix" || "$start_prefix" != "$end_prefix" ]]; then + return 1 + fi + + (( 10#$todo_number >= 10#$start_number && 10#$todo_number <= 10#$end_number )) +} + +scope_contains_todo_id() { + local scope="$1" + local todo_id="$2" + local segment range_start range_end + + while IFS= read -r segment; do + segment="$(normalize_scope_segment "$segment")" + [[ -z "$segment" ]] && continue + if [[ "$segment" == "$todo_id" ]]; then + return 0 + fi + if [[ "$segment" == *"~"* ]]; then + range_start="$(trim_whitespace "${segment%%~*}")" + range_end="$(trim_whitespace "${segment##*~}")" + if todo_id_within_range "$todo_id" "$range_start" "$range_end"; then + return 0 + fi + fi + done < <(tr ',' '\n' <<<"$scope") + + return 1 +} + +section_has_index_entry() { + local index_file="$1" + local section_heading="$2" + local path="$3" + grep -Fq -- "\`$path\`" < <(extract_markdown_section "$index_file" "$section_heading") +} + +claim_ledger_has_tokens_in_same_record() { + local file="$1" + local first_token="$2" + local second_token="$3" + local line scope_field + + while IFS= read -r line; do + [[ "$line" == \|* ]] || continue + if ! text_has_discrete_token "$line" "$second_token"; then + continue + fi + if text_has_discrete_token "$line" "$first_token"; then + return 0 + fi + scope_field="$(awk -F'|' 'NF >= 4 {field=$3; gsub(/^[[:space:]]+|[[:space:]]+$/, "", field); print field}' <<<"$line")" + if [[ -n "$scope_field" ]] && scope_contains_todo_id "$scope_field" "$first_token"; then + return 0 + fi + done < <(extract_markdown_section_matching "$file" "Claim Ledger") + + return 1 +} + +check_index_entry_targets() { + local index_file="$1" + local section_heading="$2" + local stale_label="$3" + local valid_regex="$4" + local invalid_regex="$5" + local path basename + + while IFS= read -r path; do + [[ -z "$path" ]] && continue + if [[ ! -f "$path" ]]; then + log "stale $stale_label index entry in $index_file: $path" + failures=$((failures + 1)) + continue + fi + if [[ -n "$invalid_regex" && "$path" =~ $invalid_regex ]]; then + log "invalid $stale_label index entry path in $index_file: $path" + failures=$((failures + 1)) + continue + fi + basename="${path##*/}" + if [[ "$basename" == "README.md" ]]; then + log "invalid $stale_label index entry path in $index_file: $path" + failures=$((failures + 1)) + continue + fi + if [[ ! "$path" =~ $valid_regex ]]; then + log "invalid $stale_label index entry path in $index_file: $path" + failures=$((failures + 1)) + fi + done < <( + extract_markdown_section "$index_file" "$section_heading" \ + | sed -n 's/.*`\([^`]*\)`.*/\1/p' + ) +} + +check_feature_indexes() { + local active_index="docs/features/README.md" + local archive_index="docs/features/archive/README.md" + local template="docs/features/templates/feature_aggregation_template.md" + local file + + require_file "$active_index" "active feature index" + require_file "$archive_index" "feature archive index" + require_file "$template" "feature aggregation template" + + if [[ -f "$active_index" ]]; then + require_line "docs/features/templates/feature_aggregation_template.md" "$active_index" "template link" + require_line "docs/features/archive/README.md" "$active_index" "archive index link" + require_line "## Active Entries" "$active_index" "Active Entries section" + require_line "## Migration Rules" "$active_index" "Migration Rules section" + fi + + if [[ -f "$archive_index" ]]; then + require_line "## Archived Entries" "$archive_index" "Archived Entries section" + require_line "## Archive Migration Rules" "$archive_index" "Archive Migration Rules section" + fi + + while IFS= read -r file; do + if ! section_has_index_entry "$active_index" "## Active Entries" "$file"; then + log "missing active feature index entry for $file" + failures=$((failures + 1)) + fi + done < <(find docs/features -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) + + while IFS= read -r file; do + if ! section_has_index_entry "$archive_index" "## Archived Entries" "$file"; then + log "missing archived feature index entry for $file" + failures=$((failures + 1)) + fi + done < <(find docs/features/archive -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) + + check_index_entry_targets "$active_index" "## Active Entries" "active feature" '^docs/features/[^/]+\.md$' '^docs/features/archive/' + check_index_entry_targets "$archive_index" "## Archived Entries" "archived feature" '^docs/features/archive/[^/]+\.md$' "" +} + +check_checkpoint_skill_mapping() { + local model="docs/governance/Documentation_Management_Model.md" + + require_file "$model" "documentation management model" + require_file ".codex/skills/documentation-management/SKILL.md" "documentation-management skill" + require_file ".codex/skills/development-workflow/SKILL.md" "development-workflow skill" + + if [[ -f "$model" ]]; then + require_line "## 7. Checkpoint-to-Skill Mapping" "$model" "checkpoint-to-skill mapping" + require_line "documentation-management" "$model" "documentation-management mapping" + require_line "development-workflow" "$model" "development-workflow mapping" + require_line "kickoff" "$model" "kickoff checkpoint" + require_line "execution-sync" "$model" "execution-sync checkpoint" + require_line "verification" "$model" "verification checkpoint" + require_line "review-merge-gate" "$model" "review-merge-gate checkpoint" + require_line "completion-archive" "$model" "completion-archive checkpoint" + require_checkpoint_skill_pair "kickoff" "$model" "## 7. Checkpoint-to-Skill Mapping" '`development-workflow`' '`documentation-management`' + require_checkpoint_skill_pair "execution-sync" "$model" "## 7. Checkpoint-to-Skill Mapping" '`development-workflow`' '`documentation-management`' + require_checkpoint_skill_pair "verification" "$model" "## 7. Checkpoint-to-Skill Mapping" '`development-workflow`' + require_checkpoint_skill_pair "review-merge-gate" "$model" "## 7. Checkpoint-to-Skill Mapping" '`development-workflow`' '`documentation-management`' + require_checkpoint_skill_pair "completion-archive" "$model" "## 7. Checkpoint-to-Skill Mapping" '`development-workflow`' '`documentation-management`' + fi +} + +check_feature_doc() { + local file="$1" + local frontmatter mode doc_kind key change_id todo_id matched candidate + local change_ids_raw todo_ids_raw + + frontmatter="$(extract_frontmatter "$file")" + if [[ -z "$frontmatter" ]]; then + log "missing frontmatter in $file" + failures=$((failures + 1)) + return + fi + + for key in doc_kind topics created updated status mode; do + if [[ -z "$(frontmatter_scalar "$frontmatter" "$key")" ]]; then + log "missing required frontmatter field '$key' in $file" + failures=$((failures + 1)) + fi + done + + doc_kind="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "doc_kind")")" + if [[ "$doc_kind" != "feature" ]]; then + log "feature aggregation doc must declare doc_kind: feature in $file" + failures=$((failures + 1)) + fi + + mode="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "mode")")" + if [[ "$mode" == "todo_fallback" ]]; then + if [[ -z "$(frontmatter_scalar "$frontmatter" "topic_slug")" ]]; then + log "missing required frontmatter field 'topic_slug' in $file" + failures=$((failures + 1)) + fi + return + fi + + change_ids_raw="$(frontmatter_list_items "$frontmatter" "change_ids")" + if [[ -z "$change_ids_raw" ]]; then + log "missing required frontmatter field 'change_ids' in $file" + failures=$((failures + 1)) + return + fi + + while IFS= read -r change_id; do + [[ -z "$change_id" ]] && continue + if [[ ! -f "openspec/changes/$change_id/tasks.md" ]] && \ + ! find openspec/changes/archive -type f \( -path "*/$change_id/tasks.md" -o -path "*/????-??-??-$change_id/tasks.md" \) | grep -q .; then + log "missing OpenSpec tasks artifact for change_id '$change_id' declared in $file" + failures=$((failures + 1)) + fi + done <<<"$change_ids_raw" + + todo_ids_raw="$(frontmatter_list_items "$frontmatter" "todo_ids")" + if [[ -z "$todo_ids_raw" ]]; then + return + fi + + while IFS= read -r todo_id; do + [[ -z "$todo_id" ]] && continue + matched=0 + while IFS= read -r candidate; do + [[ -z "$candidate" ]] && continue + while IFS= read -r change_id; do + [[ -z "$change_id" ]] && continue + if claim_ledger_has_tokens_in_same_record "$candidate" "$todo_id" "$change_id"; then + matched=1 + break + fi + done <<<"$change_ids_raw" + if [[ "$matched" -eq 1 ]]; then + break + fi + done < <(find docs/todos -type f -name '*.md' | sort) + + if [[ "$matched" -eq 0 ]]; then + log "missing TODO mapping for feature doc $file: $todo_id" + failures=$((failures + 1)) + fi + done <<<"$todo_ids_raw" +} + +check_feature_indexes +check_checkpoint_skill_mapping + +while IFS= read -r file; do + check_feature_doc "$file" +done < <(find docs/features -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) + +if [[ $failures -gt 0 ]]; then + log "failed with $failures issue(s)" + exit 1 +fi + +log "passed" diff --git a/tests/unit/test_governance_traceability_gate.py b/tests/unit/test_governance_traceability_gate.py new file mode 100644 index 00000000..410abff9 --- /dev/null +++ b/tests/unit/test_governance_traceability_gate.py @@ -0,0 +1,503 @@ +"""Regression tests for the governance traceability CI gate.""" + +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +GATE_SCRIPT = REPO_ROOT / "scripts" / "ci" / "check_governance_traceability.sh" + + +FEATURE_DOC = """--- +change_ids: ["demo-change"] +doc_kind: feature +topics: ["governance", "traceability"] +todo_ids: ["D2-1", "D2-2"] +created: 2026-03-03 +updated: 2026-03-03 +status: draft +mode: openspec +--- + +# Feature: demo-change + +## OpenSpec Artifacts +- Proposal: `openspec/changes/demo-change/proposal.md` +- Design: `openspec/changes/demo-change/design.md` +- Tasks: `openspec/changes/demo-change/tasks.md` + +## Evidence +### Commands +- `pytest -q` + +### Results +- pass + +### Behavior Verification +- Happy path recorded. + +### Risks and Rollback +- Risk: demo only. +- Rollback: revert demo. + +### Review and Merge Gate Links +- Intent PR: https://github.com/example/repo/pull/1 +- Implementation PR: https://github.com/example/repo/pull/2 +- Review thread: https://github.com/example/repo/pull/2#discussion_r1 +""" + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _base_tree(root: Path) -> None: + _write( + root / "docs" / "features" / "README.md", + """# Feature Aggregation Docs + +## Template +- `docs/features/templates/feature_aggregation_template.md` + +## Active Entries +- `docs/features/demo-change.md` + +## Archive Index +- `docs/features/archive/README.md` + +## Migration Rules +- Move completed docs to `docs/features/archive/`. +""", + ) + _write( + root / "docs" / "features" / "archive" / "README.md", + """# Feature Aggregation Archive + +## Archived Entries +- `docs/features/archive/archived-change.md` + +## Archive Migration Rules +- Move docs here only after closeout. +""", + ) + _write(root / "docs" / "features" / "archive" / "archived-change.md", "# archived\n") + _write( + root / "docs" / "features" / "templates" / "feature_aggregation_template.md", + """# Feature Aggregation Template + +```yaml +--- +change_ids: [""] +doc_kind: feature +topics: ["..."] +created: YYYY-MM-DD +updated: YYYY-MM-DD +status: draft +mode: openspec +--- +``` +""", + ) + _write(root / "docs" / "features" / "demo-change.md", FEATURE_DOC) + _write( + root / "docs" / "todos" / "demo_master_todo.md", + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-DEMO | D2-1~D2-2 | demo | active | 2026-03-03 | 2026-03-10 | `demo-change` | demo | +""", + ) + _write(root / "openspec" / "changes" / "demo-change" / "proposal.md", "# proposal\n") + _write(root / "openspec" / "changes" / "demo-change" / "design.md", "# design\n") + _write(root / "openspec" / "changes" / "demo-change" / "tasks.md", "- [ ] demo\n") + _write( + root / "docs" / "governance" / "Documentation_Management_Model.md", + """# Documentation Management Model + +## 7. Checkpoint-to-Skill Mapping +- kickoff -> `development-workflow` + `documentation-management` +- execution-sync -> `development-workflow` + `documentation-management` +- verification -> `development-workflow` +- review-merge-gate -> `development-workflow` + `documentation-management` +- completion-archive -> `development-workflow` + `documentation-management` +""", + ) + _write(root / ".codex" / "skills" / "documentation-management" / "SKILL.md", "# doc skill\n") + _write(root / ".codex" / "skills" / "development-workflow" / "SKILL.md", "# workflow skill\n") + + +class GovernanceTraceabilityGateTests(unittest.TestCase): + def _run_gate(self, mutate=None) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + _base_tree(root) + if mutate is not None: + mutate(root) + env = os.environ.copy() + env["GOVERNANCE_TRACEABILITY_ROOT_DIR"] = str(root) + return subprocess.run( + [str(GATE_SCRIPT)], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + def test_gate_passes_with_template_index_skill_mapping_and_todo_mapping(self) -> None: + result = self._run_gate() + + self.assertEqual(result.returncode, 0) + self.assertIn("passed", result.stdout) + + def test_gate_fails_when_feature_template_is_missing(self) -> None: + result = self._run_gate( + lambda root: (root / "docs" / "features" / "templates" / "feature_aggregation_template.md").unlink() + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing feature aggregation template", result.stdout) + + def test_gate_fails_when_active_feature_doc_is_not_indexed(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace("- `docs/features/demo-change.md`\n", ""), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing active feature index entry", result.stdout) + + def test_gate_requires_active_membership_inside_active_entries_section(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace( + "## Migration Rules\n- Move completed docs to `docs/features/archive/`.\n", + "## Migration Rules\n" + "- Move completed docs to `docs/features/archive/`.\n" + "- Migration note keeps `docs/features/demo-change.md` as an example path.\n", + ).replace("- `docs/features/demo-change.md`\n", ""), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing active feature index entry", result.stdout) + + def test_gate_fails_when_active_index_contains_stale_feature_path(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace( + "## Archive Index\n", + "- `docs/features/missing-change.md`\n\n## Archive Index\n", + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("stale active feature index entry", result.stdout) + + def test_gate_rejects_archive_paths_inside_active_entries(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace( + "## Archive Index\n", + "- `docs/features/archive/README.md`\n\n## Archive Index\n", + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid active feature index entry path", result.stdout) + + def test_gate_rejects_active_index_readme_entry(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace( + "## Archive Index\n", + "- `docs/features/README.md`\n\n## Archive Index\n", + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid active feature index entry path", result.stdout) + + def test_gate_rejects_archive_index_readme_entry(self) -> None: + def mutate(root: Path) -> None: + readme = root / "docs" / "features" / "archive" / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace( + "## Archive Migration Rules\n", + "- `docs/features/archive/README.md`\n\n## Archive Migration Rules\n", + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid archived feature index entry path", result.stdout) + + def test_gate_fails_when_checkpoint_skill_mapping_is_missing(self) -> None: + def mutate(root: Path) -> None: + model = root / "docs" / "governance" / "Documentation_Management_Model.md" + model.write_text("# Documentation Management Model\n", encoding="utf-8") + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing checkpoint-to-skill mapping", result.stdout) + + def test_gate_requires_all_declared_lifecycle_checkpoints(self) -> None: + def mutate(root: Path) -> None: + model = root / "docs" / "governance" / "Documentation_Management_Model.md" + model.write_text( + """# Documentation Management Model + +## 7. Checkpoint-to-Skill Mapping +- kickoff -> `development-workflow` + `documentation-management` +- verification -> `development-workflow` +- completion-archive -> `documentation-management` +""", + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing execution-sync checkpoint", result.stdout) + self.assertIn("missing review-merge-gate checkpoint", result.stdout) + + def test_gate_requires_explicit_checkpoint_to_skill_pairs(self) -> None: + def mutate(root: Path) -> None: + model = root / "docs" / "governance" / "Documentation_Management_Model.md" + model.write_text( + """# Documentation Management Model + +## 7. Checkpoint-to-Skill Mapping + +Required lifecycle checkpoints MUST be skillized: +- kickoff +- execution-sync +- verification +- review-merge-gate +- completion-archive + +Skill contract (minimum two skills): +- management skill: `.codex/skills/documentation-management/SKILL.md` +- workflow skill: `.codex/skills/development-workflow/SKILL.md` +""", + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing checkpoint-to-skill pair", result.stdout) + + def test_gate_fails_when_declared_todo_id_has_no_matching_todo_ledger(self) -> None: + def mutate(root: Path) -> None: + (root / "docs" / "todos" / "demo_master_todo.md").write_text("# empty\n", encoding="utf-8") + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing TODO mapping for feature doc", result.stdout) + + def test_gate_matches_todo_ids_as_discrete_tokens(self) -> None: + def mutate(root: Path) -> None: + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace('todo_ids: ["D2-1", "D2-2"]', 'todo_ids: ["D2-1"]'), + encoding="utf-8", + ) + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-DEMO | D2-10 | demo | active | 2026-03-03 | 2026-03-10 | `demo-change` | demo | +""", + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing TODO mapping for feature doc", result.stdout) + + def test_gate_requires_todo_and_change_in_same_ledger_record(self) -> None: + def mutate(root: Path) -> None: + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace('todo_ids: ["D2-1", "D2-2"]', 'todo_ids: ["D2-1"]'), + encoding="utf-8", + ) + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-TODO | D2-1 | demo | active | 2026-03-03 | 2026-03-10 | `other-change` | wrong change | +| CLM-CHANGE | D9-9 | demo | active | 2026-03-03 | 2026-03-10 | `demo-change` | wrong todo | +""", + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing TODO mapping for feature doc", result.stdout) + + def test_gate_requires_claim_ledger_record_for_todo_change_mapping(self) -> None: + def mutate(root: Path) -> None: + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace('todo_ids: ["D2-1", "D2-2"]', 'todo_ids: ["D2-1"]'), + encoding="utf-8", + ) + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-DEMO | D9-9 | demo | active | 2026-03-03 | 2026-03-10 | `other-change` | wrong claim | + +## Detail Board +| ID | OpenSpec Change | Status | +|---|---|---| +| D2-1 | `demo-change` | done | +""", + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing TODO mapping for feature doc", result.stdout) + + def test_gate_accepts_date_prefixed_archived_change_tasks(self) -> None: + def mutate(root: Path) -> None: + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace('change_ids: ["demo-change"]', 'change_ids: ["archived-change"]'), + encoding="utf-8", + ) + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + todo_doc.read_text(encoding="utf-8").replace("`demo-change`", "`archived-change`"), + encoding="utf-8", + ) + active_change_dir = root / "openspec" / "changes" / "demo-change" + for path in active_change_dir.iterdir(): + path.unlink() + active_change_dir.rmdir() + _write( + root / "openspec" / "changes" / "archive" / "2026-03-03-archived-change" / "tasks.md", + "- [x] archived\n", + ) + + result = self._run_gate(mutate) + + self.assertEqual(result.returncode, 0) + self.assertIn("passed", result.stdout) + + def test_gate_accepts_todo_ids_covered_by_claim_scope_for_same_change(self) -> None: + def mutate(root: Path) -> None: + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-DEMO | D2-1~D2-4, D4-1~D4-4 | demo | active | 2026-03-03 | 2026-03-10 | `demo-change` | demo | + +## Detail Board +| ID | Task | Status | +|---|---|---| +| D2-1 | task 1 | done | +| D2-2 | task 2 | done | +| D4-3 | task 3 | done | +""", + encoding="utf-8", + ) + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace( + 'todo_ids: ["D2-1", "D2-2"]', + 'todo_ids: ["D2-2", "D4-3"]', + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertEqual(result.returncode, 0) + self.assertIn("passed", result.stdout) + + def test_gate_accepts_claim_scope_range_without_explicit_todo_token(self) -> None: + def mutate(root: Path) -> None: + todo_doc = root / "docs" / "todos" / "demo_master_todo.md" + todo_doc.write_text( + """# Demo TODO + +## Claim Ledger +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Notes | +|---|---|---|---|---|---|---|---| +| CLM-DEMO | D2-1~D2-4, D4-1~D4-4 | demo | active | 2026-03-03 | 2026-03-10 | `demo-change` | demo | + +## Detail Board +| ID | Task | Status | +|---|---|---| +| D2-1 | task 1 | done | +| D2-2 | task 2 | done | +""", + encoding="utf-8", + ) + feature_doc = root / "docs" / "features" / "demo-change.md" + feature_doc.write_text( + feature_doc.read_text(encoding="utf-8").replace( + 'todo_ids: ["D2-1", "D2-2"]', + 'todo_ids: ["D4-3"]', + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertEqual(result.returncode, 0) + self.assertIn("passed", result.stdout) + + +if __name__ == "__main__": + unittest.main()