TenantAtlas/specs/261-provider-missing-policy-visibility/spec.md
ahmido feeaadd5ad feat: add provider-missing policy visibility and restore continuity (#316)
## Summary
- separate provider-missing policy presence from local ignore semantics by introducing `missing_from_provider_at`
- update policy, backup, and restore surfaces so current-state capture stays honest while historical restore continuity remains available
- add focused sync, Filament, backup, restore, localization, and badge coverage for the new provider-missing behavior

## Scope
- policy sync and model truth
- policy resource visibility, badges, labels, and action gating
- backup/export eligibility and restore continuity messaging
- spec 261 artifacts and focused tests

## Validation
- feature-specific Pest coverage is included in the branch
- validation was not re-run as part of this commit/push/PR handoff

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #316
2026-05-01 20:18:27 +00:00

313 lines
38 KiB
Markdown

# Feature Specification: Provider-Missing Policy Visibility & Restore Continuity v1
**Feature Branch**: `261-provider-missing-policy-visibility`
**Created**: 2026-05-01
**Status**: Ready
**Input**: User description: "Prepare the next repo-based follow-up under the lifecycle governance candidate: separate provider-missing policy presence from local ignore/delete semantics, keep restore continuity truthful, and avoid SoftDeletes, purge, or broad lifecycle-taxonomy work."
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: TenantPilot currently collapses at least two different truths into `policies.ignored_at`: intentional local suppression and sync-driven provider absence or scope loss. That makes policy visibility, backup eligibility, and restore continuity hard to trust.
- **Today's failure**: Operators can see a policy as `Ignored` even when it was not intentionally suppressed, sync can clear `ignored_at` simply because the provider returns the object again, and backup or restore surfaces cannot distinguish a locally ignored policy from a policy that is only missing from the current supported provider result set.
- **User-visible improvement**: Operators can tell whether a policy is intentionally ignored locally or missing from the current supported provider-backed view, and historical backup/restore continuity remains available without masquerading as local deletion.
- **Smallest enterprise-capable version**: Add one bounded provider-presence field to policy truth, reserve `ignored_at` for explicit local suppression only, surface the derived provider-missing state on policy and restore-adjacent surfaces, block current-state backup/export actions for missing policies, and clear provider-missing state when the provider object reappears.
- **Explicit non-goals**: No full workspace/tenant lifecycle taxonomy, no `SoftDeletes` rollout for policies, no purge or retention engine, no new policy hard-delete flow, no multi-object lifecycle framework, no cross-tenant workflow, no customer-facing portal, no billing or workspace-closure work, and no broad managed-object lifecycle engine.
- **Permanent complexity imported**: One new tenant-owned provider-presence timestamp on `policies`, one derived provider-missing state, focused audit semantics for missing/reappeared transitions, and narrow feature/unit coverage across sync, policy, backup, and restore seams.
- **Why now**: The active P0-P2 prep queue is already represented by full spec packages through Specs 251-260. The broader `Workspace, Tenant & Managed Object Lifecycle Governance v1` candidate is explicitly strategic and not ready, but that same candidate documents `Provider-Missing Managed Object Truth v1` and the narrower fallback `Provider-Missing Policy Visibility & Restore Continuity v1` if provider-missing policy behavior becomes an immediate product bug. Current repo truth already shows that bug: `ignored_at` is reused by local policy deletion, sync reclassification, provider result filtering, backup eligibility, and sync revival tests.
- **Why not local**: A page-local badge or one-off restore exemption would still leave the canonical truth split across `Policy`, `PolicySyncService`, policy actions, backup selection, restore selection, and audit behavior. This slice has to converge those shared seams or the product will keep saying two incompatible things about the same policy.
- **Approval class**: Core Enterprise
- **Red flags triggered**: New lifecycle-like field, cross-surface semantics, and a smaller follow-up chosen ahead of the full lifecycle taxonomy. Defense: the slice stays strictly policy-only, adds only one provider-presence field instead of a general lifecycle framework, keeps workspace/tenant closure and retention out of scope, and is justified by a current runtime truth bug rather than future-proofing.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 1 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: tenant
- **Primary Routes**:
- `/admin/t/{tenant}/policies`
- existing tenant-scoped policy detail surfaces reached from the policies resource
- existing tenant-scoped backup creation and backup-set policy-picker surfaces that choose current policies for snapshotting
- existing tenant-scoped restore-run creation surfaces that select historical backup items for restore continuity
- **Data Ownership**:
- `Policy` remains tenant-owned truth and keeps required `workspace_id` and `tenant_id` anchors.
- This slice adds one tenant-owned provider-observation field, `missing_from_provider_at`, on `policies`. It records that the policy is no longer observed in the current supported provider-backed result set for its canonical policy type.
- `ignored_at` remains tenant-owned local suppression truth only.
- `PolicyVersion`, `BackupSet`, `BackupItem`, and existing restore-run truth remain unchanged and continue to own historical snapshot and recovery evidence.
- **RBAC**:
- Existing workspace membership and tenant entitlement remain the first isolation boundary.
- Existing policy, backup, and restore capabilities remain authoritative; this slice adds no new role or capability family.
- Non-members and out-of-scope tenant access remain deny-as-not-found.
- Members missing the existing manage, backup, or restore capability continue to receive forbidden semantics on the affected actions.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: status messaging, table filters and badges, row/header actions, backup eligibility messaging, restore-item selection descriptions, audit labels
- **Systems touched**: `Policy`, `PolicySyncService`, policy list/detail actions, current backup creation and backup-set item selection, restore-item selection, and the existing audit logger path
- **Existing pattern(s) to extend**: existing policy visibility filter/action family, existing restore-item selection/description builders, existing backup eligibility rules, and existing audit logging
- **Shared contract / presenter / builder / renderer to reuse**: `PolicyResource`, `RestoreRunResource` restore-item option builders, `BackupService`, and the current workspace/tenant audit logger path remain the shared seams to extend
- **Why the existing shared path is sufficient or insufficient**: the existing policy, backup, and restore helpers already centralize the relevant queries and action affordances. What is missing is one truthful provider-presence distinction inside those shared seams, not a new ghost-policy service or lifecycle framework.
- **Allowed deviation and why**: none
- **Consistency impact**: `Ignored`, `Provider missing`, backup blocked messaging, restore continuity notes, and reappearance wording must mean the same thing across policy list, policy detail, backup selection, restore selection, and sync-driven audit entries.
- **Review focus**: reviewers must verify that sync and provider reclassification stop writing `ignored_at`, that missing policies remain visible and historically restorable, and that no surface keeps using `ignored_at` as a catch-all proxy for provider absence.
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
- **Touches OperationRun start/completion/link UX?**: yes
- **Shared OperationRun UX contract/layer reused**: existing policy `Sync` and policy export/backup queued-start UX remain authoritative when an action is still allowed
- **Delegated start/completion UX behaviors**: when a provider-missing policy still allows an explicit sync retry, the current queued toast and `Open operation` link behavior remain unchanged. When a missing policy is not eligible for current-state backup/export, the local action is blocked before any run is created.
- **Local surface-owned behavior that remains**: policy, backup, and restore surfaces only explain provider-missing continuity and action eligibility. They do not create a new run type or a new recovery workflow.
- **Queued DB-notification policy**: unchanged
- **Terminal notification path**: unchanged for existing sync and backup runs
- **Exception required?**: none
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
- **Shared provider/platform boundary touched?**: yes
- **Boundary classification**: mixed
- **Seams affected**: policy persistence truth, sync result interpretation, provider-presence vocabulary, restore continuity wording, and local suppression semantics
- **Neutral platform terms preserved or introduced**: provider missing, local ignore, current provider-backed state, historical restore continuity, supported provider result set
- **Provider-specific semantics retained and why**: Microsoft Graph list results and provider-specific subtype filtering remain provider-owned evidence inside the sync layer. They only determine whether the policy is currently observed in the supported provider result set.
- **Why this does not deepen provider coupling accidentally**: the persisted field records neutral provider-presence truth on the local policy record. Provider-specific endpoint or OData logic stays inside the existing sync layer and does not leak into operator-facing labels beyond the neutral `Provider missing` meaning.
- **Follow-up path**: the broader lifecycle taxonomy and any future provider-deleted distinction remain separate follow-up specs
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Tenant policies list and detail | yes | Native Filament + shared policy primitives | filters, badges, row actions, detail messaging | table, row action, detail, helper copy | no | Keeps current policy resource as the single current-state policy surface |
| Tenant backup creation and backup-set policy selection | yes | Native Filament + shared backup primitives | action gating, policy picker descriptions | action modal, picker, validation state | no | Missing policies stop pretending they can produce a fresh provider-backed snapshot |
| Tenant restore-run item selection | yes | Native Filament + shared restore primitives | restore continuity messaging, item descriptions | wizard, grouped options, descriptions | no | Historical backup items remain visible without being conflated with ignored policies |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Tenant policies list and detail | Primary Decision Surface | Decide whether the policy is actively managed, intentionally ignored, or missing from the current provider-backed view | current visibility state, display name, type, last sync, and dominant next action | historical versions, backup history, and related restore context after opening detail | Primary because operators decide what to do with a policy here before entering backup or restore flows | Keeps current-state policy truth on the policy surface instead of outsourcing it to restore screens | Removes guessing whether `Ignored` means local suppression or provider absence |
| Tenant backup creation and backup-set policy selection | Secondary Context Surface | Decide whether a current policy can still be snapshotted now | eligible versus blocked policies and the provider-missing reason when blocked | existing backup history and related policy detail | Secondary because backup selection follows the current policy decision rather than replacing it | Keeps backup selection focused on current provider-backed capture eligibility | Prevents avoidable backup failures and manual policy-by-policy checking |
| Tenant restore-run item selection | Secondary Recovery Surface | Decide whether historical backup content should be restored even when the current provider object is missing | grouped historical items, provider-missing continuity note, and restore mode | linked policy detail, backup metadata, and later restore preview | Secondary because it is only relevant once the operator is already in a recovery flow | Aligns restore continuity with historical backup truth rather than current provider presence | Removes the false impression that a missing policy cannot be restored from history |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Tenant policies list and detail | operator-MSP, support-platform | provider-missing versus ignored state, last observed sync timing, and safe next action | provider result-set interpretation, subtype mismatch context, and historical versions | raw provider payloads and low-level sync traces remain secondary | `Sync` or `Restore local visibility` depending on state | raw provider details and support diagnostics remain hidden/gated | Policy surfaces own current-state meaning; restore surfaces only add historical continuity |
| Tenant backup creation and backup-set policy selection | operator-MSP | current capture eligibility and the provider-missing reason when blocked | last observed timing and existing backup availability | raw provider request detail remains hidden | `Export to Backup` when eligible | detailed provider diagnostics stay off picker rows | Backup surfaces state only capture eligibility; they do not restate full policy lifecycle semantics |
| Tenant restore-run item selection | operator-MSP | historical restore eligibility plus provider-missing continuity note | backup quality, version metadata, and restore mode | raw payloads stay secondary in later restore detail | `Continue restore` | low-level provider diagnostics stay hidden | Restore surfaces own historical continuity only and do not replace current-state policy truth |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant policies list and detail | List / Table / Detail | CRUD / List-first Resource | Open a policy and decide whether to sync, ignore, or restore local visibility | full-row click to policy detail | required | grouped under `More` where already used | existing confirmation-protected ignore/restore actions only | `/admin/t/{tenant}/policies` | existing tenant-scoped policy detail route | tenant context, visibility state, policy type, last sync | Policies / Policy | whether the policy is active, ignored locally, or missing from provider | none |
| Tenant backup creation and backup-set policy selection | Contextual action family | Action modal / picker | Create or extend a backup with eligible current policies | explicit action modal and picker | forbidden | existing helper text and picker descriptions only | no new destructive action | existing tenant backup set and backup creation routes | existing backup-set detail route | tenant context, provider-backed eligibility, backup quality | Backup selection / Policy snapshot | whether a policy can still be snapshotted now | none |
| Tenant restore-run item selection | Wizard / Selection | Recovery selector | Select historical items to restore | grouped checkbox or selection list | forbidden | descriptions and later preview only | no destructive action on the selection step | existing tenant restore-run collection route | existing restore-run creation and preview routes | tenant context, backup set, restore mode, historical continuity | Restore items / Backup item | whether a historical item remains selectable even when current provider presence is missing | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Tenant policies list and detail | Tenant operator | Decide what the current policy state means and what action is still safe | List/detail | Is this policy intentionally ignored, currently missing from provider, or still active? | visibility state, type, platform, last sync, and safe next action | subtype mismatch context, history, and provider-backed evidence | local suppression, provider presence, last observed state | TenantPilot only until an allowed sync is started | Sync, Ignore, Restore | Ignore |
| Tenant backup creation and backup-set policy selection | Tenant operator | Decide whether a current policy can be added to a fresh snapshot | Action modal/picker | Can I still create a current backup of this policy right now? | eligible/blocked state, provider-missing reason, and existing backup availability | deeper history and provider diagnostics only when needed | provider presence, backup quality, restore mode | TenantPilot only until current backup export begins | Export to Backup, Add to Backup Set | none |
| Tenant restore-run item selection | Tenant operator | Decide whether historical backup data should be restored despite current provider absence | Wizard selector | Can I still restore this historical policy item even if the live object is missing now? | grouped item labels, backup quality, provider-missing continuity note | version metadata and later restore preview | historical continuity, restore mode, provider presence | TenantPilot only in later restore flow | Continue restore | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: yes - one new tenant-owned provider-presence timestamp on `policies`
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: no
- **New enum/state/reason family?**: yes - one derived provider-presence state family built from `ignored_at` plus `missing_from_provider_at`
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: current policy, backup, and restore workflows cannot truthfully say whether a policy was intentionally suppressed or simply dropped out of the supported provider result set.
- **Existing structure is insufficient because**: `ignored_at` already carries local suppression and sync-driven absence semantics, so any local fix would keep the shared query and action seams contradictory.
- **Narrowest correct implementation**: add exactly one provider-presence field on policy truth, reserve `ignored_at` for user suppression, and update the existing shared sync, policy, backup, and restore seams to consume those two truths consistently.
- **Ownership cost**: one new migration, small query/copy adjustments, audit entries for missing/reappeared transitions, and focused tests.
- **Alternative intentionally rejected**: the full lifecycle taxonomy was rejected as too broad for this current bug. A local badge-only workaround was rejected because it would not fix sync, backup, restore, and audit semantics together.
- **Release truth**: current-release truth driven by a repo-visible runtime ambiguity, not future-release platform preparation.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Unit
- **Validation lane(s)**: fast-feedback, confidence
- **Why this classification and these lanes are sufficient**: the business truth is server-side and sits in policy sync, policy UI, backup eligibility, and restore continuity. Focused feature coverage plus a few narrow unit assertions on derived provider-presence semantics are enough to prove the slice.
- **New or expanded test families**: expand the existing policy sync, policy action, backup selection, and restore item selection families; keep one small unit family for provider-presence semantics if the implementation adds a bounded helper
- **Fixture / helper cost impact**: low to moderate. Reuse existing tenant, policy, backup set, backup item, and restore-run fixtures. Avoid provider-wide browser harnesses or new heavy support fixtures.
- **Heavy-family visibility / justification**: none
- **Special surface test profile**: standard-native-filament, shared-detail-family
- **Standard-native relief or required special coverage**: standard Filament feature coverage is sufficient for policy, backup, and restore surfaces. No browser proof is required.
- **Reviewer handoff**: reviewers must confirm that `ignored_at` is no longer written/cleared by sync-driven provider absence logic, that missing policies stay historically restorable, that current backup actions do not pretend missing policies can snapshot live state, and that tenant isolation plus `404` versus `403` semantics stay unchanged.
- **Budget / baseline / trend impact**: low feature-local increase only
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Jobs/PolicySyncProviderMissingSemanticsTest.php tests/Feature/Jobs/PolicySyncIgnoredRevivalTest.php tests/Feature/Jobs/AppProtectionPolicySyncFilteringTest.php tests/Feature/PolicySyncServiceTest.php tests/Feature/PolicySyncEnrollmentConfigurationTypeCollisionTest.php tests/Feature/PolicySyncStartSurfaceTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PolicyProviderMissingUiTest.php tests/Feature/PolicyGeneralViewTest.php tests/Feature/BulkDeletePoliciesTest.php tests/Feature/BulkUnignorePoliciesTest.php tests/Unit/Badges/PolicyBadgesTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/BulkExportToBackupTest.php tests/Feature/Filament/BackupCreationTest.php tests/Feature/Filament/BackupSetPolicyPickerTableTest.php tests/Feature/Filament/RestoreItemSelectionTest.php tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php`
## Scope Boundaries
### In Scope
- one new `missing_from_provider_at` field on `policies`
- reserving `ignored_at` for explicit local suppression only
- sync semantics for policies that disappear from the supported provider-backed result set for their canonical policy type
- policy list/detail visibility that distinguishes local ignore from provider-missing state
- current backup eligibility changes so provider-missing policies are not treated as live snapshot candidates
- restore continuity messaging and selection behavior for historical backup items tied to provider-missing policies
- explicit auditability for provider-missing and provider-reappeared transitions
### Non-Goals
- full managed-object lifecycle taxonomy
- workspace or tenant suspension, closure, deletion, retention, or purge
- `SoftDeletes` or local deletion semantics for policies
- a new `PolicyLifecycleService`, `GhostPolicyRegistry`, or other broad framework
- multi-object rollout of provider-presence semantics beyond policies
- explicit `provider_deleted_at` semantics when the provider cannot yet prove hard deletion distinctly from absence in the supported result set
- customer-facing read-only surfaces, cross-tenant portfolio workflows, or billing-state overlays
## Dependencies
- existing policy sync and canonical policy-type reclassification logic in `PolicySyncService`
- existing policy list/detail action surfaces in `PolicyResource`
- existing backup selection and backup-set creation flows in `BackupService` and related Filament surfaces
- existing restore-item option builders in `RestoreRunResource`
- existing audit logging infrastructure
- explicit sequencing awareness with Spec 251, which owns workspace commercial lifecycle and must remain separate from this policy-level provider-presence slice
## Assumptions
- the current provider integration can truthfully detect when a previously seen policy is no longer present in the supported provider-backed result set for its canonical type, even if it cannot always distinguish hard deletion from provider-side scope change
- `last_synced_at` continues to mean last successful provider observation for the current local policy row; it does not become a generic heartbeat field for missing policies
- local policy suppression must remain explicit and reversible by operator action rather than being cleared automatically by a later sync result
- historical `PolicyVersion` and `BackupItem` records already provide enough restore continuity evidence that no new recovery persistence family is needed
- Filament v5 on Livewire v4 remains the UI substrate, and no panel/provider registration change is required
## Risks
- repo code and tests currently assume sync can revive ignored policies, so the change may reveal hidden dependencies on the old conflated meaning
- some provider subtype or canonical-type transitions may still need careful wording so `Provider missing` does not overstate certainty about hard deletion
- backup and restore surfaces could drift again if one seam continues to query only `ignored_at` after the new provider-presence field lands
- policy actions could become misleading if current-state backup is blocked without also surfacing the historical restore path clearly
## Candidate Selection Rationale
- **Selected candidate**: Provider-Missing Policy Visibility & Restore Continuity v1
- **Source locations**:
- `docs/product/spec-candidates.md` under `Workspace, Tenant & Managed Object Lifecycle Governance v1`, especially the documented follow-up `Provider-Missing Managed Object Truth v1` and the explicit narrower fallback `Provider-Missing Policy Visibility & Restore Continuity v1`
- `docs/product/roadmap.md` lifecycle-governance boundary that rejects a broad ghost-policy patch before the full taxonomy is agreed
- current repo truth in `apps/platform/app/Models/Policy.php`, `apps/platform/app/Services/Intune/PolicySyncService.php`, `apps/platform/app/Services/Intune/BackupService.php`, `apps/platform/app/Filament/Resources/PolicyResource.php`, `apps/platform/app/Filament/Resources/RestoreRunResource.php`, and the related policy sync/restore tests
- **Why selected**: the active prep queue from P0 through P2 is already represented by Specs 251-260. The broader lifecycle-governance candidate remains too strategic for immediate prep, but the repo already documents a narrower provider-missing follow-up and current code clearly shows a live `ignored_at` conflation bug that affects policy, backup, and restore truth.
- **Why this is the smallest viable implementation slice**: it touches only policy provider-presence truth and the directly dependent backup/restore seams, introduces one bounded field, and keeps workspace lifecycle, purge, retention, and multi-object rollout deferred.
- **Intentional narrowing from source candidate**: this slice does not define a general lifecycle taxonomy. It only separates provider-missing policy presence from local suppression and preserves restore continuity where historical backups already exist.
- **Why close alternatives are deferred**:
- the full `Workspace, Tenant & Managed Object Lifecycle Governance v1` candidate remains too broad and explicitly strategic
- `Workspace & Tenant Closure Lifecycle v1` is a later follow-up once the broader lifecycle taxonomy is ready
- `Cross-Tenant Compare and Promotion v1` already has refreshed spec work in Spec 043 and is not the next open prep target here
- Spec 251 already owns workspace commercial state and would be a duplicate vehicle for broader lifecycle semantics
## Follow-up Candidates
- the full `Workspace, Tenant & Managed Object Lifecycle Governance v1` taxonomy
- explicit `provider_deleted_at` versus `missing_from_provider_at` distinction if the provider surface later proves hard deletion truth cleanly
- multi-object rollout of provider-presence semantics beyond policies
- export-before-delete and retention governance after the shared lifecycle taxonomy is agreed
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Tell provider missing apart from local ignore (Priority: P1)
As a tenant operator, I want policy surfaces to show whether a policy is intentionally ignored locally or simply missing from the current supported provider-backed result set so I can choose the right next action.
**Why this priority**: This is the core trust fix. If operators still have to guess what `Ignored` means, the slice has failed.
**Independent Test**: Can be fully tested by syncing policies into missing and locally ignored states, then opening the existing policy resource and verifying the two states render differently and keep their own actions.
**Acceptance Scenarios**:
1. **Given** a previously observed policy is no longer returned in the supported provider-backed result set, **When** an entitled operator opens the policy list/detail, **Then** the policy renders as provider-missing rather than locally ignored.
2. **Given** a policy was intentionally ignored by the operator, **When** a later sync still sees the same provider object, **Then** the sync does not clear the local ignore state automatically.
---
### User Story 2 - Keep restore continuity honest (Priority: P1)
As a tenant operator, I want missing policies to remain historically restorable from existing backups without being treated as active live policies for new snapshots.
**Why this priority**: The product already has backup and restore truth. The slice must preserve that value while stopping misleading current-state actions.
**Independent Test**: Can be fully tested by marking a policy provider-missing, then verifying that current backup selection blocks it while historical backup items for that policy remain selectable in restore creation.
**Acceptance Scenarios**:
1. **Given** a policy is provider-missing but has existing backup history, **When** the operator enters the restore-item selection flow, **Then** the historical backup items remain selectable with explicit provider-missing continuity messaging.
2. **Given** a policy is provider-missing, **When** the operator attempts a current-state backup/export flow, **Then** the product keeps that policy visible but blocked with an explicit provider-missing reason instead of pretending it is a normal active policy.
---
### User Story 3 - Record and clear provider-missing transitions predictably (Priority: P2)
As an operator or support reviewer, I want provider-missing and reappeared transitions to be auditable so later policy history makes sense without new lifecycle frameworks.
**Why this priority**: The state is only trustworthy if later reviewers can see why it changed and when it returned.
**Independent Test**: Can be fully tested by running sync once with the policy missing and once with the policy reappearing, then checking the local policy state and audit evidence.
**Acceptance Scenarios**:
1. **Given** a previously observed policy disappears from the supported provider result set, **When** sync completes, **Then** the policy remains local, receives `missing_from_provider_at`, and records an audit entry for the provider-missing transition.
2. **Given** a provider-missing policy appears again in a later sync, **When** sync completes, **Then** `missing_from_provider_at` clears, local ignore state remains untouched if it exists, and an audit entry records the reappearance.
### Edge Cases
- A policy may be locally ignored and later also become provider-missing. Local suppression remains the primary local control, while provider-missing stays secondary continuity context.
- A combined-state policy appears in both the `ignored` and `provider_missing` filter views so operators can reach it from either workflow.
- A policy may move between supported canonical policy types. Where the canonical type can still be resolved, the record should be reclassified instead of marked provider-missing.
- A provider subtype can fall out of the current supported result set without proving hard deletion. The slice must not overstate that as local deletion.
- A backup item may reference a policy row that no longer exists locally. Historical restore continuity must keep following existing `BackupItem` truth rather than depending on a current policy row.
- Wrong-tenant access or missing capability must keep the current `404` versus `403` semantics even when provider-missing copy is added.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature touches existing sync and backup behavior but introduces no new provider call family and no new `OperationRun` type. It must keep tenant isolation, current sync observability, and auditability intact while avoiding any new purge or deletion behavior.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The slice introduces one new persisted provider-presence field and one derived state family because the current product truth is already wrong without it. A narrower UI-only workaround is insufficient because shared sync, backup, restore, and audit seams would stay contradictory. No new registry, resolver, strategy system, or lifecycle engine is justified.
**Constitution alignment (XCUT-001):** Policy visibility, backup eligibility, restore continuity, and audit copy are cross-cutting interaction classes already served by shared paths. This slice must land in those shared seams rather than through page-local conditionals.
**Constitution alignment (RBAC-UX):** Membership and tenant entitlement remain the isolation boundary. Non-members stay `404`, members lacking surviving manage/restore capability stay `403`, and provider-missing business-state messaging must not be used as an authorization substitute.
**Constitution alignment (PROV-001):** Provider-specific detection stays inside the provider-backed sync layer. The persisted and operator-facing truth remains neutral provider-presence language rather than Microsoft endpoint semantics.
**Constitution alignment (TEST-GOV-001):** Proof stays in focused feature and unit lanes. No browser or heavy-governance family is needed for this slice.
### Functional Requirements
- **FR-261-001**: The system MUST reserve `policies.ignored_at` for explicit local suppression only and MUST NOT set or clear it as a side effect of provider absence or provider result-set loss.
- **FR-261-002**: The system MUST persist provider-missing observation on the policy record through `missing_from_provider_at` when a previously observed policy is no longer present in the current supported provider-backed result set for its canonical policy type.
- **FR-261-003**: A provider-missing policy MUST remain visible in tenant policy surfaces as a local historical record and MUST NOT be treated as hard-deleted or soft-deleted.
- **FR-261-004**: Policy list/detail surfaces MUST present provider-missing state distinctly from local ignore state, with truthful action guidance for each state.
- **FR-261-005**: Current backup/export flows that depend on live provider-backed policy state MUST keep provider-missing policies visible but blocked with explicit reason text instead of treating them as normal active policies.
- **FR-261-006**: Existing restore flows backed by `BackupItem` history MUST continue to offer historical restore continuity for provider-missing policies when the historical item is otherwise eligible.
- **FR-261-007**: When sync later observes a provider-missing policy again, the system MUST clear `missing_from_provider_at` and keep any local ignore state unchanged.
- **FR-261-008**: Where sync can reclassify the same external object into another supported canonical policy type, it MUST reclassify the local row rather than marking the policy ignored.
- **FR-261-009**: The system MUST emit explicit audit evidence when a policy first becomes provider-missing and when it later reappears.
- **FR-261-010**: The slice MUST NOT introduce `SoftDeletes`, a new deletion action, a purge path, or a general managed-object lifecycle framework.
- **FR-261-011**: When both `ignored_at` and `missing_from_provider_at` are set, the policy MUST remain discoverable from both ignored and provider-missing filter views, and current backup/export surfaces MUST use `provider_missing` as the primary blocked reason while retaining local ignore as secondary context.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Zero sync paths continue to write or clear `ignored_at` for provider-missing semantics after the slice lands.
- **SC-002**: Policy surfaces and visibility filters show distinct provider-missing versus locally ignored states, including the combined-state rule, for 100% of covered validation scenarios.
- **SC-003**: Provider-missing policies are excluded from current backup eligibility while historical restore continuity remains available in 100% of covered restore-selection scenarios.
- **SC-004**: Reappearance of a provider-missing policy clears the provider-missing flag without automatically restoring local ignore state in 100% of covered sync scenarios.