## 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
19 KiB
Implementation Plan: Shared Diff Presentation Foundation
Branch: 141-shared-diff-presentation-foundation | Date: 2026-03-14 | Spec: 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/*withoutOperationRun - Ops-UX 3-surface feedback: if
OperationRunis used, feedback is exactly toast intent-only + progress surfaces + exactly-once terminalOperationRunCompleted(initiator-only); no queued/running DB notifications - Ops-UX lifecycle:
OperationRun.status/OperationRun.outcometransitions are service-owned (only viaOperationRunService); context-only updates allowed outside - Ops-UX summary counts:
summary_countskeys come fromOperationSummaryKeys::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:assetsstep 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)
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)
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, andscope-tags-diffBlade templates, with duplicated value formatting and state semantics. VersionDiffalready emits simpleadded,removed, andchangedbuckets suitable for future adaptation into the shared presenter.RestoreDiffGeneratoralready 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-diffrenderer 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.mdspecs/141-shared-diff-presentation-foundation/contracts/shared-diff-presentation.openapi.yamlspecs/141-shared-diff-presentation-foundation/quickstart.md
Design highlights:
- Add a presentation-only support layer under
app/Support/Diffwith 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/diffso future consumers can adopt the foundation from existingViewEntryand Blade workflows. - Keep specialized renderers such as
normalized-diffout 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
DiffRowStatusas the canonical presentation enum. - Add immutable
DiffRow,DiffSummary, and optional result-wrapper DTOs. - Implement
ValueStringifierfor 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
DiffPresenterthat 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.phpremains 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:assetsremains sufficient.