# Feature Specification: Golden Master / Baseline Governance v1 (R1.1–R1.4)

**Feature Branch**: `101-golden-master-baseline-governance-v1`  
**Created**: 2026-02-19  
**Status**: Draft  
**Input**: Introduce a workspace-owned “Golden Master” baseline that can be captured from a tenant, compared against the current tenant state, and surfaced in the UI as “Soll vs Ist” with drift findings and an operational summary.

## Spec Scope Fields *(mandatory)*

- **Scope**: workspace
- **Primary Routes**: Admin Governance “Baselines” area; tenant-facing dashboard card; drift comparison landing (“Soll vs Ist”)
- **Data Ownership**: Baselines and snapshots are workspace-owned; tenant assignments are tenant-owned (workspace_id + tenant_id); drift findings are tenant-owned (workspace_id + tenant_id)
- **RBAC**: workspace membership required for any visibility; capability gating for view vs manage (see Requirements)

## Clarifications

### Session 2026-02-19

- Q: Which v1 drift severity mapping should we use? → A: Fixed mapping: missing_policy=high, different_version=medium, unexpected_policy=low.
- Q: Which snapshot should compare runs use in v1? → A: Always use BaselineProfile.active_snapshot; if missing, block compare with a clear reason.
- Q: How should compare/capture handle precondition failures? → A: UI disables where possible AND server blocks start with 422 + stable reason_code; no OperationRun is created for failed preconditions.
- Q: Where should drift findings be stored in v1? → A: Use existing findings storage with source=baseline.compare; fingerprint/idempotency lives there.
- Q: How should tenant override filters combine with the profile filter? → A: Override narrows scope; effective filter = profile ∩ override.

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Create and manage a baseline profile (Priority: P1)

As a workspace governance owner/manager, I can define what “good” looks like by creating a Baseline Profile, controlling its status (draft/active/archived), and scoping which policy domains are included.

**Why this priority**: Without a baseline profile, there is nothing to capture or compare against.

**Independent Test**: A user with baseline manage rights can create/edit/archive a baseline profile and see it listed; a read-only user can list/view but cannot mutate.

**Acceptance Scenarios**:

1. **Given** I am a workspace member with baseline manage capability, **When** I create a baseline profile with a name, optional description, optional version label, and a policy-domain filter, **Then** the profile is persisted and visible in the Baselines list.
2. **Given** I am a workspace member with baseline view-only capability, **When** I open a baseline profile, **Then** I can view its details but cannot edit/archive/delete it.
3. **Given** I am not a member of the workspace, **When** I attempt to access baseline pages, **Then** I receive a not-found response (deny-as-not-found).

---

### User Story 2 - Capture an immutable baseline snapshot from a tenant (Priority: P2)

As a baseline manager, I can capture a snapshot of the current tenant configuration that the baseline profile covers, producing an immutable “baseline snapshot” that can later be compared.

**Why this priority**: The baseline must be based on a real, point-in-time state to be meaningful and auditable.

**Independent Test**: Capturing twice with unchanged tenant state reuses the same snapshot identity and does not create duplicates.

**Acceptance Scenarios**:

1. **Given** a baseline profile exists and I have baseline manage capability, **When** I trigger “Capture from tenant” and choose a source tenant, **Then** a new capture operation is created and eventually produces an immutable snapshot.
2. **Given** a capture was already completed for the same baseline profile and the tenant’s relevant policies are unchanged, **When** I capture again, **Then** the system reuses the existing snapshot (idempotent/deduped).
3. **Given** the baseline profile is active, **When** a capture completes successfully, **Then** the profile’s “active snapshot” points to the captured snapshot.

---

### User Story 3 - Compare baseline vs current tenant to detect drift (Priority: P3)

As an operator/manager, I can run “Compare now” for a tenant, and the system produces drift findings and a summary that can be used for assurance and triage.

**Why this priority**: Drift detection is the core governance signal; it makes the baseline actionable.

**Independent Test**: A compare run produces findings for missing policies, different versions, and unexpected policies, and stores a compact summary.

**Acceptance Scenarios**:

1. **Given** a tenant is assigned to an active baseline profile with an active snapshot, **When** I run “Compare now”, **Then** a compare operation runs and produces drift findings and a drift summary.
2. **Given** the same drift item is detected in repeated compares, **When** compares are run multiple times, **Then** the same finding is updated (idempotent fingerprint) rather than duplicated.
3. **Given** I am a workspace member without baseline view capability, **When** I try to start a compare, **Then** the request is forbidden.

### Edge Cases

- Baseline profile is draft or archived: compare is blocked; users are told what must be changed (e.g., “activate baseline”).
- Tenant has no baseline assignment: compare button is disabled and the UI explains why.
- Baseline profile has no active snapshot yet: compare is blocked with a clear reason.
- Concurrent operation starts: the system prevents multiple “active” captures/compares for the same scope.
- Baseline filter yields no relevant policies: capture creates an empty snapshot and compare returns “no items checked”, without errors.

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001 (Baseline Profiles)**: The system MUST allow workspace baseline managers to create, edit, activate, deactivate (return to draft), and archive baseline profiles. Status transitions: draft ↔ active → archived (archived is terminal in v1).
- **FR-002 (Scope Control)**: Each baseline profile MUST define which policy domains/types are included in the baseline.
- **FR-003 (Tenant Assignment)**: The system MUST support assigning exactly one baseline profile per tenant per workspace (v1), with an optional per-tenant override of the profile’s scope; in v1 the override may only narrow scope (effective filter = profile ∩ override).
- **FR-004 (Capture as Operation)**: Capturing a baseline snapshot MUST be tracked as an observable operation with a clear lifecycle (started/completed/failed).
- **FR-005 (Immutable Snapshots)**: Baseline snapshots and their snapshot items MUST be immutable once created.
- **FR-006 (Capture Idempotency)**: Captures MUST be deduplicated so that repeated captures with the same effective content reuse the existing snapshot identity.
- **FR-007 (Compare as Operation)**: Comparing a tenant against its baseline MUST be tracked as an observable operation with a clear lifecycle.
- **FR-008 (Drift Findings)**: Compare MUST produce drift findings using at least these drift types: missing baseline policy, different version, and unexpected policy within the baseline scope.
- **FR-009 (Severity Rules)**: Drift findings MUST be assigned severities using centrally defined rules so that severity is consistent and testable; v1 fixed mapping is: missing baseline policy = high, different version = medium, unexpected policy = low.
- **FR-010 (Finding Idempotency)**: Drift findings MUST be deduplicated with a stable fingerprint so repeated compares update existing open findings instead of creating duplicates.
- **FR-011 (Summary Output)**: Each compare operation MUST persist a summary containing totals and severity breakdowns suitable for dashboards.
- **FR-012 (UI “Soll vs Ist”)**: The UI MUST allow selecting a baseline profile and viewing the latest compare runs and drift findings for a tenant.
- **FR-013 (Compare Snapshot Selection)**: Compare runs MUST always use the baseline profile’s active snapshot; if no active snapshot exists, compare MUST be blocked with a clear reason.
- **FR-014 (Precondition Failure Contract)**: When capture/compare cannot start due to unmet preconditions, the UI MUST disable the action where possible, and the server MUST reject the request with HTTP 422 containing a stable `reason_code`; in this case, the system MUST NOT create an OperationRun.
- **FR-015 (Findings Storage)**: Drift findings produced by compare MUST be persisted using the existing findings system with `source = baseline.compare`.

### Precondition `reason_code` (v1)

- `baseline.compare.no_assignment`
- `baseline.compare.profile_not_active`
- `baseline.compare.no_active_snapshot`
- `baseline.capture.missing_source_tenant`

### Constitution Alignment: Safety, Isolation, Observability

- **Ops/Observability**: Capture and compare MUST be observable and auditable operations, and surfaced in the existing operations monitoring experience.
- **DB-only UI**: Baseline pages and drift pages MUST NOT require outbound network calls during page render or user clicks that only start/view operations; external calls (if any) must happen in background work.
- **Tenant Isolation / RBAC semantics**:
  - non-member of workspace or tenant scope → deny-as-not-found (404)
  - member but missing capability → forbidden (403)
- **Least privilege**: Two capabilities MUST exist and be enforced:
  - baseline view: can view baselines and start compare operations
  - baseline manage: can manage baselines, assignments, and capture snapshots
- **Auditability**: Baseline-related operations MUST emit audit log entries for started/completed/failed events.
- **Safe logging**: Failures MUST not include secrets or sensitive tenant data; failure reasons must be stable and suitable for support triage.

## UI Action Matrix *(mandatory when Filament is changed)*

| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Baseline Profiles | Admin → Governance → Baselines | Create baseline profile | View/Edit page available | View, Edit | Archive (grouped) | Create baseline profile | Capture from tenant; Activate/Deactivate; Assign to tenants | Save, Cancel | Yes | Destructive actions require confirmation; mutations are manage-gated; hard-delete is out of scope for v1 (archive only) |
| Drift Landing (“Soll vs Ist”) | Tenant view | Run compare now | Links to last compare operation and findings list | — | — | — | — | — | Yes | Starting compare is view-gated; results visible only to entitled users |
| Tenant Dashboard Card | Tenant dashboard | Run compare now | Click to drift landing and/or last compare operation | — | — | — | — | — | Yes | Button disabled with explanation when no assignment or no active snapshot |

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

- **Baseline Profile**: A workspace-owned definition of what should be in-scope for governance, with a lifecycle status (draft/active/archived).
- **Tenant Assignment**: A workspace-managed mapping that declares which baseline applies to a tenant, optionally overriding scope.
- **Baseline Snapshot**: An immutable point-in-time record of the baseline’s in-scope policy references captured from a tenant.
- **Snapshot Item**: A single baseline entry representing one in-scope policy reference in the snapshot.
- **Drift Finding**: A record representing an observed deviation between baseline and tenant state, deduplicated by a stable fingerprint.
- **Drift Finding Source**: Drift findings produced by this feature use `source = baseline.compare`.
- **Operation Summary**: A compact, persisted summary of a capture/compare run suitable for dashboard display.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: A baseline manager can create a baseline profile and perform an initial capture in under 5 minutes.
- **SC-002**: For a typical tenant (≤ 500 in-scope policies), a compare run completes and surfaces a summary within 2 minutes.
- **SC-003**: Re-running compare within 24 hours for unchanged drift does not create duplicate findings (0 duplicate drift fingerprints).
- **SC-004**: Unauthorized users (non-members) receive no baseline visibility (deny-as-not-found) and members without capability cannot mutate (forbidden).

## Non-Goals (v1)

- Evidence packs / stored reports for audit exports
- Advanced findings workflow (exceptions, auto-closing, recurrence handling)
- Cross-tenant portfolio comparisons
- Baseline inheritance across organizations (e.g., MSP → customer)
- Assignment/scope-tag baselines beyond the policy domains/types included in the profile scope

## Assumptions

- Tenants already have an inventory of policy versions that can be referenced for capture and compare.
- An operations monitoring experience exists where capture/compare runs can be viewed.
- A drift findings system exists that can store and display findings and severities.

## Dependencies

- Inventory + policy version history is available and trustworthy.
- Operation run tracking and monitoring is available.
- RBAC + UI enforcement semantics are already established (404 for non-member, 403 for missing capability).
- Alerts are optional for v1; the feature remains valuable without alert integrations.
