# 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.