# Implementation Plan: Graph Contracts — LIST `$expand` Parity Fix **Branch**: `112-list-expand-parity` | **Date**: 2026-02-25 | **Spec**: `specs/112-list-expand-parity/spec.md` **Input**: Feature specification from `specs/112-list-expand-parity/spec.md` ## Summary Bring LIST query capability parity with GET by forwarding caller-provided, contract-allowlisted `$expand` on `GraphClientInterface::listPolicies()`. Improve `$expand` normalization (top-level comma split + trim + dedupe) and enforce safety caps. Fix the Entra Admin Roles report by explicitly requesting `expand=principal` for `entraRoleAssignments`, allowing principal display names to render correctly. ## Technical Context **Language/Version**: PHP 8.4.x **Primary Dependencies**: Laravel 12, custom Microsoft Graph integration, Filament v5 (Livewire v4 compliant) **Storage**: PostgreSQL (Sail) — no schema changes for this feature **Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`) **Target Platform**: Web application (Laravel + Filament) **Project Type**: web **Performance Goals**: Prevent pathological query option inputs (bounded `$expand`) **Constraints**: Backwards compatible; no behavior change unless `expand` is explicitly provided **Scale/Scope**: Tenant-scoped Graph reads; low fan-out but called from reports ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* PASS — no violations expected. - **Read/write separation**: This feature is read-only (query shape change only). - **Single Contract Path to Graph**: Graph calls remain through `GraphClientInterface`; allowlists remain in `config/graph_contracts.php`. - **Tenant isolation**: No cross-tenant reads; only changes outbound query parameters for already-tenant-scoped calls. - **Deterministic capabilities**: Allowlist is exact-match and test-covered; no implicit expansions. - **Ops/Observability**: No new `OperationRun` usage; diagnostics are logs only and low-noise in production. Re-check post-design: PASS. ## Project Structure ### Documentation (this feature) ```text specs/112-list-expand-parity/ ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── graph-client-listPolicies-options.schema.json └── tasks.md ``` ### Source Code (repository root) ```text app/ ├── Services/ │ ├── EntraAdminRoles/ │ │ └── EntraAdminRolesReportService.php │ └── Graph/ │ ├── GraphClientInterface.php │ ├── GraphContractRegistry.php │ ├── GraphLogger.php │ ├── GraphResponse.php │ └── MicrosoftGraphClient.php config/ └── graph_contracts.php tests/ ├── Unit/ │ ├── GraphContractRegistryTest.php │ ├── MicrosoftGraphClientListPoliciesSelectTest.php │ └── EntraAdminRolesReportServiceTest.php ``` **Structure Decision**: Single Laravel web application. No new packages/modules. ## Phase 0 — Outline & Research (complete) See `specs/112-list-expand-parity/research.md`. Key resolved clarifications / decisions: - `$expand` normalization supports string + array inputs. - String inputs split on **top-level commas only** (commas inside balanced parentheses are not separators). - Safety caps: max 10 allowlisted expand tokens; max 200 chars per token. - Diagnostics: warning in non-prod, debug in prod, structured context. ## Phase 1 — Design & Contracts (complete) Artifacts: - `specs/112-list-expand-parity/data-model.md` - `specs/112-list-expand-parity/contracts/graph-client-listPolicies-options.schema.json` - `specs/112-list-expand-parity/quickstart.md` Agent context update: - Run `.specify/scripts/bash/update-agent-context.sh copilot` after Phase 1 artifacts are current. ## Phase 2 — Implementation Plan (execute next) 1) Update `GraphContractRegistry::sanitizeQuery()` - Normalize `$expand` input: - Accept `string|string[]`. - If string: split on **top-level commas only** (ignore commas inside balanced parentheses); trim whitespace. - De-dupe tokens; drop empty. - Enforce `maxTokenLen=200` (drop tokens exceeding the cap). - Filter via `allowed_expand` exact-match allowlist (no partial matching). - Enforce `maxItems=10` after allowlist (keep first N allowed). - Emit diagnostics (warn in non-prod, debug in prod) when tokens are removed/truncated. 2) Update `MicrosoftGraphClient::listPolicies()` - Accept `expand` in `$options` and forward it into `$queryInput` as `'$expand'`. - Preserve existing behavior when `expand` is not provided. 3) Fix Entra Admin Roles report - In `EntraAdminRolesReportService::fetchRoleAssignments()`, pass `expand => 'principal'` alongside resolved tenant graph options. - Keep the current fallback behavior (`Unknown`) for true upstream missing-data cases. 4) Improve discoverability of list query options - Expand PHPDoc on `GraphClientInterface::listPolicies()` (and align with `getPolicy()` docs) to describe supported keys and the `expand` input shape. 5) Add regression tests (Pest) - `GraphContractRegistryTest`: add cases for top-level comma splitting (including commas inside parentheses), dedupe, cap behavior, and non-prod diagnostic log emission. - `MicrosoftGraphClientListPoliciesSelectTest`: add assertions that an allowlisted LIST `$expand` is present in the outbound query. - `EntraAdminRolesReportServiceTest`: ensure principal display names are used when returned, and ensure the graph client is invoked with `expand=principal`. 6) Validate - `vendor/bin/sail bin pint --dirty --format agent` - Run the focused tests listed in `specs/112-list-expand-parity/quickstart.md`.