TenantAtlas/specs/366-management-report-layout-branded-report-themes-v1/plan.md
ahmido f37056e1de feat: implement management report layout branded report themes (#437)
Implemented management report layout branded report themes as requested.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #437
2026-06-08 03:35:20 +00:00

381 lines
27 KiB
Markdown

# Implementation Plan: Spec 366 - Management Report Layout & Branded Report Themes v1
**Branch**: `366-management-report-layout-branded-report-themes-v1` | **Date**: 2026-06-08 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/366-management-report-layout-branded-report-themes-v1/spec.md`
**Note**: This plan is a preparation artifact only. It defines the implementation path and validation gate; it does not implement application code.
## Summary
Spec 366 productizes the existing authenticated Review Pack rendered-report route into a management-ready, profile-aware, print-friendly report with bounded text co-branding. It builds on the repo-real Spec 356 HTML report renderer and Spec 357 static profile/disclosure policy. The implementation should improve the existing rendered-report family, not create a new report engine, PDF stack, delivery workflow, customer portal, or theme persistence layer.
The narrow technical approach is:
1. Verify current report truth and capture a small current-layout audit.
2. Add tests first for derived theme/layout behavior, rendered output, disclosure preservation, print toolbar behavior, and browser screenshots.
3. Add a bounded derived report theme/layout contract only where it reduces controller/view duplication and improves testability.
4. Refactor the existing rendered-report Blade into profile-aware sections/partials only if that makes the report safer to review.
5. Keep all report truth derived from existing Review Pack, Environment Review, Evidence Snapshot, profile, and disclosure policy data.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Laravel Sail 1.x, Tailwind CSS 4.2.2 through the existing platform build
**Storage**: PostgreSQL via existing models only. No new table, migration, persisted report theme, or upload storage is planned.
**Testing**: Pest 4 Unit, Feature, and one bounded Browser smoke. Browser tests use Pest 4 browser testing and should assert no JavaScript errors/console logs.
**Validation Lanes**: fast-feedback, confidence, browser.
**Target Platform**: Laravel web application under `apps/platform`.
**Project Type**: Laravel + Filament app with a custom authenticated Blade report view under the existing Review Pack route family.
**Performance Goals**: Rendered report remains stored-data-only. No Graph/provider calls during report render.
**Constraints**: Disclosure policy is authoritative; branding cannot hide readiness/limitations; no raw JSON/secrets/default diagnostics; no new asset registration unless implementation proves it necessary.
**Scale/Scope**: One existing rendered-report route/view family and existing owner-surface links. No new IA, panel, or navigation.
## Repo Truth Captured During Prep
- Current branch after Spec Kit scaffold: `366-management-report-layout-branded-report-themes-v1`
- Baseline HEAD before prep edits: `6ac0913f feat: implement operations UI operator actions regression gate (#436)`
- Starting worktree: clean on `platform-dev`; after scaffold only the new Spec 366 artifacts are intended to change.
- No existing `specs/366-management-report-layout-branded-report-themes-v1/` directory or branch existed before preparation.
- Spec 356 and Spec 357 packages contain completed-task and validation signals and are treated as historical context only.
- Current package baseline from Laravel Boost:
- PHP 8.4.15
- Laravel 12.52.0
- Filament 5.2.1
- Livewire 4.1.4
- Pest 4.3.1
- PostgreSQL
Relevant implementation files discovered:
```text
apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php
apps/platform/app/Services/ReviewPackService.php
apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php
apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php
apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php
apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php
apps/platform/app/Filament/Resources/EnvironmentReviewResource.php
apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php
apps/platform/app/Filament/Resources/ReviewPackResource.php
apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php
apps/platform/resources/views/review-packs/rendered-report.blade.php
apps/platform/lang/en/localization.php
apps/platform/lang/de/localization.php
apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php
apps/platform/tests/Feature/ReviewPack/Spec357RenderedReportProfileTest.php
apps/platform/tests/Browser/Spec357ReportProfilesSmokeTest.php
apps/platform/tests/Unit/Support/ReviewPacks/Spec357ReportProfileRegistryTest.php
apps/platform/tests/Unit/Support/ReviewPacks/Spec357ReportDisclosurePolicyTest.php
```
Repo-specific baseline decisions:
- `ReviewPackRenderedReportController` already builds the rendered-report payload and currently owns base branding, hero, management summary, source metadata, and profile/disclosure integration.
- `ReportProfileRegistry` already defines `customer_executive`, `customer_technical`, `internal_msp_review`, `auditor_appendix`, and placeholder `framework_readiness`.
- `ReportDisclosurePolicy` already emits mandatory disclosures, warnings, blocking reasons, proof states, and appendix/technical-detail visibility decisions.
- `rendered-report.blade.php` already has a report toolbar outside `main.report-canvas`, print CSS that hides toolbar/screen-only controls, a cover/hero section, management summary, profile, limitations, evidence basis, appendix, and disclosure areas.
- Existing tests already prove some Spec 366-adjacent behavior, so implementation must avoid duplicating broad Spec 356/357 proof and instead add the missing layout/theme/productization checks.
## Candidate Selection Gate
- **Selected candidate**: Management Report Layout & Branded Report Themes v1.
- **Source**: Direct user-provided Spec 366 draft.
- **Why selected**: It is a bounded follow-through after the report renderer and profile/disclosure policy are repo-real.
- **Deferred alternatives**: scheduled delivery, compliance framework profiles, customer portal report consumption, native PDF, and AI/HITL report review are deferred because they depend on a stable management-ready report layout.
- **Completed-spec guardrail**: Specs 356 and 357 are completed/validated context only; they are not rewritten. No completed Spec 366 package existed.
- **Result**: PASS.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed customer-facing report surface.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- existing rendered Review Pack report URL generated by `ReviewPackService::generateRenderedReportUrl()`
- `ReviewPackRenderedReportController`
- `resources/views/review-packs/rendered-report.blade.php`
- existing report launch links on Environment Review and Review Pack detail surfaces
- **No-impact class, if applicable**: N/A.
- **Native vs custom classification summary**: existing custom Blade report view inside an authenticated Laravel/Filament app. Keep owner surfaces Filament-native; report canvas may stay custom because it is a print artifact.
- **Custom surface exception**: `UI-EX-001: Legitimate Custom Surface Exception` applies to the rendered-report canvas and toolbar/print treatment. The product reason is that the customer-facing governance report requires cover identity, readiness, KPI/decision strip, profile-aware appendix hierarchy, disclosure, and print behavior that ordinary CRUD/detail/table primitives do not express cleanly.
- **Smallest custom behavior**: one local Blade report canvas with bounded toolbar/print CSS, profile-aware section ordering, and text-only co-branding. No second renderer, general design system, native PDF stack, customer portal, theme editor, upload UI, or report-layout framework.
- **Standardized behavior retained**: authorization, workspace/environment entitlement, Review Pack launch links, owner surfaces, profile registry, disclosure policy, localization, and read-only action semantics remain on existing Laravel/Filament/Review Pack paths.
- **Custom surface proof**: Feature/Browser tasks must prove report hierarchy, disclosure visibility, toolbar-hidden print behavior, screenshots, accessibility/focus basics, no text overlap, no JavaScript/console errors, and UI coverage update or no-update rationale.
- **Shared-family relevance**: evidence/report viewer, status/readiness messaging, report toolbar, customer-safe disclosure, source metadata.
- **State layers in scope**: report payload, report view, print CSS, profile-specific section ordering, derived theme/co-branding slots.
- **Audience modes in scope**: customer executive, customer technical, internal MSP review, controlled auditor appendix.
- **Decision/diagnostic/raw hierarchy plan**: management decision content first; evidence/technical appendix second; raw/support data absent by default.
- **Raw/support gating plan**: no raw JSON or support payload in default report output. If existing technical details are rendered for internal/auditor profiles, keep them structured and non-secret.
- **One-primary-action / duplicate-truth control**: report toolbar remains outside canvas; report content itself stands alone with a single readable management story.
- **Handling modes by drift class or surface**: review-mandatory for any customer-facing disclosure, print CSS, appendix hierarchy, or branding change.
- **Repository-signal treatment**: update `ui-099-rendered-review-report.md` and/or coverage matrix if the implemented visual hierarchy materially changes the page contract; otherwise record a no-update rationale.
- **Special surface test profiles**: report-viewer / customer-facing artifact surface.
- **Required tests or manual smoke**: Feature route/output tests plus one bounded Browser smoke with screenshots.
- **Exception path and spread control**: custom report CSS remains local to the rendered report unless implementation proves a shared asset is required. No new Filament asset registration is planned.
- **Active feature PR close-out entry**: Guardrail + Smoke Coverage.
- **UI/Productization coverage decision**: material existing-page/report-surface change; coverage docs must be updated or explicitly closed out.
- **Coverage artifacts to update**: likely `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md`; maybe design coverage matrix if the surface classification changes.
- **No-impact rationale**: N/A.
- **Navigation / Filament provider-panel handling**: no panel provider or navigation change planned. Laravel 12 Filament providers remain in `apps/platform/bootstrap/providers.php`.
- **Screenshot or page-report need**: yes, screenshots under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: existing Review Pack rendered report, profile registry, disclosure policy, Review Pack service URL generation, Environment Review/Review Pack owner-surface report links, localization.
- **Shared abstractions reused**: `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackOutputReadiness`, `ReviewPackOutputResolutionGuidance`, `ReviewPackService`.
- **New abstraction introduced? why?**: One bounded derived report theme/layout contract is planned because Spec 366 needs a testable contract for text co-branding, layout mode, KPI strip, and print/report section metadata. The concrete implementation may be a `ReportThemeResolver` or a controller-local view-model if that is narrower.
- **Why the existing abstraction was sufficient or insufficient**: Existing profile/disclosure abstractions are sufficient for audience and safety. They do not define theme/co-branding slots, profile-specific section ordering, or KPI strip composition.
- **Bounded deviation / spread control**: The new shape must live inside `App\Support\ReviewPacks` or the existing controller payload; it must not become a cross-domain theme engine.
## 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**: N/A.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: N/A.
- **Exception path**: none. Existing Review Pack generation OperationRun behavior is out of scope.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: no.
- **Provider-owned seams**: none.
- **Platform-core seams**: report profile/audience, rendered report source metadata, evidence/readiness truth.
- **Neutral platform terms / contracts preserved**: workspace, managed environment, report, profile, audience, readiness, evidence basis, review pack.
- **Retained provider-specific semantics and why**: Existing stored report/pack sections may contain provider-specific evidence summaries where already allowed by profile/disclosure policy; no new provider coupling is added.
- **Bounded extraction or follow-up path**: none.
## Constitution Check
*GATE: Must pass before implementation. Re-check after design and before code merge.*
- Inventory-first: PASS. Report rendering consumes existing last-observed/released review and Review Pack truth.
- Read/write separation: PASS. Report route remains read-only. No restore/remediation/provider write action is introduced.
- Graph contract path: PASS. No Graph/provider calls during report render.
- Deterministic capabilities: PASS. Existing `REVIEW_PACK_VIEW` capability and policies remain authoritative.
- RBAC-UX: PASS. Existing cross-workspace/environment not-found semantics and missing-capability 403 stay in place.
- Workspace isolation: PASS. Existing Review Pack/Environment Review workspace/environment scope must remain.
- Destructive-like actions: PASS. No destructive or high-impact action is introduced.
- Global search: PASS. No resource global-search behavior changes.
- Run observability: PASS. No new OperationRun type or queue path.
- OperationRun start UX: PASS / N/A. No OperationRun start/link UX touched.
- Ops-UX lifecycle and summary counts: PASS / N/A.
- Data minimization: PASS. No secrets/raw provider payloads/default raw JSON in report output.
- Test governance: PASS. Unit, Feature, and Browser lanes are explicit and bounded.
- Proportionality: PASS. Bounded report theme/layout resolver is justified only for current report productization and remains derived/non-persisted.
- No premature abstraction: PASS with constraint. Do not create theme persistence, editor, or generalized rendering framework.
- Persisted truth: PASS. No new persisted truth.
- Behavioral state: PASS. No new status/state family.
- UI semantics: PASS. Directly map report profile/readiness/disclosure truth into layout.
- Shared pattern first: PASS. Use existing report profile/disclosure services first.
- Provider boundary: PASS. No provider-specific theme or platform-core leakage.
- V1 explicitness / few layers: PASS. Keep local and derived.
- Spec discipline / bloat check: PASS. Proportionality review is complete.
- Badge semantics: PASS. If status-like badges are changed, use existing safe report/status presentation rather than ad-hoc truth.
- Filament-native UI: PASS. Owner surfaces remain Filament-native; custom report canvas is covered by `UI-EX-001: Legitimate Custom Surface Exception` and remains bounded to the existing print/report artifact.
- UI/UX surface taxonomy: PASS. Rendered report surface classified.
- Decision-first operating model: PASS. Report first screen supports management decision.
- Audience-aware disclosure: PASS. Customer/internal/auditor profile hierarchy is explicit.
- Filament UI Action Surface Contract: PASS. No new Filament mutating actions; report toolbar actions are navigation/download/print only.
- UI/Productization coverage: PASS. Coverage update/no-update rationale required.
## Test Governance Check
- **Test purpose / classification by changed surface**:
- Unit: derived theme/layout/profile mapping.
- Feature: rendered-report route authorization, content, disclosure, ZIP invariance, print CSS, no provider calls.
- Browser: visual/smoke proof for profile variants, print view, and screenshot artifacts.
- **Affected validation lanes**: fast-feedback, confidence, browser.
- **Why this lane mix is the narrowest sufficient proof**: Most safety is server-rendered HTML/content and deterministic derived data. Browser proof is required only for real report hierarchy, print toolbar behavior, JS/console smoke, and screenshots.
- **Narrowest proving command(s)**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec366ManagementReportLayoutSmokeTest.php --compact`
- **Fixture / helper / factory / seed / context cost risks**: Reuse Spec357 rendered-report fixtures and helpers where possible. New helpers must be local to Spec366 files.
- **Expensive defaults or shared helper growth introduced?**: no.
- **Heavy-family additions, promotions, or visibility changes**: one explicit Browser smoke file only.
- **Surface-class relief / special coverage rule**: customer-facing report surface requires browser screenshots and no raw leakage checks.
- **Closing validation and reviewer handoff**: Re-run Spec356/Spec357/ReviewPack/EnvironmentReview regressions and verify no ZIP/download/authorization behavior changed.
- **Budget / baseline / trend follow-up**: none expected beyond one browser file.
- **Review-stop questions**:
- Does branding hide or weaken disclosure/readiness truth?
- Does any metric lack a repo-backed source?
- Does customer executive layout still show appendix too prominently?
- Does print hide toolbar and keep disclosure visible?
- Did implementation add persistence/upload/theme editor/native PDF/delivery scope?
- **Escalation path**: document-in-feature for deferred optional logo/accent; follow-up-spec for scheduled delivery, theme CRUD, PDF, portal, AI, or compliance reports.
- **Active feature PR close-out entry**: Guardrail + Smoke Coverage.
- **Why no dedicated follow-up spec is needed**: This is the dedicated layout/theme productization slice. Larger delivery/compliance/portal/AI items stay separate follow-ups.
## Project Structure
### Documentation (this feature)
```text
specs/366-management-report-layout-branded-report-themes-v1/
├── spec.md
├── plan.md
├── tasks.md
├── checklists/
│ └── requirements.md
└── artifacts/
└── screenshots/ # created during implementation/browser smoke
```
Implementation tasks will create a repo-truth map and may create a current-layout audit:
```text
specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md
specs/366-management-report-layout-branded-report-themes-v1/current-report-layout-audit.md
```
`repo-truth-map.md` is required by the implementation task list to lock the current report fields, repo-backed metrics, baseline commit, and any narrower controller-local theme decision. `current-report-layout-audit.md` is optional and should only be created when screenshots or manual review reveal concrete layout problems that guide implementation. Neither artifact is runtime code.
### Source Code (repository root)
Expected implementation paths:
```text
apps/platform/app/
├── Http/Controllers/ReviewPackRenderedReportController.php
├── Services/ReviewPackService.php
├── Support/ReviewPacks/
│ ├── ReportProfileRegistry.php
│ ├── ReportDisclosurePolicy.php
│ ├── ReviewPackOutputReadiness.php
│ ├── ReviewPackOutputResolutionGuidance.php
│ └── ReportThemeResolver.php # only if justified during implementation
└── Filament/Resources/
├── EnvironmentReviewResource.php
└── ReviewPackResource.php
apps/platform/resources/views/review-packs/
├── rendered-report.blade.php
└── partials/ # only if splitting reduces review risk
├── report-toolbar.blade.php
├── report-cover.blade.php
├── report-state-hero.blade.php
├── report-kpi-strip.blade.php
├── report-executive-summary.blade.php
├── report-appendix.blade.php
└── report-disclosure-footer.blade.php
apps/platform/lang/
├── en/localization.php
└── de/localization.php
apps/platform/tests/
├── Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php
├── Feature/ReviewPack/Spec366RenderedReportLayoutTest.php
└── Browser/Spec366ManagementReportLayoutSmokeTest.php
```
**Structure Decision**: Stay inside the existing Review Pack rendered-report family. Add no new app module, no new base folder outside existing conventions, and no new package.
## Data / Domain Model Implications
- No new persisted entity, table, enum, lifecycle state, queue family, or independent report artifact.
- Derived theme/layout data may include:
- `prepared_by`
- `prepared_for`
- `generated_by`
- `generated_at`
- `accent`
- `logo`
- `layout_mode`
- `kpi_strip`
- `section_order`
- `logo` and `accent` must stay null/default unless existing safe repo-backed fields are verified.
- KPI strip values must be derived only from existing report/review/evidence payloads.
## Implementation Phases
### Phase 0 - Repo Verification and Current Layout Audit
- Confirm Specs 356 and 357 are stable in the current branch.
- Re-read current controller/view/tests.
- Create `repo-truth-map.md` to record current fields, gaps, repo-backed metrics, optional branding fields, baseline commit, and any narrower controller-local theme decision.
- Create `current-report-layout-audit.md` only if browser screenshots or manual review reveal concrete layout problems that guide implementation.
- Stop if Spec 357 profile/disclosure behavior is absent or failing.
### Phase 1 - Tests First
- Add Unit tests for derived theme/layout contract or verify current controller payload if no new class is justified.
- Add Feature tests for route output, profile layout, co-branding, print CSS, disclosure preservation, no ZIP change, no provider call, no raw/localization leakage.
- Add Browser smoke for profile variants, print class/print behavior, screenshots, and no JS/console errors.
### Phase 2 - Theme/Layout Contract
- Add `ReportThemeResolver` or equivalent controller-local view-model shape only if justified by tests and current code.
- Use workspace and managed-environment names for text co-branding.
- Keep optional logo/accent null/default unless repo-backed fields already exist.
- Add layout-mode and section-order data derived from `ReportProfileRegistry`.
### Phase 3 - Rendered Report Layout Productization
- Update `ReviewPackRenderedReportController` to provide stable theme/layout/KPI data.
- Update or split `rendered-report.blade.php` into partials only where reviewability improves.
- Render management-ready first screen: cover, state hero, KPI strip, executive summary, key risks/decisions, evidence basis, accepted risks, next action, disclosure.
- Ensure appendix is secondary for `customer_executive` and more prominent for `auditor_appendix`.
- Keep toolbar outside report canvas and hidden in print.
### Phase 4 - Localization and Report Copy
- Add EN/DE keys for new dominant report copy only.
- Avoid raw localization keys in output.
- Keep action labels and source metadata consistent with existing Review Pack/report vocabulary.
### Phase 5 - UI Coverage, Browser Smoke, and Close-Out
- Run focused validation commands.
- Save screenshots under the Spec 366 artifact directory.
- Update UI audit coverage or record no-update rationale.
- Record no migrations/packages/env/queues/scheduler/storage/asset changes.
## Rollout and Deployment Considerations
- **Staging**: Validate rendered report profile variants, print behavior, and review-pack download unchanged.
- **Production**: No schema change expected. Deploy as ordinary app/view/test change.
- **Migrations**: none planned.
- **Environment variables**: none planned.
- **Queues / scheduler**: none planned.
- **Storage**: no new storage path except test/browser screenshot artifacts in the repo during implementation.
- **Filament assets**: no new registered Filament assets expected. If implementation registers assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`; otherwise existing deployment asset handling is unchanged.
- **Reverse proxy / SSL**: no impact.
- **Rollback/forward**: revert code/view/localization changes; no data migration rollback.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| Bounded derived report theme/layout resolver or equivalent view-model | Current-release report productization needs deterministic co-branding, layout, KPI, and profile ordering proof | Scattered controller/view variables would be harder to test and could let profile/disclosure/branding drift |
## Proportionality Review
- **Current operator problem**: MSPs need a professional, customer-safe, print-ready report artifact over existing Review Pack truth.
- **Existing structure is insufficient because**: It proves a report can render, but not yet that the report has a stable management layout/theme contract across profiles and print states.
- **Narrowest correct implementation**: Local derived theme/layout contract inside the Review Pack report family, rendered by the existing route/view.
- **Ownership cost created**: One small Unit test family, one Feature test file, one Browser smoke, localization keys, and optional partial files.
- **Alternative intentionally rejected**: persisted themes, logo upload, a generic report engine, native PDF, scheduled delivery, AI, or compliance report framework.
- **Release truth**: Current-release report productization.
## Filament v5 / Livewire v4 Output Contract
- **Livewire v4.0+ compliance**: The application uses Livewire 4.1.4; this spec does not introduce Livewire v3 APIs.
- **Provider registration location**: No panel provider change planned. Laravel 12 Filament providers remain registered in `apps/platform/bootstrap/providers.php`.
- **Global search**: No globally searchable resource is changed or enabled. If implementation unexpectedly touches `ReviewPackResource` or `EnvironmentReviewResource` global search, it must verify safe View/Edit pages or keep global search disabled.
- **Destructive/high-impact actions**: None introduced. Existing Review Pack download/render/print actions remain read-only. Any unexpected mutating action must stop implementation and update spec/plan first.
- **Asset strategy**: No new registered Filament assets planned; no new `filament:assets` deployment requirement unless implementation proves otherwise.
- **Testing plan**: Pest Unit/Feature/Browser tests as listed above, plus Spec356/Spec357/ReviewPack/EnvironmentReview regressions.
## Stop Conditions
Stop and update spec/plan before code implementation continues if:
- Spec 357 profile/disclosure policy is not present or not stable.
- A native PDF package, new dependency, persisted theme table, upload UI, or theme editor appears necessary.
- Report metrics require new data collection or a new source of truth.
- A customer portal, public link, scheduled delivery, approval workflow, or AI narrative becomes necessary.
- Branding cannot be implemented from existing workspace/managed-environment truth without new persisted fields.
- Any write/mutation action is proposed inside the report surface.