Files
breadcrumb-the-shire/agent-system/runs/MODULAR-MONOLITH-V1-001/execution-report.json
fs c364e2b46d feat: introduce module system and extract address book as first module (MODULAR-MONOLITH-V1-001)
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to
contribute routes, UI slots, search providers, layout context, session
lifecycle, and permissions. Modules are activated via config/modules.php or
APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions,
slot keys) cause a fail-fast with a clear error message.

Extract the address book from hardcoded Core integration points into the
first module (modules/addressbook/). The module provides:
- Aside icon-bar tab + People panel via UI slot system
- Global search resource via AddressBookSearchProvider
- Layout context data via AddressBookLayoutProvider
- Session lifecycle via AddressBookSessionProvider

Core cleanup removes address-book hardcodings from SearchSqlResourceProvider,
SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(),
and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic
(AddressBookService) remain in Core as they gate general user visibility.

Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture
tests verifying zero hardcoded address-book references in search/templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:43:25 +01:00

94 lines
12 KiB
JSON

{
"task_id": "MODULAR-MONOLITH-V1-001",
"plan_ref": "agent-system/runs/MODULAR-MONOLITH-V1-001/plan.json",
"status": "done",
"changed_files": [
{ "path": "lib/App/Module/ModuleManifest.php", "summary": "S1: Immutable value object for module manifest — validates module.php into typed properties." },
{ "path": "lib/App/Module/ModuleRegistry.php", "summary": "S1: Central registry — boot, deterministic sort, merge contributions, fail-fast on conflicts." },
{ "path": "config/modules.php", "summary": "S1+S7: Module activation config — addressbook enabled by default." },
{ "path": "lib/App/Module/Contracts/LayoutContextProvider.php", "summary": "S2: Interface for layout context contributions." },
{ "path": "lib/App/Module/Contracts/SessionProvider.php", "summary": "S2: Interface for session population/cleanup." },
{ "path": "lib/App/Module/Contracts/SearchResourceProvider.php", "summary": "S2: Interface for search resource contributions." },
{ "path": "lib/App/registerContainer.php", "summary": "S2: ModuleRegistry boot after Core registrars, stored in container, module registrars run." },
{ "path": "db/updates/2026-03-18-module-migrations.sql", "summary": "S3: Idempotent DDL for module_migrations tracking table." },
{ "path": "bin/module-migrate.php", "summary": "S3: CLI — discovers + applies pending module SQL migrations." },
{ "path": "bin/module-build.php", "summary": "S4: CLI — builds runtime PageRoot with symlinks, lock-file protected." },
{ "path": "config/router.php", "summary": "S4: Merges module routes via ModuleRegistry after core routes." },
{ "path": "web/index.php", "summary": "S4: AccessControl receives merged core + module public paths." },
{ "path": "lib/Support/helpers/app.php", "summary": "S5+S6+S9: Pre-resolves moduleSlots, LayoutContextProvider loop, removed hardcoded addressBook block + SearchItemMapperProvider::configure call." },
{ "path": "templates/partials/app-main-aside-icon-bar.phtml", "summary": "S5+S9: Module aside tabs from $layoutNav['moduleSlots'] with lurl() resolution. Hardcoded People tab removed." },
{ "path": "templates/partials/app-main-aside.phtml", "summary": "S5+S9: Module aside panels with details_storage/details_open_active. Hardcoded People panel (76 lines) + addressBook variables removed." },
{ "path": "lib/Support/SearchConfig.php", "summary": "S7: Module search provider bridge — resolves providers, delegates tenantScopeFilters/icon/listUrl/mapItem with resolveModuleProviders() before each lookup." },
{ "path": "lib/Service/Search/SearchDataService.php", "summary": "S7: Merges module search resources into preview and result pipelines." },
{ "path": "lib/Support/Search/SearchSqlResourceProvider.php", "summary": "S9: Removed address-book resource entry + tenant scope filter." },
{ "path": "lib/Support/Search/SearchUiMetaProvider.php", "summary": "S9: Removed address-book from ICONS, LIST_URL_KEYS, listUrl() match." },
{ "path": "lib/Support/Search/SearchItemMapperProvider.php", "summary": "S9: Removed address-book branches from mapPreviewItem() + mapResultItem(). Removed unused userAvatarService() + configure()." },
{ "path": "modules/addressbook/module.php", "summary": "S8: Address book module manifest — UI slots, search, layout, session providers. Permissions + assets owned by Core." },
{ "path": "modules/addressbook/templates/aside-people-panel.phtml", "summary": "S8+S9: Full People panel markup (moved from Core aside template)." },
{ "path": "lib/Module/AddressBook/Providers/AddressBookSearchProvider.php", "summary": "S8: SearchResourceProvider — address-book SQL, mapResultItem, listUrl." },
{ "path": "lib/Module/AddressBook/Providers/AddressBookLayoutProvider.php", "summary": "S8: LayoutContextProvider — contributes addressBook key to $layoutNav." },
{ "path": "lib/Module/AddressBook/Providers/AddressBookSessionProvider.php", "summary": "S8: SessionProvider — V1 placeholder populate() + clear()." },
{ "path": "tests/App/Module/ModuleManifestTest.php", "summary": "S1: 4 tests — manifest validation." },
{ "path": "tests/App/Module/ModuleRegistryTest.php", "summary": "S1: 14 tests — boot, ordering, conflicts, env, merged contributions." },
{ "path": "tests/Architecture/ModuleRegistryContractTest.php", "summary": "S10: 7 tests — container availability, module-on load, module-off empty, manifest validity, providers, contracts, conflicts." },
{ "path": "tests/Architecture/NoAddressBookHardcodingTest.php", "summary": "S10: 5 tests — grep-verified: no address-book in search infra, SearchConfig, layout context, icon-bar, aside panel." },
{ "path": "tests/Unit/Module/SearchProviderCollectionTest.php", "summary": "S10: 9 tests — provider direct tests, SearchConfig integration, tenant filter merge, listUrl delegation." },
{ "path": "tests/Unit/Module/LayoutContextProviderTest.php", "summary": "S10: 3 tests — addressBook key presence, empty session, integration with appBuildLayoutNavContext." }
],
"guard_evidence": [
{ "guard_id": "GR-CORE-001", "status": "pass", "evidence": "Module kernel in lib/App/Module/. Providers in lib/Module/AddressBook/Providers/. Templates read $layoutNav only. SearchConfig delegates via typed interface." },
{ "guard_id": "GR-CORE-002", "status": "pass", "evidence": "Module registrars only instantiated in registerContainer.php. Must implement ContainerRegistrar." },
{ "guard_id": "GR-CORE-005", "status": "pass", "evidence": "UI slot rendering checks $layoutAuth[$slotPermission]. Search resources check permission via PermissionService in SearchDataService." },
{ "guard_id": "GR-CORE-006", "status": "pass", "evidence": "AddressBookSearchProvider declares tenant_filter. SearchDataService applies tenant scope via existing pipeline." },
{ "guard_id": "GR-CORE-008", "status": "pass", "evidence": "Templates read from $layoutNav. No superglobals in new code except AddressBookSessionProvider::clear() (session management)." },
{ "guard_id": "GR-CORE-010", "status": "pass", "evidence": "db/updates/2026-03-18-module-migrations.sql uses CREATE TABLE IF NOT EXISTS." },
{ "guard_id": "GR-CORE-011", "status": "n/a", "evidence": "Module activation via config/env, not DB settings." },
{ "guard_id": "GR-CORE-012", "status": "n/a", "evidence": "No mutating actions added." },
{ "guard_id": "GR-SEC-001", "status": "n/a", "evidence": "No POST endpoints added." },
{ "guard_id": "GR-SEC-003", "status": "pass", "evidence": "AddressBookSearchProvider SQL uses parameterized queries via SearchDataService. bin/module-migrate.php executes trusted SQL." },
{ "guard_id": "GR-LANG-001", "status": "pass", "evidence": "MintyPHP\\App\\Module\\ → lib/App/Module/. MintyPHP\\Module\\AddressBook\\ → lib/Module/AddressBook/." },
{ "guard_id": "GR-LANG-002", "status": "pass", "evidence": "PHPStan level 5: 0 errors on full codebase." },
{ "guard_id": "GR-TEST-001", "status": "pass", "evidence": "38 new tests: ModuleManifest (4), ModuleRegistry (14), ModuleRegistryContract (7), NoAddressBookHardcoding (5), SearchProviderCollection (9), LayoutContextProvider (3). All pass in full suite." },
{ "guard_id": "GR-TEST-002", "status": "pass", "evidence": "Providers are stateless. ModuleRegistry::boot() is a pure static factory. No hidden global state in new code." },
{ "guard_id": "GR-UI-007", "status": "pass", "evidence": "Module panel template included via slot renderer in existing aside partial." },
{ "guard_id": "GR-UI-008", "status": "pass", "evidence": "Templates read from $layoutNav only, no app() calls. AuthzUiContractTest passes (26 tests, 396 assertions)." },
{ "guard_id": "GR-UI-009", "status": "pass", "evidence": "Module tabs use role=tab, aria-selected, aria-controls, aria-label — identical semantics." },
{ "guard_id": "GR-UI-010", "status": "pass", "evidence": "All labels in module template wrapped in t(). i18n keys verified present in de/en." },
{ "guard_id": "GR-UI-015", "status": "pass", "evidence": "No new t() keys — module uses existing keys (Address book, No departments) already in i18n files." },
{ "guard_id": "GR-UI-016", "status": "pass", "evidence": "Module panel rendered inside existing aside layout structure via slot system." }
],
"commands": [
{ "cmd": "docker compose exec php vendor/bin/phpunit (894 tests, 15233 assertions)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M (0 errors)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php (13 tests, 6614 assertions)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/ModuleRegistryContractTest.php (7 tests)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/NoAddressBookHardcodingTest.php (5 tests)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpunit tests/Unit/Module/SearchProviderCollectionTest.php (9 tests)", "result": "pass" },
{ "cmd": "docker compose exec php vendor/bin/phpunit tests/Unit/Module/LayoutContextProviderTest.php (3 tests)", "result": "pass" }
],
"quality_gate_results": [
{ "gate_id": "QG-001", "result": "pass", "notes": "894 tests, 15233 assertions, 0 failures. +24 new tests vs pre-migration baseline (870)." },
{ "gate_id": "QG-002", "result": "pass", "notes": "PHPStan level 5: 0 errors on full codebase." },
{ "gate_id": "QG-003", "result": "pass", "notes": "CoreStarterkitContractTest: 13 tests, 6614 assertions. No regressions." },
{ "gate_id": "QG-004", "result": "pass", "notes": "NoAddressBookHardcodingTest verifies: 0 address-book references in search infra, SearchConfig, layout context builder, icon-bar template, aside panel template." },
{ "gate_id": "QG-005", "result": "skipped", "notes": "Browser smoke test requires manual verification. No automated browser testing infrastructure." },
{ "gate_id": "QG-006", "result": "skipped", "notes": "composer not accessible in Docker container (open_basedir). Run locally: composer cs:check." }
],
"test_results": [
{ "name": "ModuleManifestTest (4 tests, 18 assertions)", "result": "pass" },
{ "name": "ModuleRegistryTest (14 tests, 29 assertions)", "result": "pass" },
{ "name": "ModuleRegistryContractTest (7 tests)", "result": "pass", "notes": "Includes module-off mode test via fresh ModuleRegistry::boot() with empty enabled list." },
{ "name": "NoAddressBookHardcodingTest (5 tests)", "result": "pass", "notes": "Grep-verified: search infra, SearchConfig, layout context, icon-bar, aside panel all clean." },
{ "name": "SearchProviderCollectionTest (9 tests)", "result": "pass", "notes": "Provider direct tests + SearchConfig integration + tenant filter + listUrl delegation. Container-guard against ThemeResolutionTest pollution." },
{ "name": "LayoutContextProviderTest (3 tests)", "result": "pass", "notes": "addressBook key presence, empty session, integration with appBuildLayoutNavContext." },
{ "name": "AuthzUiContractTest (26 tests, 396 assertions)", "result": "pass" },
{ "name": "CoreStarterkitContractTest (13 tests, 6614 assertions)", "result": "pass" },
{ "name": "Full PHPUnit Suite (894 tests, 15233 assertions)", "result": "pass" }
],
"open_items": [
"QG-005: Browser smoke test — manual verification needed in running app with APP_ENABLED_MODULES='addressbook'",
"QG-006: PHP CS check — run locally via 'composer cs:check' (Docker open_basedir blocks composer)",
"Pre-existing: ThemeResolutionTest overwrites global container in setUp() without restoring in tearDown() — causes test isolation issues for any test depending on app() after it runs"
]
}