diff --git a/docs/audit/PHASE8_AUDIT.md b/docs/audit/PHASE8_AUDIT.md new file mode 100644 index 0000000..fe225bc --- /dev/null +++ b/docs/audit/PHASE8_AUDIT.md @@ -0,0 +1,69 @@ +# PHASE 8 AUDIT: Pagination & Count Semantics + +## 1. Scope & Objectives +**Phase:** Phase 8 (Pagination & Count Semantics) +**Focus:** `paginate()`, `count()`, `PaginationResultDTO` consistency across MySQL, MongoDB, and Redis. +**Sources:** `docs/adr/ADR-010.md`, `docs/adr/ADR-011.md`, `src/Generic/*`. + +## 2. ADR Compliance Matrix + +| Component | Requirement | Status | Notes | +|-----------|-------------|--------|-------| +| **ADR-010** | Return `PaginationResultDTO` only | ✅ PASS | All `paginate*` methods return strict DTO. | +| **ADR-010** | Structure (items, total, page, perPage, pages) | ✅ PASS | Handled via `PaginationHelper::buildMeta` and DTO constructor. | +| **ADR-010** | Deterministic (`pages = ceil(total/perPage)`) | ✅ PASS | Centralized in `PaginationHelper`. | +| **ADR-011** | `count()` is filter-scoped | ✅ PASS | Redis implementation correctly fails fast for unsupported filtered counts. | +| **ADR-011** | Redis Safety (SCAN-based, Bounded) | ✅ PASS | `RedisOps::keys` enforces safety limits. | +| **Phase 6** | `LimitOffsetValidator` Usage | ✅ PASS | Validated in all `paginate*` methods. | + +## 3. Findings + +### 3.1. Redis Filtered Count (Documented Limitation) +**Observation:** +`GenericRedisRepository::count(array $filters)` throws `RepositoryException` when filters are provided, while `paginateBy($filters)` successfully returns a total. + +**Rationale:** +Implementing `count($filters)` via `findBy($filters)` (scanning and filtering all keys) encourages inefficient and dangerous usage patterns on Redis. While `paginateBy` *must* perform this operation to fulfill the pagination contract, exposing it as a standalone `count` method implies a lightweight operation that does not exist in Redis. + +**Compliance:** +This adheres to ADR-011's requirement: +> "If the operation exceeds safety limits [or capabilities]: The method **must fail fast**... Silent partial counts are **forbidden**." + +The exception is the correct, safe behavior. The fact that `PaginationResultDTO.total` is available during pagination is a side-effect of the necessary in-memory processing for that specific context, not a guarantee that `count($filters)` is globally supported. + +### 3.2. Missing Documentation +The current documentation does not explicitly state that `count($filters)` is unsupported for Redis, nor does it explain the nuance of `PaginationResultDTO.total` in this context. This could lead to developer confusion. + +## 4. Verdict + +**Status:** ⚠️ **BLOCKED (Documentation Required)** + +The implementation is correct and safe, but the specific limitations of Redis `count()` must be explicitly documented to prevent misuse and clarify the API contract. + +--- + +## 5. Blueprint for Remediation + +### Goal +Document the Redis-specific limitations for `count()` and the behavior of `PaginationResultDTO.total`. + +### Action Plan +1. **Do NOT modify source code.** The current exception in `GenericRedisRepository::count` is correct. +2. **Update `docs/README.full.md`** (or create `docs/phases/README.phase8.md` if preferred) with the following clarifications. + +### Suggested Documentation + +**Location:** `docs/README.full.md` -> **Redis Repository Behavior** section (or similar). + +> ### ⚠️ Redis `count()` Limitations +> +> Unlike MySQL or MongoDB, the **Redis Repository does NOT support filtered counts**. +> +> * `count()` (no arguments) returns the total number of keys matching the repository prefix. +> * `count(['status' => 'active'])` will **throw a RepositoryException**. +> +> **Why?** +> Redis is a key-value store, not a relational database. Counting filtered items requires fetching and deserializing *every* item in the repository, which is a high-cost operation. +> +> **Note on Pagination:** +> When using `paginateBy($filters)`, the `PaginationResultDTO` **will** contain a correct `total`. This is because pagination in Redis *already* incurs the cost of fetching and filtering items to determine the result set. This `total` is a byproduct of that expensive operation and is provided for convenience within the pagination context only. Do not rely on `count($filters)` for standalone metrics.