# Feature Specification: Inventory Dependencies Graph **Feature Branch**: `feat/042-inventory-dependencies-graph` **Created**: 2026-01-07 **Status**: Draft ## Purpose Represent and surface dependency relationships between inventory items and foundational Intune objects so admins can understand blast radius and prerequisites. **Definitions**: - **Blast radius**: All resources directly or transitively affected by a change to a given item (outbound edges up to depth 2). - **Prerequisite**: A hard dependency required for an item to function; missing prerequisites are explicitly surfaced. - **Inbound edge**: A relationship pointing TO this item (e.g., "Policy A is assigned to Group X" → Group X has inbound edge from Policy A). - **Outbound edge**: A relationship pointing FROM this item (e.g., "Policy A is scoped by ScopeTag Y" → Policy A has outbound edge to ScopeTag Y). ## User Scenarios & Testing ### Scenario 1: View dependencies for an item - Given an inventory item - When the user opens its dependencies view - Then they can see inbound and outbound relationships (e.g., “uses”, “assigned to”, “scoped by”) ### Scenario 2: Identify missing prerequisites - Given an item references a prerequisite object not present in inventory - When the user views dependencies - Then missing prerequisites are clearly indicated (red badge, "Missing" label, tooltip with last-known displayName if available) ### Scenario 3: Zero dependencies - Given an item has no inbound or outbound edges - When the user opens dependencies view - Then a "No dependencies found" message is shown ### Scenario 4: Filter dependencies by relationship type - Given multiple relationship types exist - When the user filters by relationship type (single-select dropdown, default: "All") - Then only matching edges are shown (empty selection = all edges visible) ### Scenario 5: Only missing prerequisites - Given an item where all referenced targets are unresolved (no matching inventory or foundation objects) - When the user opens the dependencies view and selects "Outbound" or "All" - Then all shown edges are annotated as "Missing" with a red badge and tooltip; filtering still works and zero resolvable targets do not error ## Functional Requirements - **FR1: Relationship taxonomy** Define a normalized set of relationship types covering inventory→inventory and inventory→foundation edges. Supported types (MVP): - `assigned_to` (Policy → AAD Group) - `scoped_by` (Policy → Scope Tag) - `targets` (Update Policy → Device Category, conditional logic) - `depends_on` (Generic prerequisite, e.g., Compliance Policy referenced by Conditional Access) Each type has: - `name` (string, e.g., "assigned_to") - `display_label` (string, e.g., "Assigned to") - `directionality` (enum: `outbound`, `inbound`, `bidirectional`) - `description` (brief explanation) - **FR2: Dependency edge storage** Store edges in an `inventory_links` table with fields: - `id` (PK) - `tenant_id` (FK, indexed) - `source_type` (string: `inventory_item`, `foundation_object`) - `source_id` (UUID or stable ref) - `target_type` (string: `inventory_item`, `foundation_object`, `missing`) - `target_id` (UUID or stable ref, nullable if missing) - `relationship_type` (FK to taxonomy or enum) - `metadata` (JSONB, optional: last_known_name, raw_ref, etc.) - `created_at`, `updated_at` **In-scope foundation object types (MVP)**: - AAD Groups (`aad_group`) - Scope Tags (`scope_tag`) - Device Categories (`device_category`) **Out-of-scope foundation types** (for this iteration): Conditional Access Policies, Compliance Policies as foundation nodes (only as inventory items). - **FR3: Query inbound/outbound edges** Provide service methods: - `getOutboundEdges(item_id, relationship_type?, limit=50)` → returns edges where item is source - `getInboundEdges(item_id, relationship_type?, limit=50)` → returns edges where item is target Both return paginated, ordered by `created_at DESC`. - **FR4: Missing prerequisites** When a target reference cannot be resolved: - Create edge with `target_type='missing'`, `target_id=null` - Store `metadata.last_known_name` and `metadata.raw_ref` if available - UI displays "Missing" badge + tooltip No separate "deleted" or "archived" state in core inventory; missing is purely an edge property. - **FR5: Tenant scoping and access control** - All edges filtered by `tenant_id` matching `Tenant::current()` - Read access: any authenticated tenant user - No cross-tenant queries allowed (enforced at query builder level) ## Non-Functional Requirements - **NFR1: Idempotency** Dependency extraction must be idempotent: - Unique key: `(tenant_id, source_type, source_id, target_type, target_id, relationship_type)` - On re-run: upsert (update `updated_at`, replace `metadata` if changed) - Orphan edges (source/target no longer in inventory) are NOT auto-deleted; cleanup is manual or scheduled separately - **NFR2: Graceful unknown-reference handling** If an unknown/unsupported reference shape is encountered: - Log warning with severity `info` (not `error`) - Do NOT create an edge for unsupported types - Record warning in sync run metadata: `{type: 'unsupported_reference', policy_id, raw_ref, reason}` - Sync run continues without failure ## Graph Traversal & Cycles - UI blast radius view is limited to depth ≤ 2 (direct neighbors and their neighbors). - Traversal uses visited-node tracking to prevent revisiting nodes (cycle break); cycles are implicitly handled by not re-visiting. - No special UI cycle annotation in MVP; future work may visualize cycles explicitly. ## Success Criteria - **SC1: Blast radius determination** Admins can determine prerequisites (inbound edges) and blast radius (outbound edges, depth ≤2) for any item in under 2 minutes: - Measured from: clicking "View Dependencies" on an item detail page - To: able to answer "What would break if I delete this?" and "What does this depend on?" - Acceptance: <2s page load, ≤50 edges per direction shown initially, clear visual grouping by relationship type - **SC2: Deterministic output** For supported relationship types, dependency edges are consistent across re-runs: - Given identical inventory state (same items, same Graph API responses) - Edge set equality: same `(source, target, relationship_type)` tuples (order-independent) - Acceptance: automated test re-runs extraction twice on fixed test data; assert edge sets match (ignoring `updated_at`) ## Security & Privacy - All data is tenant-scoped; no cross-tenant queries or joins. - Foundation object visibility: - Display name shown only if available from tenant-authorized sources (inventory metadata or prior sync payloads). - If not available, show a masked or abbreviated identifier (e.g., first 6 characters of ID) with no external lookup. - Stored metadata for edges must avoid PII beyond display names surfaced by Graph within the tenant; raw references may be stored but not enriched from outside the tenant scope. ## Traceability - FR1 (taxonomy) → SC2 (deterministic types), tests: unit taxonomy load/assert - FR2 (storage) → SC2 (edge equality), tests: feature upsert and equality - FR3 (queries) → SC1 (answer in <2 min), tests: service returns inbound/outbound within limits - FR4 (missing) → SC1 (clear prerequisite view), tests: feature missing badge/tooltip - FR5 (tenant scope) → SC1/SC2 (correct data, deterministic set), tests: tenant isolation ## Out of Scope - Automatic remediation. - Cross-tenant dependency graphs. ## Related Specs - Program: `specs/039-inventory-program/spec.md` - Core: `specs/040-inventory-core/spec.md`