Summary: - Baseline Compare landing: enterprise UI (stats grid, critical drift banner, better actions), navigation grouping under Governance, and Action Surface Contract declaration. - Baseline Profile view page: switches from disabled form fields to proper Infolist entries for a clean read-only view. - Fixes tenant name column usages (`display_name` → `name`) in baseline assignment flows. - Dashboard: improved baseline governance widget with severity breakdown + last compared. Notes: - Filament v5 / Livewire v4 compatible. - Destructive actions remain confirmed (`->requiresConfirmation()`). Tests: - `vendor/bin/sail artisan test --compact tests/Feature/Baselines` - `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #123
102 lines
5.6 KiB
Markdown
102 lines
5.6 KiB
Markdown
# 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.
|