TenantAtlas/specs/141-shared-diff-presentation-foundation/plan.md
ahmido 0b5cadc234 feat: add shared diff presentation foundation (#170)
## Summary
- add a shared diff presentation layer under `app/Support/Diff` with deterministic row classification, summary derivation, and value stringification
- centralize diff-state badge semantics through `BadgeCatalog` with a dedicated `DiffRowStatusBadge`
- add reusable Filament diff partials, focused Pest coverage, and the full SpecKit artifact set for spec 141

## Testing
- `vendor/bin/sail artisan test --compact tests/Unit/Support/Diff/DiffRowStatusTest.php tests/Unit/Support/Diff/DiffRowTest.php tests/Unit/Support/Diff/DiffPresenterTest.php tests/Unit/Support/Diff/ValueStringifierTest.php tests/Unit/Badges/DiffRowStatusBadgeTest.php tests/Feature/Support/Diff/SharedDiffSummaryPartialTest.php tests/Feature/Support/Diff/SharedDiffRowPartialTest.php tests/Feature/Support/Diff/SharedInlineListDiffPartialTest.php`
- `vendor/bin/sail bin pint --dirty --format agent`

## Filament / Livewire Contract
- Livewire v4.0+ compliance: unchanged and respected; this feature adds presentation support only within the existing Filament v5 / Livewire v4 stack
- Provider registration: unchanged; no panel/provider changes were required, so `bootstrap/providers.php` remains the correct registration location
- Global search: unchanged; no Resource or global-search behavior was added or modified
- Destructive actions: none introduced in this feature
- Asset strategy: no new registered Filament assets; shared Blade partials rely on the existing asset pipeline and standard deploy step for `php artisan filament:assets` when assets change generally
- Testing coverage: presenter, DTOs, stringifier, badge semantics, summary partial, row partial, and inline-list partial are covered by focused Pest unit and feature tests

## Notes
- Spec checklist status is complete for `specs/141-shared-diff-presentation-foundation/checklists/requirements.md`
- This PR preserves specialized diff renderers and documents incremental adoption rather than forcing migration in the same change

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #170
2026-03-14 12:32:08 +00:00

276 lines
19 KiB
Markdown

# Implementation Plan: Shared Diff Presentation Foundation
**Branch**: `141-shared-diff-presentation-foundation` | **Date**: 2026-03-14 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/141-shared-diff-presentation-foundation/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
## Summary
Introduce a small presentation-only diff foundation that standardizes change-state semantics, summary badges, row rendering, inline list rendering, and value stringification across simple compare surfaces. The implementation stays inside the existing Laravel/Filament monolith, adds reusable support classes under `app/Support/Diff`, reuses centralized badge semantics via `BadgeCatalog`, delivers low-magic Blade partials under `resources/views/filament/partials/diff`, and preserves specialized diff screens such as the current normalized diff until follow-up specs adopt only the parts that fit.
## Technical Context
**Language/Version**: PHP 8.4.15 / Laravel 12
**Primary Dependencies**: Filament v5, Livewire v4.0+, Tailwind CSS v4
**Storage**: PostgreSQL via Laravel Sail for existing source records and JSON payloads; no new persistence introduced
**Testing**: Pest v4 feature and unit tests on PHPUnit 12
**Target Platform**: Laravel Sail web application with Filament admin panel at `/admin`
**Project Type**: Laravel monolith / Filament web application
**Performance Goals**: Presenter output is deterministic and lightweight for typical field-level compare surfaces, render-time logic stays server-side, no heavy client diffing is introduced, and inline list rendering remains readable for representative simple lists up to 25 items without additional network or JS cost
**Constraints**: No new routes, no Microsoft Graph calls, no database schema changes, no OperationRun usage, no authorization plane changes, no forced migration of specialized diff screens, and no generic full diff engine
**Scale/Scope**: One new shared support layer, one shared partial set, one internal contract artifact, lightweight developer guidance, and focused regression tests for presenter, stringifier, badge semantics, and shared Blade output
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: clarify what is “last observed” vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; tenant-context routes (/admin/t/{tenant}/...) are tenant-scoped; canonical workspace-context routes under /admin remain tenant-safe; non-member tenant/workspace access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
- Workspace isolation: non-member workspace access is 404; tenant-plane routes require an established workspace context; workspace context switching is separate from Filament Tenancy
- RBAC-UX: destructive-like actions require `->requiresConfirmation()` and clear warning text
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
- Ops-UX 3-surface feedback: if `OperationRun` is used, feedback is exactly toast intent-only + progress surfaces + exactly-once terminal `OperationRunCompleted` (initiator-only); no queued/running DB notifications
- Ops-UX lifecycle: `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`); context-only updates allowed outside
- Ops-UX summary counts: `summary_counts` keys come from `OperationSummaryKeys::all()` and values are flat numeric-only
- Ops-UX guards: CI has regression guards that fail with actionable output (file + snippet) when these patterns regress
- Ops-UX system runs: initiator-null runs emit no terminal DB notification; audit remains via Monitoring; tenant-wide alerting goes through Alerts (not OperationRun notifications)
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
- UI naming (UI-NAMING-001): operator-facing labels use `Verb + Object`; scope (`Workspace`, `Tenant`) is never the primary action label; source/domain is secondary unless disambiguation is required; runs/toasts/audit prose use the same domain vocabulary; implementation-first terms do not appear in primary operator UI
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer `recordUrl()` clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
- Filament UI UX-001 (Layout & IA): Create/Edit uses Main/Aside (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)); all fields inside Sections/Cards (no naked inputs); View uses Infolists (not disabled edit forms); status badges use BADGE-001; empty states have specific title + explanation + 1 CTA; max 1 primary + 1 secondary header action; tables provide search/sort/filters for core dimensions; shared layout builders preferred for consistency
- Filament v5 / Livewire v4 compliance: design stays within the existing Filament v5 panel and Livewire v4 runtime
- Provider registration (`bootstrap/providers.php`): unchanged because no new panel/provider work is introduced
- Global search resource rule: unchanged because this feature adds no Resource and no global-search behavior
- Asset strategy: shared Blade partials and badge mappings use existing panel assets only; deploy still relies on the existing `php artisan filament:assets` step when Filament assets change generally
Gate result before research: PASS.
- Inventory-first: PASS this feature only renders existing compare outputs and does not change the ownership boundary between inventory, snapshots, findings, restore previews, or baselines.
- Read/write separation: PASS the feature is presentation-only with no mutation flow.
- Graph contract path: PASS no Graph call is added.
- Deterministic capabilities: PASS no capability derivation changes are introduced.
- RBAC-UX planes and isolation: PASS existing authorization remains consumer-owned and unchanged.
- Workspace isolation: PASS the foundation renders only data already authorized by the consuming surface.
- RBAC-UX destructive confirmation: PASS / N/A no destructive action is introduced.
- RBAC-UX global search: PASS / N/A no global search behavior changes.
- Tenant isolation: PASS no tenant-scoped read/write rules change.
- Run observability and Ops-UX rules: PASS / N/A no `OperationRun`, queue, or remote workflow is introduced.
- Data minimization: PASS the support layer formats data passed in by consumers and does not fetch or expand raw source data.
- Badge semantics (BADGE-001): PASS shared diff-state badges will use centralized badge semantics instead of new inline mappings.
- Filament UI Action Surface Contract: PASS / N/A no Filament Resource, RelationManager, or Page action surface is added or changed in this feature.
- Filament UI UX-001: PASS / N/A the feature adds reusable partials, not a new screen-level layout.
## Project Structure
### Documentation (this feature)
```text
specs/141-shared-diff-presentation-foundation/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── shared-diff-presentation.openapi.yaml
├── checklists/
│ └── requirements.md
└── tasks.md
```
### Source Code (repository root)
```text
app/
├── Support/
│ ├── Diff/
│ │ ├── DiffRowStatus.php # NEW enum for shared change states
│ │ ├── DiffRow.php # NEW immutable row DTO
│ │ ├── DiffSummary.php # NEW summary DTO
│ │ ├── DiffPresentation.php # NEW required presenter result wrapper DTO
│ │ ├── DiffPresenter.php # NEW stateless simple-compare presenter
│ │ └── ValueStringifier.php # NEW shared display formatter
│ └── Badges/
│ ├── BadgeDomain.php # MODIFY if new diff-state domain(s) are added
│ ├── BadgeCatalog.php # MODIFY registration if new diff-state mapper(s) are added
│ └── Domains/
│ └── DiffRowStatusBadge.php # NEW centralized badge semantics for diff states
app/Filament/
├── Resources/
│ ├── FindingResource.php # FUTURE CONSUMER; likely untouched in this feature
│ ├── PolicyVersionResource.php # FUTURE CONSUMER; likely untouched in this feature
│ └── RestoreRunResource.php # FUTURE CONSUMER; likely untouched in this feature
resources/
└── views/
└── filament/
├── partials/
│ └── diff/
│ ├── summary-badges.blade.php # NEW reusable summary partial
│ ├── row.blade.php # NEW shared row entry partial
│ ├── row-changed.blade.php # NEW changed-row partial
│ ├── row-unchanged.blade.php # NEW unchanged-row partial
│ ├── row-added.blade.php # NEW added-row partial
│ ├── row-removed.blade.php # NEW removed-row partial
│ └── inline-list.blade.php # NEW simple list diff partial
└── infolists/
└── entries/
├── normalized-diff.blade.php # EXISTING specialized renderer, no migration in this feature
├── rbac-role-definition-diff.blade.php # EXISTING future adopter
├── assignments-diff.blade.php # EXISTING future adopter
└── scope-tags-diff.blade.php # EXISTING future adopter
docs/
└── ui/
└── shared-diff-presentation-foundation.md # NEW developer guidance for future consumers
tests/
├── Unit/
│ ├── Support/
│ │ └── Diff/
│ │ ├── DiffRowStatusTest.php # NEW enum/state coverage
│ │ ├── DiffRowTest.php # NEW DTO invariants coverage
│ │ ├── DiffPresenterTest.php # NEW presenter classification coverage
│ │ └── ValueStringifierTest.php # NEW formatting coverage
│ └── Badges/
│ └── DiffRowStatusBadgeTest.php # NEW centralized badge semantics coverage
└── Feature/
└── Support/
└── Diff/
├── SharedDiffSummaryPartialTest.php # NEW summary rendering coverage
├── SharedDiffRowPartialTest.php # NEW per-state row rendering coverage
└── SharedInlineListDiffPartialTest.php# NEW inline list rendering coverage
```
**Structure Decision**: Keep the feature inside the existing Laravel/Filament monolith. The implementation centers on a new `app/Support/Diff` presentation layer, centralized diff badge semantics in the existing badge system, reusable Blade partials under `resources/views/filament/partials/diff`, and focused Pest coverage without modifying consumer screens yet.
## Complexity Tracking
> No Constitution Check violations. No justifications needed.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| | | |
## Phase 0 — Research (DONE)
Output:
- `specs/141-shared-diff-presentation-foundation/research.md`
Key findings captured:
- Existing diff rendering is split across `normalized-diff`, `rbac-role-definition-diff`, `assignments-diff`, and `scope-tags-diff` Blade templates, with duplicated value formatting and state semantics.
- `VersionDiff` already emits simple `added`, `removed`, and `changed` buckets suitable for future adaptation into the shared presenter.
- `RestoreDiffGenerator` already produces policy-level preview summaries and per-policy diff payloads that can adopt the foundation later without changing restore business rules.
- The repo already centralizes status-like badge semantics through `BadgeCatalog`, making it the right path for shared diff-state badges.
- The current `normalized-diff` renderer contains script-aware and grouped diff logic that should remain specialized in this feature.
## Phase 1 — Design & Contracts (DONE)
Outputs:
- `specs/141-shared-diff-presentation-foundation/data-model.md`
- `specs/141-shared-diff-presentation-foundation/contracts/shared-diff-presentation.openapi.yaml`
- `specs/141-shared-diff-presentation-foundation/quickstart.md`
Design highlights:
- Add a presentation-only support layer under `app/Support/Diff` with immutable row and summary types, a small stateless presenter, and a reusable value stringifier.
- Reuse the existing badge catalog to centralize diff-state summary semantics instead of hardcoding colors in Blade.
- Deliver the UI through composable Blade partials under `resources/views/filament/partials/diff` so future consumers can adopt the foundation from existing `ViewEntry` and Blade workflows.
- Keep specialized renderers such as `normalized-diff` out of scope while making RBAC, assignments, scope tags, restore preview/results, and future baseline/evidence screens clear follow-on adopters.
## Phase 1 — Agent Context Update (DONE)
Run:
- `.specify/scripts/bash/update-agent-context.sh copilot`
## Phase 2 — Implementation Outline (tasks created in `/speckit.tasks`)
### Step 1 — Introduce shared presentation primitives
Goal: implement the shared row-state vocabulary, row/summary DTOs, and reusable stringifier that satisfy FR-001 through FR-009.
Changes:
- Create `DiffRowStatus` as the canonical presentation enum.
- Add immutable `DiffRow`, `DiffSummary`, and optional result-wrapper DTOs.
- Implement `ValueStringifier` for nulls, booleans, scalars, simple lists, and compact structured values.
Tests:
- Add unit coverage for enum values and DTO invariants.
- Add unit coverage for stringification across null, boolean, scalar, empty list, non-empty list, and compact associative arrays.
### Step 2 — Build the stateless simple-compare presenter
Goal: implement FR-003 through FR-007, FR-012, and FR-016 through FR-017.
Changes:
- Add `DiffPresenter` that accepts baseline/current associative data plus optional changed keys, labels, and metadata.
- Classify rows as unchanged, changed, added, or removed.
- Detect simple list-like values and prepare inline list fragments deterministically.
- Produce summary counts derived from the row collection.
Tests:
- Add unit coverage for unchanged, changed, added-only, removed-only, mixed-order, and list-value comparisons.
- Add unit coverage proving presenter output remains deterministic when optional labels or metadata are absent.
### Step 3 — Centralize diff-state badge semantics
Goal: satisfy FR-002, FR-010, FR-013, and BADGE-001 alignment.
Changes:
- Add a new badge domain and mapper for shared diff states if the existing badge catalog needs explicit support.
- Ensure shared summary and row partials consume centralized diff-state label and color semantics rather than inline mappings.
Tests:
- Add unit coverage for diff-state badge labels, colors, and unknown fallback behavior if applicable.
### Step 4 — Create shared Blade partials
Goal: implement FR-010 through FR-015.
Changes:
- Add summary badges partial with no-data fallback.
- Add shared row partials for changed, unchanged, added, and removed states.
- Add inline list diff partial for medium-sized simple lists.
- Ensure row rendering supports compact mode, dimmed unchanged content, semantic labels, and dark-mode-safe classes.
Tests:
- Add view-level tests for summary rendering with expected counts and fallback.
- Add view-level tests for each row state, inline list rendering, and a representative 25-item list fixture.
- Add explicit assertions that state is conveyed with text/icons, semantic labels, and logical reading order rather than color alone.
### Step 5 — Add lightweight developer guidance
Goal: implement FR-018 and make future adoption predictable.
Changes:
- Add concise developer-facing guidance explaining what the foundation is for, what it is not for, how to use the presenter, when to use the stringifier standalone, and when a specialized diff view should stay specialized.
- Keep the guidance lightweight and aligned with existing repo documentation norms.
Tests:
- No automated test required for prose itself; verify examples match actual class and partial names during implementation.
### Step 6 — Prepare consumer-ready regression safety
Goal: ensure the foundation is ready for follow-up specs without forcing migration now.
Changes:
- Keep existing diff consumers untouched unless a low-risk local reuse is needed for validation.
- Verify the new foundation does not require route, authorization, or persistence changes.
- Document early adopter targets: RBAC diff first, then assignments/scope tags, then baseline/evidence/simple compare surfaces.
Tests:
- Run focused Pest coverage for the new support layer and partials.
- Run Pint on dirty files through Sail during implementation.
## Constitution Check (Post-Design)
Re-check result: PASS.
- Livewire v4.0+ compliance: preserved because the feature remains within the existing Filament v5 and Livewire v4 stack.
- Provider registration location: unchanged; no panel or provider changes are introduced, so `bootstrap/providers.php` remains the correct registration location.
- Global-search resource rule: unchanged because no Resource or global-search behavior is added.
- Destructive actions and authorization: unchanged because this feature adds no mutation action surface; future consumers remain responsible for confirmation and server-side enforcement.
- Asset strategy: no new heavy assets; shared Blade partials rely on existing panel assets, and the existing deployment step for `php artisan filament:assets` remains sufficient.