diff --git a/code-mower-package-manifest.json b/code-mower-package-manifest.json index aef76089..9363d99f 100644 --- a/code-mower-package-manifest.json +++ b/code-mower-package-manifest.json @@ -127,6 +127,11 @@ "source": "docs/context-setup.md", "target": "docs/context-setup.md" }, + { + "kind": "doc", + "source": "docs/control-surface-telemetry.md", + "target": "docs/control-surface-telemetry.md" + }, { "kind": "doc", "source": "docs/devin-peer-support-qualification.md", @@ -892,6 +897,36 @@ "source": "src/code_mower/context_store.py", "target": "src/code_mower/context_store.py" }, + { + "kind": "fixture", + "source": "src/code_mower/control_surface_session_summary.accepted.json", + "target": "src/code_mower/control_surface_session_summary.accepted.json" + }, + { + "kind": "fixture", + "source": "src/code_mower/control_surface_session_summary.expectations.json", + "target": "src/code_mower/control_surface_session_summary.expectations.json" + }, + { + "kind": "fixture", + "source": "src/code_mower/control_surface_session_summary.fixture-manifest.json", + "target": "src/code_mower/control_surface_session_summary.fixture-manifest.json" + }, + { + "kind": "fixture", + "source": "src/code_mower/control_surface_session_summary.rejected.json", + "target": "src/code_mower/control_surface_session_summary.rejected.json" + }, + { + "kind": "schema", + "source": "src/code_mower/control_surface_session_summary.schema.json", + "target": "src/code_mower/control_surface_session_summary.schema.json" + }, + { + "kind": "core", + "source": "src/code_mower/control_surface_summary.py", + "target": "src/code_mower/control_surface_summary.py" + }, { "kind": "core", "source": "src/code_mower/controller.py", diff --git a/docs/README.md b/docs/README.md index 79c04321..c2263c43 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,6 +40,6 @@ historical release evidence and are not current operating guidance. ## Lifecycle -The manifest currently classifies 62 supporting, 53 frozen, and 5 archived document(s). +The manifest currently classifies 63 supporting, 53 frozen, and 5 archived document(s). See [Documentation lifecycle](documentation-lifecycle.md) before adding, moving, or changing release-sensitive documentation. diff --git a/docs/control-surface-telemetry.md b/docs/control-surface-telemetry.md new file mode 100644 index 00000000..1046e7d8 --- /dev/null +++ b/docs/control-surface-telemetry.md @@ -0,0 +1,92 @@ +# Control-surface session summaries + +This supporting contract describes the metadata-only lifecycle summaries used +by the optional [Slack integration](slack-setup.md). Installation and Slack +administration remain in the Slack guide. General upload behavior remains in +the [cloud data contract](cloud-data-contract.md), and local work presentation +remains in the [Board data contract](board-data-contract.md). + +## Boundary + +`code_mower.controlSurfaceSessionSummary.v1` is a closed specialization of the +existing `code_mower.benchmarkEvent.v1` envelope. It reports one meaningful +session observation rather than a command, message, or transcript stream. The +packaged schema and fixtures are the normative machine-readable contract: + +- `control_surface_session_summary.schema.json`; +- `control_surface_session_summary.accepted.json`; +- `control_surface_session_summary.rejected.json`; +- `control_surface_session_summary.expectations.json`; +- `control_surface_session_summary.fixture-manifest.json`. + +The summary allows an opaque Code Mower correlation key, repository slug, +categorical provider, lifecycle state and outcome, observation time, operation +counts, closed lifecycle reason, bounded owner action, optional pull-request +metadata, and already available elapsed-time or Devin ACU measurements. The +producer hashes its local logical session into a 32-character key before +building the event. It never forwards a Slack identity or provider session +reference. + +The lifecycle state, reason, and owner action form one closed policy triple. +Routine status observations use `none`; owner waits use `answer_question` or +`respond_to_approval`; failures and suspensions use `inspect_failure`. +`collect` observations with `result_not_ready` or `result_unavailable` use +`inspect_provider` regardless of whether the provider is still pending, +running, owner-waiting, or terminal. This preserves both facts: the provider +state remains truthful, and the missing result remains visible as the current +owner action. Unsupported combinations fail closed. + +The closed validator rejects unknown root, dimension, metric, and tool +provenance fields. Tool provenance is limited to the fixed Code Mower reporter +identity, a bounded package version, and a coarse local/CI runtime category. Task +text, messages, answers, prompts, response URLs, Slack identities, source, +diffs, transcripts, tokens, paths, context or graph data, provider references, +and raw output cannot enter this event. + +## Local Board projection + +Slack-requested work uses the existing public remote-session lifecycle and +local Board adapter. The adapter maps `pending`, `running`, owner-waiting, +completion, failure, suspension, termination, archival, and uncertainty into +the same Board phases used by other remote work. This is local observation; +it does not require cloud access and grants no dispatch or merge authority. + +## Hosted capability gate + +Production emission stays disabled until the hosted service advertises this +exact closed object: + +```json +{ + "accepting": true, + "capability_version": 1, + "fixture_manifest_sha256": "", + "schema": "code_mower.controlSurfaceSessionSummaryCapability.v1", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" +} +``` + +Every field must match, and unknown fields fail closed. A missing capability, +version mismatch, digest mismatch, or `accepting: false` leaves operation local +only. Client rollback stops new emission without rewriting accepted rows. +The client reads this object from +`GET /api/health` at `capabilities.control_surface_session_summary`; the cloud +doctor exposes an accepted exact match in its service-check detail. + +After acceptance, the producer emits the first observation and then only a +meaningful lifecycle, operation-count, owner-action, or pull-request change. +Timestamp-only changes and changing elapsed time or usage on a nonterminal +session do not create another event. Terminal elapsed-time or usage changes +remain meaningful reconciliation evidence. + +The hosted service must vendor the five packaged resources byte-for-byte, +scope repository identity through authenticated tenant policy, and implement +the retention, export, deletion, tenant-isolation, and aggregate-reconciliation +expectations in the packaged expectations fixture. + +## Contract changes + +The fixture manifest hashes exact UTF-8 file bytes. It does not hash itself; +the hosted capability advertises the SHA-256 of the manifest bytes separately. +After qualification, changing the schema or any fixture requires a new schema +or manifest version. Do not refresh hashes in place to accept changed meaning. diff --git a/docs/docs-manifest.yml b/docs/docs-manifest.yml index b718ef6d..9aa9415c 100644 --- a/docs/docs-manifest.yml +++ b/docs/docs-manifest.yml @@ -61,6 +61,8 @@ documents: status: supporting - path: docs/commercial-boundary.md status: supporting +- path: docs/control-surface-telemetry.md + status: supporting - path: docs/context-connections.md status: supporting - path: docs/context-delivery.md diff --git a/src/code_mower/cloud_client/bundle.py b/src/code_mower/cloud_client/bundle.py index 22b92b00..62c14fa0 100644 --- a/src/code_mower/cloud_client/bundle.py +++ b/src/code_mower/cloud_client/bundle.py @@ -26,6 +26,7 @@ "board_snapshot", "builder_run", "calibration_run", + "control_surface_session_summary", "controller_decision", "dogfood_upload", "lane_policy_snapshot", diff --git a/src/code_mower/cloud_client/endpoints.py b/src/code_mower/cloud_client/endpoints.py index 972d8f19..7516a09c 100644 --- a/src/code_mower/cloud_client/endpoints.py +++ b/src/code_mower/cloud_client/endpoints.py @@ -8,6 +8,8 @@ import urllib.request from typing import Any +from code_mower.control_surface_summary import capability_from_health + DEFAULT_HEALTH_PATH = "/api/health" DEFAULT_DASHBOARD_PATH = "/dashboard" @@ -100,6 +102,9 @@ def probe_cloud_service(endpoint: str, *, timeout: float) -> dict[str, Any]: for key in ("app", "supabaseConfigured"): if key in parsed and isinstance(parsed[key], str | bool | int | float): detail[key] = parsed[key] + summary_capability = capability_from_health(parsed) + if summary_capability is not None: + detail["control_surface_session_summary"] = summary_capability return { "name": "service", "status": "pass", diff --git a/src/code_mower/cloud_client/events.py b/src/code_mower/cloud_client/events.py index 017982e5..3d7f0115 100644 --- a/src/code_mower/cloud_client/events.py +++ b/src/code_mower/cloud_client/events.py @@ -11,6 +11,10 @@ from typing import Any, Mapping from code_mower import __version__ +from code_mower.control_surface_summary import ( + EVENT_TYPE as CONTROL_SURFACE_SUMMARY_EVENT_TYPE, + validate_control_surface_summary, +) from code_mower.participants import PARTICIPANTS from code_mower.provider_registry import REFERENCE_PROVIDERS from code_mower.providers import ( @@ -417,6 +421,8 @@ def validate_cloud_event(value: Any) -> dict[str, Any]: validate_adoption_run_payload(value) if value["event_type"] == REVIEWER_FINDING_OUTCOME_EVENT_TYPE: validate_reviewer_finding_outcome_payload(value) + if value["event_type"] == CONTROL_SURFACE_SUMMARY_EVENT_TYPE: + validate_control_surface_summary(value) validate_work_type_metadata(value["dimensions"], value["event_type"], value["tool"]) return value @@ -928,6 +934,7 @@ def normalize_event(value: dict[str, Any], event_type: str) -> dict[str, Any]: PR_OUTCOME_EVENT_TYPE, PRODUCTIVITY_EVENT_TYPE, REVIEWER_FINDING_OUTCOME_EVENT_TYPE, + CONTROL_SURFACE_SUMMARY_EVENT_TYPE, "value_report_snapshot", }: normalized["tool"] = build_code_mower_tool_provenance( diff --git a/src/code_mower/control_surface_session_summary.accepted.json b/src/code_mower/control_surface_session_summary.accepted.json new file mode 100644 index 00000000..d7ad665c --- /dev/null +++ b/src/code_mower/control_surface_session_summary.accepted.json @@ -0,0 +1,1069 @@ +{ + "events": [ + { + "event": { + "created_at": "2026-09-20T12:10:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "pr_number": "921", + "pr_state": "open", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000001", + "state": "archived", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000001", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "archived", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "archived_none_none" + }, + { + "event": { + "created_at": "2026-09-20T12:10:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "pr_number": "921", + "pr_state": "open", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "archived", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "archived", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "archived_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:07:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lifecycle_reason": "none", + "outcome": "succeeded", + "owner_action": "none", + "pr_number": "921", + "pr_state": "open", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000003", + "state": "complete", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000003", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 1, + "dispatch_count": 1, + "elapsed_seconds": 420.0, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "complete", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "complete_none_none" + }, + { + "event": { + "created_at": "2026-09-20T12:07:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lifecycle_reason": "result_unavailable", + "outcome": "succeeded", + "owner_action": "inspect_provider", + "pr_number": "921", + "pr_state": "open", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000004", + "state": "complete", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000004", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 1, + "dispatch_count": 1, + "elapsed_seconds": 420.0, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "complete", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "complete_result_unavailable_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:08:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "session_failed", + "outcome": "failed", + "owner_action": "inspect_failure", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000005", + "state": "failed", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000005", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "failed", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "failed_session_failed_inspect_failure" + }, + { + "event": { + "created_at": "2026-09-20T12:08:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "failed", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000006", + "state": "failed", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000006", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "failed", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "failed_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:01:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000007", + "state": "pending", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000007", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "pending", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "pending_none_none" + }, + { + "event": { + "created_at": "2026-09-20T12:01:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000008", + "state": "pending", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000008", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "pending", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "pending_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000009", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000009", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "running_none_none" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000a", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000a", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "running_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:05:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "session_suspended", + "outcome": "unknown", + "owner_action": "inspect_failure", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000b", + "state": "suspended", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000b", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "suspended", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "suspended_session_suspended_inspect_failure" + }, + { + "event": { + "created_at": "2026-09-20T12:05:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000c", + "state": "suspended", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000c", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "suspended", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "suspended_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:09:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "cancelled", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000d", + "state": "terminated", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000d", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 1, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "terminated", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "terminated_none_none" + }, + { + "event": { + "created_at": "2026-09-20T12:09:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "cancelled", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000e", + "state": "terminated", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000e", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 1, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "terminated", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "terminated_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:06:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "provider_unavailable", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "0000000000000000000000000000000f", + "state": "uncertain", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-00000000000f", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "uncertain", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "uncertain_provider_unavailable_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:06:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "reconcile_dispatch", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000010", + "state": "uncertain", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000010", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "uncertain", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "uncertain_reconcile_dispatch_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:06:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "inspect_provider_then_acknowledge", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000011", + "state": "uncertain", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000011", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "uncertain", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "uncertain_inspect_provider_then_acknowledge_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:04:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "approval_required", + "outcome": "unknown", + "owner_action": "respond_to_approval", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000012", + "state": "waiting_for_approval", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000012", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "waiting_for_approval", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "waiting_for_approval_approval_required_respond_to_approval" + }, + { + "event": { + "created_at": "2026-09-20T12:04:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000013", + "state": "waiting_for_approval", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000013", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "waiting_for_approval", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "waiting_for_approval_result_not_ready_inspect_provider" + }, + { + "event": { + "created_at": "2026-09-20T12:03:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "user_input_required", + "outcome": "unknown", + "owner_action": "answer_question", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000014", + "state": "waiting_for_user", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000014", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "waiting_for_user", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "waiting_for_user_user_input_required_answer_question" + }, + { + "event": { + "created_at": "2026-09-20T12:03:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "result_not_ready", + "outcome": "unknown", + "owner_action": "inspect_provider", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000015", + "state": "waiting_for_user", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000015", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "waiting_for_user", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "waiting_for_user_result_not_ready_inspect_provider" + } + ], + "schema": "code_mower.controlSurfaceSessionSummaryAcceptedFixtures.v1" +} diff --git a/src/code_mower/control_surface_session_summary.expectations.json b/src/code_mower/control_surface_session_summary.expectations.json new file mode 100644 index 00000000..9b6eaa8c --- /dev/null +++ b/src/code_mower/control_surface_session_summary.expectations.json @@ -0,0 +1,46 @@ +{ + "contract_schema": "code_mower.controlSurfaceSessionSummary.v1", + "expectations": [ + { + "name": "legacy_client", + "requirement": "Existing event types remain accepted when the summary event is absent." + }, + { + "name": "capability_absent", + "requirement": "The client emits no summary unless hosted acceptance names the exact schema, capability version, and fixture-manifest digest." + }, + { + "name": "capability_mismatch", + "requirement": "A schema, capability-version, or fixture-manifest digest mismatch disables emission and preserves local-only operation." + }, + { + "name": "rollback", + "requirement": "Client rollback stops new emission and does not delete or reinterpret accepted rows." + }, + { + "name": "tenant_isolation", + "requirement": "Authenticated tenant binding, not payload routing fields, scopes repository identity; equal repo slugs in two tenants remain isolated." + }, + { + "name": "export", + "requirement": "Export returns each accepted summary once and contains none of the forbidden fields represented by rejected fixtures." + }, + { + "name": "deletion", + "requirement": "Deletion removes the summary, its aggregate contribution, and its export representation." + }, + { + "name": "retention", + "requirement": "Retention removes only expired rows and recomputes aggregate counts and freshness." + }, + { + "name": "token_revocation", + "requirement": "Token revocation prevents new ingest while authenticated export and deletion continue to control existing rows." + }, + { + "name": "aggregate_reconciliation", + "requirement": "Aggregate counts and freshness reconcile with the accepted canary local Board projection." + } + ], + "schema": "code_mower.controlSurfaceSessionSummaryExpectations.v1" +} diff --git a/src/code_mower/control_surface_session_summary.fixture-manifest.json b/src/code_mower/control_surface_session_summary.fixture-manifest.json new file mode 100644 index 00000000..4649049b --- /dev/null +++ b/src/code_mower/control_surface_session_summary.fixture-manifest.json @@ -0,0 +1,32 @@ +{ + "contract_schema": "code_mower.controlSurfaceSessionSummary.v1", + "digest_scope": "exact_file_bytes", + "encoding": "utf-8", + "files": [ + { + "bytes": 36946, + "path": "control_surface_session_summary.accepted.json", + "role": "accepted", + "sha256": "17b64ce2ae10baae2f3cf84a4a39f580ef912ce436387dcebfa98724477ec486" + }, + { + "bytes": 1869, + "path": "control_surface_session_summary.expectations.json", + "role": "expectations", + "sha256": "01c8e9bb26f60fb8163a69801070ab6eabeadf20c271a41455634063966c7a21" + }, + { + "bytes": 33146, + "path": "control_surface_session_summary.rejected.json", + "role": "rejected", + "sha256": "418180e76d3c90aebb108ec9caa38258a0f28c380d85e898e419313756dece5c" + }, + { + "bytes": 22845, + "path": "control_surface_session_summary.schema.json", + "role": "schema", + "sha256": "2215ce1f085525de3320377f0e26e970a7db811afc6129fc5529a5cd67760c4e" + } + ], + "schema": "code_mower.controlSurfaceSessionFixtureManifest.v1" +} diff --git a/src/code_mower/control_surface_session_summary.rejected.json b/src/code_mower/control_surface_session_summary.rejected.json new file mode 100644 index 00000000..a3b73bc1 --- /dev/null +++ b/src/code_mower/control_surface_session_summary.rejected.json @@ -0,0 +1,985 @@ +{ + "events": [ + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "extra": "x", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "unknown_root_field" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "extra": "x", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "unknown_dimension" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "extra": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "unknown_metric" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "pull_request": { + "number": 1, + "title": "private" + }, + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "unknown_pull_request_field" + }, + { + "event": { + "answer": "private answer", + "context": "context", + "created_at": "2026-09-20T12:02:00Z", + "credential": "credential", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "graph": "graph", + "install_id": "", + "lens": "", + "local_path": "", + "message": "private message", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "prompt": "private prompt", + "provider": "codex", + "provider_reference": "provider-session", + "raw_diff": "diff", + "raw_output": "output", + "repo_slug": "example/project", + "response_url": "https://slack.invalid/private", + "schema": "code_mower.benchmarkEvent.v1", + "slack_channel": "C123", + "slack_user": "U123", + "source": "code-mower slack lifecycle", + "source_code": "secret", + "status": "running", + "task": "private task", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + }, + "transcript": "chat" + }, + "name": "forbidden_content_fields" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session_id": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "forbidden_session_id_key" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v2" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "summary_schema_v2" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v2", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "wrong_event_envelope_schema" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "email", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "wrong_surface" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "invalid_repo_slug" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "slack-team", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "tenant_identity_in_event" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "complete", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "complete", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "state_outcome_mismatch" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "answer_question", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "state_owner_action_mismatch" + }, + { + "event": { + "created_at": "2026-09-20 12:00:00", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "bad_timestamp" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "head_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "bad_pr_shape" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "devin-session-12345", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "remote_provider_reference_instead_of_opaque_session" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "none", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "details": { + "answer": "private answer", + "message": "private Slack message", + "path": "sensitive/worktree", + "prompt": "private prompt", + "response_url": "https://hooks.slack.com/private" + }, + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "forbidden_nested_tool_content" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "lifecycle_reason": "session_failed", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "lifecycle_reason_state_mismatch" + }, + { + "event": { + "created_at": "2026-09-20T12:02:00Z", + "dimensions": { + "capability_version": 1, + "control_surface": "slack", + "outcome": "unknown", + "owner_action": "none", + "privacy_classification": "metadata_only", + "session": "00000000000000000000000000000002", + "state": "running", + "summary_schema": "code_mower.controlSurfaceSessionSummary.v1" + }, + "event_id": "00000000-0000-4000-8000-000000000002", + "event_type": "control_surface_session_summary", + "install_id": "", + "lens": "", + "metrics": { + "cancel_count": 0, + "collect_count": 0, + "dispatch_count": 1, + "message_count": 0, + "summary_count": 1 + }, + "provider": "codex", + "repo_slug": "example/project", + "schema": "code_mower.benchmarkEvent.v1", + "source": "code-mower slack lifecycle", + "status": "running", + "team_id": "", + "tool": { + "integration": "code-mower slack lifecycle", + "lens": "", + "model": "", + "model_source": "not_applicable", + "model_version_raw": "", + "prompt_pack_version": "", + "provider": "code-mower", + "role": "reporter", + "runtime_environment": "local", + "schema": "code_mower.toolProvenance.v1", + "source": "code-mower slack lifecycle", + "tool_name": "code-mower", + "tool_version": "1.6.0", + "version_source": "package_version" + } + }, + "name": "missing_lifecycle_reason" + } + ], + "schema": "code_mower.controlSurfaceSessionSummaryRejectedFixtures.v1" +} diff --git a/src/code_mower/control_surface_session_summary.schema.json b/src/code_mower/control_surface_session_summary.schema.json new file mode 100644 index 00000000..c1fd477f --- /dev/null +++ b/src/code_mower/control_surface_session_summary.schema.json @@ -0,0 +1,989 @@ +{ + "$defs": { + "capability": { + "additionalProperties": false, + "properties": { + "accepting": { + "type": "boolean" + }, + "capability_version": { + "const": 1 + }, + "fixture_manifest_sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "schema": { + "const": "code_mower.controlSurfaceSessionSummaryCapability.v1" + }, + "summary_schema": { + "const": "code_mower.controlSurfaceSessionSummary.v1" + } + }, + "required": [ + "schema", + "summary_schema", + "capability_version", + "fixture_manifest_sha256", + "accepting" + ], + "type": "object" + }, + "dimensions": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "lifecycle_reason": { + "const": "none" + }, + "owner_action": { + "const": "none" + }, + "state": { + "const": "archived" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "archived" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "none" + }, + "owner_action": { + "const": "none" + }, + "state": { + "const": "complete" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_unavailable" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "complete" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "session_failed" + }, + "owner_action": { + "const": "inspect_failure" + }, + "state": { + "const": "failed" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "failed" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "none" + }, + "owner_action": { + "const": "none" + }, + "state": { + "const": "pending" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "pending" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "none" + }, + "owner_action": { + "const": "none" + }, + "state": { + "const": "running" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "running" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "session_suspended" + }, + "owner_action": { + "const": "inspect_failure" + }, + "state": { + "const": "suspended" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "suspended" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "none" + }, + "owner_action": { + "const": "none" + }, + "state": { + "const": "terminated" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "terminated" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "provider_unavailable" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "uncertain" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "reconcile_dispatch" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "uncertain" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "inspect_provider_then_acknowledge" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "uncertain" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "approval_required" + }, + "owner_action": { + "const": "respond_to_approval" + }, + "state": { + "const": "waiting_for_approval" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "waiting_for_approval" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "user_input_required" + }, + "owner_action": { + "const": "answer_question" + }, + "state": { + "const": "waiting_for_user" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + }, + { + "properties": { + "lifecycle_reason": { + "const": "result_not_ready" + }, + "owner_action": { + "const": "inspect_provider" + }, + "state": { + "const": "waiting_for_user" + } + }, + "required": [ + "state", + "lifecycle_reason", + "owner_action" + ] + } + ], + "dependentRequired": { + "head_sha": [ + "pr_number" + ], + "pr_state": [ + "pr_number" + ] + }, + "properties": { + "capability_version": { + "const": 1 + }, + "control_surface": { + "const": "slack" + }, + "head_sha": { + "pattern": "^[0-9a-f]{40}$", + "type": "string" + }, + "lifecycle_reason": { + "enum": [ + "approval_required", + "inspect_provider_then_acknowledge", + "none", + "provider_unavailable", + "reconcile_dispatch", + "result_not_ready", + "result_unavailable", + "session_failed", + "session_suspended", + "user_input_required" + ] + }, + "outcome": { + "enum": [ + "unknown", + "succeeded", + "failed", + "cancelled" + ] + }, + "owner_action": { + "enum": [ + "none", + "answer_question", + "respond_to_approval", + "inspect_provider", + "inspect_failure" + ] + }, + "pr_number": { + "pattern": "^[1-9][0-9]{0,9}$", + "type": "string" + }, + "pr_state": { + "enum": [ + "open", + "merged", + "closed_unmerged" + ] + }, + "privacy_classification": { + "const": "metadata_only" + }, + "session": { + "pattern": "^[0-9a-f]{32}$", + "type": "string" + }, + "state": { + "enum": [ + "archived", + "complete", + "failed", + "pending", + "running", + "suspended", + "terminated", + "uncertain", + "waiting_for_approval", + "waiting_for_user" + ] + }, + "summary_schema": { + "const": "code_mower.controlSurfaceSessionSummary.v1" + } + }, + "required": [ + "summary_schema", + "capability_version", + "control_surface", + "session", + "privacy_classification", + "state", + "lifecycle_reason", + "outcome", + "owner_action" + ], + "type": "object" + }, + "metrics": { + "additionalProperties": false, + "properties": { + "cancel_count": { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + "collect_count": { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + "dispatch_count": { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + "elapsed_seconds": { + "minimum": 0, + "type": "number" + }, + "message_count": { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + "summary_count": { + "const": 1 + }, + "usage_acu": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "summary_count", + "dispatch_count", + "message_count", + "cancel_count", + "collect_count" + ], + "type": "object" + } + }, + "$id": "code_mower.controlSurfaceSessionSummary.v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "anyOf": [ + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "archived" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "archived" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "succeeded" + }, + "state": { + "const": "complete" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "complete" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "failed" + }, + "state": { + "const": "failed" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "failed" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "pending" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "pending" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "running" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "running" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "suspended" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "suspended" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "cancelled" + }, + "state": { + "const": "terminated" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "terminated" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "uncertain" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "uncertain" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "waiting_for_approval" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "waiting_for_approval" + } + }, + "required": [ + "status", + "dimensions" + ] + }, + { + "properties": { + "dimensions": { + "properties": { + "outcome": { + "const": "unknown" + }, + "state": { + "const": "waiting_for_user" + } + }, + "required": [ + "state", + "outcome" + ] + }, + "status": { + "const": "waiting_for_user" + } + }, + "required": [ + "status", + "dimensions" + ] + } + ] + } + ], + "description": "Closed metadata-only Slack lifecycle event inside code_mower.benchmarkEvent.v1.", + "properties": { + "created_at": { + "format": "date-time", + "pattern": "(?:Z|\\+00:00)$", + "type": "string" + }, + "dimensions": { + "$ref": "#/$defs/dimensions" + }, + "event_id": { + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "type": "string" + }, + "event_type": { + "const": "control_surface_session_summary" + }, + "install_id": { + "const": "" + }, + "lens": { + "const": "" + }, + "metrics": { + "$ref": "#/$defs/metrics" + }, + "provider": { + "oneOf": [ + { + "enum": [ + "antigravity", + "claude", + "codex", + "cursor", + "cursor-bugbot", + "devin", + "gitar", + "greptile", + "grok-bot", + "muse", + "qodo" + ] + }, + { + "pattern": "^custom:[a-z][a-z0-9-]{0,63}$", + "type": "string" + } + ] + }, + "repo_slug": { + "pattern": "^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$", + "type": "string" + }, + "schema": { + "const": "code_mower.benchmarkEvent.v1" + }, + "source": { + "const": "code-mower slack lifecycle" + }, + "status": { + "enum": [ + "archived", + "complete", + "failed", + "pending", + "running", + "suspended", + "terminated", + "uncertain", + "waiting_for_approval", + "waiting_for_user" + ] + }, + "team_id": { + "const": "" + }, + "tool": { + "additionalProperties": false, + "properties": { + "integration": { + "const": "code-mower slack lifecycle" + }, + "lens": { + "const": "" + }, + "model": { + "const": "" + }, + "model_source": { + "const": "not_applicable" + }, + "model_version_raw": { + "const": "" + }, + "prompt_pack_version": { + "const": "" + }, + "provider": { + "const": "code-mower" + }, + "role": { + "const": "reporter" + }, + "runtime_environment": { + "enum": [ + "ci", + "github-actions", + "local" + ] + }, + "schema": { + "const": "code_mower.toolProvenance.v1" + }, + "source": { + "const": "code-mower slack lifecycle" + }, + "tool_name": { + "const": "code-mower" + }, + "tool_version": { + "maxLength": 64, + "pattern": "^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$", + "type": "string" + }, + "version_source": { + "const": "package_version" + } + }, + "required": [ + "integration", + "lens", + "model", + "model_source", + "model_version_raw", + "prompt_pack_version", + "provider", + "role", + "runtime_environment", + "schema", + "source", + "tool_name", + "tool_version", + "version_source" + ], + "type": "object" + } + }, + "required": [ + "schema", + "event_type", + "event_id", + "created_at", + "repo_slug", + "team_id", + "install_id", + "source", + "provider", + "lens", + "status", + "metrics", + "dimensions", + "tool" + ], + "title": "Control-surface session summary v1", + "type": "object", + "x-code-mower-invariants": [ + "status equals dimensions.state", + "complete=>succeeded, failed=>failed, terminated=>cancelled, every other state=>unknown", + "dimensions.state, lifecycle_reason, and owner_action must match one exact producer policy triple", + "head_sha and pr_state require pr_number", + "usage_acu is allowed only for devin and only when already observed" + ] +} diff --git a/src/code_mower/control_surface_summary.py b/src/code_mower/control_surface_summary.py new file mode 100644 index 00000000..1e8641c0 --- /dev/null +++ b/src/code_mower/control_surface_summary.py @@ -0,0 +1,588 @@ +"""Closed metadata-only telemetry for control-surface session lifecycles.""" + +from __future__ import annotations + +import datetime as dt +import hashlib +import json +import math +import re +import uuid +from functools import lru_cache +from pathlib import Path +from typing import Any, Mapping + +from . import __version__ +from .providers import build_code_mower_tool_provenance + +EVENT_TYPE = "control_surface_session_summary" +SUMMARY_SCHEMA = "code_mower.controlSurfaceSessionSummary.v1" +CAPABILITY_VERSION = 1 +CAPABILITY_SCHEMA = "code_mower.controlSurfaceSessionSummaryCapability.v1" +SOURCE = "code-mower slack lifecycle" +PRIVACY_CLASSIFICATION = "metadata_only" + +ROOT_FIELDS = frozenset( + { + "schema", + "event_type", + "event_id", + "created_at", + "repo_slug", + "team_id", + "install_id", + "source", + "provider", + "lens", + "status", + "metrics", + "dimensions", + "tool", + } +) +DIMENSION_FIELDS = frozenset( + { + "summary_schema", + "capability_version", + "control_surface", + "session", + "privacy_classification", + "state", + "lifecycle_reason", + "outcome", + "owner_action", + "pr_number", + "head_sha", + "pr_state", + } +) +REQUIRED_DIMENSION_FIELDS = frozenset( + { + "summary_schema", + "capability_version", + "control_surface", + "session", + "privacy_classification", + "state", + "lifecycle_reason", + "outcome", + "owner_action", + } +) +METRIC_FIELDS = frozenset( + { + "summary_count", + "dispatch_count", + "message_count", + "cancel_count", + "collect_count", + "elapsed_seconds", + "usage_acu", + } +) +REQUIRED_METRIC_FIELDS = frozenset( + { + "summary_count", + "dispatch_count", + "message_count", + "cancel_count", + "collect_count", + } +) +STATES = frozenset( + { + "archived", + "complete", + "failed", + "pending", + "running", + "suspended", + "terminated", + "uncertain", + "waiting_for_approval", + "waiting_for_user", + } +) +OUTCOME_BY_STATE = { + "complete": "succeeded", + "failed": "failed", + "terminated": "cancelled", +} +LIFECYCLE_POLICY = { + "archived": {"none": "none", "result_not_ready": "inspect_provider"}, + "complete": {"none": "none", "result_unavailable": "inspect_provider"}, + "failed": { + "session_failed": "inspect_failure", + "result_not_ready": "inspect_provider", + }, + "pending": {"none": "none", "result_not_ready": "inspect_provider"}, + "running": {"none": "none", "result_not_ready": "inspect_provider"}, + "suspended": { + "session_suspended": "inspect_failure", + "result_not_ready": "inspect_provider", + }, + "terminated": {"none": "none", "result_not_ready": "inspect_provider"}, + "uncertain": { + "provider_unavailable": "inspect_provider", + "reconcile_dispatch": "inspect_provider", + "inspect_provider_then_acknowledge": "inspect_provider", + }, + "waiting_for_approval": { + "approval_required": "respond_to_approval", + "result_not_ready": "inspect_provider", + }, + "waiting_for_user": { + "user_input_required": "answer_question", + "result_not_ready": "inspect_provider", + }, +} +TOOL_FIELDS = frozenset( + { + "schema", + "role", + "tool_name", + "tool_version", + "provider", + "model", + "model_version_raw", + "model_source", + "version_source", + "integration", + "lens", + "runtime_environment", + "prompt_pack_version", + "source", + } +) +TOOL_CONSTANTS = { + "schema": "code_mower.toolProvenance.v1", + "role": "reporter", + "tool_name": "code-mower", + "provider": "code-mower", + "model": "", + "model_version_raw": "", + "model_source": "not_applicable", + "version_source": "package_version", + "integration": SOURCE, + "lens": "", + "prompt_pack_version": "", + "source": SOURCE, +} +TOOL_RUNTIME_ENVIRONMENTS = frozenset({"local", "ci", "github-actions"}) +PROVIDERS = frozenset( + { + "antigravity", + "claude", + "codex", + "cursor", + "cursor-bugbot", + "devin", + "gitar", + "greptile", + "grok-bot", + "muse", + "qodo", + } +) + +_CUSTOM_PROVIDER = re.compile(r"custom:[a-z][a-z0-9-]{0,63}") +_EVENT_ID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") +_REPO_SLUG = re.compile(r"[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}") +_SESSION = re.compile(r"[0-9a-f]{32}") +_HEAD_SHA = re.compile(r"[0-9a-f]{40}") +_PR_NUMBER = re.compile(r"[1-9][0-9]{0,9}") +_TOOL_VERSION = re.compile(r"[0-9A-Za-z][0-9A-Za-z.+-]{0,63}") +_CAPABILITY_FIELDS = frozenset( + { + "schema", + "summary_schema", + "capability_version", + "fixture_manifest_sha256", + "accepting", + } +) + + +def _error(detail: str) -> Exception: + # Keep this module importable without initializing the cloud_client package; + # cloud_client.events imports this validator while that package is loading. + from .cloud_client.errors import CloudBundleError + + return CloudBundleError(f"invalid control-surface session summary: {detail}") + + +def _closed(value: Mapping[str, Any], expected: frozenset[str], name: str) -> None: + keys = set(value) + if keys != expected: + raise _error(f"{name} fields do not match the closed v1 contract") + + +def _closed_with_optional( + value: Mapping[str, Any], + allowed: frozenset[str], + required: frozenset[str], + name: str, +) -> None: + keys = set(value) + if not required <= keys or not keys <= allowed: + raise _error(f"{name} fields do not match the closed v1 contract") + + +def _utc_timestamp(value: object) -> dt.datetime: + if not isinstance(value, str): + raise _error("created_at must be a UTC timestamp") + try: + parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise _error("created_at must be an ISO 8601 timestamp") from exc + if parsed.tzinfo is None or parsed.utcoffset() != dt.timedelta(0): + raise _error("created_at must be a UTC timestamp") + return parsed + + +def _bounded_count(value: object, field: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 10000: + raise _error(f"metrics.{field} must be an integer from 0 through 10000") + + +def _optional_metric(value: object, field: str) -> None: + if isinstance(value, bool) or not isinstance(value, int | float): + raise _error(f"metrics.{field} must be a finite non-negative number") + if not math.isfinite(value) or value < 0: + raise _error(f"metrics.{field} must be a finite non-negative number") + + +def _validate_tool(value: object) -> None: + """Validate the closed, metadata-only Code Mower provenance object.""" + + if not isinstance(value, Mapping): + raise _error("tool must be an object") + _closed(value, TOOL_FIELDS, "tool") + if any(not isinstance(item, str) for item in value.values()): + raise _error("tool fields must be strings") + for field, expected in TOOL_CONSTANTS.items(): + if value.get(field) != expected: + raise _error(f"tool.{field} does not match the v1 producer contract") + if value.get("runtime_environment") not in TOOL_RUNTIME_ENVIRONMENTS: + raise _error("tool.runtime_environment is not a supported coarse category") + if _TOOL_VERSION.fullmatch(value.get("tool_version", "")) is None: + raise _error("tool.tool_version must be a bounded package version") + + +def validate_control_surface_summary(value: Mapping[str, Any]) -> None: + """Validate the new event's complete closed shape and cross-field invariants.""" + + from .cloud_client.bundle import validate_metadata_payload + + if not isinstance(value, Mapping): + raise _error("event must be an object") + validate_metadata_payload(value) + _closed(value, ROOT_FIELDS, "event") + if value.get("schema") != "code_mower.benchmarkEvent.v1": + raise _error("unsupported event envelope schema") + if value.get("event_type") != EVENT_TYPE: + raise _error("unsupported event type") + if not isinstance(value.get("event_id"), str) or _EVENT_ID.fullmatch(value["event_id"]) is None: + raise _error("event_id must be a lowercase UUID") + _utc_timestamp(value.get("created_at")) + if not isinstance(value.get("repo_slug"), str) or _REPO_SLUG.fullmatch(value["repo_slug"]) is None: + raise _error("repo_slug must be canonical OWNER/REPO metadata") + if value.get("team_id") != "" or value.get("install_id") != "": + raise _error("tenant identity must come from authenticated ingest") + if value.get("source") != SOURCE or value.get("lens") != "": + raise _error("producer provenance does not match the v1 contract") + _validate_tool(value.get("tool")) + provider = value.get("provider") + if not isinstance(provider, str) or ( + provider not in PROVIDERS and _CUSTOM_PROVIDER.fullmatch(provider) is None + ): + raise _error("provider must be a bounded categorical provider name") + + dimensions = value.get("dimensions") + metrics = value.get("metrics") + if not isinstance(dimensions, Mapping) or not isinstance(metrics, Mapping): + raise _error("dimensions and metrics must be objects") + _closed_with_optional( + dimensions, DIMENSION_FIELDS, REQUIRED_DIMENSION_FIELDS, "dimension" + ) + _closed_with_optional(metrics, METRIC_FIELDS, REQUIRED_METRIC_FIELDS, "metric") + + if dimensions.get("summary_schema") != SUMMARY_SCHEMA: + raise _error("unsupported summary schema") + if type(dimensions.get("capability_version")) is not int or dimensions["capability_version"] != CAPABILITY_VERSION: + raise _error("unsupported capability version") + if dimensions.get("control_surface") != "slack": + raise _error("unsupported control surface") + if not isinstance(dimensions.get("session"), str) or _SESSION.fullmatch(dimensions["session"]) is None: + raise _error("session must be a Code Mower opaque correlation key") + if dimensions.get("privacy_classification") != PRIVACY_CLASSIFICATION: + raise _error("privacy classification must be metadata_only") + + state = dimensions.get("state") + if state not in STATES: + raise _error("unsupported lifecycle state") + if value.get("status") != state: + raise _error("envelope status must equal the lifecycle state") + expected_outcome = OUTCOME_BY_STATE.get(state, "unknown") + if dimensions.get("outcome") != expected_outcome: + raise _error("outcome is inconsistent with lifecycle state") + reason = dimensions.get("lifecycle_reason") + expected_action = LIFECYCLE_POLICY[state].get(reason) + if expected_action is None: + raise _error("lifecycle_reason is inconsistent with lifecycle state") + if dimensions.get("owner_action") != expected_action: + raise _error("owner_action is inconsistent with lifecycle state and reason") + + pr_fields = {"pr_number", "head_sha", "pr_state"} & set(dimensions) + if pr_fields and "pr_number" not in pr_fields: + raise _error("PR metadata requires pr_number") + if "pr_number" in dimensions and ( + not isinstance(dimensions["pr_number"], str) + or _PR_NUMBER.fullmatch(dimensions["pr_number"]) is None + ): + raise _error("pr_number must be a canonical positive integer string") + if "head_sha" in dimensions and ( + not isinstance(dimensions["head_sha"], str) + or _HEAD_SHA.fullmatch(dimensions["head_sha"]) is None + ): + raise _error("head_sha must be a lowercase full commit SHA") + if "pr_state" in dimensions and dimensions["pr_state"] not in { + "open", + "merged", + "closed_unmerged", + }: + raise _error("unsupported pr_state") + + if metrics.get("summary_count") != 1 or type(metrics.get("summary_count")) is not int: + raise _error("metrics.summary_count must equal 1") + for field in ("dispatch_count", "message_count", "cancel_count", "collect_count"): + _bounded_count(metrics.get(field), field) + for field in ("elapsed_seconds", "usage_acu"): + if field in metrics: + _optional_metric(metrics[field], field) + if "usage_acu" in metrics and provider != "devin": + raise _error("usage_acu is available only for devin observations") + + +@lru_cache(maxsize=1) +def fixture_manifest_digest() -> str: + """Return the exact installed fixture-manifest identity advertised by hosted.""" + + raw = Path(__file__).with_name( + "control_surface_session_summary.fixture-manifest.json" + ).read_bytes() + return hashlib.sha256(raw).hexdigest() + + +def capability_accepts_summary(value: object) -> bool: + """Fail closed unless hosted advertises this exact installed contract.""" + + return bool( + isinstance(value, Mapping) + and set(value) == _CAPABILITY_FIELDS + and value.get("schema") == CAPABILITY_SCHEMA + and value.get("summary_schema") == SUMMARY_SCHEMA + and type(value.get("capability_version")) is int + and value["capability_version"] == CAPABILITY_VERSION + and value.get("fixture_manifest_sha256") == fixture_manifest_digest() + and value.get("accepting") is True + ) + + +def capability_from_health(value: object) -> dict[str, Any] | None: + """Read only the exact capability from the hosted health response.""" + + if not isinstance(value, Mapping): + return None + capabilities = value.get("capabilities") + if not isinstance(capabilities, Mapping): + return None + candidate = capabilities.get(EVENT_TYPE) + return dict(candidate) if capability_accepts_summary(candidate) else None + + +def opaque_session(logical_session: str) -> str: + """Derive a stable unlinkable cloud key, never reuse a Slack/provider id.""" + + if not isinstance(logical_session, str) or not logical_session or len(logical_session) > 128: + raise _error("logical session must be bounded local metadata") + seed = (SUMMARY_SCHEMA + "\0" + logical_session).encode("utf-8") + return hashlib.sha256(seed).hexdigest()[:32] + + +def _stamp(value: dt.datetime) -> str: + if not isinstance(value, dt.datetime) or value.tzinfo is None: + raise _error("observed_at must include a UTC offset") + return value.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z") + + +def _owner_action(lifecycle: Mapping[str, Any]) -> str: + state, reason = lifecycle["state"], lifecycle["reason"] + try: + return LIFECYCLE_POLICY[state][reason] + except KeyError: + raise _error("lifecycle state and reason are not a supported producer pair") from None + + +def build_control_surface_summary( + *, + logical_session: str, + repo_slug: str, + provider: str, + lifecycle: Mapping[str, Any], + observed_at: dt.datetime, + pr_number: int | None = None, + head_sha: str | None = None, + pr_state: str | None = None, + elapsed_seconds: float | None = None, + usage_acu: float | None = None, +) -> dict[str, Any]: + """Build one retry-stable allowlisted summary from a public lifecycle.""" + + from .remote_session import RemoteError, public_projection + + try: + projected = public_projection(dict(lifecycle)) + except (RemoteError, TypeError, ValueError): + raise _error("lifecycle must be the closed public projection") from None + created_at = _stamp(observed_at) + state = projected["state"] + dimensions: dict[str, Any] = { + "summary_schema": SUMMARY_SCHEMA, + "capability_version": CAPABILITY_VERSION, + "control_surface": "slack", + "session": opaque_session(logical_session), + "privacy_classification": PRIVACY_CLASSIFICATION, + "state": state, + "lifecycle_reason": projected["reason"], + "outcome": OUTCOME_BY_STATE.get(state, "unknown"), + "owner_action": _owner_action(projected), + } + if pr_number is not None: + dimensions["pr_number"] = str(pr_number) + if head_sha is not None: + dimensions["head_sha"] = head_sha + if pr_state is not None: + dimensions["pr_state"] = pr_state + metrics: dict[str, Any] = { + "summary_count": 1, + **{f"{key}_count": projected["counts"][key] for key in ("dispatch", "message", "cancel", "collect")}, + } + if elapsed_seconds is not None: + metrics["elapsed_seconds"] = elapsed_seconds + if usage_acu is not None: + metrics["usage_acu"] = usage_acu + event_seed = json.dumps( + [repo_slug, provider, created_at, dimensions, metrics], + sort_keys=True, + separators=(",", ":"), + ) + event = { + "schema": "code_mower.benchmarkEvent.v1", + "event_type": EVENT_TYPE, + "event_id": str(uuid.uuid5(uuid.NAMESPACE_URL, event_seed)), + "created_at": created_at, + "repo_slug": repo_slug, + "team_id": "", + "install_id": "", + "source": SOURCE, + "provider": provider, + "lens": "", + "status": state, + "metrics": metrics, + "dimensions": dimensions, + "tool": build_code_mower_tool_provenance( + source=SOURCE, + version=__version__, + role="reporter", + ), + } + validate_control_surface_summary(event) + return event + + +def gated_control_surface_summary( + capability: object, **summary: Any +) -> dict[str, Any] | None: + """Return no cloud event until exact hosted acceptance is advertised.""" + + if not capability_accepts_summary(capability): + return None + return build_control_surface_summary(**summary) + + +def _transition_identity(value: Mapping[str, Any]) -> tuple[Any, ...]: + dimensions = value["dimensions"] + metrics = value["metrics"] + return ( + value["repo_slug"], + value["provider"], + *( + dimensions.get(key) + for key in ( + "session", + "state", + "lifecycle_reason", + "outcome", + "owner_action", + "pr_number", + "head_sha", + "pr_state", + ) + ), + *( + metrics.get(f"{key}_count") + for key in ("dispatch", "message", "cancel", "collect") + ), + ( + metrics.get("elapsed_seconds") + if dimensions["state"] in {"complete", "failed", "terminated", "archived"} + else None + ), + ( + metrics.get("usage_acu") + if dimensions["state"] in {"complete", "failed", "terminated", "archived"} + else None + ), + ) + + +def gated_control_surface_transition( + capability: object, + previous: Mapping[str, Any] | None = None, + **summary: Any, +) -> dict[str, Any] | None: + """Emit only a meaningful lifecycle/count/PR change after capability acceptance.""" + + current = gated_control_surface_summary(capability, **summary) + if current is None: + return None + if previous is None: + return current + try: + validate_control_surface_summary(previous) + except ValueError: + return current + return None if _transition_identity(previous) == _transition_identity(current) else current + + +def slack_board_run( + *, + logical_session: str, + binding: Any, + provider: str, + observed_at: dt.datetime, + lifecycle: Mapping[str, Any], +) -> Any: + """Adapt a Slack-requested lifecycle into the existing local Board seam.""" + + from .board_local_observation import run_from_remote_lifecycle + + return run_from_remote_lifecycle( + id="slack-" + opaque_session(logical_session)[:20], + binding=binding, + provider=provider, + role="builder", + observed_at=observed_at, + lifecycle=lifecycle, + ) diff --git a/src/code_mower/package_manifest.py b/src/code_mower/package_manifest.py index 317d33ee..f6897a27 100644 --- a/src/code_mower/package_manifest.py +++ b/src/code_mower/package_manifest.py @@ -31,6 +31,36 @@ ("src/code_mower/board_local_observation.py", "src/code_mower/board_local_observation.py", "core"), ("src/code_mower/board_remote_observation.py", "src/code_mower/board_remote_observation.py", "core"), ("src/code_mower/board_service.py", "src/code_mower/board_service.py", "core"), + ( + "src/code_mower/control_surface_summary.py", + "src/code_mower/control_surface_summary.py", + "core", + ), + ( + "src/code_mower/control_surface_session_summary.schema.json", + "src/code_mower/control_surface_session_summary.schema.json", + "schema", + ), + ( + "src/code_mower/control_surface_session_summary.accepted.json", + "src/code_mower/control_surface_session_summary.accepted.json", + "fixture", + ), + ( + "src/code_mower/control_surface_session_summary.rejected.json", + "src/code_mower/control_surface_session_summary.rejected.json", + "fixture", + ), + ( + "src/code_mower/control_surface_session_summary.expectations.json", + "src/code_mower/control_surface_session_summary.expectations.json", + "fixture", + ), + ( + "src/code_mower/control_surface_session_summary.fixture-manifest.json", + "src/code_mower/control_surface_session_summary.fixture-manifest.json", + "fixture", + ), ("src/code_mower/file_locks.py", "src/code_mower/file_locks.py", "core"), ("src/code_mower/builder_runs.py", "src/code_mower/builder_runs.py", "core"), ("src/code_mower/builder_experiment.py", "src/code_mower/builder_experiment.py", "core"), @@ -668,6 +698,7 @@ ("docs/cloud-benchmarking.md", "docs/cloud-benchmarking.md", "doc"), ("docs/repo-strategy.md", "docs/repo-strategy.md", "doc"), ("docs/commercial-boundary.md", "docs/commercial-boundary.md", "doc"), + ("docs/control-surface-telemetry.md", "docs/control-surface-telemetry.md", "doc"), ("docs/public-release-checklist.md", "docs/public-release-checklist.md", "doc"), ("docs/operational-evidence.md", "docs/operational-evidence.md", "doc"), ("docs/release-qualification.md", "docs/release-qualification.md", "doc"), diff --git a/tests/test_control_surface_summary.py b/tests/test_control_surface_summary.py new file mode 100644 index 00000000..ef64c1bf --- /dev/null +++ b/tests/test_control_surface_summary.py @@ -0,0 +1,415 @@ +from __future__ import annotations + +import hashlib +import importlib.resources +import json +import tempfile +from datetime import UTC, datetime +from pathlib import Path +from unittest import TestCase, mock + +from code_mower.cloud_client.errors import CloudBundleError +from code_mower.cloud_client.events import validate_cloud_event +from code_mower.cloud_client.endpoints import probe_cloud_service +from code_mower.board_local_observation import WorkBinding +from code_mower.control_surface_summary import ( + CAPABILITY_SCHEMA, + CAPABILITY_VERSION, + EVENT_TYPE, + LIFECYCLE_POLICY, + OUTCOME_BY_STATE, + SUMMARY_SCHEMA, + build_control_surface_summary, + capability_accepts_summary, + capability_from_health, + fixture_manifest_digest, + gated_control_surface_summary, + gated_control_surface_transition, + opaque_session, + slack_board_run, + validate_control_surface_summary, +) +from code_mower.remote_session import FakeProvider, RemoteSessions, _key +from code_mower.package_manifest import PACKAGE_FILES + + +ROOT = Path(__file__).resolve().parents[1] +RESOURCE_ROOT = ROOT / "src" / "code_mower" +PREFIX = "control_surface_session_summary" + + +def _fixture(name: str) -> dict: + return json.loads((RESOURCE_ROOT / f"{PREFIX}.{name}.json").read_text()) + + +class ControlSurfaceSummaryTests(TestCase): + def test_all_canonical_accepted_events_satisfy_specialized_and_cloud_boundaries(self) -> None: + rows = _fixture("accepted")["events"] + + assert len(rows) == 21 + assert {row["event"]["dimensions"]["state"] for row in rows} == { + "archived", + "complete", + "failed", + "pending", + "running", + "suspended", + "terminated", + "uncertain", + "waiting_for_approval", + "waiting_for_user", + } + for row in rows: + validate_control_surface_summary(row["event"]) + assert validate_cloud_event(row["event"]) == row["event"] + + + def test_all_canonical_rejected_events_fail_closed(self) -> None: + rows = _fixture("rejected")["events"] + + assert len(rows) == 19 + for row in rows: + with self.assertRaises(CloudBundleError): + validate_control_surface_summary(row["event"]) + with self.assertRaises(CloudBundleError): + validate_cloud_event(row["event"]) + + + def test_fixture_manifest_binds_exact_source_and_installed_resource_bytes(self) -> None: + manifest_path = RESOURCE_ROOT / f"{PREFIX}.fixture-manifest.json" + manifest = json.loads(manifest_path.read_text()) + + assert manifest["contract_schema"] == SUMMARY_SCHEMA + assert manifest["digest_scope"] == "exact_file_bytes" + assert [row["path"] for row in manifest["files"]] == sorted( + row["path"] for row in manifest["files"] + ) + package_root = importlib.resources.files("code_mower") + for row in manifest["files"]: + source = (RESOURCE_ROOT / row["path"]).read_bytes() + installed = package_root.joinpath(row["path"]).read_bytes() + assert installed == source + assert len(source) == row["bytes"] + assert hashlib.sha256(source).hexdigest() == row["sha256"] + + + def test_contract_resources_are_part_of_materialized_packages(self) -> None: + targets = {target for _source, target, _kind in PACKAGE_FILES} + required = { + "src/code_mower/control_surface_summary.py", + *{ + f"src/code_mower/{PREFIX}.{suffix}.json" + for suffix in ( + "schema", + "accepted", + "rejected", + "expectations", + "fixture-manifest", + ) + }, + } + + assert required <= targets + + + def test_schema_and_expectations_pin_capability_and_hosted_data_controls(self) -> None: + schema = _fixture("schema") + expectations = _fixture("expectations") + + assert schema["$id"] == SUMMARY_SCHEMA + assert schema["properties"]["event_type"]["const"] == EVENT_TYPE + assert ( + schema["$defs"]["dimensions"]["properties"]["capability_version"]["const"] + == CAPABILITY_VERSION + ) + names = {row["name"] for row in expectations["expectations"]} + assert { + "capability_absent", + "capability_mismatch", + "rollback", + "tenant_isolation", + "export", + "deletion", + "retention", + "token_revocation", + "aggregate_reconciliation", + } <= names + + + def test_capability_gate_requires_exact_installed_contract_identity(self) -> None: + capability = { + "schema": CAPABILITY_SCHEMA, + "summary_schema": SUMMARY_SCHEMA, + "capability_version": CAPABILITY_VERSION, + "fixture_manifest_sha256": fixture_manifest_digest(), + "accepting": True, + } + + assert capability_accepts_summary(capability) + assert capability_from_health( + {"capabilities": {EVENT_TYPE: capability}} + ) == capability + for changed in ( + {**capability, "accepting": False}, + {**capability, "capability_version": 2}, + {**capability, "fixture_manifest_sha256": "0" * 64}, + {**capability, "unknown": "field"}, + None, + ): + assert not capability_accepts_summary(changed) + assert capability_from_health({"capabilities": {EVENT_TYPE: changed}}) is None + + + def test_summary_builder_is_retry_stable_and_capability_gated(self) -> None: + lifecycle = { + "schema": "code_mower.remote_session.v1", + "state": "complete", + "reason": "none", + "next_action": "none", + "counts": {"dispatch": 1, "message": 1, "cancel": 0, "collect": 1}, + } + values = { + "logical_session": "private-slack-session-reference", + "repo_slug": "example/project", + "provider": "devin", + "lifecycle": lifecycle, + "observed_at": datetime(2026, 9, 21, 7, 0, tzinfo=UTC), + "pr_number": 921, + "head_sha": "a" * 40, + "pr_state": "open", + "elapsed_seconds": 60.0, + "usage_acu": 0.25, + } + first = build_control_surface_summary(**values) + second = build_control_surface_summary(**values) + + assert first == second + assert first["dimensions"]["session"] == opaque_session(values["logical_session"]) + assert values["logical_session"] not in json.dumps(first) + assert gated_control_surface_summary(None, **values) is None + capability = { + "schema": CAPABILITY_SCHEMA, + "summary_schema": SUMMARY_SCHEMA, + "capability_version": CAPABILITY_VERSION, + "fixture_manifest_sha256": fixture_manifest_digest(), + "accepting": True, + } + assert gated_control_surface_summary(capability, **values) == first + + later = {**values, "observed_at": datetime(2026, 9, 21, 7, 1, tzinfo=UTC)} + assert gated_control_surface_transition(capability, first, **later) is None + changed = { + **later, + "lifecycle": { + **lifecycle, + "counts": {**lifecycle["counts"], "message": 2}, + }, + } + assert gated_control_surface_transition(capability, first, **changed) is not None + assert gated_control_surface_transition(capability, {"invalid": True}, **later) is not None + + + def test_archived_result_not_ready_is_a_valid_provider_owner_action(self) -> None: + lifecycle = { + "schema": "code_mower.remote_session.v1", + "state": "archived", + "reason": "result_not_ready", + "next_action": "inspect_provider", + "counts": {"dispatch": 1, "message": 0, "cancel": 0, "collect": 0}, + } + + event = build_control_surface_summary( + logical_session="archived-session", + repo_slug="example/project", + provider="devin", + lifecycle=lifecycle, + observed_at=datetime(2026, 9, 21, 7, 0, tzinfo=UTC), + ) + + assert event["dimensions"]["state"] == "archived" + assert event["dimensions"]["lifecycle_reason"] == "result_not_ready" + assert event["dimensions"]["owner_action"] == "inspect_provider" + validate_control_surface_summary(event) + assert validate_cloud_event(event) == event + + + def test_real_collect_lifecycles_cover_every_result_availability_state(self) -> None: + cases = ( + ("pending", "", "pending", "result_not_ready"), + ("running", "", "running", "result_not_ready"), + ("owner_action", "waiting_for_owner", "waiting_for_user", "result_not_ready"), + ("owner_action", "approval_required", "waiting_for_approval", "result_not_ready"), + ("complete", "", "complete", "result_unavailable"), + ("failed", "", "failed", "result_not_ready"), + ("suspended", "", "suspended", "result_not_ready"), + ("terminated", "", "terminated", "result_not_ready"), + ("archived", "", "archived", "result_not_ready"), + ) + for raw_state, raw_reason, state, reason in cases: + with self.subTest(raw_state=raw_state, raw_reason=raw_reason), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp).resolve() + provider = FakeProvider(root / "provider") + sessions = RemoteSessions(root / "sessions", provider) + logical_session = "summary-matrix" + sessions.run( + "dispatch", + logical_session, + prose="metadata-only lifecycle test", + repo="example/project", + apply=True, + ) + record = sessions.store.read_only(_key(logical_session)) + provider.set_state(record["binding"], raw_state, reason=raw_reason) + + lifecycle = sessions.run("collect", logical_session, apply=True) + event = build_control_surface_summary( + logical_session=logical_session, + repo_slug="example/project", + provider="codex", + lifecycle=lifecycle, + observed_at=datetime(2026, 9, 21, 7, 0, tzinfo=UTC), + ) + + assert (lifecycle["state"], lifecycle["reason"]) == (state, reason) + assert event["dimensions"]["lifecycle_reason"] == reason + assert event["dimensions"]["owner_action"] == "inspect_provider" + assert validate_cloud_event(event) == event + + + def test_schema_and_semantic_validators_enforce_the_exact_lifecycle_policy_matrix(self) -> None: + schema = _fixture("schema") + schema_policy = { + ( + row["properties"]["state"]["const"], + row["properties"]["lifecycle_reason"]["const"], + row["properties"]["owner_action"]["const"], + ) + for row in schema["$defs"]["dimensions"]["anyOf"] + } + semantic_policy = { + (state, reason, action) + for state, reasons in LIFECYCLE_POLICY.items() + for reason, action in reasons.items() + } + assert schema_policy == semantic_policy + base = next( + row["event"] + for row in _fixture("accepted")["events"] + if row["event"]["provider"] == "codex" + ) + reasons = sorted({reason for policy in LIFECYCLE_POLICY.values() for reason in policy}) + actions = ( + "none", + "answer_question", + "respond_to_approval", + "inspect_provider", + "inspect_failure", + ) + for state in sorted(LIFECYCLE_POLICY): + for reason in reasons: + for action in actions: + with self.subTest(state=state, reason=reason, action=action): + event = json.loads(json.dumps(base)) + event["status"] = state + event["dimensions"].update( + state=state, + lifecycle_reason=reason, + outcome=OUTCOME_BY_STATE.get(state, "unknown"), + owner_action=action, + ) + expected = LIFECYCLE_POLICY[state].get(reason) == action + if expected: + validate_control_surface_summary(event) + assert validate_cloud_event(event) == event + else: + with self.assertRaises(CloudBundleError): + validate_control_surface_summary(event) + with self.assertRaises(CloudBundleError): + validate_cloud_event(event) + + + def test_tool_provenance_is_closed_and_rejects_forbidden_nested_content(self) -> None: + event = _fixture("accepted")["events"][0]["event"] + for tool in ( + {**event["tool"], "message": "private Slack message"}, + {**event["tool"], "details": {"prompt": "private prompt"}}, + {**event["tool"], "runtime_environment": "developer-laptop"}, + ): + changed = {**event, "tool": tool} + with self.assertRaises(CloudBundleError): + validate_control_surface_summary(changed) + with self.assertRaises(CloudBundleError): + validate_cloud_event(changed) + + + def test_health_probe_exposes_only_the_exact_summary_capability(self) -> None: + capability = { + "schema": CAPABILITY_SCHEMA, + "summary_schema": SUMMARY_SCHEMA, + "capability_version": CAPABILITY_VERSION, + "fixture_manifest_sha256": fixture_manifest_digest(), + "accepting": True, + } + + class FakeResponse: + def __init__(self, body: dict[str, object]) -> None: + self.body = body + + def __enter__(self) -> "FakeResponse": + return self + + def __exit__(self, *args: object) -> None: + return None + + def read(self) -> bytes: + return json.dumps(self.body).encode() + + def getcode(self) -> int: + return 200 + + accepted = {"capabilities": {EVENT_TYPE: capability}} + with mock.patch( + "urllib.request.urlopen", return_value=FakeResponse(accepted) + ): + check = probe_cloud_service("https://codemower.com/api/ingest", timeout=1) + assert check["detail"][EVENT_TYPE] == capability + + mismatched = { + "capabilities": { + EVENT_TYPE: {**capability, "fixture_manifest_sha256": "0" * 64} + } + } + with mock.patch( + "urllib.request.urlopen", return_value=FakeResponse(mismatched) + ): + check = probe_cloud_service("https://codemower.com/api/ingest", timeout=1) + assert EVENT_TYPE not in check["detail"] + + + def test_slack_lifecycle_uses_existing_local_board_adapter(self) -> None: + binding = WorkBinding( + session_id="a" * 32, + work_id="work1", + repository="example/project", + worktree_id="sha256:" + "b" * 64, + ) + lifecycle = { + "schema": "code_mower.remote_session.v1", + "state": "waiting_for_user", + "reason": "user_input_required", + "next_action": "none", + "counts": {"dispatch": 1, "message": 0, "cancel": 0, "collect": 0}, + } + + run = slack_board_run( + logical_session="private-slack-session-reference", + binding=binding, + provider="codex", + observed_at=datetime(2026, 9, 21, 7, 0, tzinfo=UTC), + lifecycle=lifecycle, + ) + + assert run.binding == binding + assert run.phase == "waiting_for_user" + assert run.source_kind == "remote_session" + assert run.lifecycle == lifecycle