TenantAtlas/specs/277-stored-reports-surface/plan.md
ahmido c44f683aa6 277-stored-reports-surface → platform-dev (#333)
Auto-created PR: committing all local changes and pushing branch `277-stored-reports-surface` to remote.

Please review and adjust the title/description as needed.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #333
2026-05-06 00:04:53 +00:00

281 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation Plan: Stored Reports Surface v1
**Branch**: `277-stored-reports-surface` | **Date**: 2026-05-06 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/277-stored-reports-surface/spec.md`
## Summary
Prepare one bounded tenant-scoped browse and inspect surface over the repos existing `StoredReport` truth. The narrow implementation path is to add one read-only Filament resource with a register and a view page under the tenant panel, reuse `ArtifactTruthPresenter::forStoredReport()` for current versus historical retained lifecycle truth, introduce one explicit `permission_posture.view` capability alongside the existing `entra_roles.view` gate, and point the current admin-roles widget at the canonical stored-report detail route.
This slice stays explicitly narrow. Filament remains v5 on Livewire v4, provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, the stored-report resource stays out of global search in v1, no new asset registration is expected, and no new report engine, analytics console, cross-tenant hub, raw-download surface, or lifecycle-taxonomy rewrite is planned.
## Inherited Baseline / Explicit Delta
### Inherited baseline
- `StoredReport` already exists as tenant-owned persisted truth with `workspace_id`, `tenant_id`, `report_type`, payload, fingerprint, and previous-fingerprint lineage.
- `ArtifactTruthPresenter::forStoredReport()` already derives the retained lifecycle contract for stored reports and classifies each record as `current` or `historical`.
- `EntraAdminRolesReportService` and `PermissionPostureFindingGenerator` already produce the only two repo-real stored-report families in scope for v1.
- Evidence-source providers already anchor permission-posture and Entra-admin-roles evidence items to `StoredReport` identity via `source_record_type` and `source_record_id`.
- `AdminRolesSummaryWidget` already resolves the latest Entra admin-roles report for the active tenant, but today it leaves `viewReportUrl` unset and therefore lacks a first-class drilldown.
- There is no current first-class Filament stored-report register or detail surface in the tenant panel.
### Explicit delta in this plan
- Add one tenant-scoped, read-only stored-reports register at `/admin/t/{tenant}/stored-reports`.
- Add one tenant-scoped, read-only stored-report detail page at `/admin/t/{tenant}/stored-reports/{report}`.
- Add one explicit `permission_posture.view` capability and keep list and detail visibility family-aware.
- Reuse stored-report lifecycle, retention, and badge semantics from existing artifact-truth helpers instead of creating a second report-status system.
- Adopt the new detail route as the canonical drilldown target from `AdminRolesSummaryWidget`.
- Keep any unexpected report family outside v1 browse and detail scope instead of adding a local fallback renderer.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing `StoredReport`, `ArtifactTruthPresenter`, `BadgeCatalog`/`BadgeRenderer`, existing tenant-panel resource patterns, current report-producing services for permission posture and Entra admin roles
**Storage**: PostgreSQL via existing `stored_reports`, `evidence_snapshots`, `tenant_reviews`, and `review_packs`; no new persistence planned
**Testing**: Pest v4 feature coverage plus focused updates to an existing widget test
**Validation Lanes**: fast-feedback, confidence
**Target Platform**: Laravel monolith in `apps/platform`, tenant/admin plane under `/admin/t/{tenant}/...`
**Project Type**: web application
**Performance Goals**: keep list and detail DB-only, tenant-scoped, and eager-load-light; avoid new queues, Graph calls, or report regeneration side effects
**Constraints**: no new report engine, no analytics console, no global-search exposure, no cross-tenant browse surface, no raw payload download, no new persisted truth, and no new generic report registry
**Scale/Scope**: 1 new read-only resource surface, 2 supported stored-report families, 1 bounded new capability, and convergence on the current admin-roles widget seam only
## Likely Affected Repo Surfaces
- `apps/platform/app/Filament/Resources/StoredReportResource.php` as the new tenant-scoped read-only resource.
- `apps/platform/app/Filament/Resources/StoredReportResource/Pages/ListStoredReports.php` for the register.
- `apps/platform/app/Filament/Resources/StoredReportResource/Pages/ViewStoredReport.php` for the detail surface.
- `apps/platform/app/Models/StoredReport.php` and `apps/platform/database/factories/StoredReportFactory.php` for query shape and test setup reuse.
- `apps/platform/app/Support/Auth/Capabilities.php` and `apps/platform/app/Services/Auth/RoleCapabilityMap.php` for the new `permission_posture.view` capability and bounded role mapping.
- `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php` as the existing lifecycle-truth dependency for stored reports, likely reused without broadening the artifact envelope.
- `apps/platform/app/Filament/Widgets/Tenant/AdminRolesSummaryWidget.php` and `apps/platform/resources/views/filament/widgets/tenant/admin-roles-summary.blade.php` for the canonical detail launch target.
- `apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php` and `apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesReportService.php` as payload-shape anchors only, not as new workflow scope.
- `apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php` and `apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php` as source-record anchors that explain downstream consumer truth.
- `apps/platform/tests/Feature/StoredReports/StoredReportResourceTest.php`, `apps/platform/tests/Feature/StoredReports/StoredReportEntitlementEnforcementTest.php`, `apps/platform/tests/Feature/StoredReports/StoredReportDetailPresentationTest.php`, and `apps/platform/tests/Feature/EntraAdminRoles/AdminRolesSummaryWidgetTest.php` as the bounded proving path.
## Filament v5 / Panel Notes
- **Livewire v4.0+ compliance**: The new register and detail surfaces should be native Filament resource pages under Livewire v4. No Livewire v3 compatibility or custom page framework is planned.
- **Provider registration location**: No new panel or provider is introduced. Existing provider registration remains in `apps/platform/bootstrap/providers.php`.
- **Global search**: The stored-report surface stays out of global search in v1. If a resource is used, it should remain `isGloballySearchable = false`, so the Filament view-page rule is not relied on for search exposure.
- **Destructive actions**: None are in scope. The surface remains read-only and must not acquire edit, delete, rerun, or retention-mutation actions.
- **Asset strategy**: No new Filament assets are planned. If a later implementation unexpectedly registers shared assets, deployment remains the standard `cd apps/platform && php artisan filament:assets` path.
## Stored-Report Surface Fit
- Implement the stored-report surface as one native Filament resource family rather than a custom dashboard page or local widget-only view.
- Use a tenant-scoped register as the primary decision surface with clickable rows and no competing inline `View` action.
- Use a read-only view page with an Infolist-style inspection layout rather than a disabled edit form.
- Reuse `ArtifactTruthPresenter::forStoredReport()` for lifecycle and retention truth and keep any extra “current report” lookup local to the detail surface.
- Keep list state bounded to search, report-family filter, and history visibility. Avoid local JavaScript state machines or a second query-string vocabulary beyond what Filament table state needs.
- Keep support limited to the two repo-real report families in v1. Any unexpected family remains outside browse and detail scope until a follow-up spec expands support.
## RBAC / Data Ownership / Auditability Fit
- `StoredReport` remains tenant-owned truth with both `workspace_id` and `tenant_id` required. No workspace-owned mirror, generic artifact table, or reporting aggregate is introduced.
- Collection visibility should require at least one in-scope stored-report family capability for the current tenant.
- Detail access should remain family-aware:
- `entra.admin_roles` uses existing `Capabilities::ENTRA_ROLES_VIEW`
- `permission_posture` adds explicit `Capabilities::PERMISSION_POSTURE_VIEW`
- The new `permission_posture.view` capability should map to the same read-only tenant roles that already consume governance evidence (`owner`, `manager`, `operator`, `readonly`) unless implementation review proves a narrower current-release truth.
- Non-members or actors outside workspace or tenant scope remain `404`. In-scope actors missing the relevant report-family capability remain `403`.
- Read-only browsing does not justify a new audit action family. Auditability for this slice comes from immutable stored-report identity, measured time, fingerprint lineage, and existing lifecycle truth rather than new browse-access logging.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed surfaces
- **Native vs custom classification summary**: native Filament
- **Shared-family relevance**: evidence/report viewers, navigation entry points, artifact status messaging, and retained-artifact detail disclosure
- **State layers in scope**: page, detail, URL-query
- **Audience modes in scope**: operator-MSP, support-platform
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
- **Raw/support gating plan**: raw payload and provider-shaped identifiers remain collapsed and lower-priority on detail; no raw payload appears on the register
- **One-primary-action / duplicate-truth control**: the register keeps one primary open model through row click; the detail surface exposes `Open current report` only when viewing a historical row and avoids repeating lifecycle truth in multiple sections
- **Handling modes by drift class or surface**: review-mandatory
- **Repository-signal treatment**: review-mandatory now; future hard-stop candidate if the slice drifts into a report console, generic viewer framework, or cross-tenant hub
- **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 request to widen support beyond the two repo-real families requires a follow-up spec rather than a local fallback
- **Active feature PR close-out entry**: Guardrail
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: `StoredReport`, `ArtifactTruthPresenter`, centralized badge semantics, `AdminRolesSummaryWidget`, and report-producing services as payload anchors
- **Shared abstractions reused**: `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `BadgeCatalog`, `BadgeRenderer`, current tenant-panel resource conventions, and the existing report payload shapes already produced by permission posture and Entra admin roles
- **New abstraction introduced? why?**: none planned. If implementation needs a tiny private extraction helper for family summaries, keep it local to the stored-report surface and do not turn it into a registry or shared framework.
- **Why the existing abstraction was sufficient or insufficient**: lifecycle truth, badge semantics, and stored-report identity already exist. What is insufficient today is a first-class tenant browse and inspect destination.
- **Bounded deviation / spread control**: only the two repo-real report families get full summary rendering in v1; additional families require a follow-up spec instead of local fallback logic.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no
- **Central contract reused**: `N/A`
- **Delegated UX behaviors**: `N/A`
- **Surface-owned behavior kept local**: read-only routing and disclosure only
- **Queued DB-notification policy**: `N/A`
- **Terminal notification path**: `N/A`
- **Exception path**: none
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes
- **Provider-owned seams**: stored-report payload interpretation for `permission_posture` and `entra.admin_roles`
- **Platform-core seams**: `stored report`, `current`, `historical`, `retained`, `measured at`, register/detail routing, and downstream proof-link wording
- **Neutral platform terms / contracts preserved**: `stored report`, `current record`, `historical record`, `retained history`, `measured at`, `open report`
- **Retained provider-specific semantics and why**: the two report-family labels and their bounded summary facts stay visible because they are already current product truth and are not being generalized into a broader platform taxonomy
- **Bounded extraction or follow-up path**: future report-family expansion stays local to this surface until additional repo-real families justify broader normalization
## Constitution Check
*GATE: Must pass before implementation begins and again after the design artifacts are complete.*
- Inventory-first / snapshot truth: PASS. The slice surfaces already-retained report truth only and does not change inventory or snapshot semantics.
- Read/write separation: PASS. The surface is read-only and introduces no mutation path.
- Graph contract path: PASS. No new Graph call, provider client, or scan path is added.
- Deterministic capabilities: PASS. Report-family visibility stays on explicit capability constants and role mappings.
- Workspace and tenant isolation: PASS. `StoredReport` remains tenant-owned and all routes stay tenant-scoped.
- RBAC-UX plane separation: PASS. Everything remains in the tenant/admin plane under `/admin/t/{tenant}/...`; no `/system` crossover is added.
- Destructive action discipline: PASS by non-use. No destructive or mutating actions are introduced.
- Global search safety: PASS. The new surface is not globally searchable in v1.
- OperationRun / Ops-UX: PASS by non-use. Existing scan or generation actions remain on their current surfaces.
- Data minimization: PASS. No new persisted truth or raw payload export is introduced.
- Test governance (TEST-GOV-001): PASS. Proof stays in focused feature coverage and one updated widget test.
- Proportionality / no premature abstraction: PASS. The slice adds one bounded capability and one read-only surface family without a registry, engine, or new persistence.
- Persisted truth (PERSIST-001): PASS. No new table, entity, or stored projection is planned.
- Behavioral state (STATE-001): PASS. Current versus historical remains derived from existing stored-report truth.
- UI semantics / shared pattern first / Filament-native UI: PASS. Native Filament resource pages and existing artifact-truth helpers remain the first path.
- Provider boundary (PROV-001): PASS. Provider-shaped detail stays in family summaries and does not become platform-core route or taxonomy truth.
- Filament / Laravel planning contract: PASS. Filament stays v5 on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, the stored-report resource stays out of global search, and no asset changes are planned.
**Gate evaluation**: PASS.
**Post-design re-check**: PASS once `research.md`, `data-model.md`, `quickstart.md`, `contracts/tenant-stored-reports-surface.logical.openapi.yaml`, `checklists/requirements.md`, and `tasks.md` are present and aligned with this package.
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for register access, list behavior, detail presentation, and widget drilldown; no default `Browser` family planned
- **Affected validation lanes**: fast-feedback, confidence
- **Why this lane mix is the narrowest sufficient proof**: the slice is a native Filament register plus view surface over existing persisted truth, so list/detail behavior and widget drilldown can be proven in feature tests without widening into browser-only interaction coverage by default
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/StoredReports/StoredReportResourceTest.php tests/Feature/StoredReports/StoredReportEntitlementEnforcementTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/StoredReports/StoredReportDetailPresentationTest.php tests/Feature/EntraAdminRoles/AdminRolesSummaryWidgetTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- **Fixture / helper / factory / seed / context cost risks**: low to moderate; reuse current workspace, tenant, membership, and stored-report factory setup instead of introducing full provider or queue fixtures
- **Expensive defaults or shared helper growth introduced?**: no; any family-summary helper should stay local and explicit
- **Heavy-family additions, promotions, or visibility changes**: none planned
- **Surface-class relief / special coverage rule**: standard-native-filament relief for the register; shared-detail-family coverage for the detail page
- **Closing validation and reviewer handoff**: rerun the exact commands above, verify family-aware filtering and `404` versus `403` semantics, verify current versus historical truth is visible before raw diagnostics, confirm `AdminRolesSummaryWidget` launches the canonical detail route, and confirm no global-search exposure or raw-download action appears
- **Budget / baseline / trend follow-up**: none expected beyond a contained feature-local increase
- **Review-stop questions**: did the slice add a generic report framework, did it widen support beyond the two named families, did it leak hidden report families in rows or filter options, and did it turn raw payload into default-visible content
- **Escalation path**: `reject-or-split` if implementation widens into analytics, cross-tenant browse, a report engine, or support for additional report families
- **Active feature PR close-out entry**: Guardrail
- **Why no dedicated follow-up spec is needed**: this package already captures the bounded browse/detail productization of current stored-report truth; broader reporting or customer-facing consumption remains explicitly separate
- **Test-governance outcome**: keep
## Review Checklist Status
- **Review checklist artifact**: `checklists/requirements.md`
- **Review outcome class**: `acceptable-special-case`
- **Workflow outcome**: `keep`
- **Test-governance outcome**: `keep`
- **Escalation rule**: if implementation adds report generation, global search, raw export, cross-tenant browse, or a generic report-schema framework, flip the workflow outcome to `split` or `reject-or-split` before continuing
## Rollout Considerations
- Land the new `permission_posture.view` capability and the stored-report resource shell first so route ownership and authorization are clear before UI polish.
- Keep the canonical entry point on the new tenant stored-reports register and the canonical secondary surface on the stored-report detail page.
- Adopt the widget drilldown next, because `AdminRolesSummaryWidget` is the clearest current repo-real launch seam.
- Keep convergence bounded to `AdminRolesSummaryWidget`; do not add new stored-report links on evidence, review, or review-pack pages in v1.
- Keep the resource out of global search and keep deployment unchanged because no new assets or providers are expected.
## Risk Controls
- Reject any implementation that adds report generation, rerun, schedule, analytics, export, or delete behavior to this surface.
- Reject any implementation that introduces a generic report registry, renderer framework, or new persisted artifact family.
- Reject any implementation that exposes hidden report families through rows, filter options, or error semantics.
- Reject any implementation that shows raw payload or provider identifiers before the calm operator summary.
- Reject any implementation that widens support beyond the two named families without a follow-up spec.
## Research & Design Outputs
- `research.md` records the resource-versus-custom-surface decision, capability choice, canonical drilldown scope, supported-family boundary, and test-lane choice.
- `data-model.md` captures existing `StoredReport` truth plus the derived row, detail, summary, and launch-seam contracts.
- `quickstart.md` provides the bounded reviewer flow and focused validation commands.
- `contracts/tenant-stored-reports-surface.logical.openapi.yaml` captures the logical tenant register and detail routes plus family-aware visibility rules.
- `checklists/requirements.md` records the prep review outcome, workflow outcome, and test-governance outcome.
- `tasks.md` keeps implementation bounded to the read-only stored-report surface and the current admin-roles widget seam.
## Project Structure
### Documentation (this feature)
```text
specs/277-stored-reports-surface/
├── checklists/
│ └── requirements.md
├── contracts/
│ └── tenant-stored-reports-surface.logical.openapi.yaml
├── data-model.md
├── plan.md
├── quickstart.md
├── research.md
├── spec.md
└── tasks.md
```
### Source Code (expected implementation surfaces)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Resources/
│ │ │ ├── StoredReportResource.php
│ │ │ └── StoredReportResource/
│ │ │ └── Pages/
│ │ │ ├── ListStoredReports.php
│ │ │ └── ViewStoredReport.php
│ │ └── Widgets/
│ │ └── Tenant/
│ │ └── AdminRolesSummaryWidget.php
│ ├── Models/
│ │ └── StoredReport.php
│ ├── Services/
│ │ ├── Auth/
│ │ │ └── RoleCapabilityMap.php
│ │ ├── EntraAdminRoles/
│ │ │ └── EntraAdminRolesReportService.php
│ │ ├── Evidence/Sources/
│ │ │ ├── EntraAdminRolesSource.php
│ │ │ └── PermissionPostureSource.php
│ │ └── PermissionPosture/
│ │ └── PermissionPostureFindingGenerator.php
│ └── Support/
│ ├── Auth/
│ │ └── Capabilities.php
│ └── Ui/GovernanceArtifactTruth/
│ └── ArtifactTruthPresenter.php
├── database/
│ └── factories/
│ └── StoredReportFactory.php
└── tests/
└── Feature/
├── EntraAdminRoles/
│ └── AdminRolesSummaryWidgetTest.php
└── StoredReports/
├── StoredReportDetailPresentationTest.php
├── StoredReportEntitlementEnforcementTest.php
└── StoredReportResourceTest.php
```
**Structure Decision**: Keep the work inside the existing Laravel monolith and tenant-panel Filament resource conventions. No new base folders, panels, or shared framework layers are justified for this slice.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| none | N/A | The plan stays inside existing stored-report truth, Filament resource conventions, and existing artifact-truth semantics. |