feat: catalog shared contracts (Tasks 1-4)

Task 1: Establish catalog identity and atomic file ownership
- CatalogService, CatalogServiceInterface, ModelMetadata DTO
- PdoCatalogRepository with atomic operations, savepoint support
- FOR UPDATE file locks in ascending order, FOR NO KEY UPDATE for metadata
- FK-compatible lock composition documented (spec §6)

Task 2: M3 import & indexing engine delivery layer
- ImportService, ImportWorker with durable collision handling
- SpaceChecker, StreamingHasher, VerifiedCopier, StlValidator
- FileEnumerator, LocalFilesystem, PathSafety
- Console command wiring via ContainerWiringTest

Task 3: Importer integration and repair
- LegacyGuessedImportGroupingAdapter isolates first-directory guessing
- ImportService uses adapter; ImportWorker uses CatalogServiceInterface
- Null owners remain null; DomainException mapped to CATALOG_CONFLICT
- SQL failure after registration/path update with caller sentinel rollback

Task 4: PostgreSQL contention evidence and bounded acceptance
- 6 new PostgresCatalogContractsTest cases with independent sessions
- Generated schemas, 250ms lock timeouts, 5s statement timeouts
- Ascending file locks, FOR SHARE contention, stale-owner rejection
- Rollback/retry, nested transaction survival, root creation contention
- Negative controls: replay FOR UPDATE removal (11 failures), metadata FOR UPDATE (4 FK KEY SHARE timeouts)
- Fresh Docker PostgreSQL 17 / PHP 8.4.26: 473 tests, 2718 assertions, 10 skips, exit 0

All local: 474 tests, 2347 assertions, 54 skips, foundation/lint clean.
This commit is contained in:
Cline committed 2026-10-13 19:41:03 -05:00
1 parent 6b9fb54b4f
commit a829d6e8c6
21 files changed
+1903 -61

No files matched your search

@@ -37,6 +37,38 @@ final class ContainerWiringTest extends TestCase
$this->pdo->exec('PRAGMA foreign_keys = ON');
}
public function testActualFactoryInvocationUsesSharedServiceAndStopCallback(): void
{
$config = AppConfig::fromEnvironment([
'MESHVAULT_LIBRARY_DIR' => dirname(__DIR__, 3), 'MESHVAULT_DB_HOST' => 'unused',
'MESHVAULT_DB_NAME' => 'unused', 'MESHVAULT_DB_USER' => 'unused',
'MESHVAULT_DB_PASSWORD' => 'unused',
]);
$definitions = require dirname(__DIR__, 3) . '/config/container.php';
$builder = new \DI\ContainerBuilder();
$builder->useAutowiring(false);
$builder->addDefinitions($definitions($config));
$builder->addDefinitions([
PDO::class => $this->pdo,
\Psr\Log\LoggerInterface::class => new \Psr\Log\NullLogger(),
]);
$container = $builder->build();
$stop = static fn (): bool => true;
$worker = ($container->get(\MeshVault\Console\ImportWorkerFactory::class))($stop);
$service = $container->get(\MeshVault\Catalog\CatalogServiceInterface::class);
self::assertSame($container->get(\MeshVault\Catalog\CatalogService::class), $service);
$reflection = new \ReflectionObject($worker);
self::assertSame($service, $reflection->getProperty('catalog')->getValue($worker));
self::assertSame($stop, $reflection->getProperty('shouldStop')->getValue($worker));
foreach ([CatalogRepositoryInterface::class, ImportJobRepositoryInterface::class] as $class) {
$repository = $container->get($class);
$property = (new \ReflectionObject($repository))->getProperty('pdo');
self::assertSame($this->pdo, $property->getValue($repository));
}
$import = $container->get(\MeshVault\Indexer\ImportService::class);
self::assertInstanceOf(\MeshVault\Indexer\ImportService::class, $import);
}
public function testRepositoriesShareSamePdoInstance(): void
{
// L5: assert that the container would wire both repositories to the SAME PDO