{ "task_id": "CODE-DEDUP-REPOSITORY-001", "summary": "Extract repeated repository boilerplate (ID extraction, array sanitization, unwrap/normalization) into shared support utilities to eliminate ~290 lines of duplication across 6+ repository files.", "assumptions": [ "MintyPHP DB layer returns nested arrays with table-name keys (e.g. $row['tenants']).", "All repositories use the same unwrap, unwrapList, extractIds, and sanitizePositiveIds patterns.", "A trait or static helper class is the right abstraction — no base repository class needed (repositories are thin)." ], "scope": { "in": [ "lib/Repository/Tenant/TenantRepository.php — unwrap, unwrapList, listIds extraction", "lib/Repository/Access/RoleRepository.php — unwrap, unwrapList, listIds extraction", "lib/Repository/Org/DepartmentRepository.php — unwrap, unwrapList, listIds extraction", "lib/Repository/Access/PermissionRepository.php — ID extraction patterns", "lib/Repository/Access/UserRoleRepository.php — ID sanitization", "lib/Repository/Org/UserDepartmentRepository.php — ID sanitization", "New file: lib/Repository/Support/RepositoryArrayHelper.php" ], "out": [ "Repository method signatures (public API unchanged)", "Repository SQL queries (not touched)", "Service layer changes", "Frontend changes" ] }, "guardrails": { "required_guard_ids": [ "GR-CORE-001", "GR-LANG-001", "GR-LANG-002", "GR-TEST-001", "GR-TEST-002" ], "required_quality_gate_ids": [ "QG-001", "QG-002", "QG-003", "QG-006" ] }, "success_criteria": [ { "id": "SC-001", "criterion": "A single RepositoryArrayHelper class exists in lib/Repository/Support/ providing extractIds(), sanitizePositiveIds(), unwrap(), and unwrapList() as static methods." }, { "id": "SC-002", "criterion": "All 6+ repository files use RepositoryArrayHelper instead of inline implementations. No duplicate unwrap/extractIds/sanitize logic remains." }, { "id": "SC-003", "criterion": "Repository public method signatures and return values are identical before and after refactoring (pure internal refactor)." }, { "id": "SC-004", "criterion": "PHPUnit, PHPStan, Architecture Contract, and CS checks pass green; zero new failures introduced by this task (pre-existing failures are out of scope)." } ], "implementation_steps": [ { "id": "S1", "title": "Create RepositoryArrayHelper", "description": "Create lib/Repository/Support/RepositoryArrayHelper.php with static methods: extractIds(array $rows, string $tableKey): array, sanitizePositiveIds(array $ids): array, unwrap(?array $row, string $tableKey): ?array, unwrapList(mixed $rows, string $tableKey): array.", "guard_refs": ["GR-CORE-001", "GR-LANG-001"] }, { "id": "S2", "title": "Write unit tests for RepositoryArrayHelper", "description": "Test all 4 methods with edge cases: empty arrays, null input, missing table keys, mixed types, duplicate IDs, negative IDs.", "guard_refs": ["GR-TEST-001", "GR-TEST-002"] }, { "id": "S3", "title": "Refactor TenantRepository", "description": "Replace inline unwrap/unwrapList/extractIds with RepositoryArrayHelper calls. Verify no signature changes.", "guard_refs": ["GR-CORE-001"] }, { "id": "S4", "title": "Refactor RoleRepository and PermissionRepository", "description": "Replace inline implementations with RepositoryArrayHelper calls.", "guard_refs": ["GR-CORE-001"] }, { "id": "S5", "title": "Refactor DepartmentRepository and UserDepartmentRepository", "description": "Replace inline implementations with RepositoryArrayHelper calls.", "guard_refs": ["GR-CORE-001"] }, { "id": "S6", "title": "Refactor UserRoleRepository", "description": "Replace inline ID sanitization with RepositoryArrayHelper::sanitizePositiveIds().", "guard_refs": ["GR-CORE-001"] }, { "id": "S7", "title": "Run all quality gates", "description": "QG-001 PHPUnit, QG-002 PHPStan, QG-003 Architecture, QG-006 CS check.", "guard_refs": ["GR-LANG-002"] } ], "tests": [ "tests/Repository/Support/RepositoryArrayHelperTest.php — new, covers all 4 static methods with edge cases", "tests/Architecture/CoreStarterkitContractTest.php — must stay green", "Existing service tests that call repository methods indirectly — must stay green" ], "acceptance_checks": [ "SC-001: File lib/Repository/Support/RepositoryArrayHelper.php exists with 4 public static methods", "SC-002: rg 'foreach.*\\$row.*\\[.*id.*\\]' lib/Repository/ returns only RepositoryArrayHelper, not inline implementations", "SC-003: git diff shows no changes to public method signatures in repository files", "SC-004: QG-001 + QG-002 + QG-003 + QG-006: zero new failures introduced by this task (pre-existing failures are out of scope)" ], "risks": [ { "risk": "Subtle behavioral difference in edge cases between inline and helper implementations", "mitigation": "Write comprehensive helper tests first (TDD). Run full QG-001 after each repository file change." }, { "risk": "Table key naming inconsistency across repositories", "mitigation": "Audit all unwrap calls first to confirm table key naming. The helper accepts the key as parameter, so naming differences are handled." } ] }