# Phase 0 — Research

This document records the key technical decisions for **101 — Golden Master / Baseline Governance v1 (R1.1–R1.4)**, grounded in existing TenantPilot patterns.

## Existing System Constraints (confirmed in repo)

### Operation runs are tenant-scoped and deduped at DB level
- `OperationRunService::ensureRunWithIdentity()` requires a `Tenant` with a non-null `workspace_id`, and always creates runs with `workspace_id` + `tenant_id`.
- Active-run idempotency is enforced via the `operation_runs_active_unique` partial unique index (queued/running).

**Implication**: Baseline capture/compare must always be executed as **tenant-owned `OperationRun` records**, even though baseline profiles are workspace-owned.

### Findings fingerprinting expects sha256 (64 chars)
- `findings.fingerprint` is `string(64)` and unique by `(tenant_id, fingerprint)`.
- `DriftHasher` already implements a stable sha256 fingerprint scheme.

**Implication**: Any baseline-compare “finding key” should be hashed before storage, and must be stable across repeated compares.

## Decisions

### D-001 — Baseline snapshot storage is workspace-owned (and data-minimized)
**Decision**: Store `baseline_snapshots` and `baseline_snapshot_items` as **workspace-owned** tables (`workspace_id` NOT NULL, **no `tenant_id`**) so a golden master snapshot can be used across tenants without requiring access to the source tenant.

**Rationale**:
- The product intent is a workspace-level standard (“Golden Master”) reusable across multiple tenants.
- Treat the snapshot as a **standard artifact**, not tenant evidence, and enforce strict data-minimization so we do not leak tenant-specific content.

**Guardrails**:
- Snapshot items store only policy identity + stable content hashes and minimal display metadata.
- Any tenant identifiers (e.g., “captured from tenant”) live in the **capture `OperationRun.context`** and audit logs, not on workspace-owned snapshot rows.

**Alternatives considered**:
- Tenant-owned baseline snapshot (include `tenant_id`): rejected because it would require cross-tenant reads of tenant-owned records to compare other tenants, which would either violate tenant isolation or force “must be member of the source tenant” semantics.

### D-002 — OperationRun types and identity inputs
**Decision**:
- Introduce `OperationRun.type` values:
  - `baseline_capture`
  - `baseline_compare`
- Use `OperationRunService::ensureRunWithIdentity()` for idempotent start surfaces.

**Identity inputs**:
- `baseline_capture`: identity inputs include `baseline_profile_id`.
- `baseline_compare`: identity inputs include `baseline_profile_id`.

**Rationale**:
- Guarantees one active run per tenant+baseline profile (matches partial unique index behavior).
- Keeps identity stable even if the active snapshot is switched mid-flight; the run context should freeze `baseline_snapshot_id` at enqueue time for determinism.

**Alternatives considered**:
- Include `baseline_snapshot_id` in identity: rejected for v1 because we primarily want “single active compare per tenant/profile”, not “single active compare per snapshot”.

### D-003 — Precondition failures return 422 and do not create OperationRuns
**Decision**: Enforce FR-014 exactly:
- The start surface validates preconditions **before** calling `OperationRunService`.
- If unmet, return **HTTP 422** with a stable `reason_code` and **do not** create an `OperationRun`.

**Rationale**:
- Aligns with spec clarifications and avoids polluting Monitoring → Operations with non-startable attempts.

### D-004 — Findings storage uses existing `findings` table; add a source discriminator
**Decision**:
- Store baseline-compare drift as `Finding::FINDING_TYPE_DRIFT`.
- Persist `source = baseline.compare` per FR-015.

**Implementation note**:
- The current `findings` schema does not have a `source` column.
- In Phase 2 implementation we should add `findings.source` (string, **nullable**, default `NULL`) with an index `(tenant_id, source)` and use `source='baseline.compare'`.
- Existing findings receive `NULL` — legacy drift-generate findings are queried with `whereNull('source')` or unconditionally.
- A future backfill migration may set `source='drift.generate'` for historical findings if needed for reporting.

**Alternatives considered**:
- Store `source` only in `evidence_jsonb`: workable, but makes filtering and long-term reporting harder and is less explicit.
- Non-null default `'drift.generate'`: rejected because retroactively tagging all existing findings requires careful validation and is a separate concern.

### D-005 — Baseline compare scope_key strategy
**Decision**: Use `findings.scope_key = 'baseline_profile:' . baseline_profile_id` for baseline-compare findings.

**Rationale**:
- Keeps a stable grouping key for tenant UI (“Soll vs Ist” for the assigned baseline).
- Avoids over-coupling to inventory selection hashes in v1.

### D-006 — Authorization model (404 vs 403)
**Decision**:
- Membership is enforced as deny-as-not-found (404) via existing membership checks.
- Capability denials are 403 after membership is established.

**Capabilities**:
- Add workspace capabilities:
  - `workspace_baselines.view`
  - `workspace_baselines.manage`

**Rationale**:
- Matches the feature spec’s two-capability requirement.
- Keeps baseline governance controlled at workspace plane, while still enforcing tenant membership for tenant-context pages/actions.

**Alternatives considered**:
- Add a tenant-plane capability for compare start: rejected for v1 to keep to the two-capability spec and avoid introducing a second permission axis for the same action.

## Open Questions (none blocking Phase 1)
- None.
