# 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`.
