# TenantPilot / TenantAtlas — Handover Document

> **Generated**: 2026-03-06 · **Branch**: `dev` · **HEAD**: `da1adbd`
> **Stack**: Laravel 12 · Filament v5 · Livewire v4 · PostgreSQL 16 · Tailwind v4 · Pest 4

---

## Executive Summary

- **Product**: Enterprise Intune Governance SaaS — backup, version-control, restore, drift detection, and observability for Microsoft Intune policy configurations across multiple tenants.
- **Core value prop**: Immutable JSONB policy snapshots, restore wizard with dry-run/preview, inventory-first drift detection against Golden Master baselines, workspace-scoped RBAC, and alert/evidence pipeline.
- **28+ Intune policy types** supported (device config, settings catalog, compliance, app protection, conditional access, scripts, enrollment, endpoint security, update rings, etc.) — defined in [config/tenantpilot.php](config/tenantpilot.php).
- **Maturity**: Production-capable MVP with ~109 specs, 708 test files (582 Feature + 125 Unit + 1 Deprecation), extensive guard tests, and an architectural constitution document.
- **Three Filament panels**: `/admin` (workspace-tenant context), `/admin/t` (tenant-scoped), `/system` (platform operator console).
- **Workspace-first multi-tenancy**: All data is workspace-isolated. Tenants belong to workspaces. Non-members get 404 (deny-as-not-found).
- **Capability-first RBAC**: ~40+ capabilities in a canonical registry ([app/Support/Auth/Capabilities.php](app/Support/Auth/Capabilities.php)), enforced server-side with UI enforcement helpers.
- **Operations system**: Unified `OperationRun` model with 25+ run types, idempotent creation, stale-run detection, 3-surface feedback (toast → progress → DB notification).
- **Baseline/Drift engine**: 4-spec progression (116→119) — meta-fidelity v1 → content-fidelity v1.5 → full evidence capture v2 → legacy drift cutover. All 4 specs implemented on `dev`.
- **Key open areas**: Formal release grouping (R1/R2/R3) exists only as brainstorming; missing: exception/risk-acceptance workflows, compliance evidence packs, cross-tenant promotion, MSP portfolio dashboard, change approval workflows.
- **Biggest risk**: No `.env.example` in repo; Dokploy deployment config is external; no formal CI pipeline config in repo.

---

## Product & Roadmap Snapshot

### What exists (fully implemented on `dev`)

| Domain | Status | Key Specs |
|--------|--------|-----------|
| Policy Sync (28+ types) | ✅ Implemented | 001–030 |
| Immutable PolicyVersions (JSONB) | ✅ Implemented | Core |
| BackupSets + BackupItems | ✅ Implemented | 005, 006, 016 |
| Restore Wizard (dry-run, preview, execute) | ✅ Implemented | 011, 023, 049 |
| Assignments backup/restore + group mapping | ✅ Implemented | 004, 006, 094 |
| Settings Catalog readable names | ✅ Implemented | 003, 045 |
| Inventory sync + coverage matrix | ✅ Implemented | 039–042 |
| Dependency graph (inventory_links) | ✅ Implemented | 042, 079 |
| OperationRun unified monitoring | ✅ Implemented | 053, 054, 055, 078 |
| Provider connections + gateway | ✅ Implemented | 061, 081, 089, 108 |
| Entra OIDC sign-in | ✅ Implemented | 063, 064 |
| Workspace + Tenant RBAC | ✅ Implemented | 062, 065, 066, 070–072 |
| Managed tenant onboarding wizard | ✅ Implemented | 073 |
| Verification checklist | ✅ Implemented | 074, 075, 084 |
| Backup scheduling + retention | ✅ Implemented | 032, 091 |
| Alerts v1 (Teams + Email) | ✅ Implemented | 099, 100 |
| Baseline governance (Golden Master) | ✅ Implemented | 101, 115–119 |
| Findings lifecycle + SLA | ✅ Implemented | 044, 111 |
| Permission posture + Entra admin roles | ✅ Implemented | 104, 105 |
| Review pack export (CSV+ZIP) | ✅ Implemented | 109 |
| Settings (workspace + tenant) | ✅ Implemented | 097, 098 |
| System console + control tower | ✅ Implemented | 113, 114 |
| Platform ops runbooks | ✅ Implemented | 113 |
| Unified badges + tag catalog | ✅ Implemented | 059, 060 |
| Legacy purge (bulk ops, old runs) | ✅ Implemented | 056, 086–088, 092 |

### Roadmap (from brainstorming — [specs/0800-future-features/brainstorming.md](specs/0800-future-features/brainstorming.md))

**Priority order**: MSP Portfolio + Alerting > Drift + Approvals > Standardisierung/Linting > Promotion DEV→PROD > Recovery Confidence

| Release | Theme | Items | Code exists? |
|---------|-------|-------|-------------|
| R1 "Golden Master Governance" | Baseline drift as production feature | Baseline capture/compare/alerts, findings SLA, evidence capture | ✅ Specs 101, 111, 115–119 implemented |
| R1 cont. | Operations polish | OperationRun canonicalization, monitoring hub, action surfaces | ✅ Specs 078, 082, 090, 110 implemented |
| R2 "Tenant Reviews & Evidence" | Evidence packs, stored reports | Review packs, permission posture, Entra admin roles, stored reports | ✅ Partial — Specs 104, 105, 109 implemented; no formal "evidence pack" spec |
| R2 cont. | Exception/risk-acceptance workflow | Finding exceptions, risk acceptance tracking | ❌ Missing — Finding model has `risk_accepted` status but no exception entity |
| R2 cont. | Alert escalation + notification routing | Alert rules per event type, quiet hours, cooldown | ✅ Specs 099, 100 implemented |
| R3 "MSP Portfolio OS" | Cross-tenant compare, portfolio dashboard | Cross-tenant compare, MSP health dashboard, multi-workspace admin | ❌ Spec 043 exists (draft), no implementation |
| R3 cont. | Standardisierung / Policy linting | Policy quality checks, duplicate finder, orphaned items | ❌ Brainstorming only |
| Later | Change approval workflows | Approval gates before restore | ❌ Brainstorming only |
| Later | Recovery confidence | Automated restore tests, readiness report | ❌ Brainstorming only |
| Later | Script & secrets governance | Script diff/approval, secret scanning | ❌ Brainstorming only |
| Later | Security posture score | Blast radius, opt-in high-risk | ❌ Brainstorming only |

---

## Current Feature Map

### Implemented

| Feature | UI Entrypoint | Models | Key Jobs | DB Tables |
|---------|--------------|--------|----------|-----------|
| **Policy Sync** | `PolicyResource` | `Policy`, `PolicyVersion` | `SyncPoliciesJob`, `CapturePolicySnapshotJob` | `policies`, `policy_versions` |
| **Backup Sets** | `BackupSetResource` | `BackupSet`, `BackupItem` | `AddPoliciesToBackupSetJob`, `RemovePoliciesFromBackupSetJob` | `backup_sets`, `backup_items` |
| **Restore Wizard** | `RestoreRunResource` | `RestoreRun` | `ExecuteRestoreRunJob`, `RestoreAssignmentsJob` | `restore_runs` |
| **Backup Scheduling** | `BackupScheduleResource` | `BackupSchedule` | `RunBackupScheduleJob`, `ApplyBackupScheduleRetentionJob` | `backup_schedules` |
| **Inventory** | `InventoryItemResource`, `InventoryCoverage` page | `InventoryItem`, `InventoryLink` | `RunInventorySyncJob`, `ProviderInventorySyncJob` | `inventory_items`, `inventory_links` |
| **Findings** | `FindingResource` | `Finding` | `CompareBaselineToTenantJob`, `GeneratePermissionPostureFindingsJob` | `findings` |
| **Baselines** | `BaselineProfileResource`, `BaselineSnapshotResource`, `BaselineCompareLanding` | `BaselineProfile`, `BaselineSnapshot`, `BaselineSnapshotItem`, `BaselineTenantAssignment` | `CaptureBaselineSnapshotJob`, `CompareBaselineToTenantJob` | `baseline_profiles`, `baseline_snapshots`, `baseline_snapshot_items`, `baseline_tenant_assignments` |
| **Alerts** | `AlertRuleResource`, `AlertDestinationResource`, `AlertDeliveryResource` | `AlertRule`, `AlertDestination`, `AlertDelivery`, `AlertRuleDestination` | `Jobs/Alerts/*` | `alert_rules`, `alert_destinations`, `alert_deliveries`, `alert_rule_destinations` |
| **Operations Hub** | `Operations` page, `TenantlessOperationRunViewer` | `OperationRun` | All domain jobs | `operation_runs` |
| **Provider Connections** | `ProviderConnectionResource` | `ProviderConnection`, `ProviderCredential` | `ProviderConnectionHealthCheckJob` | `provider_connections`, `provider_credentials` |
| **RBAC** | `ChooseWorkspace`, `ChooseTenant`, `NoAccess`, `BreakGlassRecovery` | `Workspace`, `WorkspaceMembership`, `Tenant`, `TenantMembership`, `TenantRoleMapping` | `RefreshTenantRbacHealthJob` | `workspaces`, `workspace_memberships`, `tenants`, `tenant_memberships`, `tenant_role_mappings` |
| **Entra Groups** | `EntraGroupResource` | `EntraGroup` | `EntraGroupSyncJob` | `entra_groups` |
| **Verification** | `TenantDiagnostics`, `TenantRequiredPermissions` | `VerificationCheckAcknowledgement` | — | `verification_check_acknowledgements` |
| **Permission Posture** | Via `FindingResource` | `Finding`, `StoredReport` | `GeneratePermissionPostureFindingsJob`, `ScanEntraAdminRolesJob` | `findings`, `stored_reports` |
| **Review Packs** | `ReviewPackResource` | `ReviewPack` | `GenerateReviewPackJob` | `review_packs` |
| **Settings** | `WorkspaceSettings` page | `WorkspaceSetting`, `TenantSetting` | — | `workspace_settings`, `tenant_settings` |
| **Audit Log** | `AuditLog` page | `AuditLog` | — | `audit_logs` |
| **System Console** | `/system/dashboard`, `/system/ops/runbooks` | `PlatformUser` | — | `platform_users` |

### Partial / In-Progress

| Feature | Status | Gap |
|---------|--------|-----|
| **Cross-tenant compare** | Spec 043 drafted | No implementation code |
| **Policy lifecycle** | Spec 900 drafted | Addresses ghost / orphaned policies — not yet implemented |
| **Exception/Risk acceptance** | Finding has `risk_accepted` status | No formal exception entity, no evidence attachment workflow |

### Missing (no code, no spec beyond brainstorming)

- Change approval workflows / approval gates
- Policy linting / standardization engine
- Recovery confidence / automated restore tests
- Script & secrets governance
- Security posture scoring / blast radius
- MSP portfolio health dashboard
- Compliance light (formal compliance framework mapping)

---

## Architecture & Principles (Non-Negotiables)

Source: [.specify/memory/constitution.md](.specify/memory/constitution.md) (v2.0.0)

### Core Principles

1. **Inventory-first, Snapshots-second** — `InventoryItem` = last observed meta; `PolicyVersion.snapshot` = explicit immutable JSONB capture.
2. **Read/Write Separation** — Analysis is read-only; writes require preview → dry-run → confirmation → audit log.
3. **Single Contract Path to Graph** — All Microsoft Graph calls via `GraphClientInterface` ([app/Services/Graph/GraphClientInterface.php](app/Services/Graph/GraphClientInterface.php)); endpoints modeled in [config/graph_contracts.php](config/graph_contracts.php) (867 lines, 28+ type definitions).
4. **Deterministic Capabilities** — Backup/restore/risk flags derived from config via `CoverageCapabilitiesResolver`.
5. **Workspace Isolation** — Non-member → 404; workspace is primary session context. Enforced via `DenyNonMemberTenantAccess` middleware + `EnsureFilamentTenantSelected`.
6. **Tenant Isolation** — Every read/write must be tenant-scoped; `DerivesWorkspaceIdFromTenant` concern auto-fills `workspace_id` from tenant.
7. **UI/UX Constitution v1** — Every operator-facing admin surface is classified before implementation, each list gets exactly one primary inspect/open model, scope signals must be truthful, critical operational truth stays default-visible, and exceptions are catalogued and tested.
8. **Filament-native first / no ad-hoc styling** — Admin and operator UI must use Filament-native components or shared primitives before any local Blade/Tailwind assembly; page-local status styling is not an acceptable substitute.
9. **Proportionality first** — New structure, layers, persistence, and semantic machinery must be justified by current release truth, current operator workflow, and why a narrower solution is insufficient.
10. **Anti-bloat guardrails** — No premature abstraction, no new persisted truth without independent source-of-truth need, no new domain state without behavioral consequence, and specs that add structural complexity must carry an explicit proportionality review.

### RBAC-UX Rules

| Rule | Description |
|------|-------------|
| UX-001 | Server-side is source of truth (UI is never security boundary) |
| UX-002 | Non-members get 404 (deny-as-not-found) |
| UX-003 | Members without capability get 403 |
| UX-004 | Actions visible-but-disabled for members lacking capability; hidden for non-members |
| UX-005 | All destructive actions require `->requiresConfirmation()` |
| UX-006 | Capabilities from canonical registry only ([app/Support/Auth/Capabilities.php](app/Support/Auth/Capabilities.php)) |
| UX-007 | Global search must be tenant-safe |
| UX-008 | RBAC regression tests mandatory |

### Operations UX

- **3-surface feedback**: Toast (immediate) → Progress widget (polling) → DB notification (terminal).
- **OperationRun lifecycle**: Service-owned transitions only via `OperationRunService` — no direct status writes.
- **Idempotent creation**: Hash-based dedup with partial unique index.
- **Lifecycle guarantees (Spec 160)**: Covered queued runs (`baseline_capture`, `baseline_compare`, `inventory_sync`, `policy.sync`, `policy.sync_one`, `entra_group_sync`, `directory_role_definitions.sync`, `backup_schedule_run`, `restore.execute`, `tenant.review_pack.generate`, `tenant.review.compose`, `tenant.evidence.snapshot.generate`) now have a config-backed lifecycle policy, direct failed-job bridges where declared, scheduled stale-run reconciliation, and UI freshness semantics for stale or automatically reconciled runs.
- **Queue timing invariant**: Covered job `timeout` values and policy `expected_max_runtime_seconds` must remain safely below queue `retry_after` with the configured safety margin. After changing queue lifecycle settings, restart workers (`php artisan queue:restart` in the target environment) so the new contract takes effect.

### Filament Standards

- **Action Surface Contract**: List/View/Create/Edit pages each have defined required surfaces, one primary inspect model, no redundant View beside row click, and no empty overflow or bulk groups (Specs 082, 090).
- **Layout**: Main/Aside layout, sections required, view pages use Infolists.
- **Badge Semantics**: Centralized via `BadgeCatalog`/`BadgeRenderer` (Specs 059, 060).
- **Filament-native UI**: Native Filament components and shared primitives come before any local styling or replacement markup for semantic UI elements.
- **No naked forms**: Everything in sections/cards with proper enterprise IA.

### Provider Gateway

- `ProviderGateway` ([app/Services/Providers/ProviderGateway.php](app/Services/Providers/ProviderGateway.php)) — facade over Graph client.
- `ProviderConnectionResolver` resolves default connection per tenant+provider pair with validation (scope, status, credentials).
- `ProviderOperationStartGate` — pre-flight checks before any Graph write.
- `IntuneRbacWriteGate` ([app/Services/Hardening/IntuneRbacWriteGate.php](app/Services/Hardening/IntuneRbacWriteGate.php)) — freshness check before destructive Graph writes.

---

## Data Model Overview

### Core Tables

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `workspaces` | `id`, `name`, `slug`, `archived_at` | Primary isolation boundary |
| `workspace_memberships` | `workspace_id`, `user_id`, `role` | Roles: Owner/Manager/Operator/Readonly |
| `tenants` | `id`, `workspace_id`, `tenant_id`, `external_id`, `name`, `status`, `is_current` | SoftDeletes; `external_id` = Azure tenant GUID |
| `tenant_memberships` | `tenant_id`, `user_id`, `role`, `created_by_user_id` | UUID PK |
| `users` | Standard + `entra_id`, `entra_name`, `is_platform_superadmin` | SoftDeletes |
| `platform_users` | Separate from `users` | `/system` panel auth guard |

### Policy & Versioning

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `policies` | `id`, `workspace_id`, `tenant_id`, `external_id`, `policy_type`, `display_name`, `odata_type`, `metadata` (JSONB), `ignored_at` | One row per Intune policy per tenant |
| `policy_versions` | `id`, `workspace_id`, `tenant_id`, `policy_id`, `version_number`, `snapshot` (JSONB), `metadata` (JSONB), `assignments` (JSONB), `content_hash`, `captured_at`, `capture_purpose` | Immutable snapshots; SoftDeletes |
| `backup_sets` | `id`, `workspace_id`, `tenant_id`, `name`, `status`, `metadata` (JSONB) | SoftDeletes |
| `backup_items` | `id`, `workspace_id`, `backup_set_id`, `policy_id`, `policy_version_id` | Links backup set to specific version |
| `restore_runs` | `id`, `workspace_id`, `tenant_id`, `backup_set_id`, `operation_run_id`, `status`, `is_dry_run`, `idempotency_key`, `requested_items` (JSONB), `preview` (JSONB), `results` (JSONB), `group_mapping` (JSONB) | SoftDeletes |

### Inventory & Dependencies

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `inventory_items` | `id`, `workspace_id`, `tenant_id`, `policy_type`, `external_id`, `display_name`, `meta_jsonb` (JSONB), `last_seen_at`, `last_seen_operation_run_id` | Current state from Graph |
| `inventory_links` | `id`, `workspace_id`, `source_type`, `source_id` (text), `target_type`, `target_id` (text), `relationship_type` | Dependency graph edges |

### Operations

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `operation_runs` | `id`, `workspace_id`, `tenant_id`, `user_id`, `type`, `status` (queued/running/completed), `outcome` (pending/succeeded/partially_succeeded/blocked/failed), `summary_counts` (JSONB), `failure_summary` (JSONB), `context` (JSONB), `started_at`, `completed_at` | Central run tracking for all async operations |
| `audit_logs` | `id`, `workspace_id`, `tenant_id`, `user_id`, `action`, `details` (JSONB) | Immutable audit trail |

### Baselines

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `baseline_profiles` | `id`, `workspace_id`, `name`, `status` (draft/active/archived), `capture_mode`, `scope_jsonb` (JSONB), `active_snapshot_id` | Golden Master definition |
| `baseline_snapshots` | `id`, `baseline_profile_id`, `operation_run_id`, `tenant_id`, `version_label`, `item_count` | Point-in-time snapshot |
| `baseline_snapshot_items` | `id`, `baseline_snapshot_id`, `policy_type`, `external_id`, `display_name`, `content_hash`, `meta_jsonb` (JSONB), `subject_key` | Individual policy item in snapshot |
| `baseline_tenant_assignments` | `id`, `baseline_profile_id`, `tenant_id` | Which tenants are compared against this baseline |

### Findings

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `findings` | `id`, `workspace_id`, `tenant_id`, `finding_type` (drift/permission_posture/entra_admin_roles), `severity`, `status`, `source`, `title`, `description`, `evidence_jsonb` (JSONB), `recurrence_key`, `fingerprint`, `first_seen_at`, `last_seen_at`, `times_seen`, `sla_days`, `due_at`, `evidence_fidelity`, `baseline_operation_run_id`, `current_operation_run_id` | Full lifecycle: new → triaged → in_progress → resolved/closed/risk_accepted |

### Alerts

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `alert_destinations` | `id`, `workspace_id`, `type` (teams_webhook/email), `config` (JSONB) | Where alerts go |
| `alert_rules` | `id`, `workspace_id`, `event_type`, `is_enabled`, `tenant_scope_mode`, `tenant_allowlist` (JSONB), `cooldown_seconds`, `quiet_hours_*` | When to alert |
| `alert_rule_destinations` | `alert_rule_id`, `alert_destination_id`, `workspace_id` | M:N join with workspace scope |
| `alert_deliveries` | `id`, `workspace_id`, `alert_rule_id`, `tenant_id`, `status`, `attempts`, `payload` (JSONB) | Delivery tracking with retry |

### Provider

| Table | Key Columns | Notes |
|-------|-------------|-------|
| `provider_connections` | `id`, `workspace_id`, `tenant_id`, `provider`, `external_tenant_id`, `is_default`, `status`, `scopes_granted` (JSONB), `last_health_check_at` | Graph API connection per tenant |
| `provider_credentials` | `id`, `provider_connection_id`, `credential_type`, `client_id`, `client_secret` (encrypted) | Secrets storage |

### Other

| Table | Notes |
|-------|-------|
| `stored_reports` | Permission posture + Entra admin roles reports, fingerprint dedup |
| `review_packs` | Exportable audit artifact (ZIP), signed download URLs |
| `backup_schedules` | Cron-based backup scheduling with retention keep_last |
| `entra_groups` | Cached directory groups for assignment mapping |
| `entra_role_definitions` | Cached Entra role definitions for admin roles evidence |
| `settings_catalog_definitions` / `settings_catalog_categories` | Settings catalog human-readable name mapping |
| `workspace_settings` / `tenant_settings` | Key-value settings with typed slices |

---

## Operations & Observability

### OperationRun System

- **Central model**: `OperationRun` ([app/Models/OperationRun.php](app/Models/OperationRun.php))
- **Type registry**: `OperationRunType` enum (25+ types) ([app/Support/OperationRunType.php](app/Support/OperationRunType.php))
- **Catalog**: `OperationCatalog` — labels, expected durations, allowed summary keys ([app/Support/OperationCatalog.php](app/Support/OperationCatalog.php))
- **Lifecycle service**: `OperationRunService` (877 lines) — idempotent `ensureRun()`, stale detection, state transitions, summary normalization ([app/Services/OperationRunService.php](app/Services/OperationRunService.php))
- **Status**: `queued` → `running` → `completed`
- **Outcome**: `pending` | `succeeded` | `partially_succeeded` | `blocked` | `failed` | `cancelled` (reserved)
- **Context**: JSONB bag storing `selection_hash`, `policy_types`, `categories`, etc.

### Monitoring UI

- **Operations page**: `/admin/operations` — workspace-scoped, tenantless canonical view ([app/Filament/Pages/Monitoring/Operations.php](app/Filament/Pages/Monitoring/Operations.php))
- **Run viewer**: `/admin/operations/{run}` — detail view per run ([app/Filament/Pages/Operations/TenantlessOperationRunViewer.php](app/Filament/Pages/Operations/TenantlessOperationRunViewer.php))
- **Audit log**: `/admin/audit-log` — immutable event log ([app/Filament/Pages/Monitoring/AuditLog.php](app/Filament/Pages/Monitoring/AuditLog.php))

### Scheduled Tasks ([routes/console.php](routes/console.php))

| Schedule | Frequency | Job |
|----------|-----------|-----|
| Backup schedule dispatch | Every minute | `tenantpilot:schedules:dispatch` |
| Directory groups dispatch | Every minute | `tenantpilot:directory-groups:dispatch` |
| Alerts dispatch | Every minute (no overlap) | `tenantpilot:alerts:dispatch` |
| Prune old operations | Daily | `PruneOldOperationRunsJob` |
| Reconcile adapter runs | Every 30 min | `ReconcileAdapterRunsJob` |
| Stored reports prune | Daily | `stored-reports:prune` |
| Review pack prune | Daily | `tenantpilot:review-pack:prune` |
| Baseline evidence prune | Daily | `tenantpilot:baseline-evidence:prune` |
| Entra admin roles scan | Daily (per tenant) | `ScanEntraAdminRolesJob` |

---

## Security: Tenancy / RBAC / Auth

### Three-panel architecture

| Panel | Path | Guard | Purpose |
|-------|------|-------|---------|
| Admin | `/admin` | `web` | Workspace + Tenant management (main UI) |
| Tenant | `/admin/t` | `web` | Tenant-scoped views (within admin session) |
| System | `/system` | `platform` | Platform operator console (separate cookies) |

### Auth flows

- **Entra OIDC**: `/auth/entra/redirect` → `/auth/entra/callback` (via Socialite)
- **Admin consent**: `/admin/consent/start` → `/admin/consent/callback` (app registration)
- **RBAC delegated auth**: `/admin/rbac/start` → `/admin/rbac/callback`
- **Break-glass**: `/system` panel with `BreakGlassRecovery` page, config-gated TTL

### Role hierarchy

| Level | Roles | Source |
|-------|-------|--------|
| Workspace | Owner / Manager / Operator / Readonly | [app/Support/Auth/WorkspaceRole.php](app/Support/Auth/WorkspaceRole.php) |
| Tenant | Owner / Manager / Operator / Readonly | [app/Support/TenantRole.php](app/Support/TenantRole.php) |
| Platform | Capabilities: `access_system_panel`, `use_break_glass`, `console.*`, `ops.*`, `runbooks.*` | [app/Support/Auth/PlatformCapabilities.php](app/Support/Auth/PlatformCapabilities.php) |

### Capability resolver chain

- `Capabilities::all()` — reflection-based registry of ~40+ tenant/workspace capabilities
- `UiEnforcement` ([app/Support/Rbac/UiEnforcement.php](app/Support/Rbac/UiEnforcement.php)) — builder pattern for Filament actions: `forAction()->requireMembership()->requireCapability()->destructive()`
- `WorkspaceUiEnforcement` — mirror for workspace-scoped actions
- **12 Laravel Policy classes** in [app/Policies/](app/Policies/)

### Workspace isolation enforcement

- `DerivesWorkspaceIdFromTenant` concern on models — auto-fills `workspace_id`
- DB-level: `NOT NULL` + FK constraints on `workspace_id` for all tenant-owned tables (migration `2026_02_14_220114`)
- Check constraints on `audit_logs` (migration `2026_02_14_220117`)
- Middleware: `ensure-workspace-selected`, `ensure-workspace-member`, `DenyNonMemberTenantAccess`

---

## Drift/Baseline/Findings: Current State & Gaps

### Baseline Drift Engine (Specs 116–119, all merged to `dev`)

| Spec | Title | Status |
|------|-------|--------|
| 116 | Meta-fidelity + coverage guard | ✅ Merged |
| 117 | Content-fidelity via provider chain (v1.5) | ✅ Merged |
| 118 | Full content capture (v2) | ✅ Merged |
| 119 | Drift cutover — legacy drift removal | ✅ Merged |

### Architecture

1. **Capture**: `CaptureBaselineSnapshotJob` → `BaselineCaptureService` captures inventory metadata + optional full content evidence into `baseline_snapshot_items`.
2. **Compare**: `CompareBaselineToTenantJob` → `BaselineCompareService` compares snapshot against current tenant state. `CurrentStateEvidenceProvider` chain resolves best available evidence (content > meta).
3. **Findings**: Creates/updates/auto-resolves `Finding` records with `source = 'baseline.compare'`, `finding_type = 'drift'`.
4. **Evidence fidelity**: `evidence_fidelity` field on findings tracks whether comparison was meta-level or content-level.
5. **Guard test**: [tests/Feature/Guards/Spec116OneEngineGuardTest.php](tests/Feature/Guards/Spec116OneEngineGuardTest.php), [tests/Feature/Guards/Spec118NoLegacyBaselineDriftGuardTest.php](tests/Feature/Guards/Spec118NoLegacyBaselineDriftGuardTest.php) — prevent regression to legacy drift patterns.

### Findings lifecycle

- Statuses: `new` → `acknowledged` → `triaged` → `in_progress` → `resolved` / `closed` / `risk_accepted` → `reopened`
- SLA: `FindingSlaPolicy` computes `due_at` from workspace `findings.sla_days` setting
- Auto-resolve: `BaselineAutoCloseService` resolves findings when drift disappears
- Recurrence: `recurrence_key` + `times_seen` + `first_seen_at` / `last_seen_at` track repeating drift

### Gaps

- **Exception entity**: No formal `FindingException` model — only the `risk_accepted` status exists
- **Evidence pack**: No formal evidence export spec as a standalone artifact (review pack covers partial ground)
- **Cross-tenant compare**: Spec 043 drafted, not implemented — uses `policy_type + normalized display_name` matching

---

## Testing & Tooling

### Test suite

- **Framework**: Pest 4 with `RefreshDatabase`
- **DB**: SQLite `:memory:` (via phpunit.xml)
- **Test files**: 582 Feature + 125 Unit + 1 Deprecation = **708 total**
- **Infrastructure**: [tests/Pest.php](tests/Pest.php) (366 lines) — shared helpers: `createUserWithTenant()`, `fakeIdToken()`, `bindFailHardGraphClient()`, etc.

### Key test categories

| Category | Path | Count (approx) |
|----------|------|-------|
| Guard tests | `tests/Feature/Guards/` | 14 files — enforce architectural rules via code scanning |
| RBAC tests | `tests/Feature/Rbac/`, `tests/Feature/TenantRBAC/` | ~15 files |
| Baseline/Drift | `tests/Feature/BaselineDriftEngine/`, `tests/Feature/Baselines/`, `tests/Feature/Drift/` | ~20 files |
| Operations | `tests/Feature/Operations/`, `tests/Feature/Monitoring/` | ~10 files |
| Bulk operations | `tests/Feature/Bulk*Test.php` | ~20 files |
| Restore | `tests/Feature/Restore*Test.php` | ~15 files |
| Provider/Graph | `tests/Feature/ProviderConnections/`, `tests/Feature/Graph/`, `tests/Unit/Graph*` | ~15 files |
| Alerts | `tests/Feature/Alerts/`, `tests/Unit/Alerts/` | ~10 files |
| Inventory | `tests/Feature/Inventory/`, `tests/Unit/Inventory/` | ~15 files |
| Workspace isolation | `tests/Feature/WorkspaceIsolation/` | ~5 files |
| Deprecation | `tests/Deprecation/` | 1 file (is_platform_superadmin ban) |

### Guard tests (architectural enforcement)

These tests scan source code to prevent regressions:

| Test | Enforces |
|------|----------|
| `ActionSurfaceContractTest` | All resources have required action surfaces |
| `NoAdHocFilamentAuthPatternsTest` | No ad-hoc auth patterns in Filament |
| `NoAdHocStatusBadgesTest` | Use badge catalog, not ad-hoc badges |
| `NoLegacyRunsTest` | No legacy run patterns in codebase |
| `NoLegacyTenantGraphOptionsTest` | No legacy graph options usage |
| `NoTenantCredentialRuntimeReadsSpec081Test` | No runtime credential reads |
| `Spec116OneEngineGuardTest` | Single drift engine architecture |
| `Spec118NoLegacyBaselineDriftGuardTest` | No legacy baseline drift code |

### Tooling

| Tool | Command | Notes |
|------|---------|-------|
| Run all tests | `vendor/bin/sail artisan test --compact` | |
| Run specific file | `vendor/bin/sail artisan test --compact tests/Feature/SomeTest.php` | |
| Filter test | `vendor/bin/sail artisan test --compact --filter=testName` | |
| Pint formatter | `vendor/bin/sail bin pint --dirty` | Default Laravel preset |
| No `.pint.json` | Uses default rules | |
| No PHPStan config found | UNKNOWN — check if integrated | |

---

## Deployment & Runbooks

### Local development

```bash
# Start services
vendor/bin/sail up -d

# Run migrations
vendor/bin/sail artisan migrate

# Start queue worker (or use docker-compose queue service)
vendor/bin/sail artisan queue:work --tries=3 --timeout=300

# Build frontend assets
vendor/bin/sail npm run build

# Run tests
vendor/bin/sail artisan test --compact
```

### Docker services ([docker-compose.yml](docker-compose.yml))

| Service | Image | Ports |
|---------|-------|-------|
| `laravel.test` | PHP 8.4 Sail | 80, 5173 (Vite) |
| `queue` | Same image | `queue:work --tries=3 --timeout=300 --sleep=3 --max-jobs=1000` |
| `pgsql` | PostgreSQL 16 | 5432 |
| `redis` | Redis 7 Alpine | 6379 |

### Deployment: Dokploy (staging → production)

- **No Dokploy config in repo** — configuration lives on the VPS per [Agents.md](Agents.md)
- **Two environments**: Staging (mandatory gate) → Production
- **Required env vars** (partial — no `.env.example`):
  - Standard Laravel: `APP_KEY`, `DB_*`, `REDIS_*`, `QUEUE_CONNECTION`
  - Entra auth: `AZURE_AD_CLIENT_ID`, `AZURE_AD_CLIENT_SECRET`, `AZURE_AD_TENANT_ID`
  - Feature flags: `BREAK_GLASS_ENABLED`, `ALLOW_ADMIN_MAINTENANCE_ACTIONS`
  - Alerts: `TENANTPILOT_ALERTS_ENABLED`
  - Baselines: `TENANTPILOT_BASELINE_FULL_CONTENT_CAPTURE_ENABLED`
  - Write gate: `TENANTPILOT_INTUNE_WRITE_GATE_ENABLED`
- **Deploy checklist**: `php artisan migrate`, `php artisan filament:assets`, queue restart

### Platform runbooks

- `FindingsLifecycleBackfillRunbookService` ([app/Services/Runbooks/FindingsLifecycleBackfillRunbookService.php](app/Services/Runbooks/FindingsLifecycleBackfillRunbookService.php)) — safe backfill of findings lifecycle fields
- Accessible at `/system/ops/runbooks` with platform capabilities

---

## Risks & Gaps (Top 10)

| # | Risk | Location | Impact | Suggested Action |
|---|------|----------|--------|-----------------|
| 1 | **No `.env.example`** | Repo root | New dev can't set up without tribal knowledge | Create `.env.example` with all required keys from `config/*.php` |
| 2 | **No CI pipeline config in repo** | Missing | No automated test gating on PRs | Add Gitea CI / GitHub Actions config |
| 3 | **No PHPStan/Larastan** | Missing | Type-safety gaps undetected | UNKNOWN — check if integrated; if not, add baseline |
| 4 | **Exception/risk-acceptance workflow incomplete** | `Finding.STATUS_RISK_ACCEPTED` exists, no exception entity | Can't track formal risk acceptances with evidence | Spec needed for `FindingException` model with approval chain |
| 5 | **SQLite for tests vs PostgreSQL in prod** | [phpunit.xml](phpunit.xml) | JSONB, GIN indexes, PG-specific features untested | Add [phpunit.pgsql.xml](phpunit.pgsql.xml) — file exists but may not be in CI |
| 6 | **No formal release process** | Roadmap only in brainstorming doc | No versioned release artifacts, no changelog | Define release cadence and changelog automation |
| 7 | **Dokploy config external** | Per [Agents.md](Agents.md) | Deployment not reproducible from repo alone | Document or codify Dokploy config |
| 8 | **Cross-tenant compare unimplemented** | Spec 043 draft only | Key MSP feature missing | Prioritize for next sprint if MSP use case is active |
| 9 | **Policy lifecycle / ghost policies** | Spec 900 draft only | Deleted-in-Intune policies persist indefinitely | Implement orphan detection + soft-archive flow |
| 10 | **Break-glass recovery surface** | Config-gated, TTL-limited | Limited testing evidence | Verify break-glass flow has smoke tests in test suite |

---

## Next Actions (Top 5)

1. **Create `.env.example`** — Document all required env vars from `config/*.php` files. Low effort, high impact for onboarding.

2. **Add CI pipeline** — Gitea / runner config for `vendor/bin/sail artisan test --compact` + `vendor/bin/sail bin pint --test` on every PR. Critical for sustained quality.

3. **Formal exception/risk-acceptance spec** — Design `FindingException` entity with approval chain, evidence attachment, and UI workflow. Blocks R2 "Evidence Packs" roadmap item.

4. **PostgreSQL test mode** — Enable [phpunit.pgsql.xml](phpunit.pgsql.xml) in CI for a subset of tests that exercise JSONB/GIN-specific behavior.

5. **Cross-tenant compare implementation** (Spec 043) — Build read-only compare view first (N=2 tenants). This is the first step toward the MSP portfolio use case.

---

## Appendix

### Specs Index (109 specs)

| # | Slug | Status |
|---|------|--------|
| 001 | rbac-onboarding | ✅ Implemented |
| 003 | settings-catalog-readable | ✅ Implemented |
| 004 | assignments-scope-tags | ✅ Implemented |
| 005 | bulk-operations | ✅ Implemented |
| 006 | sot-foundations-assignments | ✅ Implemented |
| 007 | device-config-compliance | ✅ Implemented |
| 008 | apps-app-management | ✅ Implemented |
| 009 | app-protection-policy | ✅ Implemented |
| 011 | restore-run-wizard | ✅ Implemented |
| 012 | windows-update-rings | ✅ Implemented |
| 013 | scripts-management | ✅ Implemented |
| 014 | enrollment-autopilot | ✅ Implemented |
| 017 | policy-types-mam-endpoint-security-baselines | ✅ Implemented |
| 018 | driver-updates-wufb | ✅ Implemented |
| 023 | endpoint-security-restore | ✅ Implemented |
| 024 | terms-and-conditions | ✅ Implemented |
| 026 | custom-compliance-scripts | ✅ Implemented |
| 027 | enrollment-config-subtypes | ✅ Implemented |
| 028 | device-categories | ✅ Implemented |
| 029 | wip-policies | ✅ Implemented |
| 030 | intune-rbac-backup | ✅ Implemented |
| 031 | tenant-portfolio-context-switch | ✅ Implemented |
| 039 | inventory-program | ✅ Meta-spec (040–044) |
| 040 | inventory-core | ✅ Implemented |
| 041 | inventory-ui | ✅ Implemented |
| 043 | cross-tenant-compare-and-promotion | 📋 Draft |
| 045 | settingscatalog-classification | ✅ Implemented |
| 049 | backup-restore-job-orchestration | ✅ Implemented |
| 051 | entra-group-directory-cache | ✅ Implemented |
| 052 | async-add-policies | ✅ Implemented |
| 053 | unify-runs-monitoring | ✅ Implemented |
| 054 | unify-runs-suitewide | ✅ Implemented |
| 055 | ops-ux-rollout | ✅ Implemented |
| 058 | tenant-ui-polish | ✅ Implemented |
| 059 | unified-badges | ✅ Implemented |
| 060 | tag-badge-catalog | ✅ Implemented |
| 061 | provider-foundation | ✅ Implemented |
| 062 | tenant-rbac-v1 | ✅ Implemented |
| 063 | entra-signin | ✅ Implemented |
| 064 | auth-structure | ✅ Implemented |
| 065 | tenant-rbac-v1 | ✅ Implemented |
| 070 | workspace-create-membership-fix | ✅ Implemented |
| 071 | tenant-selection-workspace-scope | ✅ Implemented |
| 072 | managed-tenants-workspace-enforcement | ✅ Implemented |
| 074 | verification-checklist | ✅ Implemented |
| 077 | workspace-nav-monitoring-hub | ✅ Implemented |
| 078 | operations-tenantless-canonical | ✅ Implemented |
| 079 | inventory-links-non-uuid-ids | ✅ Implemented |
| 081 | provider-connection-cutover | ✅ Implemented |
| 084 | verification-surfaces-unification | ✅ Implemented |
| 085 | tenant-operate-hub | ✅ Implemented |
| 086 | retire-legacy-runs-into-operation-runs | ✅ Implemented |
| 087 | legacy-runs-removal | ✅ Implemented |
| 088 | remove-tenant-graphoptions-legacy | ✅ Implemented |
| 089 | provider-connections-tenantless-ui | ✅ Implemented |
| 091 | backupschedule-retention-lifecycle | ✅ Implemented |
| 092 | legacy-purge-final | ✅ Implemented |
| 093 | scope-001-workspace-id-isolation | ✅ Implemented |
| 094 | assignment-ops-observability-hardening | ✅ Implemented |
| 095 | graph-contracts-registry-completeness | ✅ Implemented |
| 096 | ops-polish-assignment-dedupe-system-tracking | ✅ Implemented |
| 097 | settings-foundation | ✅ Implemented |
| 098 | settings-slices-v1-backup-drift-ops | ✅ Implemented |
| 099 | alerts-v1-teams-email | ✅ Implemented |
| 100 | alert-target-test-actions | ✅ Implemented |
| 101 | golden-master-baseline-governance-v1 | ✅ Implemented |
| 102 | filament-5-2-1-upgrade | ✅ Implemented |
| 103 | ia-scope-filter-semantics | ✅ Implemented |
| 104 | provider-permission-posture | ✅ Implemented |
| 105 | entra-admin-roles-evidence-findings | ✅ Implemented |
| 106 | required-permissions-sidebar-context | ✅ Implemented |
| 107 | workspace-chooser | ✅ Implemented |
| 108 | provider-access-hardening | ✅ Implemented |
| 109 | review-pack-export | ✅ Implemented |
| 110 | ops-ux-enforcement | ✅ Implemented |
| 111 | findings-workflow-sla | ✅ Implemented |
| 112 | list-expand-parity | ✅ Implemented |
| 113 | platform-ops-runbooks | ✅ Implemented |
| 114 | system-console-control-tower | ✅ Implemented |
| 115 | baseline-operability-alerts | ✅ Implemented |
| 116 | baseline-drift-engine (meta v1) | ✅ Implemented |
| 117 | baseline-drift-engine (content v1.5) | ✅ Implemented |
| 118 | baseline-drift-engine (full capture v2) | ✅ Implemented |
| 119 | baseline-drift-engine (cutover) | ✅ Implemented |
| 700 | bugfix | ✅ Implemented |
| 900 | policy-lifecycle | 📋 Draft |
| 0800 | future-features (brainstorming) | 📋 Brainstorming |

> Specs not listed above (e.g., 002, 010, 015, 016, 025, 032, 042, 044, 046–048, 056–057, 066–067, 073, 075–076, 080, 082–083, 090, 999) have spec directories but were not individually verified. Based on merge history and code presence, the vast majority are implemented.

### Key File Map (Top 20 files to read first)

| File | Why |
|------|-----|
| [.specify/memory/constitution.md](.specify/memory/constitution.md) | Non-negotiable architectural rules |
| [Agents.md](Agents.md) | Agent workflow, branching, environment |
| [config/tenantpilot.php](config/tenantpilot.php) | 28+ policy types, feature flags, all config |
| [config/graph_contracts.php](config/graph_contracts.php) | Graph API contract registry (867 lines) |
| [app/Support/Auth/Capabilities.php](app/Support/Auth/Capabilities.php) | Canonical RBAC capability registry |
| [app/Support/OperationRunType.php](app/Support/OperationRunType.php) | All operation run types |
| [app/Support/OperationCatalog.php](app/Support/OperationCatalog.php) | Labels, durations, summary keys |
| [app/Services/OperationRunService.php](app/Services/OperationRunService.php) | Central run lifecycle (877 lines) |
| [app/Support/Rbac/UiEnforcement.php](app/Support/Rbac/UiEnforcement.php) | RBAC UI enforcement builder (678 lines) |
| [app/Services/Providers/ProviderGateway.php](app/Services/Providers/ProviderGateway.php) | Graph API facade |
| [app/Services/Providers/ProviderConnectionResolver.php](app/Services/Providers/ProviderConnectionResolver.php) | Connection resolution + validation |
| [app/Services/Baselines/BaselineCompareService.php](app/Services/Baselines/BaselineCompareService.php) | Drift comparison engine |
| [app/Services/Baselines/BaselineCaptureService.php](app/Services/Baselines/BaselineCaptureService.php) | Snapshot capture |
| [app/Models/Finding.php](app/Models/Finding.php) | Finding lifecycle model |
| [app/Models/OperationRun.php](app/Models/OperationRun.php) | Central run model |
| [app/Models/Tenant.php](app/Models/Tenant.php) | Tenant model (320 lines) |
| [routes/web.php](routes/web.php) | All routes (3 panels, auth, workspace) |
| [routes/console.php](routes/console.php) | Scheduler definitions |
| [tests/Pest.php](tests/Pest.php) | Test helpers & infrastructure |
| [specs/0800-future-features/brainstorming.md](specs/0800-future-features/brainstorming.md) | Future roadmap ideas |

### Key Commands

```bash
# Local dev
vendor/bin/sail up -d                          # Start all services
vendor/bin/sail artisan migrate                # Run migrations
vendor/bin/sail artisan queue:work --tries=3   # Process queue
vendor/bin/sail npm run build                  # Build frontend

# Testing
vendor/bin/sail artisan test --compact                           # Full suite
vendor/bin/sail artisan test --compact --filter=testName         # Single test
vendor/bin/sail artisan test --compact tests/Feature/Guards/     # Guard tests only

# Code quality
vendor/bin/sail bin pint --dirty               # Format changed files

# Deployment
vendor/bin/sail artisan filament:assets        # Publish Filament assets
vendor/bin/sail artisan migrate --force        # Production migration
```

### Quickstart (exact steps)

1. Clone repo, `cd TenantAtlas`
2. `cp .env.example .env` (MISSING — create from config files or request from team)
3. `composer install`
4. `vendor/bin/sail up -d`
5. `vendor/bin/sail artisan key:generate`
6. `vendor/bin/sail artisan migrate`
7. `vendor/bin/sail npm install && vendor/bin/sail npm run build`
8. `vendor/bin/sail artisan filament:assets`
9. Login: Navigate to `/admin` — redirects to workspace chooser. For first-time setup, create a workspace and onboard a tenant via the wizard.
10. System console: Navigate to `/system` — requires `platform_users` record with `platform` guard.
