# Feature Specification: Action Surface Contract + CI Enforcement

**Feature Branch**: `082-action-surface-contract`  
**Created**: 2026-02-08  
**Status**: Draft  
**Input**: User description: "Spec 082 — Action Surface Contract + CI Enforcement"

## Clarifications

### Session 2026-02-08

- Q: Group label standard: “More” vs “Actions” → A: Use “More” as the standard label for secondary row actions and bulk action groups.
- Q: Standard safe bulk action for ReadOnly/RunLog: allow Export everywhere? → A: Yes — Export is the default safe bulk action for ReadOnly and RunLog surfaces when data exists; otherwise an explicit exemption with reason is required.
- Q: Exemption policy: time-boxed vs reason-only? → A: Exemptions must include a non-empty reason; an optional tracking link/reference may be included; no deadline is required.
- Q: Audit policy: destructive-only vs all mutations? → A: Audit events are required for all mutations and for operation-start triggers (run/sync/verify/dispatch).
- Q: Validator scope/discovery: which components are in-scope for CI enforcement? → A: Tenant + Admin panels; validate Resources, Pages, and embedded relationship sub-lists; exclude Widgets.

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Prevent incomplete UI action surfaces (Priority: P1)

As a developer, when I add or modify an admin-console UI component (list, detail, embedded relationship sub-list), I must explicitly declare its expected action surface (or explicitly exempt parts with a reason), so that automated checks prevent regressions before merge.

**Why this priority**: This is the regression gate. Without it, incomplete action surfaces ship repeatedly and create inconsistent UX and RBAC drift.

**Independent Test**: Add a minimal new UI component without an action surface declaration and confirm the automated gate fails with a precise message; then add a declaration (or exemption) and confirm the gate passes.

**Acceptance Scenarios**:

1. **Given** a new or modified in-scope UI component without an action surface declaration, **When** validation runs, **Then** it fails with a message naming the component and the missing required slots.
2. **Given** a component with a complete declaration (or exemptions with reasons), **When** validation runs, **Then** it passes.
3. **Given** a component uses an exemption, **When** validation runs, **Then** it fails if the exemption reason is missing or empty.

---

### User Story 2 - Consistent, enterprise-grade actions and empty-state guidance (Priority: P2)

As an admin-console user, I see predictable and consistent actions across lists and details: primary actions are obvious, secondary actions are grouped, bulk actions exist where appropriate, and empty states provide an actionable next step.

**Why this priority**: Reduces training cost and operational mistakes; improves speed and confidence in admin workflows.

**Independent Test**: Pick one CRUD-style list and one run-log style list; verify each has the expected action areas (header/row/bulk/empty-state; detail header actions) and that ordering/grouping conventions are consistent.

**Acceptance Scenarios**:

1. **Given** a list view with records, **When** a user inspects available actions, **Then** they see at most two visible row actions and all secondary actions are grouped under a consistent label.
2. **Given** a list view with zero records, **When** the page is shown, **Then** it includes at least one CTA that helps resolve the empty state.
3. **Given** a list view that supports selection, **When** the user selects one or more records, **Then** at least one bulk action is available or the UI explicitly documents why bulk is intentionally not offered.

---

### User Story 3 - Predictable RBAC behavior for members vs non-members (Priority: P3)

As a security reviewer, I need consistent authorization semantics across all in-scope actions:
- A non-member must not discover tenant/workspace surfaces (deny-as-not-found).
- A member without the required capability must be prevented from completing the action (deny-as-forbidden), and the UI must communicate why.

**Why this priority**: Prevents information disclosure and capability drift; keeps the product aligned with least privilege.

**Independent Test**: Use a representative surface and verify (a) non-member receives not-found, and (b) member without capability receives forbidden and sees disabled UI with a helpful explanation.

**Acceptance Scenarios**:

1. **Given** a user who is not a member of the relevant tenant/workspace scope, **When** they attempt to access a related page or record, **Then** the response is not-found.
2. **Given** a user who is a member but lacks capability for a specific action, **When** they attempt to execute the action, **Then** the response is forbidden.
3. **Given** a member without capability viewing the UI, **When** they inspect the action surface, **Then** disallowed actions are visible-but-disabled with a clear explanation (not silently missing).

### Edge Cases

- A surface is intentionally read-only and has no meaningful bulk actions (must be explicitly exempted with a reason).
- A user loses membership/capability between page load and action execution (server-side enforcement must still apply).
- Bulk mutations above a risk threshold must require stronger confirmation (typed confirmation) to reduce accidental damage.
- Empty states that cannot be resolved from within the current surface (CTA should route users to the correct “next step” surface).
- Global search and cross-scope links must not reveal existence of tenant/workspace data to non-members.

## Requirements *(mandatory)*

**Constitution alignment (required):** This feature is primarily a UI governance and authorization-consistency feature. It MUST not introduce new external calls or long-running operations as part of the enforcement gate.

**Constitution alignment (RBAC-UX):** This feature formalizes 404 vs 403 semantics and requires both UI signaling and server-side enforcement for in-scope actions.

**Constitution alignment (Action Surfaces):** This feature defines and enforces an Action Surface Contract, and introduces a mandatory UI Action Matrix for future admin UI-related specs.

### Functional Requirements

- **FR-001 (Contract profiles)**: The system MUST define a small set of action-surface profiles (e.g., CRUD, ReadOnly, RunLog, embedded relationship sub-lists) that describe minimum expected action areas for list and detail surfaces.
- **FR-002 (Required action slots)**: For each profile, the system MUST define required action slots for:
  - list/table header actions,
  - row actions,
  - bulk actions,
  - empty-state CTA(s),
  - detail header actions,
  - create/edit save/cancel conventions (where applicable).
- **FR-003 (Exemptions)**: The system MUST allow explicit exemptions for specific slots, and each exemption MUST include:
  - a non-empty reason, and
  - an optional tracking reference (issue/PR link or identifier).
- **FR-004 (Mandatory declaration)**: For every in-scope admin-console UI component that is new or modified, a declaration MUST exist that:
  - identifies the profile,
  - declares which slots are satisfied,
  - lists any exemptions with reasons.
- **FR-005 (Automated gate)**: The system MUST automatically validate contract compliance for all in-scope UI components in the code review pipeline, and MUST fail with actionable messages when requirements are not met.
- **FR-006 (Consistency rules)**: The system MUST enforce the following UX conventions through (a) declaration validation for all in-scope surfaces and (b) runtime tests on representative surfaces:
  - no more than two visible row actions (typically View/Edit),
  - secondary actions grouped under the standard label “More”,
  - bulk actions grouped under the standard label “More”,
  - destructive actions are never primary.
- **FR-006a (Default safe bulk action)**: For ReadOnly and RunLog profiles, the default bulk action MUST be “Export” when the surface contains data; if “Export” is not offered, an explicit exemption with a reason is required.
- **FR-007 (Safety + confirmation)**: Any mutating or destructive action MUST require confirmation. For this feature, compliance is enforced via declaration checks plus representative runtime tests for touched surfaces.
- Mutation classes for FR-007:
  - data mutation: create/update/delete/archive/restore/attach/detach,
  - operation-start trigger: enqueue/dispatch/run/sync/verify/retry actions,
  - access/safety mutation: role or membership changes, credential or permission updates, safety gate toggles.
- High-risk bulk actions MUST require typed confirmation when any of the following is true:
  - the action is destructive/irreversible, or
  - the action affects more than 25 records, or
  - the action changes tenant/workspace access, credentials, or operational safety gates.
- **FR-008 (Auditability)**: All mutations and operation-start triggers MUST use existing audit sinks and MUST write an audit event that records at minimum: actor, scope (tenant/workspace), action type, target(s), timestamp, and outcome.
- Audit sinks for FR-008:
  - tenant-scoped actions: `App\Services\Intune\AuditLogger` (writes `audit_logs`),
  - workspace/platform-scoped actions: `App\Services\Audit\WorkspaceAuditLogger` (writes `audit_logs`).
- Operation-start triggers MAY satisfy FR-008 via canonical `OperationRun` creation when the action is run-oriented and existing architecture uses `OperationRun` as the observability record.
- For this feature, FR-008 is enforced as: no regressions on touched representative surfaces and no new ad-hoc audit sink introduced.
- **FR-009 (RBAC UI gating standard)**: UI gating MUST follow the project’s standard semantics:
  - non-member → deny-as-not-found behavior,
  - member without capability → deny-as-forbidden behavior,
  - UI communicates “forbidden” as visible-but-disabled actions with a tooltip/explanation.
- **FR-010 (Server-side authorization)**: Server-side authorization MUST be enforced for every action execution (not just UI visibility) and MUST align with FR-009 semantics (see constitution RBAC-UX-001..004).
- **FR-011 (Canonical run links)**: Any “View run” deep link MUST use the canonical tenantless operations path as the primary link; tenant-scoped convenience links may exist but must not be primary.
- **FR-012 (Scope)**: The contract MUST cover both the tenant-scoped admin console and the platform/admin console surfaces for resources, pages, and embedded relationship sub-lists.
- **FR-012a (Explicit CI scope)**: The automated gate MUST validate Resources, Pages, and embedded relationship sub-lists in both tenant-scoped and platform/admin consoles, and MUST exclude Widgets from enforcement.

### Non-Goals

- This feature does not redesign visual layout or page styling.
- This feature does not change domain workflows (sync/restore/verify), except to ensure action surfaces are complete or explicitly exempted.
- This feature does not change panel architecture or audience boundaries.

### Assumptions

- A safe, broadly applicable bulk action exists for read-only/run-log surfaces (“Export”) where data volume and permissions allow.
- Some existing surfaces will require temporary exemptions to keep the repository in a releasable state while retrofit work is completed.

### Enforcement Boundaries

- The CI validator for this feature is declaration-driven and deterministic (filesystem + reflection only).
- Runtime behavior is verified via representative tests for each profile and each critical rule family (grouping, confirmation, RBAC semantics, canonical run links).
- Deep runtime introspection of every Filament action in CI is explicitly out of scope for this iteration.

## UI Action Matrix *(mandatory when admin UI components are changed)*

_Note: This matrix applies whenever admin UI components (resources, pages, or embedded relationship sub-lists) are added or changed._

This feature introduces a contract that applies broadly. The matrix below documents the minimum action surface expectations by profile.

| Surface | Location | Header Actions | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|
| CRUD list + detail | Tenant panel + Admin panel | Primary: Create (if allowed). Optional: domain CTA (e.g., Sync/Run) | Primary: View, Edit (if allowed). Secondary: grouped under “More” | At least 1 bulk action (e.g., Archive/Restore/Export) | Primary CTA: Create or domain CTA | Primary: Edit (if allowed). Secondary grouped under “More” | Consistent Save + Cancel; no destructive primary | Yes (for mutations + operation-start) | Exempt bulk only with explicit reason (e.g., no safe bulk operation exists) |
| ReadOnly list + detail | Tenant panel + Admin panel | At least 1 CTA that provides value (e.g., Export/Refresh) | Primary: View. Secondary grouped under “More” | Bulk: Export (default) | CTA that resolves empty state (e.g., Refresh now) | “More” group for secondary actions | N/A | Maybe (typically for operation-start actions) | Exempt Export only with explicit reason (e.g., legal/security constraints) |
| RunLog list + detail | Tenant panel + Admin panel | Optional CTA routing to Operations hub | Primary: View | Bulk: Export (default) or Prune if retention exists; otherwise exempt with reason | Optional CTA routing to Operations hub | “View run” canonical link; optional Export | N/A | Maybe (for prune/retention changes) | “View run” must be canonical tenantless as primary deep link |
| Embedded relationship sub-list | Under parent detail | Header: Add/Attach/Create/Refresh depending on context | Primary: View and one context action (e.g., Detach/Remove). Secondary grouped under “More” | At least 1 bulk action or exemption with reason | CTA: next step (Add/Attach/Refresh) | N/A | N/A | Yes (for mutations) | Bulk may be exempted if relationship action is inherently single-record |

### Key Entities *(include if feature involves data)*

- **Action Surface Declaration**: A structured, human-reviewed statement of what actions a surface offers (and why some may be exempt).
- **Action Surface Profile**: A category of surfaces with shared minimum action expectations (CRUD, ReadOnly, RunLog, embedded relationship sub-list).
- **Action Slot**: A required location/type of action (header, row, bulk, empty-state, detail header, save/cancel).
- **Exemption**: A documented, explicit deviation from the contract for a specific slot.
- **Capability**: The permission concept used to decide whether an action is allowed.
- **Audit Event**: A record that an action was attempted/executed and its outcome.

### Dependencies

- A single, canonical capability registry exists and is used consistently for authorization decisions.
- Standard UI gating helpers exist for tenant/workspace scopes and are the only allowed approach for UI enable/disable behavior.
- A centralized audit-event sink exists to record user-triggered mutations and operation starts.

### Acceptance Notes (verification approach)

- Contract compliance is verified via an automated validation gate that enumerates in-scope UI components and checks required slots/exemptions.
- Authorization correctness is verified via representative automated tests that prove not-found vs forbidden behavior across at least one surface per profile.
- Consistency/confirmation/canonical-link behavior is verified on representative runtime surfaces; declaration checks remain the repo-wide completeness gate.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001 (Coverage)**: 100% of in-scope UI components are contract-compliant or have explicit exemptions with reasons.
- **SC-002 (Regression prevention)**: Any new/modified in-scope UI component without a declaration fails validation in the review pipeline.
- **SC-003 (Action completeness)**: For all CRUD and ReadOnly list surfaces, the selection state always presents at least one bulk action or an explicit exemption.
- **SC-004 (Authorization correctness)**: At least one representative surface per profile has automated tests proving not-found vs forbidden behavior (non-member vs member without capability).
- **SC-005 (Developer experience)**: Validation failures provide actionable messages that identify the component and missing slot(s), enabling a fix without additional investigation.
