9.2 KiB
9.2 KiB
Implementation Plan: Inventory Dependencies Graph
Date: 2026-01-07
Spec: specs/042-inventory-dependencies-graph/spec.md
Summary
Add dependency edge model, extraction logic, and UI views to explain relationships between inventory items and prerequisite/foundation objects.
Dependencies
- Inventory items and stable identifiers (Spec 040)
- Inventory UI detail pages (Spec 041) or equivalent navigation
- Assignments payload (from Spec 006 or equivalent) for
assigned_torelationships - Scope tags and device categories as foundation objects
Deliverables
- Relationship taxonomy (enum + metadata)
- Persisted dependency edges (
inventory_linkstable) - Extraction pipeline (integrated into inventory sync)
- Query service methods (inbound/outbound)
- UI components (Filament section + filtering)
- Tests (unit, feature, UI smoke, tenant isolation)
Risks
- Heterogeneous reference shapes across policy types → Mitigation: Normalize references early; unsupported shapes → soft warning, skip edge creation
- Edge explosion for large tenants → Mitigation: Hard limit ≤50 edges per item per direction (enforced at extraction); pagination UI for future
1. Extraction Pipeline
Timing & Integration
- When: Dependencies extracted during inventory sync run (not on-demand)
- Hook: After
InventorySyncServicecreates/updatesInventoryItem, callDependencyExtractionService - Idempotency: Each sync run reconciles edges for synced items (upsert based on unique key)
Inputs
- InventoryItem (
raw,meta,resource_type,external_id) - Assignments payload (if available): AAD Group IDs
- Scope tags (from item
meta->scopeTagsor raw payload) - Device categories (from conditional logic in raw payload, if applicable)
Extraction Logic
DependencyExtractionService::extractForItem(InventoryItem $item): array
- Parse
$item->rawand$item->metafor known reference shapes - For each reference:
- Normalize to
{type, external_id, display_name?} - Determine
relationship_type(assigned_to, scoped_by, targets, depends_on) - Resolve target:
- If
InventoryItemexists with matchingexternal_id→target_type='inventory_item' - If foundation object (AAD Group, Scope Tag, Device Category) →
target_type='foundation_object' - If unresolvable →
target_type='missing',target_id=null, storemetadata.last_known_name
- If
- Create/update edge
- Normalize to
Error & Warning Logic
- Unsupported reference shape: Log warning (
infoseverity), record in sync run metadata, skip edge creation - Low confidence: If reference parsing heuristic uncertain, create edge with
metadata.confidence='low' - Unresolved target: Create edge with
target_type='missing'(not an error) - All targets missing: Extraction still emits
missingedges; UI must handle zero resolvable targets without error
Hard Limits
- Max 50 edges per item per direction (outbound/inbound)
- Enforced at extraction: sort by priority (assigned_to > scoped_by > targets > depends_on), keep top 50, log truncation warning
2. Storage & Upsert Details
Schema: inventory_links Table
Schema::create('inventory_links', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('source_type'); // 'inventory_item', 'foundation_object'
$table->uuid('source_id'); // InventoryItem.id or foundation stable ID
$table->string('target_type'); // 'inventory_item', 'foundation_object', 'missing'
$table->uuid('target_id')->nullable(); // null if target_type='missing'
$table->string('relationship_type'); // 'assigned_to', 'scoped_by', etc.
$table->jsonb('metadata')->nullable(); // {last_known_name, raw_ref, confidence, ...}
$table->timestamps();
// Unique key (idempotency)
$table->unique([
'tenant_id',
'source_type',
'source_id',
'target_type',
'target_id',
'relationship_type'
], 'inventory_links_unique');
$table->index(['tenant_id', 'source_type', 'source_id']);
$table->index(['tenant_id', 'target_type', 'target_id']);
});
Upsert Strategy
- On extraction:
InventoryLink::upsert($edges, uniqueBy: ['tenant_id', 'source_type', 'source_id', 'target_type', 'target_id', 'relationship_type'], update: ['metadata', 'updated_at']) - On item deletion: Edges NOT auto-deleted (orphan cleanup is manual or scheduled job)
- On re-run: Existing edges updated (
updated_atbumped), new edges created
Hard Limit Enforcement
- Extraction service limits to 50 edges per direction before upserting
- Query methods (
getOutboundEdges,getInboundEdges) have defaultlimit=50parameter
3. UI Components (Filament)
Location
- InventoryItemResource → ViewInventoryItem page
- New section: "Dependencies" (below "Details" section)
Component Structure
// app/Filament/Resources/InventoryItemResource.php (or dedicated Infolist)
Section::make('Dependencies')
->schema([
// Filter: Inbound / Outbound / All (default: All)
Forms\Components\Select::make('direction')
->options(['all' => 'All', 'inbound' => 'Inbound', 'outbound' => 'Outbound'])
->default('all')
->live(),
// Edges table (or custom Blade component)
ViewEntry::make('edges')
->view('filament.components.dependency-edges')
->state(fn (InventoryItem $record) =>
DependencyService::getEdges($record, request('direction', 'all'))
),
])
Blade View: resources/views/filament/components/dependency-edges.blade.php
- Zero-state: If
$edges->isEmpty(): "No dependencies found" - Edge rendering:
- Group by
relationship_type(collapsible groups or headings) - Each edge: icon + target name + link (if resolvable) + "Missing" red badge (if target_type='missing') + tooltip (metadata.last_known_name)
- Group by
- Performance: Load edges synchronously (≤50 per direction, fast query with indexes)
- Privacy: If display name unavailable, render masked identifier (e.g.,
ID: abcd12…), no cross-tenant lookups
Filter Behavior
- Single-select dropdown (not multi-select)
- Default: "All" (both inbound + outbound shown)
- Empty/null selection → treated as "All"
4. Tests
Unit Tests
-
DependencyExtractionServiceTest.php:test_normalizes_references_deterministically(): same raw input → same edges (order-independent)test_respects_unique_key(): re-extraction → no duplicate edgestest_handles_unsupported_references(): logs warning, skips edge
-
InventoryLinkTest.php:test_unique_constraint_enforced(): duplicate insert → exception or upserttest_tenant_scoping(): edges filtered by tenant_id
Feature Tests
-
DependencyExtractionFeatureTest.php:test_extraction_creates_expected_edges(): fixture item → assert edges created (assigned_to, scoped_by)test_extraction_marks_missing_targets(): item references non-existent group → edge with target_type='missing'test_extraction_respects_50_edge_limit(): item with 60 refs → only 50 edges createdtest_only_missing_prerequisites_scenario(): item whose all targets are unresolved → onlymissingedges, UI renders badges
-
DependencyQueryServiceTest.php:test_get_outbound_edges_filters_by_tenant(): cannot see other tenant's edgestest_get_inbound_edges_returns_correct_direction()
UI Smoke Tests
InventoryItemDependenciesTest.php:test_dependencies_section_renders(): view page → see "Dependencies" sectiontest_filter_works(): select "Inbound" → only inbound edges showntest_zero_state_shown(): item with no edges → "No dependencies found"test_missing_badge_shown(): edge with target_type='missing' → red badge visibletest_masks_identifier_when_name_unavailable(): foundation object without display name → masked ID shown
Security Tests
DependencyTenantIsolationTest.php:test_cannot_see_other_tenant_edges(): tenant A user → cannot query tenant B edgestest_edges_scoped_by_tenant(): all query methods enforce tenant_id filtertest_no_cross_tenant_enrichment(): service never fetches/display names across tenants
Implementation Sequence
- T001: Define relationship taxonomy (enum + migration for reference data or config)
- T002: Create
inventory_linksmigration + model + factory - T003: Implement
DependencyExtractionService(extraction logic + normalization) - T004: Integrate extraction into
InventorySyncService(post-item-creation hook) - T005: Implement
DependencyQueryService(getInbound/Outbound + tenant scoping) - T006: Add Filament "Dependencies" section to InventoryItem detail page
- T007: Implement filter UI (direction select + live wire)
- T008: Add Blade view for edge rendering (zero-state, missing badge, tooltip)
- T009: Write unit tests (extraction, unique key, tenant scoping)
- T010: Write feature tests (extraction, query, limits)
- T011: Write UI smoke tests (section render, filter, zero-state)
- T012: Run full test suite + Pint