diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml index 20afe8dd..feb272a7 100644 --- a/.github/workflows/ci-gate.yml +++ b/.github/workflows/ci-gate.yml @@ -157,6 +157,17 @@ jobs: - name: Check governance traceability contract (template + indexes + TODO linkage) run: ./scripts/ci/check_governance_traceability.sh + failure-ownership-map: + name: failure-ownership-map + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify failure-test ownership mapping + run: python scripts/ci/check_test_failure_ownership.py + governance-intent-gate: name: governance-intent-gate if: github.event_name == 'pull_request' || github.event_name == 'merge_group' diff --git a/dare_framework/checkpoint/defaults.py b/dare_framework/checkpoint/defaults.py new file mode 100644 index 00000000..cf6b6a89 --- /dev/null +++ b/dare_framework/checkpoint/defaults.py @@ -0,0 +1,200 @@ +"""Supported default checkpoint implementations and contributors. + +This module keeps legacy checkpoint symbols importable after checkpoint internals +were consolidated into ``dare_framework.checkpoint.kernel``. +""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import Any +from uuid import uuid4 + +from dare_framework.context.types import Message + +STM = "stm" +SESSION_STATE = "session_state" +SESSION_CONTEXT = "session_context" +WORKSPACE_FILES = "workspace_files" + + +class MemoryCheckpointStore: + """In-memory checkpoint payload store kept for facade compatibility.""" + + def __init__(self) -> None: + self._store: dict[str, dict[str, Any]] = {} + + def put(self, checkpoint_id: str, payload: dict[str, Any]) -> None: + self._store[checkpoint_id] = dict(payload) + + def get(self, checkpoint_id: str) -> dict[str, Any] | None: + if checkpoint_id not in self._store: + return None + return dict(self._store[checkpoint_id]) + + def delete(self, checkpoint_id: str) -> bool: + if checkpoint_id in self._store: + del self._store[checkpoint_id] + return True + return False + + +def _scope_keys(scope: Any, method_name: str) -> list[str]: + method = getattr(scope, method_name, None) + if callable(method): + values = method() + if isinstance(values, (list, tuple, set)): + return [str(v) for v in values] + if isinstance(scope, (list, tuple, set)): + return [str(v) for v in scope] + return [] + + +class DefaultCheckpointSaveRestore: + """Legacy save/restore coordinator over contributor payload components.""" + + def __init__(self, store: MemoryCheckpointStore, contributors: list[Any]) -> None: + self._store = store + self._contributors = { + str(c.component_key): c + for c in contributors + if getattr(c, "component_key", None) is not None + } + + def save(self, scope: Any, ctx: Any) -> str: + payload: dict[str, Any] = {} + for key in _scope_keys(scope, "keys_for_save"): + contributor = self._contributors.get(key) + if contributor is None: + continue + payload[key] = contributor.serialize(ctx) + checkpoint_id = uuid4().hex[:16] + self._store.put(checkpoint_id, payload) + return checkpoint_id + + def restore(self, checkpoint_id: str, scope: Any, ctx: Any) -> None: + payload = self._store.get(checkpoint_id) + if payload is None: + raise LookupError(f"Checkpoint not found: {checkpoint_id!r}") + for key in _scope_keys(scope, "keys_for_restore"): + if key not in payload: + continue + contributor = self._contributors.get(key) + if contributor is None: + continue + contributor.deserialize_and_apply(payload[key], ctx) + + +class StmContributor: + """Serialize/restore short-term memory messages.""" + + component_key = STM + + def serialize(self, ctx: Any) -> list[dict[str, Any]]: + context = getattr(ctx, "context", None) + if context is None: + return [] + messages = context.stm_get() + return [ + { + "role": m.role, + "content": m.content, + "name": m.name, + "metadata": dict(getattr(m, "metadata", {}) or {}), + } + for m in messages + ] + + def deserialize_and_apply(self, payload: list[Any], ctx: Any) -> None: + context = getattr(ctx, "context", None) + if context is None: + return + context.stm_clear() + for item in payload or []: + if not isinstance(item, dict): + continue + context.stm_add( + Message( + role=item.get("role", "user"), + content=item.get("content", ""), + name=item.get("name"), + metadata=dict(item.get("metadata") or {}), + ) + ) + + +class SessionStateContributor: + """Serialize/restore minimal session-state fields.""" + + component_key = SESSION_STATE + + def serialize(self, ctx: Any) -> dict[str, Any] | None: + state = getattr(ctx, "session_state", None) + if state is None: + return None + try: + return asdict(state) + except Exception: + return { + "current_milestone_idx": getattr(state, "current_milestone_idx", None), + "task_id": getattr(state, "task_id", None), + "run_id": getattr(state, "run_id", None), + } + + def deserialize_and_apply(self, payload: Any, ctx: Any) -> None: + state = getattr(ctx, "session_state", None) + if state is None or not isinstance(payload, dict): + return + if "current_milestone_idx" in payload and hasattr(state, "current_milestone_idx"): + state.current_milestone_idx = payload["current_milestone_idx"] + + +class SessionContextContributor: + """Serialize session context for audit trails (restore intentionally no-op).""" + + component_key = SESSION_CONTEXT + + def serialize(self, ctx: Any) -> dict[str, Any] | None: + session_context = getattr(ctx, "session_context", None) + if session_context is None: + return None + try: + serialized = asdict(session_context) + except Exception: + return { + "session_id": getattr(session_context, "session_id", None), + "task_id": getattr(session_context, "task_id", None), + } + config_value = serialized.get("config") + if config_value is not None and not isinstance(config_value, dict): + try: + serialized["config"] = asdict(config_value) + except Exception: + serialized["config"] = None + return serialized + + def deserialize_and_apply(self, payload: Any, ctx: Any) -> None: + _ = (payload, ctx) + # SessionContext is construction-time state; legacy restore path keeps this as no-op. + + +class WorkspaceGitContributor: + """Compatibility no-op contributor after workspace-git checkpoint removal.""" + + component_key = WORKSPACE_FILES + + def serialize(self, ctx: Any) -> dict[str, Any]: + _ = ctx + return {} + + def deserialize_and_apply(self, payload: Any, ctx: Any) -> None: + _ = (payload, ctx) + +__all__ = [ + "MemoryCheckpointStore", + "DefaultCheckpointSaveRestore", + "StmContributor", + "WorkspaceGitContributor", + "SessionStateContributor", + "SessionContextContributor", +] diff --git a/dare_framework/embedding/__init__.py b/dare_framework/embedding/__init__.py index 8d1de770..60d4b447 100644 --- a/dare_framework/embedding/__init__.py +++ b/dare_framework/embedding/__init__.py @@ -1,8 +1,8 @@ """embedding domain facade.""" -from dare_framework.embedding.interfaces import IEmbeddingAdapter -from dare_framework.embedding.types import EmbeddingOptions, EmbeddingResult -from dare_framework.embedding._internal.openai_embedding import OpenAIEmbeddingAdapter +from dare_framework.embedding.interfaces import IEmbeddingAdapter +from dare_framework.embedding.types import EmbeddingOptions, EmbeddingResult +from dare_framework.embedding.defaults import OpenAIEmbeddingAdapter __all__ = [ "IEmbeddingAdapter", diff --git a/dare_framework/embedding/defaults.py b/dare_framework/embedding/defaults.py new file mode 100644 index 00000000..88e38524 --- /dev/null +++ b/dare_framework/embedding/defaults.py @@ -0,0 +1,5 @@ +"""Supported default embedding implementations.""" + +from dare_framework.embedding._internal.openai_embedding import OpenAIEmbeddingAdapter + +__all__ = ["OpenAIEmbeddingAdapter"] diff --git a/dare_framework/event/__init__.py b/dare_framework/event/__init__.py index a7ac3831..703a2740 100644 --- a/dare_framework/event/__init__.py +++ b/dare_framework/event/__init__.py @@ -1,6 +1,6 @@ """event domain facade.""" -from dare_framework.event._internal.sqlite_event_log import DefaultEventLog, SQLiteEventLog +from dare_framework.event.defaults import DefaultEventLog, SQLiteEventLog from dare_framework.event.kernel import IEventLog from dare_framework.event.types import Event, RuntimeSnapshot diff --git a/dare_framework/event/defaults.py b/dare_framework/event/defaults.py new file mode 100644 index 00000000..d5eee182 --- /dev/null +++ b/dare_framework/event/defaults.py @@ -0,0 +1,5 @@ +"""Supported default event-log implementations.""" + +from dare_framework.event._internal.sqlite_event_log import DefaultEventLog, SQLiteEventLog + +__all__ = ["SQLiteEventLog", "DefaultEventLog"] diff --git a/dare_framework/hook/__init__.py b/dare_framework/hook/__init__.py index 3621b4f1..1d1d3c25 100644 --- a/dare_framework/hook/__init__.py +++ b/dare_framework/hook/__init__.py @@ -2,7 +2,7 @@ from dare_framework.hook.interfaces import IHookManager from dare_framework.hook.kernel import IExtensionPoint, IHook, HookFn -from dare_framework.hook._internal.hook_extension_point import HookExtensionPoint +from dare_framework.hook.defaults import HookExtensionPoint from dare_framework.hook.types import HookDecision, HookEnvelope, HookPhase, HookResult __all__ = [ diff --git a/dare_framework/hook/defaults.py b/dare_framework/hook/defaults.py new file mode 100644 index 00000000..2037a192 --- /dev/null +++ b/dare_framework/hook/defaults.py @@ -0,0 +1,5 @@ +"""Supported default extension-point implementation.""" + +from dare_framework.hook._internal.hook_extension_point import HookExtensionPoint + +__all__ = ["HookExtensionPoint"] diff --git a/dare_framework/plan/__init__.py b/dare_framework/plan/__init__.py index 9daa9392..59716d99 100644 --- a/dare_framework/plan/__init__.py +++ b/dare_framework/plan/__init__.py @@ -23,8 +23,7 @@ ValidatedStep, VerifyResult, ) -from dare_framework.plan._internal.default_planner import DefaultPlanner -from dare_framework.plan._internal.default_remediator import DefaultRemediator +from dare_framework.plan.defaults import DefaultPlanner, DefaultRemediator __all__ = [ # Interfaces diff --git a/dare_framework/plan/defaults.py b/dare_framework/plan/defaults.py new file mode 100644 index 00000000..6f0edc33 --- /dev/null +++ b/dare_framework/plan/defaults.py @@ -0,0 +1,6 @@ +"""Supported default planner/remediator implementations.""" + +from dare_framework.plan._internal.default_planner import DefaultPlanner +from dare_framework.plan._internal.default_remediator import DefaultRemediator + +__all__ = ["DefaultPlanner", "DefaultRemediator"] diff --git a/dare_framework/security/__init__.py b/dare_framework/security/__init__.py index add8eb8a..ade0d6f6 100644 --- a/dare_framework/security/__init__.py +++ b/dare_framework/security/__init__.py @@ -1,6 +1,5 @@ """Security domain facade.""" -from dare_framework.security._internal.default_security_boundary import DefaultSecurityBoundary from dare_framework.security.errors import ( SECURITY_APPROVAL_MANAGER_MISSING, SECURITY_POLICY_CHECK_FAILED, @@ -8,7 +7,11 @@ SECURITY_TRUST_DERIVATION_FAILED, SecurityBoundaryError, ) -from dare_framework.security.impl import NoOpSecurityBoundary, PolicySecurityBoundary +from dare_framework.security.impl import ( + DefaultSecurityBoundary, + NoOpSecurityBoundary, + PolicySecurityBoundary, +) from dare_framework.security.kernel import ISecurityBoundary from dare_framework.security.types import PolicyDecision, RiskLevel, SandboxSpec, TrustedInput diff --git a/dare_framework/security/impl/default_security_boundary.py b/dare_framework/security/impl/default_security_boundary.py index 46b17f4b..e723783e 100644 --- a/dare_framework/security/impl/default_security_boundary.py +++ b/dare_framework/security/impl/default_security_boundary.py @@ -5,6 +5,9 @@ import inspect from typing import Any, Callable, Iterable +from dare_framework.security._internal.default_security_boundary import ( + DefaultSecurityBoundary as LegacyDefaultSecurityBoundary, +) from dare_framework.security.errors import ( SECURITY_TRUST_DERIVATION_FAILED, SecurityBoundaryError, @@ -259,8 +262,8 @@ async def execute_safe( return await _execute_callable(fn) -# Backward-compatible alias for legacy references. -DefaultSecurityBoundary = PolicySecurityBoundary +# Preserve historical facade behavior where `DefaultSecurityBoundary` is permissive. +DefaultSecurityBoundary = LegacyDefaultSecurityBoundary __all__ = [ diff --git a/dare_framework/transport/__init__.py b/dare_framework/transport/__init__.py index 69197374..326be795 100644 --- a/dare_framework/transport/__init__.py +++ b/dare_framework/transport/__init__.py @@ -16,7 +16,7 @@ ActionHandlerDispatcher, ResourceAction, ) -from dare_framework.transport._internal import ( +from dare_framework.transport.adapters import ( DefaultAgentChannel, DirectClientChannel, StdioClientChannel, diff --git a/dare_framework/transport/adapters.py b/dare_framework/transport/adapters.py new file mode 100644 index 00000000..9dadb2c9 --- /dev/null +++ b/dare_framework/transport/adapters.py @@ -0,0 +1,15 @@ +"""Supported default transport channel adapters.""" + +from dare_framework.transport._internal import ( + DefaultAgentChannel, + DirectClientChannel, + StdioClientChannel, + WebSocketClientChannel, +) + +__all__ = [ + "DefaultAgentChannel", + "DirectClientChannel", + "StdioClientChannel", + "WebSocketClientChannel", +] diff --git a/docs/design/Framework_MinSurface_Review.md b/docs/design/Framework_MinSurface_Review.md index 4d80b193..bc8ec18a 100644 --- a/docs/design/Framework_MinSurface_Review.md +++ b/docs/design/Framework_MinSurface_Review.md @@ -268,3 +268,8 @@ MCP adapters, etc. - Tight coupling to concrete classes prevents safe refactors. - External users rely on internal DTOs that must then be supported forever. - Increased likelihood of breaking changes with each internal improvement. + +## 5. Implementation Note (2026-03-04) +- T0-4 facade compliance batch moved public-domain `__init__.py` imports away from direct `._internal` references into explicit public export layers (`defaults.py`, `impl`, `adapters`). +- The following facades now import only public modules directly: `checkpoint`, `embedding`, `event`, `hook`, `plan`, `security`, `transport`. +- A regression rule was added in `tests/unit/test_package_initializers_facade_pattern.py` to block future direct `._internal` imports from public facades. diff --git a/docs/features/enhance-doc-governance-traceability.md b/docs/features/enhance-doc-governance-traceability.md index 26294cf1..d9e0f39e 100644 --- a/docs/features/enhance-doc-governance-traceability.md +++ b/docs/features/enhance-doc-governance-traceability.md @@ -39,6 +39,7 @@ Unify documentation management structure, lifecycle governance, and SOP-to-skill ### Results - `../../.venv/bin/python -m pytest -q tests/unit/test_governance_intent_gate.py tests/unit/test_governance_traceability_gate.py tests/unit/test_governance_evidence_truth_gate.py`: passed (`56 passed`) after adding intent-gate regression coverage for docs-only skip behavior, implementation-path gating, governed feature-doc requirement, intent PR link extraction, merged-state enforcement, and draft-status exclusion while preserving existing traceability/evidence truth coverage. - `./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. +- Closeout sync update: traceability checks now also cover archived-feature frontmatter validation and master/detail claim consistency against OpenSpec `tasks.md` completeness for executable claims. - `./scripts/ci/check_governance_evidence_truth.sh`: passed, confirming the new traceability assets do not break the existing evidence-first contract. - `GOVERNANCE_INTENT_GATE_CHANGED_FILES=$'client/main.py\ndocs/features/enhance-doc-governance-traceability.md' GOVERNANCE_INTENT_GATE_PR_STATE_FIXTURE='zts212653/Deterministic-Agent-Runtime-Engine#126=merged' ./scripts/ci/check_governance_intent_gate.sh`: passed, confirming implementation-path changes are now hard-blocked unless governed feature docs carry a merged `Intent PR`. - `openspec validate enhance-doc-governance-traceability --type change --strict --json --no-interactive`: passed (`1/1` change valid, `0` issues). @@ -74,17 +75,17 @@ Unify documentation management structure, lifecycle governance, and SOP-to-skill ### Structured Review Report - Changed Module Boundaries / Public API: governance scope only; no new runtime public API added. -- New State: adds intent gate policy state (`implementation-path diff -> governed feature doc -> merged intent PR`) with one new gate script and one new regression test suite. +- New State: adds intent gate policy state (`implementation-path diff -> governed feature doc -> merged intent PR`) and extends traceability closeout checks for claim-to-task and master/detail consistency. - 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: intent-merge gating plus existing 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. +- Coverage and Residual Risk: intent-merge gating plus template/index/skill-mapping/TODO-linkage checks are covered; residual risk remains broader frontmatter enforcement expansion across additional governance doc families. ### Behavior Verification - Happy path: when implementation files change, CI now requires a same-PR governed feature-doc update and validates that the referenced `Intent PR` is already merged before allowing merge. - Error/fallback path: the intent gate fails deterministically when implementation changes omit governed feature docs, when feature docs miss `Intent PR` links, or when the referenced intent PR state is not `merged`. ### Risks and Rollback -- Risk: `3.2-3.4` are still open, so the governance suite still does not enforce full frontmatter coverage for every governance-tracked doc family or full master-TODO/task completeness. +- Risk: promoting `p0-gate` to a required check still depends on repository-admin permissions outside this change scope. - Risk: active/archive indexes are now explicit manual ledgers, so closeout changes that forget to update them will fail the new gate. - Risk: local runs without `GITHUB_TOKEN` need PR-state fixture or CI context for merged-state lookup. - Rollback: remove `governance-intent-gate` from `.github/workflows/ci-gate.yml` and revert `scripts/ci/check_governance_intent_gate.sh` if false positives block delivery. @@ -110,4 +111,4 @@ Unify documentation management structure, lifecycle governance, and SOP-to-skill - `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/175#discussion_r2881421738` ## Next Milestone -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. +Promote `p0-gate` to branch protection required check and then archive this change entry in the next closeout pass. diff --git a/docs/features/p0-conformance-gate.md b/docs/features/p0-conformance-gate.md index feb6a290..e0a4b014 100644 --- a/docs/features/p0-conformance-gate.md +++ b/docs/features/p0-conformance-gate.md @@ -3,7 +3,7 @@ change_ids: ["p0-conformance-gate"] doc_kind: feature topics: ["p0", "conformance", "ci-gate", "runtime-validation"] created: 2026-03-03 -updated: 2026-03-03 +updated: 2026-03-04 status: active mode: openspec --- @@ -44,6 +44,7 @@ mode: openspec - `../../.venv/bin/python -m pytest -q tests/unit/test_dare_agent_security_policy_gate.py tests/unit/test_dare_agent_security_boundary.py tests/unit/test_five_layer_agent.py` - `../../.venv/bin/python -m pytest -q tests/unit/test_p0_gate_ci.py` - `../../.venv/bin/python scripts/ci/p0_gate.py` +- `python scripts/ci/check_test_failure_ownership.py` - `openspec validate p0-conformance-gate --type change --strict --json --no-interactive` - `./scripts/ci/check_governance_evidence_truth.sh` @@ -63,9 +64,11 @@ mode: openspec `- STEP_EXEC_REGRESSION: 0 failures` `- AUDIT_CHAIN_REGRESSION: 0 failures` which confirms the repository now has a single deterministic command entrypoint for the three frozen P0 categories. +- `python scripts/ci/check_test_failure_ownership.py`: passed and now enforces category-level failure ownership integrity (`test selector -> module scope -> owner`) as a machine-checkable precondition; `.github/workflows/ci-gate.yml` now runs this as `failure-ownership-map`. - `openspec validate p0-conformance-gate --type change --strict --json --no-interactive`: passed (`1/1` change valid, `0` issues) after restoring the missing active feature aggregation record for this change. - `./scripts/ci/check_governance_evidence_truth.sh`: initially failed because the restored feature doc lacked historical PR/review links; after linking the already-landed P0 evidence PRs, the governance gate passed, and remained green after task `1.1-1.3` synchronized the gate scope matrix and rollout contract into the active docs/spec set. - `./scripts/ci/check_governance_evidence_truth.sh`: remained green after adding `docs/guides/P0_Gate_Runbook.md` plus the new navigation links in `docs/README.md` and `docs/guides/Team_Agent_Collab_Playbook.md`, confirming the operationalization docs did not break the governance acceptance pack. +- `docs/governance/branch-protection.md`: now includes a dedicated `p0-gate` required-check rollout checklist (preconditions, execution, acceptance, and evidence fields) so task `3.2` can be closed with deterministic admin-side proof. ### Behavior Verification @@ -95,6 +98,7 @@ mode: openspec ### Review and Merge Gate Links +- Intent PR: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/172` - Current continuation branch: `codex/p0-conformance-gate` - Current implementation PR: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/172` - Historical PR for task `2.4`: `https://github.com/zts212653/Deterministic-Agent-Runtime-Engine/pull/130` @@ -105,4 +109,4 @@ mode: openspec ## Next Milestone -Schedule the repo-admin follow-up for task `3.2`: add `p0-gate` to the protected-branch required checks / ruleset after this change merges. +Execute the admin rollout checklist in `docs/governance/branch-protection.md` (`P0-Gate Required Check Rollout`) and then close task `3.2` with settings + blocked/pass run evidence links. diff --git a/docs/governance/branch-protection.md b/docs/governance/branch-protection.md index ece18bd6..edb8eabb 100644 --- a/docs/governance/branch-protection.md +++ b/docs/governance/branch-protection.md @@ -36,6 +36,39 @@ If your GitHub plan supports merge queue: - summary contract: `p0-gate` must emit deterministic category labels and failing test/module pointers before it can become a required branch check - repo-admin action: after this change merges, add `p0-gate` to the protected-branch required checks list / ruleset +## P0-Gate Required Check Rollout (Admin Checklist) + +> Tracking scope: `openspec/changes/p0-conformance-gate/tasks.md` item `3.2`. + +### Preconditions + +1. `p0-conformance-gate` change has been merged to `main`. +2. Latest `main` run shows `p0-gate` green with all three categories reported. +3. `.github/workflows/ci-gate.yml` still contains job name `p0-gate` (required-check name must match exactly). + +### Execution Steps + +1. Open `Settings -> Branches` (or repository Rulesets) for `main`. +2. Enable `Require status checks to pass before merging` if not already enabled. +3. Add `p0-gate` to required status checks. +4. Keep existing required checks (`lint`, `build`, and other active phases) unchanged. +5. Save the branch protection / ruleset change. + +### Acceptance + +1. Open a test PR targeting `main` and force one `p0-gate` anchor failure; merge must be blocked. +2. Fix the failure and rerun; merge must be unblocked only after `p0-gate` is green. +3. Verify merge-queue path (if enabled) also enforces `p0-gate` on `merge_group`. + +### Evidence to Record + +1. Branch protection / ruleset screenshot or settings URL proving `p0-gate` is required. +2. One blocked PR run URL where `p0-gate` failed. +3. One unblocked PR run URL where `p0-gate` passed. +4. Update: + - `openspec/changes/p0-conformance-gate/tasks.md` item `3.2` to `done` with links + - `docs/features/p0-conformance-gate.md` `Results` and `Next Milestone` + ## Fallback if Merge Queue Is Unavailable Use pre-merge combined checks: diff --git a/docs/guides/P0_Gate_Runbook.md b/docs/guides/P0_Gate_Runbook.md index 19bb3854..30f28d88 100644 --- a/docs/guides/P0_Gate_Runbook.md +++ b/docs/guides/P0_Gate_Runbook.md @@ -21,6 +21,23 @@ p0-gate: PASS The same command is used by `.github/workflows/ci-gate.yml` job `p0-gate`. +## 1.1 Ownership Mapping Health Check + +Run ownership-map巡检 from repository root: + +```bash +python scripts/ci/check_test_failure_ownership.py +``` + +Expected success output starts with: + +```text +[failure-ownership] passed +``` + +This command is used by `.github/workflows/ci-gate.yml` job `failure-ownership-map` and enforces the +`失败测试 -> 责任模块 -> owner` mapping integrity for `p0-gate` categories. + ## 2. Category Mapping ### SECURITY_REGRESSION diff --git a/docs/todos/project_overall_todos.md b/docs/todos/project_overall_todos.md index 92518ae8..d6f6568e 100644 --- a/docs/todos/project_overall_todos.md +++ b/docs/todos/project_overall_todos.md @@ -28,8 +28,8 @@ | CLM-20260304-AG7 | T5-4 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `CLM-20260304-D6 + CLM-20260304-D8` | TODO 级认领;切片仅在 AgentScope 详细区维护。 | | CLM-20260304-AG8 | T1-2 + T1-5 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | 项目层高复杂 TODO 组待分配;切片见详细拆分区。 | | CLM-20260304-AG9 | T2-3 + T2-4 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | 项目层治理 TODO 组待分配;切片见详细拆分区。 | -| CLM-20260304-AG10 | T0-6 | lang | active | 2026-03-04 | 2026-03-11 | `pending` | `—` | 下一项 fix:P0 红灯且无上游依赖,先修复 `search_file` 路径契约回归。 | -| CLM-20260304-AG11 | T0-4 + T0-5 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | P0 未完成项补齐认领声明:facade 合规修复与失败映射责任化待分配。 | +| CLM-20260304-AG10 | T0-6 | lang | done | 2026-03-04 | 2026-03-11 | `pending` | `—` | `search_file` 路径契约回归已修复并回归通过。 | +| CLM-20260304-AG11 | T0-4 + T0-5 | lang | done | 2026-03-04 | 2026-03-11 | `pending` | `—` | T0-4/T0-5 均已完成并纳入回归与 CI 巡检。 | | CLM-20260304-AG12 | T1-3 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | P1 未完成项补齐认领声明:`ISecurityBoundary` 接入待分配。 | | CLM-20260304-AG13 | T2-1(剩余范围) + T2-2 + T5-1 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | Layer-2 策略补齐:D5 子范围已完成,剩余上下文融合/多阶段 prompt/session 补齐待分配。 | | CLM-20260304-AG14 | T3-1 + T3-2 + T3-3 + T3-4 | | planned | 2026-03-04 | 2026-03-11 | `pending` | `—` | Layer-3 工程化与文档治理未完成项补齐认领声明。 | @@ -42,11 +42,9 @@ ## 2. 当前基线 -- 测试基线(2026-03-03):`.venv/bin/pytest -q` => `1 failed, 642 passed, 12 skipped, 1 warning`。 +- 测试基线(2026-03-04):`.venv/bin/pytest -q` => `676 passed, 12 skipped, 1 warning`。 - 关键问题聚类: - - `search_file` 输出路径契约回归:实现返回绝对路径,测试期望相对路径(`tests/unit/test_v4_file_tools.py::test_search_file_finds_matching_paths`)。 - 全量 TODO / Claim / Feature 状态存在漂移(已 merge 或 archived 的 change 仍标记 active/draft)。 - - 若干 package `__init__.py` 不满足 facade 约束。 - 设计已定义但实现未闭环:plan attempt 隔离(snapshot/rollback)、Context 检索融合、完整 HITL 语义、P0 conformance gate 与治理自动化门禁。 ## 3. 优先级路线图 @@ -66,13 +64,17 @@ Evidence: `examples/05-dare-coding-agent-enhanced/cli.py`、`examples/06-dare-coding-agent-mcp/cli.py`(`_invoke_approval_action` 统一为 `invoke(action, **params)` 形态,移除 `params={...}` 调用分叉);`tests/unit/test_examples_cli.py`、`tests/unit/test_examples_cli_mcp.py`(新增 kwargs 调用契约回归) Commands: `.venv/bin/pytest -q tests/unit/test_examples_cli.py tests/unit/test_examples_cli_mcp.py` => `22 passed, 1 warning` Last Updated: `2026-03-01` -- [ ] T0-4 修复 `__init__.py` facade 违规并固化回归检查。 - Status: `planned` -- [ ] T0-5 建立“失败测试 -> 责任模块 -> owner”映射并例行巡检。 - Status: `planned` -- [ ] T0-6 修复 `search_file` 输出路径契约回归(绝对路径 vs 相对路径)。 - Status: `active` - Evidence: `.venv/bin/pytest -q` 失败用例 `tests/unit/test_v4_file_tools.py::test_search_file_finds_matching_paths`;实现位置 `dare_framework/tool/_internal/tools/search_file.py` +- [x] T0-4 修复 `__init__.py` facade 违规并固化回归检查。 + Status: `done` + Evidence: `tests/unit/test_package_initializers_facade_pattern.py` 新增“公开 facade 禁止直接 `._internal` 导入”规则并通过;`dare_framework/{checkpoint,embedding,event,hook,plan,security,transport}/__init__.py` 已切换为 `defaults` / `impl` / `adapters` 公开导出层;新增 `dare_framework/{checkpoint,embedding,event,hook,plan}/defaults.py` 与 `dare_framework/transport/adapters.py`;`.venv/bin/python -m pytest -q tests/unit/test_package_initializers_facade_pattern.py tests/unit/test_event_sqlite_event_log.py tests/unit/test_security_boundary.py tests/unit/test_embedding_openai_adapter.py tests/unit/test_default_planner.py tests/unit/test_default_remediator.py tests/unit/test_hook_extension_point_governance.py tests/unit/test_transport_channel.py tests/unit/test_execution_control.py` => `52 passed` + Last Updated: `2026-03-04` +- [x] T0-5 建立“失败测试 -> 责任模块 -> owner”映射并例行巡检。 + Status: `done` + Evidence: `scripts/ci/p0_gate.py`(category summary 新增 `owner`);`scripts/ci/check_test_failure_ownership.py`(映射完整性巡检脚本);`.github/workflows/ci-gate.yml`(新增 `failure-ownership-map` job);`docs/guides/P0_Gate_Runbook.md`(命令与排障入口更新);`.venv/bin/python -m pytest -q tests/unit/test_p0_gate_ci.py tests/unit/test_check_test_failure_ownership.py` => `7 passed` + Last Updated: `2026-03-04` +- [x] T0-6 修复 `search_file` 输出路径契约回归(绝对路径 vs 相对路径)。 + Status: `done` + Evidence: `.venv/bin/pytest -q tests/unit/test_v4_file_tools.py::test_search_file_finds_matching_paths` => `1 passed`;`.venv/bin/pytest -q` => `676 passed, 12 skipped, 1 warning` Last Updated: `2026-03-04` 验收: diff --git a/openspec/changes/enhance-doc-governance-traceability/tasks.md b/openspec/changes/enhance-doc-governance-traceability/tasks.md index 07fec7ae..5c542751 100644 --- a/openspec/changes/enhance-doc-governance-traceability/tasks.md +++ b/openspec/changes/enhance-doc-governance-traceability/tasks.md @@ -28,9 +28,12 @@ - [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. +- [x] 3.2 Implement or extend CI checks to validate required frontmatter fields for governance-tracked docs. + Evidence: `scripts/ci/check_governance_traceability.sh` now validates required frontmatter keys and value contract for active + archived feature aggregation docs; `tests/unit/test_governance_traceability_gate.py::test_gate_fails_when_archived_feature_doc_frontmatter_is_missing_required_field`. +- [x] 3.3 Implement or extend CI checks to validate gap/TODO -> OpenSpec task mapping completeness. + Evidence: `scripts/ci/check_governance_traceability.sh` now validates Claim Ledger rows (`active/done` + non-`pending`) resolve to real OpenSpec `tasks.md` artifacts; `tests/unit/test_governance_traceability_gate.py::test_gate_fails_when_done_or_active_claim_has_no_tasks_artifact`. +- [x] 3.4 Implement or extend CI checks to validate master TODO -> OpenSpec change-slice mapping consistency. + Evidence: `scripts/ci/check_governance_traceability.sh` now validates `project_overall_todos.md` `Detail Claim Ref` entries resolve in `agentscope_domain_execution_todos.md` and keep non-pending change-id consistency; `tests/unit/test_governance_traceability_gate.py::test_gate_fails_when_project_claim_detail_ref_is_missing`. - [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`. - [x] 3.6 Implement hard intent-merged-before-implementation gate and wire it into `ci-gate`. @@ -52,4 +55,5 @@ - [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. +- [x] 5.3 Update TODO/archive records and mark this OpenSpec change as complete with evidence links. + Evidence: `docs/features/enhance-doc-governance-traceability.md` updated with closeout command results and residual-risk refresh; `docs/todos/project_overall_todos.md` updated with `T0-6` status/evidence alignment and baseline refresh. diff --git a/openspec/changes/p0-conformance-gate/tasks.md b/openspec/changes/p0-conformance-gate/tasks.md index a958434f..5ebdb06e 100644 --- a/openspec/changes/p0-conformance-gate/tasks.md +++ b/openspec/changes/p0-conformance-gate/tasks.md @@ -2,7 +2,7 @@ - [x] 1.1 定义 `p0-gate` 覆盖的三类不变量与验收阈值。 Evidence: `openspec/changes/p0-conformance-gate/design.md` 已固化 `SECURITY_REGRESSION` / `STEP_EXEC_REGRESSION` / `AUDIT_CHAIN_REGRESSION` 三类 category matrix,并为 required-mode promotion 定义 “单次运行全绿” 阈值。 - Last Updated: `2026-03-03` + Last Updated: `2026-03-04` - [x] 1.2 明确每类不变量对应的测试文件与责任模块。 Evidence: `openspec/changes/p0-conformance-gate/design.md` 的 Gate Scope Matrix 已列出现有 anchor suites、后续必须补的 integration anchors,以及各 category 的 primary ownership modules。 Last Updated: `2026-03-03` @@ -37,10 +37,10 @@ Commands: `../../.venv/bin/python scripts/ci/p0_gate.py` => `p0-gate: PASS` Last Updated: `2026-03-03` - [ ] 3.2 将 `p0-gate` 配置为主分支 required check。 - Note: 该项需要 GitHub branch protection / ruleset 管理员权限;当前仓库内已完成 job 名称与 rollout 文档对齐,但尚未执行远端仓库设置。 + Note: 该项需要 GitHub branch protection / ruleset 管理员权限;当前仓库内已完成 job 名称与 rollout 文档对齐,但尚未执行远端仓库设置。执行清单见 `docs/governance/branch-protection.md` 的 `P0-Gate Required Check Rollout (Admin Checklist)` 章节,关闭时需补 settings + blocked/pass run 证据链接。 - [x] 3.3 输出标准化门禁报告(通过率、失败类型、建议排查点)。 - Evidence: `scripts/ci/p0_gate.py` 的 `format_summary()` 已固定 `PASS/FAIL + category label + failing tests + modules + action` 文本格式,并写入 `GITHUB_STEP_SUMMARY`;`tests/unit/test_p0_gate_ci.py` 锁定该 summary contract。 - Commands: `../../.venv/bin/python -m pytest -q tests/unit/test_p0_gate_ci.py` => `3 passed`;`../../.venv/bin/python scripts/ci/p0_gate.py` => `p0-gate: PASS` + Evidence: `scripts/ci/p0_gate.py` 的 `format_summary()` 已固定 `PASS/FAIL + category label + failing tests + modules + owner + action` 文本格式,并写入 `GITHUB_STEP_SUMMARY`;`scripts/ci/check_test_failure_ownership.py` + `.github/workflows/ci-gate.yml` `failure-ownership-map` job 形成例行映射巡检;`tests/unit/test_p0_gate_ci.py` 锁定 summary contract。 + Commands: `../../.venv/bin/python -m pytest -q tests/unit/test_p0_gate_ci.py` => `4 passed`;`python scripts/ci/check_test_failure_ownership.py` => `[failure-ownership] passed`;`../../.venv/bin/python scripts/ci/p0_gate.py` => `p0-gate: PASS` Last Updated: `2026-03-03` ## 4. Operationalization diff --git a/scripts/ci/check_governance_intent_gate.sh b/scripts/ci/check_governance_intent_gate.sh index 536525de..d8ec5874 100755 --- a/scripts/ci/check_governance_intent_gate.sh +++ b/scripts/ci/check_governance_intent_gate.sh @@ -221,12 +221,17 @@ main() { fi local implementation_changed="false" - local -a changed_feature_docs=() + # Store doc paths as newline-separated strings for set -u portability across + # shell versions where empty array expansion can be treated as unbound. + local changed_feature_docs="" local path while IFS= read -r path; do [[ -z "$path" ]] && continue if [[ "$path" =~ ^docs/features/[^/]+\.md$ ]] && [[ "$path" != "docs/features/README.md" ]]; then - changed_feature_docs+=("$path") + if [[ -n "$changed_feature_docs" ]]; then + changed_feature_docs+=$'\n' + fi + changed_feature_docs+="$path" fi if ! is_governance_only_path "$path"; then implementation_changed="true" @@ -238,14 +243,15 @@ main() { exit 0 fi - if [[ ${#changed_feature_docs[@]} -eq 0 ]]; then + if [[ -z "$changed_feature_docs" ]]; then log "implementation changes detected but PR must update at least one governed feature doc under docs/features/*.md" failures=$((failures + 1)) fi - local -a governed_feature_docs=() + local governed_feature_docs="" local doc frontmatter status normalized_status - for doc in "${changed_feature_docs[@]}"; do + while IFS= read -r doc; do + [[ -z "$doc" ]] && continue if [[ ! -f "$doc" ]]; then log "changed governed feature doc is missing in workspace: $doc" failures=$((failures + 1)) @@ -256,17 +262,21 @@ main() { status="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "status")")" normalized_status="$(normalize_status "$status")" if [[ "$normalized_status" == "active" || "$normalized_status" == "in_review" ]]; then - governed_feature_docs+=("$doc") + if [[ -n "$governed_feature_docs" ]]; then + governed_feature_docs+=$'\n' + fi + governed_feature_docs+="$doc" fi - done + done <<<"$changed_feature_docs" - if [[ ${#governed_feature_docs[@]} -eq 0 ]]; then + if [[ -z "$governed_feature_docs" ]]; then log "implementation changes detected but PR must update at least one governed feature doc (status active/in_review)" failures=$((failures + 1)) fi local intent_pr_url components owner repo pr_number pr_state - for doc in "${governed_feature_docs[@]}"; do + while IFS= read -r doc; do + [[ -z "$doc" ]] && continue intent_pr_url="$(extract_intent_pr_url "$doc")" if [[ -z "$intent_pr_url" ]]; then log "missing Intent PR link in $doc" @@ -294,7 +304,7 @@ main() { fi log "validated merged Intent PR for $doc: $intent_pr_url" - done + done <<<"$governed_feature_docs" if [[ $failures -gt 0 ]]; then log "failed with $failures issue(s)" diff --git a/scripts/ci/check_governance_traceability.sh b/scripts/ci/check_governance_traceability.sh index 6da5b8c6..2f1c714c 100755 --- a/scripts/ci/check_governance_traceability.sh +++ b/scripts/ci/check_governance_traceability.sh @@ -190,6 +190,57 @@ trim_whitespace() { sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' <<<"$1" } +normalize_markdown_cell() { + local value="$1" + value="$(trim_whitespace "$value")" + if [[ "$value" == '~~'*'~~' ]]; then + value="${value#\~\~}" + value="${value%\~\~}" + fi + if [[ "$value" == '`'*'`' ]]; then + value="${value#\`}" + value="${value%\`}" + fi + trim_whitespace "$value" +} + +markdown_table_column() { + local row="$1" + local column="$2" + awk -F'|' -v column="$column" ' + NF >= column + 1 { + value = $(column + 1) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + print value + } + ' <<<"$row" +} + +claim_ledger_rows() { + local file="$1" + extract_markdown_section_matching "$file" "Claim Ledger" \ + | awk ' + /^\|/ { + if ($0 ~ /^\|[[:space:]]*Claim ID[[:space:]]*\|/) next + if ($0 ~ /^\|[[:space:]-]+\|/) next + print + } + ' +} + +change_has_tasks_artifact() { + local change_id="$1" + if [[ -f "openspec/changes/$change_id/tasks.md" ]]; then + return 0 + fi + if [[ ! -d "openspec/changes/archive" ]]; then + return 1 + fi + find openspec/changes/archive -type f \ + \( -path "*/$change_id/tasks.md" -o -path "*/????-??-??-$change_id/tasks.md" \) \ + | grep -q . +} + normalize_scope_segment() { local segment="$1" segment="$(trim_whitespace "$segment")" @@ -261,21 +312,25 @@ claim_ledger_has_tokens_in_same_record() { local file="$1" local first_token="$2" local second_token="$3" - local line scope_field + local line scope_field change_field status_field while IFS= read -r line; do - [[ "$line" == \|* ]] || continue - if ! text_has_discrete_token "$line" "$second_token"; then + scope_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 2)")" + status_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 4)")" + change_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 7)")" + + if [[ "$status_field" == "deprecated" ]]; then continue fi - if text_has_discrete_token "$line" "$first_token"; then - return 0 + + if ! text_has_discrete_token "$change_field" "$second_token"; then + continue 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") + done < <(claim_ledger_rows "$file") return 1 } @@ -382,8 +437,8 @@ check_checkpoint_skill_mapping() { 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 + local frontmatter mode doc_kind key change_id todo_id matched candidate status created updated + local change_ids_raw todo_ids_raw topics_raw frontmatter="$(extract_frontmatter "$file")" if [[ -z "$frontmatter" ]]; then @@ -405,7 +460,37 @@ check_feature_doc() { failures=$((failures + 1)) fi + status="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "status")")" + if [[ -n "$status" && ! "$status" =~ ^(draft|active|done|archived)$ ]]; then + log "invalid frontmatter status '$status' in $file" + failures=$((failures + 1)) + fi + + created="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "created")")" + if [[ -n "$created" && ! "$created" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + log "invalid frontmatter created date in $file: $created" + failures=$((failures + 1)) + fi + + updated="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "updated")")" + if [[ -n "$updated" && ! "$updated" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + log "invalid frontmatter updated date in $file: $updated" + failures=$((failures + 1)) + fi + + topics_raw="$(frontmatter_list_items "$frontmatter" "topics")" + if [[ -z "$topics_raw" ]]; then + log "missing required frontmatter field 'topics' in $file" + failures=$((failures + 1)) + fi + mode="$(trim_quotes "$(frontmatter_scalar "$frontmatter" "mode")")" + if [[ "$mode" != "openspec" && "$mode" != "todo_fallback" ]]; then + log "invalid frontmatter mode '$mode' in $file" + failures=$((failures + 1)) + return + fi + if [[ "$mode" == "todo_fallback" ]]; then if [[ -z "$(frontmatter_scalar "$frontmatter" "topic_slug")" ]]; then log "missing required frontmatter field 'topic_slug' in $file" @@ -423,8 +508,7 @@ check_feature_doc() { 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 + if ! change_has_tasks_artifact "$change_id"; then log "missing OpenSpec tasks artifact for change_id '$change_id' declared in $file" failures=$((failures + 1)) fi @@ -459,6 +543,80 @@ check_feature_doc() { done <<<"$todo_ids_raw" } +check_todo_claim_to_openspec_task_mapping() { + local file line claim_id status_field change_field + + while IFS= read -r file; do + while IFS= read -r line; do + claim_id="$(normalize_markdown_cell "$(markdown_table_column "$line" 1)")" + status_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 4)")" + change_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 7)")" + + [[ -z "$claim_id" || -z "$status_field" ]] && continue + [[ "$status_field" != "active" && "$status_field" != "done" ]] && continue + [[ -z "$change_field" || "$change_field" == "pending" ]] && continue + + if ! change_has_tasks_artifact "$change_field"; then + log "missing OpenSpec tasks mapping for TODO claim $claim_id in $file: $change_field" + failures=$((failures + 1)) + fi + done < <(claim_ledger_rows "$file") + done < <(find docs/todos -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) +} + +check_master_todo_slice_mapping_consistency() { + local project_file="docs/todos/project_overall_todos.md" + local detail_file="docs/todos/agentscope_domain_execution_todos.md" + local line status_field claim_id change_field detail_refs ref detail_change detail_claim_found + local detail_claim_map + + if [[ ! -f "$project_file" || ! -f "$detail_file" ]]; then + return + fi + + detail_claim_map="$(mktemp)" + trap 'rm -f "$detail_claim_map"' RETURN + + while IFS= read -r line; do + claim_id="$(normalize_markdown_cell "$(markdown_table_column "$line" 1)")" + status_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 4)")" + change_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 7)")" + + [[ -z "$claim_id" ]] && continue + [[ "$status_field" == "deprecated" ]] && continue + printf '%s|%s\n' "$claim_id" "$change_field" >>"$detail_claim_map" + done < <(claim_ledger_rows "$detail_file") + + while IFS= read -r line; do + status_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 4)")" + [[ "$status_field" == "deprecated" ]] && continue + + change_field="$(normalize_markdown_cell "$(markdown_table_column "$line" 7)")" + detail_refs="$(normalize_markdown_cell "$(markdown_table_column "$line" 8)")" + + [[ -z "$detail_refs" || "$detail_refs" == "—" ]] && continue + + while IFS= read -r ref; do + [[ -z "$ref" ]] && continue + detail_claim_found="$(awk -F'|' -v target="$ref" '$1 == target {print "1"; exit}' "$detail_claim_map")" + detail_change="$(awk -F'|' -v target="$ref" '$1 == target {print $2; exit}' "$detail_claim_map")" + if [[ -z "$detail_claim_found" ]]; then + log "missing Detail Claim Ref mapping in $project_file: $ref" + failures=$((failures + 1)) + continue + fi + + if [[ -n "$change_field" && "$change_field" != "pending" && -n "$detail_change" && "$detail_change" != "pending" && "$detail_change" != "$change_field" ]]; then + log "inconsistent OpenSpec change between $project_file and $detail_file for ref $ref" + failures=$((failures + 1)) + fi + done < <(grep -oE 'CLM-[A-Za-z0-9-]+' <<<"$detail_refs") + done < <(claim_ledger_rows "$project_file") + + rm -f "$detail_claim_map" + trap - RETURN +} + check_feature_indexes check_checkpoint_skill_mapping @@ -466,6 +624,13 @@ while IFS= read -r file; do check_feature_doc "$file" done < <(find docs/features -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) +while IFS= read -r file; do + check_feature_doc "$file" +done < <(find docs/features/archive -maxdepth 1 -type f -name '*.md' ! -name 'README.md' | sort) + +check_todo_claim_to_openspec_task_mapping +check_master_todo_slice_mapping_consistency + if [[ $failures -gt 0 ]]; then log "failed with $failures issue(s)" exit 1 diff --git a/scripts/ci/check_test_failure_ownership.py b/scripts/ci/check_test_failure_ownership.py new file mode 100644 index 00000000..cabd40e5 --- /dev/null +++ b/scripts/ci/check_test_failure_ownership.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Validate failure-test ownership mapping used by the P0 gate.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +if __package__ in {None, ""}: + # Allow direct execution: `python scripts/ci/check_test_failure_ownership.py` + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.ci.p0_gate import DEFAULT_CATEGORY_SPECS, CategorySpec + + +def _normalize_selector_parts(selector: str) -> tuple[list[str], list[str]]: + segments = [segment.strip() for segment in selector.split("::") if segment.strip()] + if not segments: + return [], [] + + raw_path = segments[0].replace("\\", "/") + path_parts = [part for part in raw_path.split("/") if part and part != "."] + node_parts = [ + # `pytest` parametrization suffixes (`test_x[param]`) are narrower matches + # than the base node (`test_x`) and should count as overlapping selectors. + segment.split("[", 1)[0] + for segment in segments[1:] + ] + return path_parts, node_parts + + +def _selectors_overlap(left: str, right: str) -> bool: + left_path, left_nodes = _normalize_selector_parts(left) + right_path, right_nodes = _normalize_selector_parts(right) + if not left_path or not right_path: + return False + + def _is_prefix(prefix: list[str], full: list[str]) -> bool: + if len(prefix) > len(full): + return False + return prefix == full[: len(prefix)] + + if left_path == right_path: + return _is_prefix(left_nodes, right_nodes) or _is_prefix(right_nodes, left_nodes) + + # `pytest` accepts file_or_dir selectors; a directory selector overlaps any + # descendant file/node selector under the same subtree. + if _is_prefix(left_path, right_path): + return not left_nodes + if _is_prefix(right_path, left_path): + return not right_nodes + return False + + +def validate_category_specs(specs: list[CategorySpec]) -> list[str]: + """Return validation issues for failure ownership mapping specs.""" + issues: list[str] = [] + test_to_label: dict[str, str] = {} + selector_mappings: list[tuple[str, str]] = [] + + for spec in specs: + if not spec.tests: + issues.append(f"{spec.label}: missing test selectors") + if not spec.modules: + issues.append(f"{spec.label}: missing module ownership scope") + + owner = spec.owner.strip() + if not owner: + issues.append(f"{spec.label}: missing owner") + elif not owner.startswith("@"): + issues.append(f"{spec.label}: owner must be a GitHub handle (starts with '@')") + + for selector in spec.tests: + normalized = selector.strip() + if not normalized: + issues.append(f"{spec.label}: empty test selector") + continue + + existing = test_to_label.get(normalized) + if existing and existing != spec.label: + issues.append( + "duplicate test selector mapping: " + f"{normalized} is mapped by both {existing} and {spec.label}" + ) + continue + + for mapped_selector, mapped_label in selector_mappings: + if mapped_label == spec.label: + continue + if _selectors_overlap(normalized, mapped_selector): + issues.append( + "overlapping test selector mapping: " + f"{normalized} ({spec.label}) overlaps with " + f"{mapped_selector} ({mapped_label})" + ) + break + + test_to_label[normalized] = spec.label + selector_mappings.append((normalized, spec.label)) + + return issues + + +def main() -> int: + specs = list(DEFAULT_CATEGORY_SPECS) + issues = validate_category_specs(specs) + + if issues: + print("[failure-ownership] failed") + for issue in issues: + print(f"- {issue}") + return 1 + + print("[failure-ownership] passed") + for spec in specs: + print(f"- {spec.label}: owner={spec.owner}; tests={len(spec.tests)}; modules={len(spec.modules)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/p0_gate.py b/scripts/ci/p0_gate.py index cd28ee19..b61d5562 100644 --- a/scripts/ci/p0_gate.py +++ b/scripts/ci/p0_gate.py @@ -18,6 +18,7 @@ class CategorySpec: label: str tests: list[str] modules: list[str] + owner: str action: str @@ -47,6 +48,7 @@ class CategoryResult: "examples/05-dare-coding-agent-enhanced/cli.py", "examples/06-dare-coding-agent-mcp/cli.py", ], + owner="@zts212653", action="inspect trust/policy/approval flow before tool invocation", ), CategorySpec( @@ -61,6 +63,7 @@ class CategoryResult: "dare_framework/agent/_internal/execute_engine.py", "dare_framework/plan", ], + owner="@zts212653", action="inspect step execution order, fail-fast handling, and validated-plan routing", ), CategorySpec( @@ -76,6 +79,7 @@ class CategoryResult: "dare_framework/observability/_internal/event_trace_bridge.py", "dare_framework/agent/builder.py", ], + owner="@zts212653", action="inspect SQLite event append/hash-chain/replay wiring and trace-aware event-log bridging", ), ) @@ -104,6 +108,7 @@ def format_summary(results: list[CategoryResult]) -> str: f"- {result.spec.label}", f" tests: {failed_tests}", f" modules: {modules}", + f" owner: {result.spec.owner}", f" action: {result.spec.action}", ] ) diff --git a/tests/unit/test_check_test_failure_ownership.py b/tests/unit/test_check_test_failure_ownership.py new file mode 100644 index 00000000..84a42d0d --- /dev/null +++ b/tests/unit/test_check_test_failure_ownership.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from scripts.ci.check_test_failure_ownership import validate_category_specs +from scripts.ci.p0_gate import CategorySpec + + +def _spec( + label: str, + *, + tests: list[str] | None = None, + modules: list[str] | None = None, + owner: str = "@zts212653", +) -> CategorySpec: + return CategorySpec( + label=label, + tests=tests or ["tests/example.py::test_case"], + modules=modules or ["dare_framework/example.py"], + owner=owner, + action="inspect category", + ) + + +def test_validate_category_specs_accepts_valid_mapping() -> None: + issues = validate_category_specs( + [ + _spec("SECURITY_REGRESSION", tests=["tests/a.py::test_x"]), + _spec("STEP_EXEC_REGRESSION", tests=["tests/b.py::test_y"]), + ] + ) + + assert issues == [] + + +def test_validate_category_specs_rejects_duplicate_test_mapping() -> None: + issues = validate_category_specs( + [ + _spec("SECURITY_REGRESSION", tests=["tests/a.py::test_x"]), + _spec("STEP_EXEC_REGRESSION", tests=["tests/a.py::test_x"]), + ] + ) + + assert any("duplicate test selector mapping" in issue for issue in issues) + + +def test_validate_category_specs_rejects_overlapping_test_mapping() -> None: + issues = validate_category_specs( + [ + _spec("SECURITY_REGRESSION", tests=["tests/a.py"]), + _spec("STEP_EXEC_REGRESSION", tests=["tests/a.py::test_x"]), + ] + ) + + assert any("overlapping test selector mapping" in issue for issue in issues) + + +def test_validate_category_specs_rejects_overlapping_directory_selector_mapping() -> None: + issues = validate_category_specs( + [ + _spec("SECURITY_REGRESSION", tests=["tests/unit"]), + _spec("STEP_EXEC_REGRESSION", tests=["tests/unit/test_a.py::test_x"]), + ] + ) + + assert any("overlapping test selector mapping" in issue for issue in issues) + + +def test_validate_category_specs_rejects_missing_owner() -> None: + issues = validate_category_specs( + [ + _spec("SECURITY_REGRESSION", owner=""), + ] + ) + + assert any("missing owner" in issue for issue in issues) diff --git a/tests/unit/test_checkpoint_defaults.py b/tests/unit/test_checkpoint_defaults.py new file mode 100644 index 00000000..017d9edf --- /dev/null +++ b/tests/unit/test_checkpoint_defaults.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import importlib +from dataclasses import dataclass + + +def test_checkpoint_defaults_module_exports_are_importable() -> None: + defaults = importlib.import_module("dare_framework.checkpoint.defaults") + + for symbol in ( + "MemoryCheckpointStore", + "DefaultCheckpointSaveRestore", + "StmContributor", + "WorkspaceGitContributor", + "SessionStateContributor", + "SessionContextContributor", + ): + assert hasattr(defaults, symbol), f"missing checkpoint default symbol: {symbol}" + + +@dataclass +class _DummyConfig: + model: str + + +@dataclass +class _DummySessionContext: + session_id: str + task_id: str + config: _DummyConfig | None + + +@dataclass +class _DummyCtx: + session_context: _DummySessionContext | None + + +def test_session_context_contributor_preserves_serialized_config_dict() -> None: + defaults = importlib.import_module("dare_framework.checkpoint.defaults") + contributor = defaults.SessionContextContributor() + ctx = _DummyCtx( + session_context=_DummySessionContext( + session_id="s-1", + task_id="t-1", + config=_DummyConfig(model="gpt-4.1"), + ) + ) + + payload = contributor.serialize(ctx) + + assert payload is not None + assert payload["config"] == {"model": "gpt-4.1"} diff --git a/tests/unit/test_governance_intent_gate.py b/tests/unit/test_governance_intent_gate.py index 50ac15b7..ea051540 100644 --- a/tests/unit/test_governance_intent_gate.py +++ b/tests/unit/test_governance_intent_gate.py @@ -103,6 +103,7 @@ def test_implementation_changes_require_governed_feature_doc_update(self) -> Non self.assertNotEqual(result.returncode, 0) self.assertIn("must update at least one governed feature doc", result.stdout) + self.assertNotIn("unbound variable", result.stderr) def test_implementation_changes_fail_when_intent_pr_not_merged(self) -> None: result = self._run_gate( diff --git a/tests/unit/test_governance_traceability_gate.py b/tests/unit/test_governance_traceability_gate.py index 410abff9..fe818e40 100644 --- a/tests/unit/test_governance_traceability_gate.py +++ b/tests/unit/test_governance_traceability_gate.py @@ -86,7 +86,21 @@ def _base_tree(root: Path) -> None: - Move docs here only after closeout. """, ) - _write(root / "docs" / "features" / "archive" / "archived-change.md", "# archived\n") + _write( + root / "docs" / "features" / "archive" / "archived-change.md", + """--- +change_ids: ["archived-change"] +doc_kind: feature +topics: ["governance", "archive"] +created: 2026-03-01 +updated: 2026-03-01 +status: archived +mode: openspec +--- + +# Feature: archived-change +""", + ) _write( root / "docs" / "features" / "templates" / "feature_aggregation_template.md", """# Feature Aggregation Template @@ -113,11 +127,34 @@ def _base_tree(root: Path) -> None: | 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 / "docs" / "todos" / "project_overall_todos.md", + """# Demo Project Overall TODO + +## 1.1 认领声明(Claim Ledger) + +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Detail Claim Ref | Notes | +|---|---|---|---|---|---|---|---|---| +| CLM-AG1 | T5-2 | demo | done | 2026-03-03 | 2026-03-10 | `demo-change` | `CLM-D9` | demo | +""", + ) + _write( + root / "docs" / "todos" / "agentscope_domain_execution_todos.md", + """# Demo AgentScope Board + +## 0.1 认领声明(Claim Ledger) + +| Claim ID | TODO Scope | Owner | Status | Declared At | Expires At | OpenSpec Change | Project Claim Ref | Notes | +|---|---|---|---|---|---|---|---|---| +| CLM-D9 | D9-1~D9-2 | demo | done | 2026-03-03 | 2026-03-10 | `demo-change` | `CLM-AG1` | 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 / "openspec" / "changes" / "archive" / "2026-03-01-archived-change" / "tasks.md", "- [x] archived\n") _write( root / "docs" / "governance" / "Documentation_Management_Model.md", """# Documentation Management Model @@ -166,6 +203,19 @@ def test_gate_fails_when_feature_template_is_missing(self) -> None: self.assertNotEqual(result.returncode, 0) self.assertIn("missing feature aggregation template", result.stdout) + def test_gate_fails_when_archived_feature_doc_frontmatter_is_missing_required_field(self) -> None: + def mutate(root: Path) -> None: + archived = root / "docs" / "features" / "archive" / "archived-change.md" + archived.write_text( + archived.read_text(encoding="utf-8").replace("status: archived\n", ""), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing required frontmatter field 'status'", 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" @@ -405,6 +455,48 @@ def mutate(root: Path) -> None: self.assertNotEqual(result.returncode, 0) self.assertIn("missing TODO mapping for feature doc", result.stdout) + def test_gate_fails_when_done_or_active_claim_has_no_tasks_artifact(self) -> None: + def mutate(root: Path) -> None: + project_doc = root / "docs" / "todos" / "project_overall_todos.md" + project_doc.write_text( + project_doc.read_text(encoding="utf-8").replace("`demo-change`", "`missing-change`"), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing OpenSpec tasks mapping for TODO claim", result.stdout) + + def test_gate_fails_when_project_claim_detail_ref_is_missing(self) -> None: + def mutate(root: Path) -> None: + project_doc = root / "docs" / "todos" / "project_overall_todos.md" + project_doc.write_text( + project_doc.read_text(encoding="utf-8").replace("`CLM-D9`", "`CLM-D404`"), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing Detail Claim Ref mapping in docs/todos/project_overall_todos.md", result.stdout) + + def test_gate_accepts_detail_claim_ref_with_empty_change_cell(self) -> None: + def mutate(root: Path) -> None: + detail_doc = root / "docs" / "todos" / "agentscope_domain_execution_todos.md" + detail_doc.write_text( + detail_doc.read_text(encoding="utf-8").replace( + "`demo-change` | `CLM-AG1`", + " | `CLM-AG1`", + ), + encoding="utf-8", + ) + + result = self._run_gate(mutate) + + self.assertEqual(result.returncode, 0) + self.assertIn("passed", 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" @@ -417,6 +509,16 @@ def mutate(root: Path) -> None: todo_doc.read_text(encoding="utf-8").replace("`demo-change`", "`archived-change`"), encoding="utf-8", ) + project_doc = root / "docs" / "todos" / "project_overall_todos.md" + project_doc.write_text( + project_doc.read_text(encoding="utf-8").replace("`demo-change`", "`archived-change`"), + encoding="utf-8", + ) + detail_doc = root / "docs" / "todos" / "agentscope_domain_execution_todos.md" + detail_doc.write_text( + detail_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() diff --git a/tests/unit/test_p0_gate_ci.py b/tests/unit/test_p0_gate_ci.py index 61b1ec2d..e7e391e7 100644 --- a/tests/unit/test_p0_gate_ci.py +++ b/tests/unit/test_p0_gate_ci.py @@ -7,12 +7,14 @@ def _spec( label: str, *, modules: list[str] | None = None, + owner: str = "@zts212653", action: str = "inspect category", ) -> CategorySpec: return CategorySpec( label=label, tests=["tests/example.py::test_case"], modules=modules or ["module.one", "module.two"], + owner=owner, action=action, ) @@ -89,6 +91,7 @@ def test_format_summary_reports_failures_with_modules_and_action() -> None: "- STEP_EXEC_REGRESSION", " tests: tests/integration/test_p0_conformance_gate.py::test_step_driven_session_stops_after_first_failed_step", " modules: dare_framework/agent/dare_agent.py, dare_framework/agent/_internal/execute_engine.py", + " owner: @zts212653", " action: inspect step execution order and fail-fast handling", ] ) diff --git a/tests/unit/test_package_initializers_facade_pattern.py b/tests/unit/test_package_initializers_facade_pattern.py index 3990969b..1bc66235 100644 --- a/tests/unit/test_package_initializers_facade_pattern.py +++ b/tests/unit/test_package_initializers_facade_pattern.py @@ -4,6 +4,42 @@ from pathlib import Path +def _is_internal_module_path(module: str) -> bool: + return module == "_internal" or module.startswith("_internal.") or "._internal" in module + + +def _find_direct_internal_import(node: ast.stmt) -> str | None: + if isinstance(node, ast.ImportFrom): + module = node.module or "" + if module and _is_internal_module_path(module): + return f"{'.' * node.level}{module}" + + # `from . import _internal` uses `module is None` and stores names in aliases. + for alias in node.names: + if _is_internal_module_path(alias.name): + return f"{'.' * node.level}{alias.name}" + return None + + if isinstance(node, ast.Import): + for alias in node.names: + if _is_internal_module_path(alias.name): + return alias.name + return None + + +def test_internal_import_detection_covers_relative_syntax() -> None: + cases = { + "from ._internal.foo import Bar": "._internal.foo", + "from .._internal import Foo": ".._internal", + "from . import _internal": "._internal", + "from dare_framework.tool._internal.tools import EchoTool": "dare_framework.tool._internal.tools", + "from dare_framework.tool.defaults import EchoTool": None, + } + for source, expected in cases.items(): + node = ast.parse(source).body[0] + assert _find_direct_internal_import(node) == expected + + def test_package_initializers_follow_facade_pattern() -> None: """Asserts that all dare_framework initializers use the facade pattern. @@ -12,6 +48,8 @@ def test_package_initializers_follow_facade_pattern() -> None: 2. No class or function definitions. 3. Assignments allowed only for metadata (e.g., __all__, __version__). 4. Imports allowed for re-exporting. + 5. Star imports are prohibited. + 6. Public facades must not import from `._internal` directly. """ repo_root = Path(__file__).resolve().parents[2] package_root = repo_root / "dare_framework" @@ -21,6 +59,7 @@ def test_package_initializers_follow_facade_pattern() -> None: violations: list[str] = [] for path in init_files: source = path.read_text(encoding="utf-8") + is_public_facade = "_internal" not in path.parts if not source.strip(): # Empty files are okay if they are just placeholders, # but the user wanted docstrings. @@ -68,6 +107,19 @@ def test_package_initializers_follow_facade_pattern() -> None: violations.append(f"{path.relative_to(repo_root)}: Prohibited assignment (only __all__/__version__ etc allowed)") # Rule 4: Imports are allowed (for re-exporting) + if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names): + violations.append(f"{path.relative_to(repo_root)}: Prohibited star import in package initializer") + continue + + # Rule 6: Public facades must not directly import internal modules. + if is_public_facade: + direct_internal_import = _find_direct_internal_import(node) + if direct_internal_import is not None: + violations.append( + f"{path.relative_to(repo_root)}: Prohibited direct _internal import ({direct_internal_import})" + ) + continue + if isinstance(node, (ast.Import, ast.ImportFrom)): continue diff --git a/tests/unit/test_security_boundary.py b/tests/unit/test_security_boundary.py index 029c7a19..ef6776c0 100644 --- a/tests/unit/test_security_boundary.py +++ b/tests/unit/test_security_boundary.py @@ -2,6 +2,7 @@ import pytest +from dare_framework.security import DefaultSecurityBoundary from dare_framework.security.errors import SECURITY_TRUST_DERIVATION_FAILED, SecurityBoundaryError from dare_framework.security.impl import NoOpSecurityBoundary, PolicySecurityBoundary from dare_framework.security.types import PolicyDecision, RiskLevel @@ -44,6 +45,26 @@ async def test_policy_security_boundary_requires_approval_for_high_risk() -> Non assert decision is PolicyDecision.APPROVE_REQUIRED +@pytest.mark.asyncio +async def test_default_security_boundary_remains_permissive_for_high_risk() -> None: + boundary = DefaultSecurityBoundary() + trusted = await boundary.verify_trust( + input={"command": "rm -rf /tmp/foo"}, + context={ + "capability_id": "run_command", + "risk_level": RiskLevel.NON_IDEMPOTENT_EFFECT.value, + "requires_approval": True, + }, + ) + decision = await boundary.check_policy( + action="invoke_tool", + resource="run_command", + context={"trusted_input": trusted, "capability_id": "run_command", "requires_approval": True}, + ) + + assert decision is PolicyDecision.ALLOW + + @pytest.mark.asyncio async def test_policy_security_boundary_denies_blocked_capability() -> None: boundary = PolicySecurityBoundary(deny_capability_ids={"run_command"})