26 KiB
Implementation Plan: Provider-Missing Policy Visibility & Restore Continuity v1
Branch: 261-provider-missing-policy-visibility | Date: 2026-05-01 | Spec: spec.md
Input: Feature specification from spec.md
Summary
Prepare one bounded policy-truth correction that separates provider-missing presence from intentional local suppression without opening the broader workspace or tenant lifecycle program. The narrow implementation path is to add missing_from_provider_at on policies, reserve ignored_at for explicit user suppression only, stop sync and type-filter logic from writing ignored_at for provider absence, keep missing policies visible as historical local truth, block current-state backup/export actions for missing policies, and preserve restore continuity for historical BackupItem records.
Repo truth already exposes the exact seams this slice needs: ../../apps/platform/app/Models/Policy.php has ignored_at but no provider-presence field; ../../apps/platform/app/Services/Intune/PolicySyncService.php currently clears ignored_at on updateOrCreate() and marks reclassified or filtered records ignored; ../../apps/platform/app/Filament/Resources/PolicyResource.php only distinguishes active versus ignored; ../../apps/platform/app/Services/Intune/BackupService.php selects backup candidates with whereNull('ignored_at'); ../../apps/platform/app/Filament/Resources/RestoreRunResource.php filters restore-option continuity through the same ignored check; and current tests such as ../../apps/platform/tests/Feature/Jobs/PolicySyncIgnoredRevivalTest.php codify the conflated behavior that this spec intentionally corrects.
V1 therefore stays narrow: no new panel or provider, no global-search change, no new asset registration, no SoftDeletes, no lifecycle engine, no provider-deleted distinction, no purge flow, no multi-object rollout, and no cross-tenant workflow. The work is a policy-only truth correction that reuses the existing policy, backup, restore, audit, and sync seams.
Technical Context
Language/Version: PHP 8.4, Laravel 12
Primary Dependencies: Filament v5, Livewire v4, Pest v4, existing PolicySyncService, BackupService, PolicyResource, RestoreRunResource, policy bulk jobs, audit infrastructure, and policy badge helpers
Storage: PostgreSQL via existing policies, policy_versions, backup_sets, backup_items, restore_runs, operation_runs, and audit_logs; one nullable timestamp is planned on policies
Testing: Pest v4 feature and focused unit coverage
Validation Lanes: fast-feedback, confidence
Target Platform: Laravel monolith in apps/platform, existing admin and tenant-scoped Filament surfaces only
Project Type: Web application (Laravel monolith with Filament resources and pages)
Performance Goals: keep provider-missing detection inside existing sync result processing, avoid extra Graph calls, keep policy list and restore option queries eager-load safe, and avoid widening queue or asset cost
Constraints: no SoftDeletes, no provider_deleted_at, no new lifecycle registry/service, no panel/provider changes, no asset strategy change, no new globally searchable resource, and no customer-facing portal work
Scale/Scope: 1 model/migration, 1 sync service cluster, 1 policy resource, 1 backup service seam, 1 restore selection seam, bounded audit updates, and targeted policy/backup/restore tests
Likely Affected Repo Surfaces
- ../../apps/platform/app/Models/Policy.php for
missing_from_provider_at, casts, scopes, and derived provider-presence helpers. - ../../apps/platform/database/migrations for one migration adding
missing_from_provider_attopolicies. - ../../apps/platform/app/Services/Intune/PolicySyncService.php for provider-missing detection, reappearance clearing, subtype filtering, and reclassification semantics.
- ../../apps/platform/app/Jobs/Operations/PolicyBulkDeleteWorkerJob.php and existing unignore flows as the retained local-suppression path that must stay on
ignored_atonly. - ../../apps/platform/app/Filament/Resources/PolicyResource.php plus its
Pages/ListPolicies.phpandPages/ViewPolicy.phpsurfaces for badges, filters, helper copy, and action availability. - ../../apps/platform/app/Services/Intune/BackupService.php and current backup/export actions that choose policies from current local truth.
- ../../apps/platform/app/Filament/Resources/BackupSetResource.php or related picker helpers if the current backup-set policy picker needs provider-missing reason text.
- ../../apps/platform/app/Filament/Resources/RestoreRunResource.php for restore item option continuity and provider-missing messaging.
- ../../apps/platform/app/Support/Audit/AuditActionId.php and the existing audit logger path for provider-missing and reappeared events if no existing audit action is sufficient.
- ../../apps/platform/tests/Feature/Jobs/PolicySyncIgnoredRevivalTest.php, ../../apps/platform/tests/Feature/PolicySyncServiceTest.php, ../../apps/platform/tests/Feature/BulkDeletePoliciesTest.php, ../../apps/platform/tests/Feature/BulkUnignorePoliciesTest.php, ../../apps/platform/tests/Feature/BulkExportToBackupTest.php, ../../apps/platform/tests/Feature/Filament/BackupCreationTest.php, ../../apps/platform/tests/Feature/Filament/BackupSetPolicyPickerTableTest.php, ../../apps/platform/tests/Feature/Filament/RestoreItemSelectionTest.php, ../../apps/platform/tests/Feature/PolicySyncStartSurfaceTest.php, and ../../apps/platform/tests/Unit/Badges/PolicyBadgesTest.php for bounded regression proof.
Policy Presence / Sync Fit
- Treat provider presence as a property of the local policy row, not a second entity or lifecycle engine. The current narrow truth is
ignored_atplusmissing_from_provider_at. - The derived state family stays bounded:
- active:
ignored_at = nullandmissing_from_provider_at = null - ignored locally:
ignored_at != nullandmissing_from_provider_at = null - provider missing:
ignored_at = nullandmissing_from_provider_at != null - ignored locally + provider missing: both timestamps present, with local suppression as the primary local-control meaning
- active:
- Policy list filtering stays deterministic: the combined state belongs to both the
ignoredandprovider_missingfilter views so operators can reach it from either workflow. - ../../apps/platform/app/Services/Intune/PolicySyncService.php must stop blindly resetting
ignored_atonupdateOrCreate(). - Any sync path that currently uses
ignored_atfor subtype filtering or reclassification must instead either reclassify the row to a supported canonical type or markmissing_from_provider_atwhen the object falls out of the supported provider-backed result set. - Reappearance must clear
missing_from_provider_atwithout automatically clearingignored_at. - Existing local delete/restore semantics in
ignore()andunignore()remain unchanged and stay rooted inignored_at.
Backup / Restore Continuity Fit
- ../../apps/platform/app/Services/Intune/BackupService.php and any current backup/export picker that selects policies for fresh capture must keep provider-missing policies visible but blocked from current-state capture.
- When both
ignored_atandmissing_from_provider_atare present, current backup/export usesprovider_missingas the primary blocked reason because the product cannot truthfully claim fresh provider-backed capture is possible. - Existing historical
BackupItemtruth remains authoritative for restore continuity. The system should not require a live provider-present policy row to keep a historical restore item selectable. - ../../apps/platform/app/Filament/Resources/RestoreRunResource.php should keep continuity messaging secondary and truthful: the backup item is historical and selectable, while the live policy is currently provider-missing.
- Current-state backup blocking should prefer calm explanatory copy and zero new run creation. Historical restore remains a separate allowed path.
UI / Filament & Livewire Fit
- Filament remains v5 on Livewire v4. No new panel or provider is planned, and provider registration remains in ../../apps/platform/bootstrap/providers.php.
- ../../apps/platform/app/Filament/Resources/PolicyResource.php already exposes a list and a view page and explicitly sets
$isGloballySearchable = false; this feature does not change global-search posture. - Keep the existing policy resource as the primary current-state decision surface. Do not add a new ghost-policy page, special diagnostics page, or alternate provider-missing registry.
- Any destructive action that survives this slice remains the existing local ignore/restore family and must continue to use confirmation and server-side authorization. This feature does not introduce a new destructive action.
- Asset strategy remains unchanged. No new Filament assets or deployment
filament:assetssteps are expected beyond the existing platform process. - If status badges or helper copy expand, reuse the existing badge and policy presentation seam instead of adding local ad-hoc mappings.
RBAC / Authorization Fit
- Workspace membership and tenant entitlement remain the first boundary. Nothing in provider-missing semantics changes
404versus403scope handling. - Existing policy, backup, and restore capabilities remain authoritative. No new capability string or role check is justified.
- Backup blocking for provider-missing policies is business-state gating, not authorization. In-scope operators still see the policy and the historical restore path; out-of-scope actors still receive deny-as-not-found.
- Any reused detail or action route must keep current server-side authorization checks and confirmation behavior.
Audit / Logging Fit
- Reuse the existing audit infrastructure; do not create a second lifecycle-log family.
- The narrow expectation is one explicit audit event when a previously observed policy becomes provider-missing and one when it later reappears.
- Audit payload should stay tenant-safe and minimal: policy id, policy external id, canonical policy type, transition timestamp, and sync source context are sufficient.
- No new
OperationRuntype is planned. Current sync actions may continue to use the existing run UX only when a sync actually starts.
Data & Query Fit
- Add one nullable timestamp column:
policies.missing_from_provider_at. - Keep
workspace_idandtenant_idas required anchors on policy truth; this slice does not weaken tenant ownership. - Prefer deriving provider-presence state via model helpers or narrow query scopes instead of storing a second status enum.
- Update current policy-eligibility queries to use both
ignored_atandmissing_from_provider_atwhere current-state capture must stay live-only. - Combined-state filter membership is derived, not stored: ignored views include
ignored_locally_provider_missing, provider-missing views includeignored_locally_provider_missing, and current backup/export blocked-reason precedence resolves toprovider_missingwhen both timestamps are present. - Keep restore queries historical-first: if a
BackupItemremains eligible, provider-missing context is descriptive rather than disqualifying unless a separate restore rule already blocks it. - Avoid adding speculative
provider_state_reasonor lifecycle-history tables in v1.
UI / Surface Guardrail Plan
- Guardrail scope: changed surfaces
- Native vs custom classification summary: native Filament
- Shared-family relevance: status messaging, badges, row/detail actions, backup eligibility messaging, restore continuity descriptions, and audit labels
- State layers in scope: page, detail, action modal, picker description, query/model state
- Audience modes in scope: operator-MSP, support-platform
- Decision/diagnostic/raw hierarchy plan: decision-first current policy meaning, diagnostics-second sync/history context, raw/support third and only where existing surfaces already expose it
- Raw/support gating plan: no new raw payload disclosure; any provider detail remains secondary on existing policy/version evidence surfaces only
- One-primary-action / duplicate-truth control: policy list/detail own current-state meaning; backup flows own current capture eligibility; restore flows own historical continuity; no surface should restate another surface's primary truth as an equal-priority summary
- Handling modes by drift class or surface: review-mandatory
- Repository-signal treatment: review-mandatory because this slice changes a shared state vocabulary across policy, backup, and restore seams
- Special surface test profiles: standard-native-filament, shared-detail-family
- Required tests or manual smoke: functional-core, state-contract
- Exception path and spread control: none planned; any attempt to add a new provider-missing page, new lifecycle registry, or new panel becomes exception-required drift
- Active feature PR close-out entry: Guardrail / State Vocabulary
Shared Pattern & System Fit
- Cross-cutting feature marker: yes
- Systems touched:
Policy,PolicySyncService,PolicyResource,BackupService,BackupSetResourcepicker helpers if needed,RestoreRunResource, audit action ids, and the policy badge/presentation seam - Shared abstractions reused: existing policy model truth, shared policy filters/actions, existing backup selection path, existing restore-item option builders, existing audit logger, and existing badge rendering tests
- New abstraction introduced? why?: none planned. If implementation needs a tiny provider-presence helper, keep it on
Policyor an existing policy-presenter seam instead of creating a new lifecycle framework. - Why the existing abstraction was sufficient or insufficient: current shared seams already own the right surfaces. They are simply fed by the wrong state vocabulary today.
- Bounded deviation / spread control: none planned. Any proposal for a separate ghost-policy registry, dedicated diagnostics page, or generic managed-object lifecycle layer should be deferred to the broader lifecycle taxonomy follow-up.
OperationRun UX Impact
- Touches OperationRun start/completion/link UX?: yes, by narrowing when current backup/export actions may start and by leaving sync-retry behavior on the existing shared path
- Central contract reused: existing policy sync and backup/export start UX
- Delegated UX behaviors: allowed sync actions keep the current queued toast/link behavior; blocked provider-missing current backup/export actions stop before run creation and explain why locally
- Surface-owned behavior kept local: policy, backup, and restore surfaces own provider-missing messaging only; they do not create a new run type or monitoring surface
- Queued DB-notification policy: unchanged
- Terminal notification path: unchanged
- Exception path: none
Provider Boundary & Portability Fit
- Shared provider/platform boundary touched?: yes
- Provider-owned seams: Microsoft Graph list results, subtype inclusion/exclusion, and canonical-type mapping remain provider-owned evidence in sync
- Platform-core seams: local suppression truth, provider-missing truth, current backup eligibility, historical restore continuity, and operator-facing policy vocabulary
- Neutral platform terms / contracts preserved:
provider missing,ignored locally,current backup eligibility, andrestore continuity - Retained provider-specific semantics and why: subtype filtering remains provider-specific inside sync because it already reflects supported endpoint scope
- Bounded extraction or follow-up path:
document-in-featurefor the narrow provider-presence wording now;follow-up-speclater if the broader lifecycle taxonomy or explicit provider-deleted distinction is approved
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
- Inventory-first / snapshot truth: PASS. The slice keeps historical
PolicyVersionandBackupItemtruth authoritative and does not invent a new shadow registry. - Read/write separation: PASS. No new destructive flow, purge path, or customer-facing mutation surface is introduced.
- Graph contract path: PASS. The slice reuses the existing provider sync boundary and adds no new Graph family.
- Deterministic capabilities: PASS. Existing capability registries remain canonical.
- Workspace and tenant isolation: PASS.
workspace_idandtenant_idremain required anchors on policy truth. - RBAC-UX plane separation: PASS. The slice stays inside existing admin and tenant-scoped policy, backup, and restore surfaces.
- Destructive confirmation standard: PASS. Existing local ignore/restore destructive actions remain the only destructive family and keep confirmation plus server-side authorization.
- Global search safety: PASS.
PolicyResourceis already globally disabled and remains so. - OperationRun / Ops-UX: PASS. No new run type is introduced; blocked backup/export states stop before run creation.
- Data minimization: PASS. Provider-missing copy stays calm and decision-first; no new raw payload exposure is added.
- Test governance (TEST-GOV-001): PASS. Planned proof stays inside focused feature and unit lanes.
- Proportionality / no premature abstraction: PASS. One timestamp and derived state family are the narrowest defensible shape.
- Persisted truth (PERSIST-001): PASS. One field is added to existing policy truth; no new table or artifact family is created.
- Behavioral state (STATE-001): PASS. The state family is derived from existing and new timestamps rather than adding a new enum/persistence layer.
- Provider boundary (PROV-001): PASS. Provider-specific result interpretation stays in sync; operator-facing language stays platform-neutral.
- Filament / Laravel planning contract: PASS. Filament remains v5 on Livewire v4, provider registration remains in ../../apps/platform/bootstrap/providers.php, no panel change is planned,
PolicyResourceglobal search remains disabled, and no assets are expected.
Gate evaluation: PASS.
- The narrow path is defensible if implementation keeps
ignored_atuser-owned andmissing_from_provider_atprovider-observation-only. - The plan fails the gate if it drifts into
SoftDeletes, a new lifecycle service, a provider-deleted taxonomy, or multi-object rollout.
Post-design re-check: PASS. research.md, data-model.md, quickstart.md, and contracts/provider-missing-policy-visibility.openapi.yaml are present and aligned with the spec package.
Test Governance Check
- Test purpose / classification by changed surface: Feature for sync behavior, policy UI, backup eligibility, restore continuity, and authorization continuity; Unit for policy badge/state or narrow derived helper behavior if one is introduced
- Affected validation lanes: fast-feedback, confidence
- Why this lane mix is the narrowest sufficient proof: the behavior is server-side and already anchored in existing sync, policy, backup, and restore tests. Focused feature coverage plus one small unit seam is enough without browser cost.
- Narrowest proving command(s):
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.phpexport 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.phpexport 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
- Fixture / helper / factory / seed / context cost risks: low to moderate; reuse existing tenant, policy, backup-set, backup-item, restore-run, and policy-version fixtures
- Expensive defaults or shared helper growth introduced?: none expected; any new helper should stay policy-local and explicit
- Heavy-family additions, promotions, or visibility changes: none
- Surface-class relief / special coverage rule: standard-native-filament relief for policy and backup surfaces; shared-detail-family proof for restore continuity
- Closing validation and reviewer handoff: rerun the commands above, verify sync never writes
ignored_atfor provider absence, verify missing policies remain visible, verify current backup/export blocks without starting a run, verify historical restore continuity remains selectable, and verify non-members or out-of-scope actors still resolve as404 - Budget / baseline / trend follow-up: none expected beyond small feature-local test additions
- Review-stop questions: hidden
ignored_atwrites left in sync, blocked backup/export starting a run anyway, restore continuity silently filtered, or provider-specific wording leaking into platform truth - Escalation path:
document-in-featurefor bounded wording or audit metadata notes;follow-up-specfor broader lifecycle rollout;reject-or-splitfor SoftDeletes or new framework drift - Active feature PR close-out entry: Guardrail / State Vocabulary
Rollout & Risk Controls
- Keep policy truth anchored to the existing
policiesrow; do not create a side table or ghost-policy registry. - Keep local ignore and provider-missing semantics orthogonal. Sync may clear provider-missing on reappearance, but only an explicit user action clears local ignore.
- Keep current-state capture conservative. If the provider cannot currently supply live state for a policy, current backup/export should explain that and stop.
- Keep historical restore continuity permissive where
BackupItemtruth already exists. - Keep global search posture, panel registration, and asset strategy unchanged.
Project Structure
Documentation (this feature)
specs/261-provider-missing-policy-visibility/
├── checklists/
│ └── requirements.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── provider-missing-policy-visibility.openapi.yaml
└── tasks.md
Source Code (repository root)
apps/platform/
├── app/
│ ├── Filament/
│ │ └── Resources/
│ │ ├── BackupSetResource.php
│ │ ├── PolicyResource.php
│ │ └── RestoreRunResource.php
│ ├── Jobs/
│ │ └── Operations/PolicyBulkDeleteWorkerJob.php
│ ├── Models/
│ │ └── Policy.php
│ ├── Services/
│ │ ├── Audit/
│ │ └── Intune/
│ │ ├── BackupService.php
│ │ └── PolicySyncService.php
│ └── Support/
│ ├── Audit/AuditActionId.php
│ └── Ui/
├── database/
│ └── migrations/
└── tests/
├── Feature/
│ ├── Filament/
│ ├── Jobs/
│ └── ...
└── Unit/
└── Badges/
Complexity Tracking
- New persistence: 1 nullable timestamp on
policies - New derived state family: 1 provider-presence family layered onto the existing local ignore semantics
- New routes/pages/panels/providers: none
- New assets: none
- New queues or long-running operations: none
- Expected implementation risk: moderate, because multiple existing tests currently encode the conflated semantics and must be corrected together