# Feature Specification: Foundations in Inventory (047)

**Feature Branch**: `feat/047-inventory-foundations-nodes`  
**Created**: 2026-01-10  
**Status**: Draft  

## Purpose

Make foundational Intune objects (Scope Tags, Assignment Filters, Notification Templates) first-class Inventory nodes so:
- Dependency name resolution (Spec 042.2) can resolve display names locally
- Inventory coverage can communicate both **Policies** and **Foundations**
- Sync behavior matches selection flags (`include_foundations=true`)

## Clarifications

### Session 2026-01-10

- Q: How should NFR-002 (Data minimization) be defined/tested? → A: Define it as `meta_jsonb == InventoryMetaSanitizer::sanitize(meta_jsonb)` and `json_encode(meta_jsonb)` must not contain `Bearer `.
- Q: Should “no UI Graph calls” be enforced by an automated test guard? → A: Yes — add a test that fails if any UI rendering/resolution path calls `GraphClientInterface`.
- Q: What should happen if a foundation object is not returned by Graph in a later run? → A: Do not delete InventoryItem rows; treat Inventory as “last observed” and let entries become stale (no `last_seen_*` update).
- Q: Should `include_foundations=false` hide/purge foundations in the UI? → A: No — it only controls what this run syncs and counts; foundations remain visible if they exist and may become stale via `last_seen_*`.
- Q: How should unresolved dependency targets be displayed (without Graph calls)? → A: Show `Unresolved (<id>)` so missing foundations are visible for debugging.

## Scope

### In scope (MVP)
- Sync foundational object types as InventoryItems when `include_foundations=true`
- Show foundations in Inventory UI (items list + coverage)
- Enable local resolution in 042.2 (no additional Graph calls from the UI)

### Out of scope
- Entra group inventory / group name resolution
- Additional foundation types beyond the initial list (can be extended later)
- Any Intune write paths (create/update/delete)

## Users

- Tenant Admin (primary)
- MSP Operator (read-only cross-tenant later; not required here)

## Terminology

- **Policy Nodes**: InventoryItems whose `policy_type` is in `tenantpilot.supported_policy_types`
- **Foundation Nodes**: InventoryItems whose `policy_type` is in `tenantpilot.foundation_types`
- **Edges**: Dependency relationships stored in `inventory_links` (Spec 042)

## User Scenarios & Testing

### Scenario 1: Sync policies + foundations
Given I start an inventory sync with `include_foundations=true`  
When the sync completes successfully  
Then foundation nodes (scope tags, assignment filters, templates) exist as InventoryItems for the tenant.

### Scenario 2: Resolve dependency names
Given an InventoryItem has dependencies referencing scope tags/assignment filters  
When I view the item’s dependencies  
Then the UI shows the resolved display names (local DB) instead of unresolved targets.
If a dependency target cannot be resolved locally, the UI MUST display `Unresolved (<id>)` (no Graph calls).

### Scenario 3: Inventory browsing
Given Inventory Items contain both policies and foundations  
When I filter inventory to “Foundations”  
Then I only see foundation nodes (and can search by name).

### Scenario 4: Coverage communication
When I open Inventory Coverage  
Then I can view both “Policies” and “Foundations” support matrices.

## Functional Requirements

### FR-001 Foundations types (MVP)
System MUST support syncing the following foundation policy_types as InventoryItems:
- `roleScopeTag` (Scope Tags)
- `assignmentFilter` (Assignment Filters)
- `notificationMessageTemplate` (Notification Message Templates)

Source of truth: `config('tenantpilot.foundation_types')`.

### FR-002 Selection behavior
If `include_foundations=true`, an inventory sync run MUST:
- sync selected policy types
- AND sync all foundation types from `tenantpilot.foundation_types` for the tenant

If `include_foundations=false`, foundation types MUST NOT be synced as inventory items.

`include_foundations` only controls what the run observes/upserts (and therefore run counts); it MUST NOT purge existing foundation InventoryItems or “magically” hide them in the UI.

### FR-003 InventoryItems shape
Foundation nodes MUST be stored as InventoryItems using the existing schema:
- `tenant_id`, `policy_type`, `external_id`, `display_name`, `category`, `platform`, `meta_jsonb`, `last_seen_at`, `last_seen_run_id`

Foundation nodes MUST be stored as InventoryItems and MUST have:
- `policy_type` set to the foundation type key (e.g. `roleScopeTag`, `assignmentFilter`, `notificationMessageTemplate`)
- `category` set to the literal string `Foundations` (used for UI filtering/presets)

### FR-004 Inventory Coverage UI
Coverage page MUST present:
- “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)
- searching by display name
- viewing details (existing view)

### FR-006 No extra Graph calls in UI
The UI MUST NOT perform Graph lookups for foundation name resolution. Resolution MUST come from local InventoryItems.

This MUST be enforced by an automated test that fails if any UI rendering/resolution path calls `GraphClientInterface`.

## Non-Functional Requirements

### NFR-001 Tenant isolation
All reads/writes MUST be tenant-scoped and covered by tests.

### NFR-002 Data minimization
Foundation sync MUST store only a safe subset of metadata consistent with Inventory rules:
- For any stored InventoryItem, `meta_jsonb` MUST equal `InventoryMetaSanitizer::sanitize(meta_jsonb)`.
- `json_encode(meta_jsonb)` MUST NOT contain `Bearer `.

### NFR-003 Idempotency
Re-running foundation sync MUST be idempotent (no duplicates) and update `last_seen_at`/`last_seen_run_id` deterministically.

The sync MUST NOT delete InventoryItem rows when objects are not observed in a run; absence is treated as “not observed” (e.g., permission/scope/transient failure) and becomes stale via `last_seen_at`/run evaluation.

### NFR-004 Observability
Sync run record MUST be accurate:
- counts include foundations when `include_foundations=true`
- warnings/errors are persisted on the run record as per Inventory conventions

## Success Criteria

- SC1: After a foundations-enabled sync, dependencies for scope_tag/assignment_filter render as resolved for the majority of items that reference them.
- SC2: Inventory Coverage clearly communicates what is supported for “Policies” vs “Foundations”.
- SC3: No new permissions beyond existing foundation read scopes are required for this feature.

## Related Specs

- Core Inventory: `specs/040-inventory-core/spec.md`
- Inventory UI: `specs/041-inventory-ui/spec.md`
- Dependencies Graph: `specs/042-inventory-dependencies-graph/spec.md`
- Inventory Sync Button: `specs/046-inventory-sync-button/spec.md`
