feat/047-inventory-foundations-nodes #51

Merged
ahmido merged 5 commits from feat/047-inventory-foundations-nodes into dev 2026-01-10 20:47:30 +00:00
8 changed files with 94 additions and 3 deletions
Showing only changes of commit 71996083aa - Show all commits

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages;
use App\Services\Inventory\CoverageCapabilitiesResolver;
use BackedEnum;
use Filament\Pages\Page;
use UnitEnum;
@ -31,7 +32,24 @@ public function mount(): void
$policyTypes = config('tenantpilot.supported_policy_types', []);
$foundationTypes = config('tenantpilot.foundation_types', []);
$this->supportedPolicyTypes = is_array($policyTypes) ? $policyTypes : [];
$this->foundationTypes = is_array($foundationTypes) ? $foundationTypes : [];
$resolver = app(CoverageCapabilitiesResolver::class);
$this->supportedPolicyTypes = collect(is_array($policyTypes) ? $policyTypes : [])
->map(function (array $row) use ($resolver): array {
$type = (string) ($row['type'] ?? '');
return array_merge($row, [
'dependencies' => $type !== '' && $resolver->supportsDependencies($type),
]);
})
->all();
$this->foundationTypes = collect(is_array($foundationTypes) ? $foundationTypes : [])
->map(function (array $row): array {
return array_merge($row, [
'dependencies' => false,
]);
})
->all();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Services\Inventory;
class CoverageCapabilitiesResolver
{
public function supportsDependencies(string $type): bool
{
$contracts = config('graph_contracts.types', []);
if (! is_array($contracts)) {
return false;
}
$meta = $contracts[$type] ?? null;
if (! is_array($meta)) {
return false;
}
if (array_key_exists('assignments_list_path', $meta)) {
return true;
}
return ($meta['supports_scope_tags'] ?? false) === true;
}
}

View File

@ -8,6 +8,7 @@
<th class="py-2 pr-4 font-medium">Type</th>
<th class="py-2 pr-4 font-medium">Label</th>
<th class="py-2 pr-4 font-medium">Category</th>
<th class="py-2 pr-4 font-medium">Dependencies</th>
<th class="py-2 pr-4 font-medium">Restore</th>
<th class="py-2 pr-4 font-medium">Risk</th>
</tr>
@ -18,6 +19,7 @@
<td class="py-2 pr-4">{{ $row['type'] ?? '' }}</td>
<td class="py-2 pr-4">{{ $row['label'] ?? '' }}</td>
<td class="py-2 pr-4">{{ $row['category'] ?? '' }}</td>
<td class="py-2 pr-4">{{ ($row['dependencies'] ?? false) ? '✅' : '—' }}</td>
<td class="py-2 pr-4">{{ $row['restore'] ?? 'enabled' }}</td>
<td class="py-2 pr-4">{{ $row['risk'] ?? 'normal' }}</td>
</tr>
@ -36,6 +38,7 @@
<th class="py-2 pr-4 font-medium">Type</th>
<th class="py-2 pr-4 font-medium">Label</th>
<th class="py-2 pr-4 font-medium">Category</th>
<th class="py-2 pr-4 font-medium">Dependencies</th>
<th class="py-2 pr-4 font-medium">Restore</th>
<th class="py-2 pr-4 font-medium">Risk</th>
</tr>
@ -46,6 +49,7 @@
<td class="py-2 pr-4">{{ $row['type'] ?? '' }}</td>
<td class="py-2 pr-4">{{ $row['label'] ?? '' }}</td>
<td class="py-2 pr-4">{{ $row['category'] ?? '' }}</td>
<td class="py-2 pr-4">{{ ($row['dependencies'] ?? false) ? '✅' : '—' }}</td>
<td class="py-2 pr-4">{{ $row['restore'] ?? 'enabled' }}</td>
<td class="py-2 pr-4">{{ $row['risk'] ?? 'normal' }}</td>
</tr>

View File

@ -17,6 +17,7 @@ ## Feature 047 Functional Coverage
- [x] FR-002 include_foundations=false produces no foundation node sync side effects.
- [x] FR-003 Foundation nodes stored as InventoryItems with stable identity (tenant_id + policy_type + external_id).
- [x] FR-004 Inventory Coverage UI shows Policies + Foundations.
- [x] FR-COV-DEP: Coverage shows deterministic Dependencies support column (✅/—) derived from existing capabilities (no Graph calls).
- [x] FR-005 Inventory Items UI can filter/browse foundations.
## Test Gates

View File

@ -98,6 +98,27 @@ ### FR-004 Inventory Coverage UI
- “Policies” table (existing behavior)
- “Foundations” table (new; derived from `tenantpilot.foundation_types`)
#### FR-COV-DEP-001 Dependencies column
Coverage MUST display an additional column:
- Header: `Dependencies`
- Value: `✅` or `—`
#### FR-COV-DEP-002 Deterministic derivation
The `Dependencies` value MUST be derived deterministically from existing capabilities (config/contracts) only:
`✅` if at least one holds:
- the type supports Assignments extraction, or
- the type supports Scope Tags, or
- the type can reference Assignment Filters, or
- the type has dependency extraction rules in Spec 042 (relationship taxonomy / extractor mapping)
Otherwise: `—`.
This is **feature support**, not “Graph supports $expand”.
MVP decision:
- For foundation types, default to `—`.
### FR-005 Inventory Items UI
Inventory Items list MUST allow:
- filtering to Foundations (e.g., Category = Foundations)

View File

@ -70,6 +70,12 @@ ## Phase 5: User Story 3 — Coverage communication (Priority: P2)
- [ ] T015 [US3] Update Coverage Blade view to render two tables in resources/views/filament/pages/inventory-coverage.blade.php
- [ ] T016 [P] [US3] Add/adjust Pest test assertions for both headings in tests/Feature/Filament/InventoryPagesTest.php
### Coverage Dependencies Support (UI-only)
- [x] T026 [US3] Add Coverage table column `Dependencies` (✅/—) in resources/views/filament/pages/inventory-coverage.blade.php
- [x] T027 [US3] Add deterministic resolver CoverageCapabilitiesResolver::supportsDependencies($type) (contracts/config derived) + unit test in tests/Unit/CoverageCapabilitiesResolverTest.php
- [x] T028 [P] [US3] Update Pest UI/feature test to assert Coverage renders `Dependencies` column and at least one ✅ in tests/Feature/Filament/InventoryPagesTest.php
---
## Phase 6: User Story 4 — Resolve dependency names (Priority: P3)

View File

@ -25,5 +25,7 @@
->assertOk()
->assertSee('Coverage')
->assertSee('Policies')
->assertSee('Foundations');
->assertSee('Foundations')
->assertSee('Dependencies')
->assertSee('✅');
});

View File

@ -0,0 +1,14 @@
<?php
use App\Services\Inventory\CoverageCapabilitiesResolver;
it('derives dependency support deterministically from graph contracts', function (string $type, bool $expected) {
$resolver = app(CoverageCapabilitiesResolver::class);
expect($resolver->supportsDependencies($type))->toBe($expected);
})->with([
'settingsCatalogPolicy' => ['settingsCatalogPolicy', true],
'deviceConfiguration' => ['deviceConfiguration', true],
'conditionalAccessPolicy' => ['conditionalAccessPolicy', false],
'roleScopeTag (foundation, MVP)' => ['roleScopeTag', false],
]);