# Feature Specification: Graph Contracts — LIST $expand Parity Fix

**Feature Branch**: `112-list-expand-parity`  
**Created**: 2026-02-25  
**Status**: Draft  
**Input**: Platform hardening: make “LIST” query capabilities match “GET” for explicit, allowlisted expansions. Fix Entra Admin Roles report showing deterministic “Unknown” principal names when expanded data is expected.

## Spec Scope Fields *(mandatory)*

- **Scope**: tenant
- **Primary Routes**: Entra Admin Roles report (admin interface)
- **Data Ownership**: No database schema changes. This is an integration-layer query-shape change; contracts are workspace-owned configuration, and fetched directory data is tenant-scoped.
- **RBAC**: No RBAC model changes. Existing access controls for viewing reports remain unchanged.

## Clarifications

### Session 2026-02-25

- Q: What input shape should `expand` accept on LIST query options? → A: Accept `expand` as either comma-separated string or string array (normalize internally).
- Q: When `expand` contains disallowed values, what should happen? → A: Remove disallowed values; emit diagnostic signal (warn in non-production, low-noise debug-level in production).
- Q: What should the performance guard do if callers provide too many expand values / overly long expand input? → A: Keep the first 10 allowed expands (after normalization + allowlist), drop the rest, emit diagnostic. Each token is capped to 200 characters.

## Assumptions & Dependencies

- The upstream directory API supports returning principal details for role assignments when explicitly requested.
- The contract registry remains the single source of truth for which expansions are permitted per policy type.
- The Admin Roles report already has a safe fallback for missing principal details (e.g., showing “Unknown”).

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Admin roles report shows real principal names (Priority: P1)

As an admin, I want the Admin Roles report to display the real person/service principal names for role assignments, so findings are accurate and actionable.

**Why this priority**: The current output can be deterministically wrong (“Unknown”) even when the directory system would provide the principal details, which undermines trust in the report.

**Independent Test**: Fully testable by simulating a tenant role-assignment list retrieval and verifying the report output uses the principal display name when the directory API returns it.

**Acceptance Scenarios**:

1. **Given** the directory API returns role assignments with principal details included, **When** the Admin Roles report is generated, **Then** the report displays the principal display name from the returned data (not “Unknown”).
2. **Given** the directory API returns a role assignment without principal details (true upstream edge case), **When** the report is generated, **Then** the report falls back to “Unknown” for that assignment only.

---

### User Story 2 - Engineers can request explicit LIST expansions safely (Priority: P2)

As a platform engineer, I want list queries to support the same explicit query options as single-item queries (including expansions), so services can request the data shape they need without silent gaps.

**Why this priority**: Without parity, new features can accidentally rely on expanded fields that are never requested on LIST, producing “quietly wrong” data.

**Independent Test**: Testable by inspecting the outgoing list request and confirming expansions are only included when explicitly requested and allowed by the contract.

**Acceptance Scenarios**:

1. **Given** a list request includes an explicit expand value that is allowed by the contract, **When** the outgoing request is constructed, **Then** the request includes that expand value.
2. **Given** a list request includes expand values that are not allowed by the contract, **When** the outgoing request is constructed, **Then** disallowed values are removed and none of them are sent.

---

### User Story 3 - Misuse is detectable in non-production (Priority: P3)

As a maintainer, I want visibility when a caller requests expansions that are removed by sanitization, so I can catch incorrect assumptions early.

**Why this priority**: Sanitization is necessary for safety, but silent removal can hide mistakes during development.

**Independent Test**: Testable by triggering a request with mixed allowed/disallowed expansions and asserting that a non-production warning/diagnostic record is emitted.

**Acceptance Scenarios**:

1. **Given** a request includes at least one disallowed expand value, **When** validation removes it, **Then** a non-production diagnostic signal is emitted containing the policy type and removed values.

### Edge Cases

- Expand is requested but the contract allows no expansions for that policy type → validation removes all values → no expansion is sent.
- Multiple expand values are provided (string or list) → values are trimmed, split on top-level commas (for string), de-duplicated, and normalized.
- Values that include subquery/select syntax must match an allowed value exactly (no partial matches).

## Requirements *(mandatory)*

**Constitution alignment (required):** This feature affects outbound directory API query shapes. It must maintain tenant isolation (no cross-tenant leakage), keep contract registry allowlists authoritative, and add regression tests that prevent query capability divergence.

This feature does not introduce write/change behavior and does not add long-running/queued/scheduled work.

### Functional Requirements

- **FR-112-001 — LIST supports explicit expansions**: List retrieval MUST accept an optional expand input (either comma-separated string or list of strings), normalize it (trim, split on top-level commas for string input, de-duplicate), and forward the resulting allowed expand values to the directory API as a query option. Commas inside balanced parentheses (e.g., subquery/select-style expansions) MUST NOT be treated as separators.
- **FR-112-002 — Explicit-only behavior**: Expansions MUST be sent only when explicitly provided by the caller. Default behavior remains: no expansions.
- **FR-112-003 — Contract allowlist validation**: Expand values MUST be validated against the contract allowlist for the policy type. Disallowed/invalid values MUST NOT be sent.
- **FR-112-004 — Exact-match values**: Allowed expand values are matched as exact values; subquery/select-style expansions are allowed only if the exact value is allowlisted.
- **FR-112-005 — Entra Admin Roles bugfix**: The Admin Roles report MUST explicitly request the principal expansion when listing Entra role assignments so principal display names are available when the upstream system provides them.
- **FR-112-006 — Query options are discoverable**: The supported query option shape for list retrieval (at minimum: select, expand, filter, top, platform) MUST be documented where implementers naturally discover it (e.g., developer-facing docs on the client interface).

### Non-Functional Requirements (Enterprise)

- **NFR-112-001 — Backwards compatible**: Existing call sites that do not pass expansions MUST behave exactly as before.
- **NFR-112-002 — Performance guard**: The system MUST NOT auto-apply expansions from contracts. It MUST apply a hard cap to expand input size. The cap is: at most 10 allowlisted expand tokens, and at most 200 characters per token. If the caller provides more than the cap, the system MUST keep the first 10 allowed expand values (after normalization + allowlist), drop the rest, and emit a diagnostic signal.
- **NFR-112-003 — Observability without spam**: When validation removes disallowed expand values, the system MUST emit a diagnostic signal in non-production environments. In production it MUST be low-noise (debug-level) to avoid log spam.

### Key Entities *(include if feature involves data)*

- **Directory Contract**: A per-policy-type definition of allowed query options, including expansion allowlists.
- **Query Options**: The caller-provided set of optional query inputs for list retrieval (select, expand, filter, top, platform).
- **Entra Role Assignment**: A role assignment record included in the Admin Roles report.
- **Principal**: The expanded identity object associated with a role assignment, including the display name.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-112-001**: In automated tests, when the upstream response contains a principal display name, the Admin Roles report never renders “Unknown” for those records.
- **SC-112-002**: In the 7 days after release, the share of role-assignment rows shown as “Unknown” is < 1% in normal operation, excluding upstream missing-data cases.
- **SC-112-003**: Automated tests prove that callers who do not request expansions observe no change in outbound list request shape.

### Verification (Regression Guards)

- Automated tests verify that list requests forward allowed expand values and never forward disallowed values.
- Automated tests verify that the Admin Roles report explicitly requests the principal expansion when listing role assignments.
