36 tasks across 8 phases organized by user story (US1-US5). Phase 1-2: Setup + Foundational (migration, model, enums, config, badge). Phase 3: US1 Generate+Download (P1 MVP) - service, job, controller, resource, widget. Phase 4: US2 Fingerprint Dedupe (P2) - dedupe edge case tests. Phase 5: US3 RBAC Enforcement (P2) - policy + authorization tests. Phase 6: US4 Retention+Prune (P3) - command, schedule, AlertRule cleanup. Phase 7: US5 Schedule Wiring (P3) - scan dispatch verification. Phase 8: Polish - pint, test suite, e2e validation.
22 KiB
Tasks: Tenant Review Pack Export v1 (CSV + ZIP)
Input: Design documents from /specs/109-review-pack-export/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/api-contracts.md, quickstart.md
Tests: Required (Pest Feature tests). Six test files covering generation, download, RBAC, prune, resource, widget, and schedule.
Operations: OperationRun of type tenant.review_pack.generate tracks all generation runs. Failure reason_code stored in context jsonb column. Active-run dedupe via application-level guard (scopeActive()).
RBAC:
- Gate/Policy enforcement:
REVIEW_PACK_VIEW(list + download),REVIEW_PACK_MANAGE(generate + expire). - Non-member → 404 (deny-as-not-found, workspace/tenant isolation). Member missing capability → 403.
- Capabilities registered in canonical
Capabilities.phpregistry — no raw strings in feature code. - Authorization plane: tenant-context (
/admin/t/{tenant}/...). - ReviewPackResource does NOT participate in global search (intentionally excluded; no
$recordTitleAttribute). - Destructive actions (Expire) use
->requiresConfirmation(). - Tests include positive + negative authorization scenarios.
Filament UI Action Surfaces: UI Action Matrix in spec.md fulfilled. List: header "Generate Pack" modal, row "Download" + "Expire" (max 2 visible), empty state CTA. View: header "Download" + "Regenerate". Dashboard card: "Generate" + "Download" conditional.
Filament UI UX-001: View page uses Infolist (not disabled edit form). All modal fields inside Sections. Status badges use BADGE-001. Empty state has title + explanation + 1 CTA. Clickable rows via
recordUrl(). Badges:ReviewPackStatusdomain added toBadgeDomainenum withReviewPackStatusBadgemapper per BADGE-001. Tests assert each status renders correct badge.
Organization: Tasks grouped by user story per spec.md priorities (P1 → P2 → P3).
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies on incomplete tasks)
- [Story]: Which user story (US1–US5) this task belongs to
- Include exact file paths in descriptions
Phase 1: Setup
Purpose: Database schema — must complete before model/factory creation
- T001 Create migration for
review_packstable with all columns (id, workspace_id FK, tenant_id FK, operation_run_id FK nullable, initiated_by_user_id FK nullable, status, fingerprint, previous_fingerprint, summary jsonb, options jsonb, file_disk, file_path, file_size bigint, sha256, generated_at timestampTz, expires_at timestampTz, timestamps), three indexes, and partial unique indexWHERE fingerprint IS NOT NULL AND status NOT IN ('expired','failed')indatabase/migrations/XXXX_create_review_packs_table.php
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Core enums, config, capabilities, badges, model, and factory that ALL user stories depend on
⚠️ CRITICAL: No user story work can begin until this phase is complete
- T002 [P] Create
ReviewPackStatusenum with 5 string-backed cases (Queued, Generating, Ready, Failed, Expired) inapp/Support/ReviewPackStatus.php - T003 [P] Add
ReviewPackGeneratecase with valuetenant.review_pack.generatetoapp/Support/OperationRunType.php - T004 [P] Add
REVIEW_PACK_VIEW = 'review_pack.view'andREVIEW_PACK_MANAGE = 'review_pack.manage'constants toapp/Support/Auth/Capabilities.php - T005 [P] Add
ReviewPackStatus = 'review_pack_status'case toapp/Support/Badges/BadgeDomain.php - T006 [P] Create
ReviewPackStatusBadgemapper returningBadgeSpecfor 5 statuses (queued→warning, generating→info, ready→success, failed→danger, expired→gray) inapp/Support/Badges/Mappers/ReviewPackStatusBadge.php - T007 [P] Add
exportsdisk with local driver atstorage_path('app/private/exports'),serve => false,throw => truetoconfig/filesystems.php - T008 [P] Add
review_packconfig section with retention_days (90), hard_delete_grace_days (30), download_url_ttl_minutes (60), include_pii_default (true), include_operations_default (true) toconfig/tenantpilot.php - T009 Create
ReviewPackmodel withDerivesWorkspaceIdFromTenant+HasFactorytraits,guarded = [], casts (summary/options→array, generated_at/expires_at→datetime, file_size→integer), relationships (workspace, tenant, operationRun, initiator via initiated_by_user_id), scopes (ready, expired, pastRetention, forTenant, latestReady), and STATUS_* constants inapp/Models/ReviewPack.php - T010 Create
ReviewPackFactorywith default definition (ready state) and 5 named states (queued, generating, ready, failed, expired) that set correct status + nullable file fields per state indatabase/factories/ReviewPackFactory.php - T011 Run migration and verify model + factory (
vendor/bin/sail artisan migrate && vendor/bin/sail artisan test --compact --filter=ReviewPack)
Checkpoint: Foundation ready — user story implementation can now begin
Phase 3: User Story 1 — Generate and Download Review Pack (Priority: P1) 🎯 MVP
Goal: Produce an auditable ZIP artifact (summary.json, findings.csv, operations.csv, hardening.json, reports/*.json, metadata.json) from cached DB data, store privately on exports disk, deliver via signed URL, and provide full Filament UI.
Independent Test: Create tenant with stored reports + findings, trigger generation as REVIEW_PACK_MANAGE user, assert OperationRun created, review_packs row transitions to ready, ZIP file exists on exports disk with correct 7-file set, download via signed URL returns correct headers and content.
Implementation for User Story 1
- T012 [US1] Implement
ReviewPackServicewithgenerate(Tenant, User, array $options): ReviewPack(creates OperationRun + ReviewPack + dispatches job),computeFingerprint(Tenant, array $options): string(SHA-256 of tenant_id + options + report fingerprints + max finding last_seen_at + hardening tuple),generateDownloadUrl(ReviewPack): string(URL::signedRoute with configurable TTL),findExistingPack(Tenant, string $fingerprint): ?ReviewPack(ready + unexpired dedupe check), andcheckActiveRun(Tenant): bool(active OperationRun guard) inapp/Services/ReviewPackService.php - T013 [US1] Implement
GenerateReviewPackJob(ShouldQueue) with 12-step pipeline: load records, mark running, collect StoredReports (permission_posture + entra.admin_roles), collect Findings (status in open/acknowledged, chunked), collect tenant hardening fields (via accessor, not raw DB), collect recent OperationRuns (30 days), compute data_freshness, build file map with PII redaction when include_pii=false, assemble ZIP via ZipArchive (alphabetical order, temp file), compute SHA-256, store on exports disk, update ReviewPack (status=ready, fingerprint, sha256, file_size, file_path, file_disk, generated_at, summary, expires_at), mark OperationRun completed; on failure: mark failed with reason_code in context, notify initiator inapp/Jobs/GenerateReviewPackJob.php - T014 [P] [US1] Create
ReviewPackStatusNotification(database channel) with conditional rendering: ready payload (title, body with tenant name, View action URL) and failed payload (title, body with sanitized reason, View action URL) inapp/Notifications/ReviewPackStatusNotification.php - T015 [US1] Create
ReviewPackDownloadControllerwith single__invoke(Request, ReviewPack)method: validate status is ready, stream file viaStorage::disk($pack->file_disk)->download()with headers Content-Type application/zip, Content-Disposition attachment with filenamereview-pack-{tenant_external_id}-{YYYY-MM-DD}.zip, X-Review-Pack-SHA256; return 404 for expired/non-ready packs inapp/Http/Controllers/ReviewPackDownloadController.php - T016 [US1] Add signed download route
GET /admin/review-packs/{reviewPack}/downloadnamedadmin.review-packs.downloadwithsignedmiddleware inroutes/web.php - T017 [US1] Create
ReviewPackResourcewith: table columns (status badge via BADGE-001, tenant name, generated_at datetime, expires_at datetime, file_size formatted),recordUrl()for clickable rows to ViewReviewPack, header action "Generate Pack" (modal with Section containing include_pii Toggle + include_operations Toggle, authorized by REVIEW_PACK_MANAGE, calls ReviewPackService::generate), row actions "Download" (visible when ready, REVIEW_PACK_VIEW, openUrlInNewTab with signed URL) + "Expire" (REVIEW_PACK_MANAGE, color danger, requiresConfirmation, sets status=expired + deletes file), empty state with title "No review packs yet" + description + "Generate first pack" CTA, filters (status SelectFilter, generated_at date range), search on tenant name, sort on generated_at/status, nav group Monitoring → Exports inapp/Filament/Resources/ReviewPackResource.php - T018 [US1] Create
ListReviewPackspage extending ListRecords inapp/Filament/Resources/ReviewPackResource/Pages/ListReviewPacks.php - T019 [US1] Create
ViewReviewPackpage with Infolist layout: status badge (BADGE-001), summary section (data_freshness per-source timestamps, generated_at, expires_at, file_size, sha256), options section (include_pii, include_operations), initiator + OperationRun link; header actions "Download" (REVIEW_PACK_VIEW, visible when ready) + "Regenerate" (REVIEW_PACK_MANAGE, requiresConfirmation when ready pack exists, pre-fills current options, sets previous_fingerprint) inapp/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php - T020 [P] [US1] Create
TenantReviewPackCardwidget with 5 display states: no pack ("No review pack yet" + Generate CTA), queued/generating (status badge + "Generation in progress"), ready (badge + generated_at + expires_at + file_size + Download action REVIEW_PACK_VIEW + "Generate new" REVIEW_PACK_MANAGE), failed (badge + sanitized reason + Retry=Generate REVIEW_PACK_MANAGE), expired (badge + expiry date + "Generate new" REVIEW_PACK_MANAGE); data = latest ReviewPack for current tenant inapp/Filament/Widgets/TenantReviewPackCard.php - T021 [P] [US1] Create generation tests: happy path (job processes, status→ready, file on disk, sha256+file_size populated, notification sent to initiator, OperationRun completed+success), failure path (exception → status→failed, OperationRun failed, reason_code in context, failure notification), empty reports (job succeeds with empty report sections, status→ready), PII redaction (include_pii=false → display_name replaced with placeholder, UUIDs retained), ZIP contents (exactly 7 files in alphabetical order: findings.csv, hardening.json, metadata.json, operations.csv, reports/entra_admin_roles.json, reports/permission_posture.json, summary.json) in
tests/Feature/ReviewPack/ReviewPackGenerationTest.php - T022 [P] [US1] Create download tests: signed URL → 200 with Content-Type application/zip + Content-Disposition + X-Review-Pack-SHA256 headers; expired signature → 403; expired pack (status=expired) → 404; non-ready pack (status=queued) → 404; non-existent pack → 404 in
tests/Feature/ReviewPack/ReviewPackDownloadTest.php - T023 [P] [US1] Create resource Livewire tests: list page renders with columns, empty state shows CTA, generate action modal opens with toggle fields, view page displays infolist sections with correct data, expire action requires confirmation and updates status in
tests/Feature/ReviewPack/ReviewPackResourceTest.php - T024 [P] [US1] Create widget Livewire tests: no-pack state shows generate CTA, ready state shows download + generate actions, generating state shows in-progress message, failed state shows retry action, expired state shows generate action in
tests/Feature/ReviewPack/ReviewPackWidgetTest.php
Checkpoint: User Story 1 fully functional — can generate, download, view, and manage review packs
Phase 4: User Story 2 — Fingerprint Dedupe (Priority: P2)
Goal: Prevent duplicate pack generation; reuse existing ready pack with same fingerprint; reject concurrent generation for same tenant.
Independent Test: Trigger generation twice with identical inputs, assert only one ready ReviewPack row and one file on disk.
Implementation for User Story 2
- T025 [US2] Add fingerprint dedupe integration tests: identical inputs → returns existing ready pack (no new row, no new file), active OperationRun for same tenant → rejection with "generation already in progress" notification, expired pack with same fingerprint → allows new generation (partial unique index excludes expired), different options (include_pii toggled) → new pack with different fingerprint, fingerprint computation is deterministic (same inputs → same hash) in
tests/Feature/ReviewPack/ReviewPackGenerationTest.php
Checkpoint: Fingerprint dedupe verified — duplicate requests handled correctly
Phase 5: User Story 3 — RBAC Enforcement (Priority: P2)
Goal: Non-members get 404 (deny-as-not-found), view-only members can list + download but not generate, manage members can generate + expire.
Independent Test: Test each role (no membership, REVIEW_PACK_VIEW, REVIEW_PACK_MANAGE) against each action (list, view, download, generate, expire) and assert correct HTTP status or Filament authorization result.
Implementation for User Story 3
- T026 [US3] Create
ReviewPackPolicywithviewAny,view,create,deletemethods mapping toREVIEW_PACK_VIEW(viewAny, view) andREVIEW_PACK_MANAGE(create, delete) capability checks via canonical registry; enforce workspace_id + tenant_id scope (non-member → null = 404 semantics) inapp/Policies/ReviewPackPolicy.php - T027 [US3] Create comprehensive RBAC enforcement tests: non-member → 404 on list page, non-member → 404 on view page, non-member → 404 on download route; REVIEW_PACK_VIEW member → list succeeds, view succeeds, download signed URL succeeds, generate action hidden/403; REVIEW_PACK_MANAGE member → generate succeeds, expire succeeds, download succeeds; signed URL without valid signature → 403; expire action shows requiresConfirmation dialog before execution in
tests/Feature/ReviewPack/ReviewPackRbacTest.php
Checkpoint: RBAC enforcement verified across all surfaces (resource, controller, widget)
Phase 6: User Story 4 — Retention & Prune (Priority: P3)
Goal: Auto-expire packs past retention deadline, delete storage files, optionally hard-delete DB rows after grace period. Clean up dead sla_due AlertRule option.
Independent Test: Create ReviewPack with expires_at in past, run prune command, verify status=expired and file deleted from exports disk.
Implementation for User Story 4
- T028 [US4] Create
PruneReviewPacksCommandwith signaturetenantpilot:review-pack:prune {--hard-delete}: query ready packs where expires_at < now → set status=expired + delete file from exports disk; when --hard-delete: query expired packs where updated_at < now - grace_days → hard-delete rows; output summary "{n} packs expired, {m} packs hard-deleted" inapp/Console/Commands/PruneReviewPacksCommand.php - T029 [US4] Add prune command schedule entry
tenantpilot:review-pack:prunewithdaily()+withoutOverlapping()inroutes/console.php - T030 [P] [US4] Remove or hide
sla_dueevent_type option from AlertRuleResource form dropdown (keep EVENT_SLA_DUE constant on model for backward compatibility) inapp/Filament/Resources/AlertRuleResource.php - T031 [US4] Create prune tests: past retention → status=expired + file deleted from disk; future retention → unaffected; --hard-delete past grace_days → rows removed from DB; --hard-delete within grace_days → rows kept; command output includes correct counts; AlertRule form no longer shows sla_due option in dropdown in
tests/Feature/ReviewPack/ReviewPackPruneTest.php
Checkpoint: Retention automation verified — packs expire and files cleaned up automatically on schedule
Phase 7: User Story 5 — Scheduled Scan Wiring (Priority: P3)
Goal: Ensure daily permission-posture and Entra admin-roles scans are wired in the scheduler so review packs consume fresh cached data.
Independent Test: Assert the Laravel console schedule contains the expected dispatch entries with at least daily frequency.
Implementation for User Story 5
- T032 [US5] Verify existing Entra admin roles schedule closure in
routes/console.phpruns daily; add TODO comment fortenantpilot:posture:dispatchcommand (creation deferred per research.md — command infrastructure does not yet exist; FR-015 partially fulfilled) - T033 [US5] Create schedule assertion test: Entra admin roles dispatch appears with daily frequency; document posture:dispatch deferral as explicit test skip with rationale in
tests/Feature/ReviewPack/ReviewPackScheduleTest.php
Checkpoint: Scheduled scan wiring verified or documented as deferred
Phase 8: Polish & Cross-Cutting Concerns
Purpose: Code quality, formatting, and end-to-end validation
- T034 [P] Run Pint formatter on all new and modified files (
vendor/bin/sail bin pint --dirty --format agent) - T035 Run full ReviewPack test suite and fix any failures (
vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/) - T036 Validate quickstart.md scenarios end-to-end: create tenant with stored reports + findings, generate pack, verify ZIP contents (7 files), download via signed URL, expire pack, run prune command, confirm file deleted
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — can start immediately
- Foundational (Phase 2): Depends on Phase 1 (migration must exist) — BLOCKS all user stories
- US1 (Phase 3): Depends on Phase 2 — delivers complete MVP
- US2 (Phase 4): Depends on Phase 3 (needs generate pipeline to test dedupe)
- US3 (Phase 5): Depends on Phase 3 (needs resource + controller to test RBAC)
- US4 (Phase 6): Depends on Phase 2 only (prune operates on model, not UI) — can parallel with US1
- US5 (Phase 7): Depends on Phase 2 only — can parallel with US1
- Polish (Phase 8): Depends on all desired phases being complete
User Story Dependencies
- US1 (P1): Core MVP — blocked only by Foundational phase
- US2 (P2): Tests dedupe behavior in generate pipeline — depends on US1
- US3 (P2): Tests RBAC across all surfaces — depends on US1
- US4 (P3): Prune command + schedule — can start after Foundational (parallel with US1 if desired)
- US5 (P3): Schedule wiring — can start after Foundational (parallel with US1 if desired)
Within Each User Story
- Service (T012) before Job (T013) — job calls service methods
- Controller (T015) + Route (T016) before Resource download action (T017)
- Resource (T017) before Pages (T018, T019)
- All implementations before their corresponding tests
- Service (T012) before Widget (T020)
Parallel Opportunities
- Phase 2: All tasks T002–T008 run in parallel (7 independent files)
- Phase 3: T014 (notification) parallels T013 (job); T020 (widget) parallels T017–T019 (resource/pages) after T012 (service) completes
- Phase 3 tests: T021–T024 all run in parallel (4 independent test files)
- Cross-phase: US4 (T028–T031) can parallel with US1 (T012–T024) if team capacity allows
- Cross-phase: US5 (T032–T033) can parallel with US1
Parallel Example: Foundational Phase
# Launch all parallel foundational tasks together:
T002: ReviewPackStatus enum → app/Support/ReviewPackStatus.php
T003: OperationRunType case → app/Support/OperationRunType.php
T004: Capabilities constants → app/Support/Auth/Capabilities.php
T005: BadgeDomain case → app/Support/Badges/BadgeDomain.php
T006: ReviewPackStatusBadge → app/Support/Badges/Mappers/ReviewPackStatusBadge.php
T007: exports disk config → config/filesystems.php
T008: review_pack config → config/tenantpilot.php
# Then sequentially (dependency chain):
T009: ReviewPack model → app/Models/ReviewPack.php (needs T002)
T010: ReviewPackFactory → database/factories/ReviewPackFactory.php (needs T009)
T011: Migrate + verify
Parallel Example: US1 Tests
# After all US1 implementations complete, launch test files in parallel:
T021: ReviewPackGenerationTest → tests/Feature/ReviewPack/ReviewPackGenerationTest.php
T022: ReviewPackDownloadTest → tests/Feature/ReviewPack/ReviewPackDownloadTest.php
T023: ReviewPackResourceTest → tests/Feature/ReviewPack/ReviewPackResourceTest.php
T024: ReviewPackWidgetTest → tests/Feature/ReviewPack/ReviewPackWidgetTest.php
Implementation Strategy
MVP First (User Story 1 Only)
- Complete Phase 1: Setup (migration)
- Complete Phase 2: Foundational (model, enums, config, badge)
- Complete Phase 3: User Story 1 (service, job, controller, resource, widget, tests)
- STOP and VALIDATE: Run
vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ - Deploy/demo if ready — full generate + download + UI works
Incremental Delivery
- Setup + Foundational → Foundation ready
- Add US1 → Generate + Download + UI works → Deploy MVP
- Add US2 → Dedupe verified → Add US3 → RBAC hardened → Deploy
- Add US4 → Retention automation → Add US5 → Schedule wiring → Deploy
- Polish → Final validation → Production-ready
Parallel Team Strategy
With multiple developers:
- Team completes Setup + Foundational together
- Once Foundational is done:
- Developer A: US1 (core pipeline + UI)
- Developer B: US4 (prune command — independent of UI)
- After US1 complete:
- Developer A: US2 + US3 (dedupe + RBAC tests)
- Developer B: US5 (schedule wiring)
- Polish phase together
Notes
- [P] tasks = different files, no dependencies on incomplete tasks
- [Story] label maps task to specific user story for traceability
- Tests are REQUIRED per project conventions (Pest Feature tests)
- FR-015 (
tenantpilot:posture:dispatch) partially deferred — command infrastructure does not yet exist (documented in research.md §7) - FR-016 (
sla_duecleanup) is a minor AlertRule form change in US4 phase - ReviewPackResource does NOT participate in global search (intentional, per plan.md Filament v5 contract §3)
- All destructive actions (Expire) use
->requiresConfirmation()per Filament v5 blueprint §9 - No custom frontend assets — standard Filament components only
- Commit after each task or logical group; stop at any checkpoint to validate independently