TenantAtlas/specs/261-provider-missing-policy-visibility/plan.md
Ahmed Darrazi 91f327a7c2
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m40s
feat(specs/261): add provider missing policy visibility
2026-05-01 22:17:29 +02:00

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

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_at plus missing_from_provider_at.
  • The derived state family stays bounded:
    • active: ignored_at = null and missing_from_provider_at = null
    • ignored locally: ignored_at != null and missing_from_provider_at = null
    • provider missing: ignored_at = null and missing_from_provider_at != null
    • ignored locally + provider missing: both timestamps present, with local suppression as the primary local-control meaning
  • Policy list filtering stays deterministic: the combined state belongs to both the ignored and provider_missing filter views so operators can reach it from either workflow.
  • ../../apps/platform/app/Services/Intune/PolicySyncService.php must stop blindly resetting ignored_at on updateOrCreate().
  • Any sync path that currently uses ignored_at for subtype filtering or reclassification must instead either reclassify the row to a supported canonical type or mark missing_from_provider_at when the object falls out of the supported provider-backed result set.
  • Reappearance must clear missing_from_provider_at without automatically clearing ignored_at.
  • Existing local delete/restore semantics in ignore() and unignore() remain unchanged and stay rooted in ignored_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_at and missing_from_provider_at are present, current backup/export uses provider_missing as the primary blocked reason because the product cannot truthfully claim fresh provider-backed capture is possible.
  • Existing historical BackupItem truth 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:assets steps 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 404 versus 403 scope 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 OperationRun type 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_id and tenant_id as 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_at and missing_from_provider_at where 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 include ignored_locally_provider_missing, and current backup/export blocked-reason precedence resolves to provider_missing when both timestamps are present.
  • Keep restore queries historical-first: if a BackupItem remains eligible, provider-missing context is descriptive rather than disqualifying unless a separate restore rule already blocks it.
  • Avoid adding speculative provider_state_reason or 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, BackupSetResource picker 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 Policy or 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, and restore 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-feature for the narrow provider-presence wording now; follow-up-spec later 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 PolicyVersion and BackupItem truth 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_id and tenant_id remain 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. PolicyResource is 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, PolicyResource global search remains disabled, and no assets are expected.

Gate evaluation: PASS.

  • The narrow path is defensible if implementation keeps ignored_at user-owned and missing_from_provider_at provider-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.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
  • 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_at for 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 as 404
  • Budget / baseline / trend follow-up: none expected beyond small feature-local test additions
  • Review-stop questions: hidden ignored_at writes 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-feature for bounded wording or audit metadata notes; follow-up-spec for broader lifecycle rollout; reject-or-split for SoftDeletes or new framework drift
  • Active feature PR close-out entry: Guardrail / State Vocabulary

Rollout & Risk Controls

  • Keep policy truth anchored to the existing policies row; 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 BackupItem truth 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