TenantAtlas/specs/101-golden-master-baseline-governance-v1/research.md
Ahmed Darrazi 74ab2d1404 feat: Phase 2 foundational - capabilities, migrations, models, factories, badges, support classes
T003-T018b: Add workspace_baselines.view/manage capabilities, role mappings,
baseline_capture/baseline_compare operation labels, severity summary keys,
5 migrations, 4 models, 4 factories, BaselineScope, BaselineReasonCodes,
BaselineProfileStatus badge domain + mapper.
2026-02-19 14:15:46 +01:00

102 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 0 — Research
This document records the key technical decisions for **101 — Golden Master / Baseline Governance v1 (R1.1R1.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 specs 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.