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

5.4 KiB

Research: Request-Scoped Derived State and Resolver Memoization

Decision 1: Use a dedicated request-scoped in-memory store bound through the Laravel container

  • Decision: Introduce one dedicated request-scoped derived-state store with request-local lifecycle semantics instead of static arrays or persistent cache stores.
  • Rationale: The feature needs explicit reuse within one HTTP or Livewire request and explicit isolation across requests. A request-local container binding makes that boundary visible and testable while avoiding new persistence and avoiding cross-request staleness.
  • Alternatives considered:
    • Static caches inside presenters or resources: rejected because they hide scope boundaries, duplicate behavior across families, and make invalidation inconsistent.
    • Cache::remember() or Redis-backed caching: rejected because the spec explicitly excludes cross-request caching and because stale semantic reuse would become much harder to reason about.

Decision 2: Key derivations by family, record identity, variant, and scope-sensitive context

  • Decision: Define one deterministic key contract that includes the derived-state family, stable record identity, variant or surface mode, and any workspace, tenant, or visibility-sensitive context required to produce the correct result.
  • Rationale: Existing repeated work happens because the same deterministic question is asked multiple times. Correct reuse therefore depends on a stable definition of “same question.” Model-object identity alone is insufficient because the same record can appear through different model instances or under different scopes.
  • Alternatives considered:
    • Model ID only: rejected because it cannot distinguish list vs detail variants or capability-sensitive outputs.
    • spl_object_hash() only: rejected because it prevents convergence across separate model instances representing the same record.

Decision 3: Integrate through the existing family entry points, not by adding a new presentation framework

  • Decision: Route reuse through ArtifactTruthPresenter, OperationUxPresenter, and RelatedNavigationResolver entry points instead of creating a generic presenter base class, a universal decorator, or a new UI taxonomy layer.
  • Rationale: The business semantics already live in these families. The feature's goal is to reduce repeated deterministic work beneath them, not to redesign how operator meaning is modeled.
  • Alternatives considered:
    • New cross-domain presentation framework: rejected because it would layer new semantics on top of already-correct families and violate the spec's narrow foundation intent.
    • Surface-only fixes per page: rejected because the same repeated-cost pattern already spans multiple domains and would continue to reappear elsewhere.

Decision 4: Converge existing hidden caches into the shared contract and keep negative results reusable

  • Decision: Standardize existing local request-like caches, such as the finding primary related-entry cache, behind the shared contract and allow deterministic negative results like “no related entry” or “no next action” to be reused within one request.
  • Rationale: The repo already contains evidence that request-local reuse is useful, but it is unevenly applied. Converging on one contract avoids parallel caching patterns and still prevents repeated work when the correct result is the absence of a link or action.
  • Alternatives considered:
    • Leave existing local caches untouched and optimize only the worst pages: rejected because it would preserve multiple hidden patterns and make future adoption harder.
    • Cache only positive results: rejected because deterministic negative results can also drive repeated work and should be equally reusable when scope-stable.

Decision 5: Treat mutation freshness as an explicit family rule

  • Decision: Covered mutating flows must explicitly invalidate or bypass request-local reuse after business state changes within the same request.
  • Rationale: The spec requires “no stale within request ambiguity.” A clear family-level freshness rule is safer than assuming the existing code path order will always avoid stale derived values.
  • Alternatives considered:
    • Cache for the full request without exceptions: rejected because post-action state could become stale and misleading.
    • Disable reuse on every Livewire action: rejected because many actions still have deterministic pre- and post-action read phases where request-local reuse remains valuable.

Decision 6: Test derivation-count behavior directly instead of proxying everything through query-count assertions

  • Decision: Validate the feature with focused unit and feature tests that prove one full derivation per request for representative covered families, plus explicit scope-safety and mutation-path tests.
  • Rationale: The repeated-cost problem is not just SQL chatter. It is repeated presenter and resolver work across closures and page fragments. Query-count assertions alone would miss important non-query work and would not prove freshness rules.
  • Alternatives considered:
    • Measure only query counts: rejected because the problem is broader than SQL and includes repeated in-memory translation and navigation assembly.
    • Rely on manual profiling only: rejected because this feature needs regression protection against future local cache drift.