feat: module architecture improvements — session keys, dependency graph, event dispatcher, deactivation hooks

Six targeted improvements to the modular monolith platform:

1. Fix AddressBook session key prefix to follow module.<id>.* convention
2. Move ADDRESS_BOOK_VIEW permission constant from core PermissionService into module
3. Add declarative JSON schema for module manifests (.agents/contracts/)
4. Add `requires` field with missing-dependency and circular-dependency detection
5. Add lightweight fire-and-forget event dispatcher (user.created/deleted/login/logout)
6. Add module deactivation hook interface and CLI script (bin/module-deactivate.php)

Includes 15 new architecture/unit tests covering all new functionality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:20:13 +01:00
parent 866d43e15a
commit ef72b34c40
31 changed files with 1041 additions and 75 deletions

View File

@@ -143,6 +143,57 @@ final class ModuleRegistryContractTest extends TestCase
);
}
public function testMissingDependencyThrowsException(): void
{
$fixturesDir = sys_get_temp_dir() . '/module-dep-test-' . uniqid();
mkdir($fixturesDir . '/mod-dep', 0777, true);
file_put_contents($fixturesDir . '/mod-dep/module.php', '<?php return ' . var_export([
'id' => 'mod-dep',
'requires' => ['nonexistent-module'],
], true) . ';');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("requires module 'nonexistent-module' which is not enabled");
try {
ModuleRegistry::boot($fixturesDir, ['mod-dep']);
} finally {
@unlink($fixturesDir . '/mod-dep/module.php');
@rmdir($fixturesDir . '/mod-dep');
@rmdir($fixturesDir);
}
}
public function testCircularDependencyThrowsException(): void
{
$fixturesDir = sys_get_temp_dir() . '/module-circular-test-' . uniqid();
mkdir($fixturesDir . '/mod-x', 0777, true);
mkdir($fixturesDir . '/mod-y', 0777, true);
file_put_contents($fixturesDir . '/mod-x/module.php', '<?php return ' . var_export([
'id' => 'mod-x',
'requires' => ['mod-y'],
], true) . ';');
file_put_contents($fixturesDir . '/mod-y/module.php', '<?php return ' . var_export([
'id' => 'mod-y',
'requires' => ['mod-x'],
], true) . ';');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Circular module dependency');
try {
ModuleRegistry::boot($fixturesDir, ['mod-x', 'mod-y']);
} finally {
@unlink($fixturesDir . '/mod-x/module.php');
@unlink($fixturesDir . '/mod-y/module.php');
@rmdir($fixturesDir . '/mod-x');
@rmdir($fixturesDir . '/mod-y');
@rmdir($fixturesDir);
}
}
public function testConflictDetectionIsStrict(): void
{
$fixturesDir = sys_get_temp_dir() . '/module-arch-test-' . uniqid();