TenantAtlas/specs/109-review-pack-export/plan.md

8.5 KiB

Implementation Plan: Tenant Review Pack Export v1 (CSV + ZIP)

Branch: 109-review-pack-export | Date: 2026-02-23 | Spec: spec.md Input: Feature specification from /specs/109-review-pack-export/spec.md

Summary

Implement an exportable audit artifact (ZIP with CSV + JSON) for MSP/Enterprise tenant reviews. The system collects pre-cached stored reports, findings, tenant hardening status, and recent operations from the database (no Graph API calls), assembles them into a deterministic ZIP archive, and stores it on a private local disk. Downloads use signed temporary URLs. Features include fingerprint-based content deduplication, RBAC-gated access (REVIEW_PACK_VIEW / REVIEW_PACK_MANAGE), retention pruning with configurable expiry, and OperationRun-based observability. The Filament UI provides a ReviewPackResource (list + view), a Tenant Dashboard card, and a modal-based generation trigger.

Technical Context

Language/Version: PHP 8.4 (Laravel 12) Primary Dependencies: Filament v5, Livewire v4, Laravel Framework v12 Storage: PostgreSQL (jsonb columns for summary/options), local filesystem (exports disk) for ZIP artifacts Testing: Pest v4 (Feature tests with Livewire component testing) Target Platform: Linux server (Sail/Docker local, Dokploy VPS staging/production) Project Type: web (Laravel monolith with Filament admin panel) Performance Goals: Pack generation < 60s for 1,000 findings + 10 stored reports; download streaming with no memory loading of full file Constraints: DB-only generation (no Graph API calls); chunked iteration for large finding sets; temp file for ZIP assembly (no full in-memory ZIP) Scale/Scope: Multi-workspace, multi-tenant; packs are tenant-scoped; typical pack size < 10 MB

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

Rule Status Notes
Inventory-first PASS ReviewPack reads from existing stored_reports + findings (pre-cached inventory data). No new inventory sources introduced.
Read/write separation PASS Generation is a queued job (async write). User action is enqueue-only. Destructive actions (expire/delete) require ->requiresConfirmation(). All writes audit-logged via OperationRun.
Graph contract path PASS No Graph API calls in this feature. All data sourced from DB. FR-006 explicitly forbids Graph calls during generation.
Deterministic capabilities PASS REVIEW_PACK_VIEW and REVIEW_PACK_MANAGE added to canonical Capabilities.php registry. Discoverable via Capabilities::all() reflection. No raw strings.
RBAC-UX: two planes PASS All routes under /admin plane. Tenant-context routes scoped via /admin/t/{tenant}/.... No /system plane involvement.
RBAC-UX: non-member = 404 PASS Non-member workspace/tenant access returns 404 (deny-as-not-found). Policy enforces workspace_id + tenant_id before any data returned. Download endpoint returns 404 for non-existent/inaccessible packs.
RBAC-UX: member missing cap = 403 PASS Members without REVIEW_PACK_VIEW get 403 on list/download. Members without REVIEW_PACK_MANAGE get 403 on generate/expire.
RBAC-UX: destructive confirmation PASS Expire and Delete actions use ->requiresConfirmation() with clear warning text. Regenerate requires confirmation if a ready pack exists.
RBAC-UX: global search N/A ReviewPackResource does not participate in global search (no $recordTitleAttribute). Intentionally excluded.
Workspace isolation PASS DerivesWorkspaceIdFromTenant auto-sets workspace_id. All queries scope by workspace_id + tenant_id. Download controller validates pack ownership before streaming.
Tenant isolation PASS Partial unique index scoped to (workspace_id, tenant_id, fingerprint). All scopes include tenant_id. Dashboard widget reads only current tenant packs.
Run observability PASS OperationRun of type tenant.review_pack.generate tracks lifecycle. Active-run dedupe via partial unique index. Failure reason stored in context['reason_code'].
Automation PASS Prune command uses withoutOverlapping(). GenerateReviewPackJob unique via OperationRun active dedupe (existing pattern).
Data minimization PASS FR-008: no webhooks, tokens, secrets, or raw Graph dumps exported. FR-009: PII redaction via include_pii toggle.
BADGE-001 PASS ReviewPackStatus added to BadgeDomain enum with ReviewPackStatusBadge mapper. All 5 statuses mapped.
Filament Action Surface Contract PASS UI Action Matrix in spec. List: header action (Generate), row actions (Download + Expire), empty state CTA. View: header actions (Download + Regenerate). Card: inline actions. Destructive actions confirmed.
UX-001 PASS View page uses Infolist. Empty state has specific title + explanation + 1 CTA. Status badges use BADGE-001. Generate modal fields in Sections. Table has search/sort/filters.

Post-design re-evaluation: All checks PASS. No violations found.

Project Structure

Documentation (this feature)

specs/109-review-pack-export/
├── plan.md              # This file
├── spec.md              # Feature specification
├── research.md          # Phase 0: resolved unknowns
├── data-model.md        # Phase 1: entity design
├── quickstart.md        # Phase 1: implementation guide
├── contracts/           # Phase 1: API + action contracts
│   └── api-contracts.md
├── checklists/
│   └── requirements.md
└── tasks.md             # Phase 2 output (created by /speckit.tasks)

Source Code (repository root)

app/
├── Console/Commands/
│   └── PruneReviewPacksCommand.php
├── Filament/
│   ├── Resources/
│   │   └── ReviewPackResource.php
│   │       └── Pages/
│   │           ├── ListReviewPacks.php
│   │           └── ViewReviewPack.php
│   └── Widgets/
│       └── TenantReviewPackCard.php
├── Http/Controllers/
│   └── ReviewPackDownloadController.php
├── Jobs/
│   └── GenerateReviewPackJob.php
├── Models/
│   └── ReviewPack.php
├── Notifications/
│   └── ReviewPackStatusNotification.php
├── Services/
│   └── ReviewPackService.php
└── Support/
    ├── ReviewPackStatus.php
    └── Badges/Mappers/
        └── ReviewPackStatusBadge.php

config/
├── filesystems.php          # Modified: add exports disk
└── tenantpilot.php          # Modified: add review_pack section

database/
├── factories/
│   └── ReviewPackFactory.php
└── migrations/
    └── XXXX_create_review_packs_table.php

routes/
├── web.php                  # Modified: add download route
└── console.php              # Modified: add prune schedule entry

tests/Feature/ReviewPack/
├── ReviewPackGenerationTest.php
├── ReviewPackDownloadTest.php
├── ReviewPackRbacTest.php
├── ReviewPackPruneTest.php
├── ReviewPackResourceTest.php
└── ReviewPackWidgetTest.php

Structure Decision: Standard Laravel monolith structure. All new files follow existing directory conventions. No new base folders introduced.

Complexity Tracking

No Constitution violations detected. No complexity justifications needed.

Filament v5 Agent Output Contract

  1. Livewire v4.0+ compliance: Yes. Filament v5 requires Livewire v4 — all components are Livewire v4 compatible.
  2. Provider registration: Panel provider already registered in bootstrap/providers.php. No new panel required.
  3. Global search: ReviewPackResource does NOT participate in global search. No $recordTitleAttribute set. Intentional.
  4. Destructive actions: Expire action uses ->action(...) + ->requiresConfirmation() + ->color('danger'). Regenerate uses ->requiresConfirmation() when a ready pack exists. Hard-delete in prune command requires explicit --hard-delete flag.
  5. Asset strategy: No custom frontend assets. Standard Filament components only. php artisan filament:assets already in deploy pipeline.
  6. Testing plan: Pest Feature tests covering: generation job (happy + failure paths), download controller (signed URL, expired, RBAC), RBAC enforcement (404/403 matrix), prune command (expire + hard-delete), Filament Resource (Livewire component tests for list/view pages + actions), Dashboard widget (Livewire component test).