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:
1 parent
6b9fb54b4f
commit
a829d6e8c6
21 files changed
+1903
-61
No files matched your search
@@ -5,14 +5,13 @@ declare(strict_types=1);
|
||||
namespace MeshVault\Indexer;
|
||||
|
||||
use MeshVault\Indexer\Contracts\FileEnumeratorInterface;
|
||||
use MeshVault\Repository\CatalogRepositoryInterface;
|
||||
use MeshVault\Repository\ImportJobRepositoryInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Turns a source tree into a durable import job manifest.
|
||||
*
|
||||
* Grouping policy (explicit, never inferred from file contents): each file is
|
||||
* Legacy guessed grouping (not user-confirmed associations): each file is
|
||||
* assigned to the model named after its first directory component below the
|
||||
* source root; files sitting directly in the source root join a model named
|
||||
* after the source directory itself. Every model lives in a folder of the same
|
||||
@@ -24,7 +23,7 @@ final readonly class ImportService
|
||||
public function __construct(
|
||||
private FileEnumeratorInterface $enumerator,
|
||||
private ImportJobRepositoryInterface $jobs,
|
||||
private CatalogRepositoryInterface $catalog,
|
||||
private LegacyGuessedImportGroupingAdapter $groupingAdapter,
|
||||
private SpaceChecker $space,
|
||||
private string $libraryRoot,
|
||||
) {
|
||||
@@ -59,15 +58,12 @@ final readonly class ImportService
|
||||
/** @return list<array<string, mixed>> */
|
||||
private function manifest(string $sourcePath, ScanResult $scan): array
|
||||
{
|
||||
$models = [];
|
||||
$items = [];
|
||||
foreach ($scan->files as $file) {
|
||||
$group = $this->group($sourcePath, $file);
|
||||
$models[$group] ??= $this->catalog->ensureModel('/' . $group, $group);
|
||||
$items[] = [
|
||||
'source_path' => $file->sourcePath,
|
||||
'size' => $file->size,
|
||||
'model_id' => $models[$group],
|
||||
'model_id' => $this->groupingAdapter->modelFor($sourcePath, $file),
|
||||
];
|
||||
}
|
||||
// Scan failures become terminal items so the job reports them durably.
|
||||
@@ -82,13 +78,4 @@ final readonly class ImportService
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function group(string $sourcePath, ScannedFile $file): string
|
||||
{
|
||||
$parent = trim(str_replace('\\', '/', dirname($file->relativePath)), '/');
|
||||
if ($parent === '' || $parent === '.') {
|
||||
return basename(rtrim(str_replace('\\', '/', $sourcePath), '/'));
|
||||
}
|
||||
return explode('/', $parent)[0];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user