# Product Principles > Permanent product principles that govern every spec, every UI decision, and every architectural choice. > New specs must align with these. If a principle needs to change, update this file first. **Last reviewed**: 2026-03-08 --- ## Identity & Isolation ### Workspace-first context Workspace is the primary session context. Every UI surface, every query, every action is workspace-scoped. Non-members receive deny-as-not-found (404 semantics) — they never learn the resource exists. ### Tenant isolation (non-negotiable) Every read/write is tenant-scoped. Cross-tenant views are explicit, access-checked, aggregation-based. Non-member → 404. No cross-embedding of workspace-owned and tenant-owned data. ### SCOPE-001: Strict ownership model - **Workspace-owned** = standards, templates, configuration, baselines - **Tenant-owned** = observed state, evidence, artifacts, inventory --- ## Authorization & Safety ### Capability-first RBAC Single canonical registry (`Capabilities.php`). No raw strings. CI fails on unknown capabilities. UI visibility is never a security boundary — missing server-side auth is a P0 bug. ### Visible-but-disabled UX Members see disabled actions with tooltip explaining the missing capability. Non-members see nothing (404 semantics). ### Destructive actions require safe flows All destructive actions → `requiresConfirmation()`. No exceptions. Write operations require: preview/dry-run → confirmation → audit log → tests. High-risk types default to `preview-only`. --- ## Operations & Observability ### 3-Surface Feedback (non-negotiable) 1. **Toast** — intent acknowledged 2. **Progress** — active work visible 3. **Terminal DB Notification** — audit record No other feedback patterns. No silent mutations. ### OperationRun lifecycle is service-owned All status/outcome transitions via `OperationRunService` only. Summary counts via `OperationSummaryKeys::all()`. Flat numeric only. ### Enterprise-grade auditability Every mutation has a trail. Backup created, restore attempted, policy change detected — logged, tenant-scoped, RBAC-respecting. --- ## Data & Architecture ### Inventory-first, Snapshots-second - `InventoryItem` = last observed metadata - `PolicyVersion.snapshot` = explicit immutable JSONB capture - Intune remains external source of truth ### Single Contract Path to Graph All MS Graph calls via `GraphClientInterface`. Endpoints modeled in `config/graph_contracts.php`. No hardcoded "quick endpoints". Unknown types fail safe. ### Deterministic Capabilities Backup/restore/risk flags derived deterministically from config via Capabilities Resolver. Must be snapshot-testable. ### Data minimization & safe logging Inventory = metadata only. No secrets in logs. Monitoring relies on run records + error codes. --- ## UI & Information Architecture ### UX-001: Layout & IA Standards Main/Aside layout. Sections required. View pages use Infolists. Empty states with specific title + explanation + exactly 1 CTA. ### Action Surface Contract (non-negotiable) Required surfaces per page type (list/view/create/edit). Max 2 visible row actions. Destructive requires confirmation. Every spec with UI changes must include a UI Action Matrix. ### Badge semantics centralized All status badges via `BadgeCatalog` / `BadgeRenderer`. No ad-hoc badge mappings. ### Canonical navigation and terminology Consistent naming, consistent routing, consistent mental model. No competing terms for the same concept. --- ## Process ### Spec-first workflow Runtime behavior changes require spec update first. Every spec must declare: scope, primary routes, data ownership, RBAC requirements (SCOPE-002). ### Regression guards mandatory RBAC regression tests per role. Ops-UX regression guards prevent direct status writes and ad-hoc notifications. Architectural guard tests enforce code-level contracts. --- ## Filament v5 Alignment ### Non-negotiables - Livewire v4.0+ - Panel providers in `bootstrap/providers.php` - Global search requires Edit/View page or is disabled - Prefer render hooks + CSS hooks over publishing internal views - Heavy assets loaded on-demand (`loadedOnRequest()`)