feat/042-inventory-dependencies-graph #50

Merged
ahmido merged 22 commits from feat/042-inventory-dependencies-graph into dev 2026-01-10 12:50:08 +00:00
5 changed files with 207 additions and 38 deletions
Showing only changes of commit 7357c546df - Show all commits

View File

@ -8,66 +8,66 @@ # PR Gate Checklist: Inventory Dependencies Graph
## Requirement Completeness
- [x] CHK001 Ist die Relationship-Taxonomie vollständig enumeriert (Set an Typen) und pro Typ mit Definition + Richtung + Beispiel beschrieben? [Completeness, Spec §FR1]
- [x] CHK002 Sind alle in-scope „foundational Intune objects“ explizit aufgelistet (Enumerate now) inkl. klarer Exclusions, um Scope zu binden? [Gap, Spec §FR2, Plan §Risks]
- [x] CHK003 Sind Mindestfelder einer Dependency-Edge als Requirement beschrieben (source, target, relationship_type, directionality, provenance/derived_from, timestamps/created_at, optional metadata)? [Gap, Spec §FR2]
- [x] CHK004 Ist klar spezifiziert, wie „inventory item“ eindeutig referenziert wird (stable identifier), und wie foundation objects referenziert werden (ID/uri/type tuple)? [Gap, Plan §Dependencies]
- [x] CHK005 Sind inbound UND outbound Abfragen als explizite Requirements beschrieben (inkl. erwarteter Sortierung/Limitierung oder bewusst „unspecified“)? [Completeness, Spec §FR3]
- [x] CHK006 Sind Missing-Prerequisites als eigene Requirement-Klasse beschrieben (Erkennung, Darstellung, und welche Daten minimal gezeigt werden dürfen)? [Completeness, Spec §FR4, Scenario 2]
- [x] CHK007 Ist Relationship-Type-Filtering vollständig spezifiziert (Scope, Default, Mehrfachauswahl ja/nein, Verhalten bei „none selected“)? [Completeness, Spec §Scenario 3]
- [x] CHK008 Sind Tenant-Scoping und Access Control Requirements konkretisiert (welche Rollen/Capabilities; Read vs View Details; ggf. audit expectations)? [Gap, Spec §FR5]
- [x] CHK001 Ist die Relationship-Taxonomie vollständig enumeriert (Set an Typen) und pro Typ mit Definition + Richtung + Beispiel beschrieben? [Completeness, Spec §FR1]
- [x] CHK002 Sind alle in-scope „foundational Intune objects“ explizit aufgelistet (Enumerate now) inkl. klarer Exclusions, um Scope zu binden? [Gap, Spec §FR2, Plan §Risks]
- [x] CHK003 Sind Mindestfelder einer Dependency-Edge als Requirement beschrieben (source, target, relationship_type, directionality, provenance/derived_from, timestamps/created_at, optional metadata)? [Gap, Spec §FR2]
- [x] CHK004 Ist klar spezifiziert, wie „inventory item“ eindeutig referenziert wird (stable identifier), und wie foundation objects referenziert werden (ID/uri/type tuple)? [Gap, Plan §Dependencies]
- [x] CHK005 Sind inbound UND outbound Abfragen als explizite Requirements beschrieben (inkl. erwarteter Sortierung/Limitierung oder bewusst „unspecified“)? [Completeness, Spec §FR3]
- [x] CHK006 Sind Missing-Prerequisites als eigene Requirement-Klasse beschrieben (Erkennung, Darstellung, und welche Daten minimal gezeigt werden dürfen)? [Completeness, Spec §FR4, Scenario 2]
- [x] CHK007 Ist Relationship-Type-Filtering vollständig spezifiziert (Scope, Default, Mehrfachauswahl ja/nein, Verhalten bei „none selected“)? [Completeness, Spec §Scenario 3]
- [x] CHK008 Sind Tenant-Scoping und Access Control Requirements konkretisiert (welche Rollen/Capabilities; Read vs View Details; ggf. audit expectations)? [Gap, Spec §FR5]
## Requirement Clarity
- [x] CHK009 Ist „inbound“ vs „outbound“ formal definiert pro Relationship-Type (nicht nur im Textbeispiel), um Interpretationsspielraum zu vermeiden? [Clarity, Spec §Scenario 1, FR3]
- [x] CHK010 Sind Relationship-Namen und Semantik konsistent (z.B. „assigned to“ ist eindeutig Richtung A→B) und nicht synonym/überlappend? [Clarity, Spec §FR1]
- [x] CHK011 Ist „blast radius“ in messbare Graph-Konzepte übersetzt (z.B. „outbound edges bis Tiefe N“, „both directions“, oder explizit „only direct neighbors“)? [Ambiguity, Spec §Purpose]
- [x] CHK012 Ist „prerequisite“ eindeutig definiert (hard vs informational, required vs optional) und ist diese Definition in Missing-Prerequisites konsistent wiederverwendet? [Ambiguity, Spec §Purpose, Scenario 2]
- [x] CHK013 Ist „safe warning“ (NFR2) klar operationalisiert: Inhalt/Felder, Severity, Persistenz, und wo es sichtbar wird (Run-Log vs UI vs Audit)? [Clarity, Spec §NFR2]
- [x] CHK009 Ist „inbound“ vs „outbound“ formal definiert pro Relationship-Type (nicht nur im Textbeispiel), um Interpretationsspielraum zu vermeiden? [Clarity, Spec §Scenario 1, FR3]
- [x] CHK010 Sind Relationship-Namen und Semantik konsistent (z.B. „assigned to“ ist eindeutig Richtung A→B) und nicht synonym/überlappend? [Clarity, Spec §FR1]
- [x] CHK011 Ist „blast radius“ in messbare Graph-Konzepte übersetzt (z.B. „outbound edges bis Tiefe N“, „both directions“, oder explizit „only direct neighbors“)? [Ambiguity, Spec §Purpose]
- [x] CHK012 Ist „prerequisite“ eindeutig definiert (hard vs informational, required vs optional) und ist diese Definition in Missing-Prerequisites konsistent wiederverwendet? [Ambiguity, Spec §Purpose, Scenario 2]
- [x] CHK013 Ist „safe warning“ (NFR2) klar operationalisiert: Inhalt/Felder, Severity, Persistenz, und wo es sichtbar wird (Run-Log vs UI vs Audit)? [Clarity, Spec §NFR2]
## Requirement Consistency
- [x] CHK014 Sind Scenario-Beispiele („uses“, „assigned to“, „scoped by“) vollständig Teil der FR1-Taxonomie (keine scenario-only Typen)? [Consistency, Spec §Scenario 1, FR1]
- [x] CHK015 Ist FR4 („missing prerequisites“) konsistent mit „ohne deleted state in core inventory“ beschrieben (kein implizites soft-delete/archived eingeführt)? [Consistency, Spec §FR4]
- [x] CHK016 Sind NFR1 (idempotent) und SC2 (deterministic output) konfliktfrei und eindeutig, was Gleichheit bedeutet (Edge-Set, Normalisierung, Sortierung)? [Consistency, Spec §NFR1, SC2]
- [x] CHK017 Ist der Tenant-Scope konsistent in Storage, Query und UI (keine impliziten cross-tenant Graphs; Out-of-scope ist explizit)? [Consistency, Spec §FR5, Out of Scope]
- [x] CHK014 Sind Scenario-Beispiele („uses“, „assigned to“, „scoped by“) vollständig Teil der FR1-Taxonomie (keine scenario-only Typen)? [Consistency, Spec §Scenario 1, FR1]
- [x] CHK015 Ist FR4 („missing prerequisites“) konsistent mit „ohne deleted state in core inventory“ beschrieben (kein implizites soft-delete/archived eingeführt)? [Consistency, Spec §FR4]
- [x] CHK016 Sind NFR1 (idempotent) und SC2 (deterministic output) konfliktfrei und eindeutig, was Gleichheit bedeutet (Edge-Set, Normalisierung, Sortierung)? [Consistency, Spec §NFR1, SC2]
- [x] CHK017 Ist der Tenant-Scope konsistent in Storage, Query und UI (keine impliziten cross-tenant Graphs; Out-of-scope ist explizit)? [Consistency, Spec §FR5, Out of Scope]
## Acceptance Criteria Quality
- [x] CHK018 Ist SC1 („unter 2 Minuten“) so definiert, dass Reviewer objektiv prüfen können, was „determine prerequisites and blast radius“ konkret bedeutet (Tiefe, Umfang, Informationsumfang)? [Measurability, Spec §SC1]
- [x] CHK019 Ist SC2 so messbar formuliert, dass deterministische Output-Gleichheit ohne Interpretationsspielraum prüfbar ist (z.B. canonical ordering + uniqueness rules)? [Measurability, Spec §SC2]
- [x] CHK020 Gibt es eine klare Traceability zwischen FR1FR5 und Success Criteria (jedes FR hat mindestens ein objektives Akzeptanzkriterium oder ist bewusst als „non-testable“ markiert)? [Gap, Spec §Functional Requirements, Success Criteria]
- [x] CHK018 Ist SC1 („unter 2 Minuten“) so definiert, dass Reviewer objektiv prüfen können, was „determine prerequisites and blast radius“ konkret bedeutet (Tiefe, Umfang, Informationsumfang)? [Measurability, Spec §SC1]
- [x] CHK019 Ist SC2 so messbar formuliert, dass deterministische Output-Gleichheit ohne Interpretationsspielraum prüfbar ist (z.B. canonical ordering + uniqueness rules)? [Measurability, Spec §SC2]
- [x] CHK020 Gibt es eine klare Traceability zwischen FR1FR5 und Success Criteria (jedes FR hat mindestens ein objektives Akzeptanzkriterium oder ist bewusst als „non-testable“ markiert)? [Gap, Spec §Functional Requirements, Success Criteria]
## Scenario Coverage
- [x] CHK021 Deckt die Spec explizit den Zero-State ab („no edges“ / „no deps“), inkl. erwarteter UI-Messaging-Requirement? [Gap, Spec §User Scenarios & Testing]
- [x] CHK022 Deckt die Spec explizit Mixed-Targets ab (Inventory→Foundation, Inventory→Inventory) und ob Foundation→Inventory als inbound dargestellt werden soll? [Gap, Spec §FR2, FR3]
- [x] CHK023 Gibt es definierte Requirements für „only missing prerequisites“ (alle Targets missing) und wie Filter/Display damit umgehen? [Gap, Spec §Scenario 2]
- [x] CHK021 Deckt die Spec explizit den Zero-State ab („no edges“ / „no deps“), inkl. erwarteter UI-Messaging-Requirement? [Gap, Spec §User Scenarios & Testing]
- [x] CHK022 Deckt die Spec explizit Mixed-Targets ab (Inventory→Foundation, Inventory→Inventory) und ob Foundation→Inventory als inbound dargestellt werden soll? [Gap, Spec §FR2, FR3]
- [x] CHK023 Gibt es definierte Requirements für „only missing prerequisites“ (alle Targets missing) und wie Filter/Display damit umgehen? [Gap, Spec §Scenario 2]
## Edge Case Coverage
- [x] CHK024 Sind Unknown/Unsupported References (NFR2) vollständig als Requirements abgedeckt: ob Edge erzeugt wird, ob Node „unknown“ erlaubt ist, ob raw reference gespeichert wird? [Coverage, Spec §NFR2]
- [x] CHK025 Sind Duplicate References innerhalb eines Items geregelt (Dedup-Key, Merge Rules), um NFR1/SC2 deterministisch einzuhalten? [Gap, Spec §NFR1, SC2]
- [x] CHK026 Sind zyklische Dependencies als Requirement adressiert (Erkennung/Handling/Traversal-Limits), damit „blast radius“ nicht unendlich wird? [Gap, Spec §Purpose, Plan §Risks]
- [x] CHK027 Sind Grenzen für Edge-Explosion als Requirements spezifiziert (Limits, pagination, depth caps, server-side constraints), nicht nur als Risiko erwähnt? [Gap, Plan §Risks]
- [x] CHK024 Sind Unknown/Unsupported References (NFR2) vollständig als Requirements abgedeckt: ob Edge erzeugt wird, ob Node „unknown“ erlaubt ist, ob raw reference gespeichert wird? [Coverage, Spec §NFR2]
- [x] CHK025 Sind Duplicate References innerhalb eines Items geregelt (Dedup-Key, Merge Rules), um NFR1/SC2 deterministisch einzuhalten? [Gap, Spec §NFR1, SC2]
- [x] CHK026 Sind zyklische Dependencies als Requirement adressiert (Erkennung/Handling/Traversal-Limits), damit „blast radius“ nicht unendlich wird? [Gap, Spec §Purpose, Plan §Risks]
- [x] CHK027 Sind Grenzen für Edge-Explosion als Requirements spezifiziert (Limits, pagination, depth caps, server-side constraints), nicht nur als Risiko erwähnt? [Gap, Plan §Risks]
## Non-Functional Requirements
- [x] CHK028 Ist Idempotenz (NFR1) präzisiert: Scope (per-run vs global), Unique Keys, Upsert vs Replace-All, und ob Deletions/Orphan-Edges geregelt sind? [Clarity, Spec §NFR1]
- [x] CHK029 Ist „must not fail an inventory sync run“ präzisiert: welche Fehler sind soft-fail, welche sind hard-fail, und wie wird das für Reviewer nachvollziehbar? [Clarity, Spec §NFR2]
- [x] CHK030 Sind Performance-/Skalierungsanforderungen spezifiziert (UI Query Latency, max edges returned, extraction time budget) statt nur „Risiko“? [Gap, Plan §Risks]
- [x] CHK031 Sind Security/Privacy-Anforderungen spezifiziert, welche foundation-object Daten sichtbar sein dürfen (IDs vs Names) und ob das tenant- & permission-scoped ist? [Gap, Spec §FR5]
- [x] CHK028 Ist Idempotenz (NFR1) präzisiert: Scope (per-run vs global), Unique Keys, Upsert vs Replace-All, und ob Deletions/Orphan-Edges geregelt sind? [Clarity, Spec §NFR1]
- [x] CHK029 Ist „must not fail an inventory sync run“ präzisiert: welche Fehler sind soft-fail, welche sind hard-fail, und wie wird das für Reviewer nachvollziehbar? [Clarity, Spec §NFR2]
- [x] CHK030 Sind Performance-/Skalierungsanforderungen spezifiziert (UI Query Latency, max edges returned, extraction time budget) statt nur „Risiko“? [Gap, Plan §Risks]
- [x] CHK031 Sind Security/Privacy-Anforderungen spezifiziert, welche foundation-object Daten sichtbar sein dürfen (IDs vs Names) und ob das tenant- & permission-scoped ist? [Gap, Spec §FR5]
## Dependencies & Assumptions
- [x] CHK032 Sind Abhängigkeiten zu Spec 040 (stable identifiers) und Spec 041 (UI navigation/detail pages) als Requirements eindeutig dokumentiert (hard requirement vs optional)? [Clarity, Plan §Dependencies]
- [x] CHK033 Sind Annahmen über heterogene Reference-Shapes explizit dokumentiert und ist klar, wie neue Shapes in Scope aufgenommen werden (Change control / taxonomy update)? [Assumption, Plan §Risks]
- [x] CHK034 Ist explizit dokumentiert, wann Edges extrahiert werden (im sync run vs post-processing), und wie Staleness/Refresh geregelt ist? [Gap, Spec §NFR1]
- [x] CHK032 Sind Abhängigkeiten zu Spec 040 (stable identifiers) und Spec 041 (UI navigation/detail pages) als Requirements eindeutig dokumentiert (hard requirement vs optional)? [Clarity, Plan §Dependencies]
- [x] CHK033 Sind Annahmen über heterogene Reference-Shapes explizit dokumentiert und ist klar, wie neue Shapes in Scope aufgenommen werden (Change control / taxonomy update)? [Assumption, Plan §Risks]
- [x] CHK034 Ist explizit dokumentiert, wann Edges extrahiert werden (im sync run vs post-processing), und wie Staleness/Refresh geregelt ist? [Gap, Spec §NFR1]
## Ambiguities & Conflicts
- [x] CHK035 Sind Begriffe „missing“, „not present“, „excluded“, „out-of-scope“ sauber definiert und konsistent verwendet (kein Vermischen von Datenzustand und Scope)? [Ambiguity, Spec §FR4, Out of Scope]
- [x] CHK036 Ist klar, ob Filtering nur Relationship-Types betrifft oder auch Node-Types (Inventory vs Foundation) und ob beides kombinierbar sein soll? [Gap, Spec §Scenario 3]
- [x] CHK035 Sind Begriffe „missing“, „not present“, „excluded“, „out-of-scope“ sauber definiert und konsistent verwendet (kein Vermischen von Datenzustand und Scope)? [Ambiguity, Spec §FR4, Out of Scope]
- [x] CHK036 Ist klar, ob Filtering nur Relationship-Types betrifft oder auch Node-Types (Inventory vs Foundation) und ob beides kombinierbar sein soll? [Gap, Spec §Scenario 3]
## Notes

View File

@ -26,3 +26,9 @@ ## MVP Notes
- Limit-only, no pagination.
- Shows <=50 edges per direction (<=100 total when showing both directions).
- Unknown/unsupported reference shapes are warning-only and should be visible via `InventorySyncRun.error_context.warnings[]`.
## Manual Performance Check (<2s)
1. Open an Inventory Item with ~50 inbound and/or ~50 outbound edges.
2. Use browser devtools Network tab to confirm the page request completes quickly.
3. Toggle `direction` and `relationship_type` filters and confirm responses remain fast.

View File

@ -11,8 +11,8 @@ ## Phase 1: Setup (Shared)
**Purpose**: Ensure feature docs and scope constraints are locked before code changes.
- [ ] T001 Validate MVP constraints in `specs/042-inventory-dependencies-graph/plan.md` remain aligned with `specs/042-inventory-dependencies-graph/spec.md`
- [ ] T002 Validate scope + NFR checkboxes in `specs/042-inventory-dependencies-graph/checklists/requirements.md` cover all accepted MVP constraints
- [x] T001 Validate MVP constraints in `specs/042-inventory-dependencies-graph/plan.md` remain aligned with `specs/042-inventory-dependencies-graph/spec.md`
- [x] T002 Validate scope + NFR checkboxes in `specs/042-inventory-dependencies-graph/checklists/requirements.md` cover all accepted MVP constraints
---
@ -32,6 +32,16 @@ ## Phase 2: Foundational (Blocking Prerequisites)
- [ ] T010 Implement limit-only inbound/outbound queries with optional relationship filter in `app/Services/Inventory/DependencyQueryService.php` (ordered by `created_at DESC`)
- [ ] T011 [P] Add determinism + idempotency tests in `tests/Unit/DependencyExtractionServiceTest.php`
- [ ] T012 [P] Add tenant isolation tests for queries in `tests/Feature/DependencyExtractionFeatureTest.php`
- [x] T003 [P] Ensure relationship types and labels are centralized in `app/Support/Enums/RelationshipType.php`
- [x] T004 Ensure `inventory_links` schema (unique key + indexes) matches spec in `database/migrations/2026_01_07_150000_create_inventory_links_table.php`
- [x] T005 [P] Ensure `InventoryLink` casts and tenant-safe query patterns in `app/Models/InventoryLink.php`
- [x] T006 [P] Ensure factory coverage for dependency edges in `database/factories/InventoryLinkFactory.php`
- [x] T007 Align idempotent upsert semantics for edges in `app/Services/Inventory/DependencyExtractionService.php`
- [x] T008 Implement warning-only handling for unknown/unsupported reference shapes in `app/Services/Inventory/DependencyExtractionService.php`
- [x] T009 Persist warnings on the sync run record at `InventorySyncRun.error_context.warnings[]` via `app/Services/Inventory/InventorySyncService.php`
- [x] T010 Implement limit-only inbound/outbound queries with optional relationship filter in `app/Services/Inventory/DependencyQueryService.php` (ordered by `created_at DESC`)
- [x] T011 [P] Add determinism + idempotency tests in `tests/Unit/DependencyExtractionServiceTest.php`
- [x] T012 [P] Add tenant isolation tests for queries in `tests/Feature/DependencyExtractionFeatureTest.php`
---
@ -46,6 +56,11 @@ ## Phase 3: User Story 1 — View Dependencies (Priority: P1) 🎯 MVP
- [ ] T015 [US1] Add direction filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php`
- [ ] T016 [US1] Parse/validate `direction` and pass to query service in `app/Filament/Resources/InventoryItemResource.php`
- [ ] T017 [US1] Add UI smoke test for dependencies rendering + direction filter in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T013 [P] [US1] Wire dependencies section into Filament item view in `app/Filament/Resources/InventoryItemResource.php`
- [x] T014 [P] [US1] Render edges grouped by relationship type in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T015 [US1] Add direction filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T016 [US1] Parse/validate `direction` and pass to query service in `app/Filament/Resources/InventoryItemResource.php`
- [x] T017 [US1] Add UI smoke test for dependencies rendering + direction filter in `tests/Feature/InventoryItemDependenciesTest.php`
---
@ -59,6 +74,10 @@ ## Phase 4: User Story 2 — Identify Missing Prerequisites (Priority: P2)
- [ ] T019 [US2] Display “Missing” badge + tooltip for missing edges in `resources/views/filament/components/dependency-edges.blade.php`
- [ ] T020 [US2] Add feature test for missing edge creation + metadata in `tests/Feature/DependencyExtractionFeatureTest.php`
- [ ] T021 [US2] Add UI test asserting missing badge/tooltip is visible in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T018 [US2] Ensure unresolved targets create `target_type='missing'` edges with safe metadata in `app/Services/Inventory/DependencyExtractionService.php`
- [x] T019 [US2] Display “Missing” badge + tooltip for missing edges in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T020 [US2] Add feature test for missing edge creation + metadata in `tests/Feature/DependencyExtractionFeatureTest.php`
- [x] T021 [US2] Add UI test asserting missing badge/tooltip is visible in `tests/Feature/InventoryItemDependenciesTest.php`
---
@ -71,6 +90,9 @@ ## Phase 5: User Story 3 — Filter By Relationship Type (Priority: P2)
- [ ] T022 [US3] Add relationship-type dropdown filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php`
- [ ] T023 [US3] Parse/validate `relationship_type` and pass to query service in `app/Filament/Resources/InventoryItemResource.php`
- [ ] T024 [US3] Add UI smoke test verifying relationship filter limits edges in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T022 [US3] Add relationship-type dropdown filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T023 [US3] Parse/validate `relationship_type` and pass to query service in `app/Filament/Resources/InventoryItemResource.php`
- [x] T024 [US3] Add UI smoke test verifying relationship filter limits edges in `tests/Feature/InventoryItemDependenciesTest.php`
---
@ -82,6 +104,8 @@ ## Phase 6: User Story 4 — Zero Dependencies (Priority: P3)
- [ ] T025 [US4] Render zero-state message in `resources/views/filament/components/dependency-edges.blade.php`
- [ ] T026 [US4] Add UI test for zero-state rendering in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T025 [US4] Render zero-state message in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T026 [US4] Add UI test for zero-state rendering in `tests/Feature/InventoryItemDependenciesTest.php`
---
@ -94,6 +118,11 @@ ## Phase 7: Polish & Cross-Cutting Concerns
- [ ] T029 Update requirement-gate checkboxes in `specs/042-inventory-dependencies-graph/checklists/pr-gate.md`
- [ ] T030 Run Pint and fix formatting in `app/`, `resources/views/filament/components/`, and `tests/` (touching `app/Support/Enums/RelationshipType.php`, `resources/views/filament/components/dependency-edges.blade.php`, `tests/Feature/InventoryItemDependenciesTest.php`)
- [ ] T031 Run targeted tests: `tests/Feature/InventoryItemDependenciesTest.php`, `tests/Feature/DependencyExtractionFeatureTest.php`, `tests/Unit/DependencyExtractionServiceTest.php`
- [x] T027 [P] Align filter docs + URLs in `specs/042-inventory-dependencies-graph/quickstart.md`
- [x] T028 [P] Ensure schema reflects metadata requirements in `specs/042-inventory-dependencies-graph/contracts/dependency-edge.schema.json`
- [x] T029 Update requirement-gate checkboxes in `specs/042-inventory-dependencies-graph/checklists/pr-gate.md`
- [x] T030 Run Pint and fix formatting in `app/`, `resources/views/filament/components/`, and `tests/` (touching `app/Support/Enums/RelationshipType.php`, `resources/views/filament/components/dependency-edges.blade.php`, `tests/Feature/InventoryItemDependenciesTest.php`)
- [x] T031 Run targeted tests: `tests/Feature/InventoryItemDependenciesTest.php`, `tests/Feature/DependencyExtractionFeatureTest.php`, `tests/Unit/DependencyExtractionServiceTest.php`
---
@ -107,6 +136,12 @@ ## Phase 8: Consistency & Security Coverage (Cross-Cutting)
- [ ] T035 [P] Add auth behavior test for the inventory item dependencies view (guest blocked; authenticated tenant user allowed) in `tests/Feature/InventoryItemDependenciesTest.php`
- [ ] T036 [P] Ensure unknown/unsupported reference warnings are logged at `info` severity in `app/Services/Inventory/DependencyExtractionService.php` and add a unit test using `Log::fake()` in `tests/Unit/DependencyExtractionServiceTest.php`
- [ ] T037 Document how to verify the <2s dependency section goal (manual acceptance check) in `specs/042-inventory-dependencies-graph/quickstart.md`
- [x] T032 [P] Add test asserting inbound/outbound query ordering by `created_at DESC` + limit-only behavior in `tests/Feature/DependencyExtractionFeatureTest.php` (or `tests/Unit/DependencyExtractionServiceTest.php` if more appropriate)
- [x] T033 [US2] Implement masked/abbreviated identifier rendering when `metadata.last_known_name` is null in `resources/views/filament/components/dependency-edges.blade.php`
- [x] T034 [US2] Add UI test for masked identifier behavior in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T035 [P] Add auth behavior test for the inventory item dependencies view (guest blocked; authenticated tenant user allowed) in `tests/Feature/InventoryItemDependenciesTest.php`
- [x] T036 [P] Ensure unknown/unsupported reference warnings are logged at `info` severity in `app/Services/Inventory/DependencyExtractionService.php` and add a unit test using `Log::fake()` in `tests/Unit/DependencyExtractionServiceTest.php`
- [x] T037 Document how to verify the <2s dependency section goal (manual acceptance check) in `specs/042-inventory-dependencies-graph/quickstart.md`
---

View File

@ -1,9 +1,11 @@
<?php
use App\Models\InventoryItem;
use App\Models\InventoryLink;
use App\Models\Tenant;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\Inventory\DependencyQueryService;
use App\Services\Inventory\InventorySyncService;
class FakeGraphClientForDeps implements GraphClientInterface
@ -186,3 +188,88 @@ public function request(string $method, string $path, array $options = []): Grap
expect(InventoryLink::query()->where('tenant_id', $tenant->getKey())->count())->toBe(0);
});
it('orders inbound/outbound edges by created_at desc and applies limit-only behavior', function () {
$tenant = Tenant::factory()->create();
/** @var InventoryItem $item */
$item = InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
]);
$svc = app(DependencyQueryService::class);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => $item->external_id,
'target_type' => 'foundation_object',
'target_id' => '11111111-1111-1111-1111-111111111111',
'relationship_type' => 'assigned_to',
'created_at' => now()->subMinutes(10),
'updated_at' => now()->subMinutes(10),
]);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => $item->external_id,
'target_type' => 'foundation_object',
'target_id' => '22222222-2222-2222-2222-222222222222',
'relationship_type' => 'assigned_to',
'created_at' => now()->subMinutes(5),
'updated_at' => now()->subMinutes(5),
]);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => $item->external_id,
'target_type' => 'foundation_object',
'target_id' => '33333333-3333-3333-3333-333333333333',
'relationship_type' => 'assigned_to',
'created_at' => now()->subMinutes(1),
'updated_at' => now()->subMinutes(1),
]);
$outbound = $svc->getOutboundEdges($item, null, 2);
expect($outbound)->toHaveCount(2);
expect($outbound[0]->target_id)->toBe('33333333-3333-3333-3333-333333333333');
expect($outbound[1]->target_id)->toBe('22222222-2222-2222-2222-222222222222');
expect($outbound[0]->created_at->greaterThan($outbound[1]->created_at))->toBeTrue();
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'target_type' => 'inventory_item',
'target_id' => $item->external_id,
'relationship_type' => 'depends_on',
'created_at' => now()->subMinutes(9),
'updated_at' => now()->subMinutes(9),
]);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'target_type' => 'inventory_item',
'target_id' => $item->external_id,
'relationship_type' => 'depends_on',
'created_at' => now()->subMinutes(2),
'updated_at' => now()->subMinutes(2),
]);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
'target_type' => 'inventory_item',
'target_id' => $item->external_id,
'relationship_type' => 'depends_on',
'created_at' => now()->subMinutes(1),
'updated_at' => now()->subMinutes(1),
]);
$inbound = $svc->getInboundEdges($item, null, 2);
expect($inbound)->toHaveCount(2);
expect($inbound[0]->source_id)->toBe('cccccccc-cccc-cccc-cccc-cccccccccccc');
expect($inbound[1]->source_id)->toBe('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
expect($inbound[0]->created_at->greaterThan($inbound[1]->created_at))->toBeTrue();
});

View File

@ -145,3 +145,44 @@
->assertOk()
->assertDontSee('Other Tenant Edge');
});
it('shows masked identifier when last known name is missing', function () {
[$user, $tenant] = createUserWithTenant();
$this->actingAs($user);
/** @var InventoryItem $item */
$item = InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'external_id' => (string) Str::uuid(),
]);
InventoryLink::factory()->create([
'tenant_id' => $tenant->getKey(),
'source_type' => 'inventory_item',
'source_id' => $item->external_id,
'target_type' => 'foundation_object',
'target_id' => '12345678-1234-1234-1234-123456789012',
'relationship_type' => 'assigned_to',
'metadata' => [
'last_known_name' => null,
],
]);
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
$this->get($url)
->assertOk()
->assertSee('ID: 123456…');
});
it('blocks guest access to inventory item dependencies view', function () {
$tenant = Tenant::factory()->create();
/** @var InventoryItem $item */
$item = InventoryItem::factory()->create([
'tenant_id' => $tenant->getKey(),
'external_id' => (string) Str::uuid(),
]);
$url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant);
$this->get($url)->assertRedirect();
});