TenantAtlas/specs/167-derived-state-memoization/plan.md
2026-03-28 15:57:45 +01:00

22 KiB

Implementation Plan: Request-Scoped Derived State and Resolver Memoization

Branch: 167-derived-state-memoization | Date: 2026-03-28 | Spec: /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/167-derived-state-memoization/spec.md Input: Feature specification from /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/167-derived-state-memoization/spec.md

Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.

Summary

Introduce one explicit request-scoped derived-state contract for deterministic presenter and resolver outputs that are currently recalculated multiple times per record and per surface during one HTTP or Livewire request. The first implementation slice will bind a per-request in-memory store inside the Laravel container, define a stable key contract, adopt the store behind ArtifactTruthPresenter, OperationUxPresenter, and RelatedNavigationResolver, converge existing local cache behavior where appropriate, and protect freshness with focused mutation-path and scope-safety tests.

Technical Context

Language/Version: PHP 8.4.15
Primary Dependencies: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing ArtifactTruthPresenter, OperationUxPresenter, RelatedNavigationResolver, AppServiceProvider, BadgeCatalog, BadgeRenderer, and current Filament resource/page seams
Storage: PostgreSQL unchanged; feature adds no persistence and relies on request-local in-memory state only
Testing: Pest 4 unit and feature tests, including focused Filament page/component coverage and mutation-path regression tests run through Laravel Sail
Target Platform: Laravel monolith web application in Sail locally and containerized Linux deployment in staging/production
Project Type: web application
Performance Goals: Covered deterministic derived-state families resolve at most once per request for the same family + record + variant + scope tuple; covered list/detail/widget pages keep DB-only render behavior and avoid repeated presenter/resolver fan-out in one render pass
Constraints: No cross-request caching, no new persistent summaries, no business-semantic changes, no RBAC drift, request-local reuse must work for both HTTP and Livewire requests, mutation freshness must be explicit, and covered surfaces must preserve current operator-visible meaning
Scale/Scope: One cross-cutting runtime contract, three covered derived-state families, representative adoption across review register, evidence overview, baseline snapshot, operation-run detail/list, and related-context surfaces serving dozens of rows and multi-section pages per request

Constitution Check

GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.

Principle Pre-Research Post-Design Notes
Inventory-first / snapshots-second PASS PASS No inventory or snapshot ownership semantics change; reuse only affects request-time derivation.
Read/write separation PASS PASS Default path is read-only. Mutation-path freshness is a guardrail, not a new write flow.
Graph contract path N/A N/A No Graph calls or config/graph_contracts.php changes.
Deterministic capabilities PASS PASS Capability semantics remain unchanged; any capability-sensitive derivation must key scope explicitly or bypass reuse.
Workspace + tenant isolation PASS PASS Request-local reuse is explicitly scoped and must not cross workspace, tenant, or request boundaries.
RBAC-UX authorization semantics PASS PASS No new policies or capabilities; tests must prove no 404/403 leakage through reused derived values.
Run observability / Ops-UX PASS PASS No new OperationRun family or feedback path; covered operation surfaces reuse guidance only.
Data minimization PASS PASS No additional persistence or log payloads; cached state lives only inside the current request.
Proportionality / no premature abstraction PASS WITH JUSTIFIED ABSTRACTION PASS WITH JUSTIFIED ABSTRACTION One new abstraction is introduced because three existing families already share the same repeated-cost shape and page-local caches are insufficient.
Persisted truth / behavioral state PASS PASS No new table, artifact, status, or reason family.
UI semantics / few layers PASS PASS The design sits below existing presenters and resolvers and must not create a second interpretation layer.
Badge semantics (BADGE-001) PASS PASS Existing badge catalogs and renderers remain the single source for visible state mappings.
Filament Action Surface Contract PASS PASS No action inventory or inspect-affordance change; only repeated derivation behind existing surfaces changes.
Filament UX-001 PASS PASS No layout redesign; current list/detail/widget structures stay intact.
Filament v5 / Livewire v4 compliance PASS PASS The design remains inside the existing Filament v5 + Livewire v4 stack with no legacy API introduction.
Provider registration location PASS PASS No panel or provider change; Laravel 11+ provider registration remains in bootstrap/providers.php.
Global-search hard rule PASS PASS No global-search behavior changes are proposed.
Asset strategy PASS PASS No new assets, no shared asset registration, and no new filament:assets requirement.

Phase 0 Research

Research outcomes are captured in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/167-derived-state-memoization/research.md.

Key decisions:

  • Use a dedicated per-request in-memory store bound through the Laravel container rather than static arrays or persistent caches.
  • Define one stable key contract around family, record identity, variant, and scope context rather than relying on model-object identity or page-local assumptions.
  • Integrate through the existing ArtifactTruthPresenter, OperationUxPresenter, and RelatedNavigationResolver entry points instead of introducing a new presentation framework.
  • Standardize row-safe and page-safe consumer seams on covered Filament surfaces so multiple closures can share one resolved family result.
  • Treat mutation freshness as an explicit invalidation or fresh-access rule rather than as an accidental side effect of current code order.
  • Validate behavior with focused derivation-count, scope-safety, and mutation-path tests instead of relying on ad hoc microbenchmarks.

Phase 1 Design

Design artifacts are created under /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/167-derived-state-memoization/:

  • data-model.md: runtime entities for the request-scoped store, key contract, supported family contract, and freshness policy
  • contracts/request-scoped-derived-state.logical.openapi.yaml: logical service contract for resolve, invalidate, and consumer-validation behavior
  • contracts/request-scoped-derived-state-key.schema.json: structural schema for deterministic key composition
  • quickstart.md: focused implementation and verification workflow

Design decisions:

  • The request-scoped store is a single narrow abstraction, not a generic caching platform.
  • The store is bound in the Laravel container per request and not persisted to cache stores or the database.
  • Existing family entry points remain the only place where covered derivations are resolved; adoption happens behind those seams.
  • Existing local caches such as FindingResource::$primaryRelatedEntryCache become convergence points, not a parallel long-term pattern.
  • Covered mutation flows must either invalidate affected keys or force a fresh derivation path after business-state changes.
  • Regression protection focuses on repeated-read elimination, cross-scope safety, and post-mutation freshness rather than on implementation trivia.

Project Structure

Documentation (this feature)

specs/167-derived-state-memoization/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   ├── request-scoped-derived-state.logical.openapi.yaml
│   └── request-scoped-derived-state-key.schema.json
├── checklists/
│   └── requirements.md
└── tasks.md

Source Code (repository root)

app/
├── Filament/
│   ├── Pages/
│   │   ├── Monitoring/
│   │   │   └── EvidenceOverview.php
│   │   ├── Operations/
│   │   │   └── TenantlessOperationRunViewer.php
│   │   └── Reviews/
│   │       └── ReviewRegister.php
│   └── Resources/
│       ├── BaselineSnapshotResource.php
│       ├── EvidenceSnapshotResource.php
│       ├── FindingResource.php
│       ├── OperationRunResource.php
│       ├── ReviewPackResource.php
│       ├── TenantReviewResource.php
│       └── PolicyVersionResource/
│           └── Pages/
│               └── ViewPolicyVersion.php
├── Providers/
│   └── AppServiceProvider.php
└── Support/
    ├── Navigation/
    │   └── RelatedNavigationResolver.php
    ├── OpsUx/
    │   └── OperationUxPresenter.php
    └── Ui/
        ├── DerivedState/
        │   ├── DerivedStateKey.php
        │   ├── DerivedStateFamily.php
        │   └── RequestScopedDerivedStateStore.php
        └── GovernanceArtifactTruth/
            └── ArtifactTruthPresenter.php

tests/
├── Feature/
│   ├── Filament/
│   │   ├── ReviewRegisterDerivedStateMemoizationTest.php
│   │   ├── EvidenceOverviewDerivedStateMemoizationTest.php
│   │   ├── OperationRunDerivedStateMemoizationTest.php
│   │   └── DerivedStateMutationFreshnessTest.php
│   ├── Guards/
│   │   └── DerivedStateConsumerAdoptionGuardTest.php
│   └── Navigation/
│       └── RelatedNavigationResolverMemoizationTest.php
└── Unit/
    └── Support/
        └── Ui/
            └── DerivedState/
                └── RequestScopedDerivedStateStoreTest.php

Structure Decision: Keep the existing Laravel monolith structure. Introduce the new runtime support types as a narrow support layer near the covered presenter/resolver families, bind them in AppServiceProvider, and adopt them through current Filament resources/pages rather than introducing new base directories or a broader platform package.

Implementation Strategy

Phase A — Introduce the Request-Scoped Store and Key Contract

Goal: Add one explicit per-request store with deterministic keys and no persistence.

Step File Change
A.1 app/Providers/AppServiceProvider.php Bind the new derived-state store with request-local lifecycle semantics and no cross-request persistence
A.2 app/Support/Ui/DerivedState/* Add the narrow store, key, and supported-family support types needed for resolve and invalidate behavior
A.3 Covered unit tests Verify hit/miss, negative-result reuse, distinct-variant separation, and explicit invalidation behavior

Phase B — Adopt Artifact Truth on Representative Surfaces

Goal: Route repeated artifact-truth resolution through the shared contract and expose row-safe/page-safe access on representative surfaces.

Step File Change
B.1 app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php Route forBaselineSnapshot(), forEvidenceSnapshot(), forTenantReview(), forReviewPack(), and forOperationRun() through the request-scoped store while preserving existing envelope semantics
B.2 app/Filament/Pages/Reviews/ReviewRegister.php Replace repeated per-closure forTenantReview() calls with one row-safe access path
B.3 app/Filament/Pages/Monitoring/EvidenceOverview.php Reuse one artifact-truth resolution per active snapshot row in the canonical evidence overview
B.4 app/Filament/Resources/BaselineSnapshotResource.php, app/Filament/Resources/BaselineSnapshotResource/Pages/ViewBaselineSnapshot.php, app/Filament/Resources/EvidenceSnapshotResource.php, app/Filament/Resources/TenantReviewResource.php, app/Filament/Resources/ReviewPackResource.php, and app/Filament/Resources/OperationRunResource.php Keep the first-slice baseline snapshot, evidence snapshot, tenant review, review pack, and operation-run helper consumers aligned to the shared presenter contract without changing visible meaning

Goal: Reuse operation guidance and related-context resolution through the same contract while converging existing hidden caches.

Step File Change
C.1 app/Support/OpsUx/OperationUxPresenter.php Route covered guidance/explanation reads through the request-scoped store
C.2 app/Support/Navigation/RelatedNavigationResolver.php Route primary, detail, and header entry resolution through the request-scoped store
C.3 app/Filament/Resources/OperationRunResource.php and app/Filament/Pages/Operations/TenantlessOperationRunViewer.php Reuse operation guidance and related context on run list/detail surfaces
C.4 app/Filament/Resources/FindingResource.php and app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php Replace page-local related-navigation repetition and align the existing finding-specific cache behavior with the shared contract

Phase D — Freshness Rules and Consumer Guardrails

Goal: Make post-mutation freshness explicit and prevent future heavy closures from bypassing the shared contract.

Step File Change
D.1 Covered mutating resource/page actions Document and implement invalidation or forced-fresh access where visible truth, guidance, or related navigation changes within the same request
D.2 Covered resources/pages Introduce clearly named row-safe or page-safe helper seams for repeated derived-state reads
D.3 specs/167-derived-state-memoization/quickstart.md, specs/167-derived-state-memoization/contracts/request-scoped-derived-state.logical.openapi.yaml, and tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php Document the future-family adoption path and add an automated guard that locks the supported-family, keying, access-pattern, and freshness rules so future surfaces cannot drift back to ad hoc local caches

Phase E — Regression Protection and Verification

Goal: Prove the first slice delivers one-derivation-per-request behavior without leaks or stale post-mutation state.

Step File Change
E.1 tests/Unit/Support/Ui/DerivedState/RequestScopedDerivedStateStoreTest.php Add core store and key behavior coverage
E.2 tests/Feature/Filament/ReviewRegisterDerivedStateMemoizationTest.php Prove repeated artifact-truth reads on one row resolve once per request
E.3 tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php Prove canonical evidence overview reuses one artifact-truth result per row without scope leakage
E.4 tests/Feature/Filament/OperationRunDerivedStateMemoizationTest.php and tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php Prove operation-guidance and related-navigation reuse plus authorization safety
E.5 tests/Feature/Filament/DerivedStateMutationFreshnessTest.php Prove covered post-mutation truth and navigation follow explicit fresh-derivation rules within the same request
E.6 tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php Fail CI with actionable output if a covered family introduces an ad hoc local cache or adopts the shared store without explicit declaration metadata
E.7 vendor/bin/sail bin pint --dirty --format agent and focused Pest runs Required formatting and targeted verification before implementation completion

Key Design Decisions

D-001 — Request-local reuse must be explicit, not hidden in page-local static arrays

The repo already shows local cache behavior in isolated places, but the repeated-cost problem spans three families and multiple surfaces. One explicit request-scoped contract is narrower and safer than multiplying hidden static caches.

D-002 — Keys must include scope context, not just record identity

Model ID alone is insufficient because some derived values depend on route context, surface variant, or capability-sensitive visibility. The key contract must include scope-sensitive inputs or the family must bypass reuse.

D-003 — Adoption should happen behind existing presenters and resolvers, not above them

The feature exists to reduce duplicate work beneath already-correct semantics. Replacing the current presenters or adding a new presentation meta-framework would violate the spec's bounded intent.

D-004 — Row-safe and page-safe helper seams are the UI-level adoption point

Most repeated work comes from multiple closures on the same list row or detail section. The consumer-side seam should therefore be one named row-safe or page-safe accessor rather than another layer of inline app() calls.

D-005 — Mutation freshness is part of the contract, not an afterthought

Request-local reuse is only safe if covered post-action flows either invalidate affected keys or force a fresh derivation path. Blanket caching to request end would create stale-state ambiguity.

Risk Assessment

Risk Impact Likelihood Mitigation
Incorrect key composition leaks state across variants or scopes High Medium Include scope/context fields in the key contract and add explicit cross-tenant/workspace regression tests
Overbroad first slice creates a large diff with mixed concerns Medium Medium Keep adoption to representative list/detail/canonical surfaces in the three covered families
Mutation flows accidentally reuse stale pre-action values High Medium Define family-specific freshness policy and add at least one mutation-path regression per covered family
Existing local caches continue living in parallel with the shared contract Medium Medium Converge finding-specific cache behavior into the shared contract and document page-local cache exceptions as temporary only
New contract becomes a general cache framework over time Medium Low Keep support types narrow, document non-goals, and require future families to prove deterministic fit before adoption

Test Strategy

  • Add one narrow unit suite for the store and key contract instead of snapshotting every consuming presenter branch.
  • Add focused Filament feature tests for representative list/detail/canonical surfaces where repeated closure calls exist today.
  • Prove business-visible non-regression by asserting current labels, badge properties, next-action text, and related URLs remain unchanged on covered examples.
  • Add scope-safety tests that exercise tenant-context and canonical workspace-context reuse without leaking unauthorized tenant state, including at least one explicit deny-as-not-found regression for non-members or wrong-scope users and one explicit forbidden regression for in-scope users lacking capability.
  • Add a dedicated post-mutation freshness suite in tests/Feature/Filament/DerivedStateMutationFreshnessTest.php to prove stale pre-action truth, guidance, or related navigation is not reused after business state changes in the same request.
  • Keep the future-family adoption path documented in quickstart.md and the logical contract so new presenter or resolver families declare supported family, scope inputs, access pattern, and freshness behavior before using the shared store.
  • Add a lightweight guard test in tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php that scans covered source paths and fails with file-and-snippet output when new ad hoc local caches or undeclared adoption patterns appear.
  • Keep Livewire v4-compatible page tests for covered pages and use the minimum focused Sail test set needed for implementation verification.

Complexity Tracking

Violation Why Needed Simpler Alternative Rejected Because
New request-scoped derived-state abstraction Three existing families already exhibit the same deterministic repeated-cost pattern across shipped surfaces, and the contract must enforce scope safety plus freshness rules consistently Page-local static caches and one-off helper memoization hide scope boundaries, duplicate logic, and cannot provide one enforceable adoption path

Proportionality Review

  • Current operator problem: Covered pages recompute the same deterministic truth, guidance, and related-navigation state multiple times in one request, increasing render cost and making consistency across closures harder to defend.
  • Existing structure is insufficient because: Existing presenters and resolvers are correct but have no explicit request-local reuse boundary, so every closure and page fragment can trigger a full derivation again.
  • Narrowest correct implementation: One request-scoped store plus one stable key contract beneath the existing families is enough to remove repeated work without persistence, without semantic changes, and without a new UI meta-framework.
  • Ownership cost created: The codebase gains one bounded runtime abstraction, adoption work on covered consumers, and focused tests for keying, scope safety, and freshness.
  • Alternative intentionally rejected: Static arrays, ad hoc per-page caches, and persistent cache stores were rejected because they either obscure scope semantics or solve the wrong problem layer.
  • Release truth: Current-release truth. The hotspot exists today on Review Register, Evidence Overview, Baseline Snapshot, Operation Run, and related navigation surfaces.