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

5.6 KiB
Raw Blame History

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.