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

+14 -9
View File
@@ -3,6 +3,8 @@
declare(strict_types=1);
use MeshVault\Catalog\CatalogService;
use MeshVault\Catalog\CatalogServiceInterface;
use MeshVault\Indexer\LegacyGuessedImportGroupingAdapter;
use MeshVault\Config\AppConfig;
use MeshVault\Console\ConsoleServices;
use MeshVault\Console\ContainerConsoleServices;
@@ -53,10 +55,13 @@ return static fn (AppConfig $config): array => [
=> new CatalogService($c->get(CatalogRepositoryInterface::class)),
ImportJobRepositoryInterface::class => static fn (ContainerInterface $c): ImportJobRepositoryInterface
=> new PdoImportJobRepository($c->get(PDO::class)),
CatalogServiceInterface::class => DI\get(CatalogService::class),
LegacyGuessedImportGroupingAdapter::class => static fn (ContainerInterface $c): LegacyGuessedImportGroupingAdapter
=> new LegacyGuessedImportGroupingAdapter($c->get(CatalogRepositoryInterface::class)),
ImportService::class => static fn (ContainerInterface $c): ImportService => new ImportService(
$c->get(FileEnumeratorInterface::class),
$c->get(ImportJobRepositoryInterface::class),
$c->get(CatalogRepositoryInterface::class),
$c->get(LegacyGuessedImportGroupingAdapter::class),
$c->get(SpaceChecker::class),
$config->libraryPath,
),
@@ -69,17 +74,17 @@ return static fn (AppConfig $config): array => [
public function __invoke(callable $shouldStop): ImportWorker
{
/** @var PathSafety $safety */
$safety = $c->get(PathSafety::class);
$safety = $this->c->get(PathSafety::class);
return new ImportWorker(
$c->get(ImportJobRepositoryInterface::class),
$c->get(CatalogRepositoryInterface::class),
$c->get(StreamingHasher::class),
$c->get(VerifiedCopier::class),
$c->get(StlValidator::class),
$this->c->get(ImportJobRepositoryInterface::class),
$this->c->get(CatalogServiceInterface::class),
$this->c->get(StreamingHasher::class),
$this->c->get(VerifiedCopier::class),
$this->c->get(StlValidator::class),
$safety,
$c->get(SpaceChecker::class),
$this->c->get(SpaceChecker::class),
$safety->libraryRoot(),
$c->get(LoggerInterface::class),
$this->c->get(LoggerInterface::class),
$shouldStop,
);
}