Files
MeshVault/docs/superpowers/plans/2026-10-12-catalog-core.md

12 KiB

Catalog Core Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Deliver the bounded #28 foundation: explicit model identity and atomic expected-owner file associations without changing physical storage.

Architecture: Extend the existing catalog repository and expose its savepoint-backed transaction boundary to a thin catalog service. New Phinx migrations enforce identity constraints; PostgreSQL row locks protect association decisions. SQLite remains the fast test backend.

Tech Stack: Existing PHP/PDO, Phinx, Slim/PHP-DI and PHPUnit; no new dependencies.

Spec: C:\Users\ksolo\Projects\3D Printing Stuff\MeshVault\docs\superpowers\specs\2026-10-12-catalog-core-design.md

Global Constraints

  • No new framework or runtime dependency.
  • Add new migrations, never rewrite applied migrations.
  • Preserve file IDs, bytes, paths and timestamps when associations change.
  • Explicit null means unassigned, not no precondition.
  • Use RESTRICT on model deletion and explicit detach.
  • No browse UI, moves, physical deletion, downloads, duplicate decisions, retry engine or import crash-recovery fix in this first slice.
  • Do not silently relabel unresolved M2/M3 acceptance as complete.
  • Commit checkpoints require authorization; do not push automatically.

Review Focus

  • Migration/downgrade preflight must leave schema and data intact on failure (Task 1).
  • Legacy ensureModel races after uniqueness removal require creation serialization (Tasks 2/4).
  • Null ownership must never become zero or a wildcard (Tasks 2/3).
  • Savepoint failures must preserve caller transactions and unrelated writes (Tasks 3/4).
  • Reversed input and same-owner no-ops must not bypass locking (Task 4).

File map

All paths below resolve against C:\Users\ksolo\Projects\3D Printing Stuff\MeshVault; use absolute paths when executing.

  • Create db/migrations/20261012120001_catalog_identity.php: explicit up/down and safe preflights.
  • Create src/Catalog/ManagedPath.php: lexical canonical-path validation shared by migration and repository.
  • Modify src/Repository/CatalogRepositoryInterface.php and src/Repository/PdoCatalogRepository.php: identity, nullable registration, transaction boundary and associations.
  • Reuse src/Repository/PdoRepository.php; modify only if failing transaction tests demonstrate a need.
  • Create src/Catalog/CatalogService.php; register in config/container.php using the existing shared repository/PDO.
  • Extend tests/Integration/RepositoryDatabase.php for isolated migration targets and rollback.
  • Create tests/Unit/Catalog/ManagedPathTest.php, tests/Integration/CatalogMigrationTest.php, tests/Integration/CatalogCoreTest.php, tests/Integration/PostgresCatalogTest.php.
  • Retain RepositoryTest and ImportPipelineTest as compatibility gates; update interface doubles wherever found.
  • Update db/migrations/README.md and docs/project-status.md with observed scope/results.

Task 1 — Safe identity migration

Interfaces: ManagedPath::validate(string $path): string returns unchanged input or throws InvalidArgumentException. Extend RepositoryDatabase::migrate(string $path, ?int $target = null): PDO; add migrateTo(string $path, int $target): void and rollbackTo(string $path, int $target): void using Phinx and the registered isolated connection/configuration.

Path contract: Nonempty slash-separated relative names; reject backslashes, NUL, URI syntax, leading slash/drive prefixes, empty segments, dot and dot-dot segments. Preserve case and Unicode spelling. Apply existing Win32 trailing-dot/space and colon restrictions on Windows. This is lexical identity, not symlink authorization or filesystem case equivalence; PathSafety still governs I/O. No filesystem access in migration validation.

  • Write valid/invalid path table tests and migration tests starting at 20261011120006 with populated models, files and dependent metadata.
  • Test preflight failure for duplicate managed paths across models and noncanonical paths, even without collision. Test down failure for unassigned files and duplicate same-folder titles. Compare rows, indexes/FKs and migration version before/after failure.
  • Run php vendor/phpunit/phpunit/phpunit --filter 'ManagedPathTest|CatalogMigrationTest'; require failures for missing behavior, not harness errors.
  • Implement validator and isolated migration helpers. Implement up/down preflights before DDL; require stopped writers during migration, documented in migration README. Report offending IDs and remediation guidance without repairing data.
  • Up: nullable files.model_id with RESTRICT; replace idx_files_model_path with unique idx_files_managed_path and nonunique idx_files_model_id; make idx_models_folder_name nonunique. Down restores original constraints only after preflight. Preserve other indexes/FKs/columns across SQLite rebuilds.
  • Test direct SQL constraints, including unique paths with null owners, invalid model FK, deletion restriction and detached file survival. Verify IDs, timestamps, dependent rows and subsequent generated IDs survive clean down/up.
  • Run targeted SQLite tests and isolated PostgreSQL tests, then composer lint; review checkpoint before any authorized commit.

Task 2 — Explicit identity and importer compatibility

Interfaces: Add atomic(callable $operation): mixed and createModel(string $folderPath, string $modelName): int to CatalogRepositoryInterface. Make recordFile's first parameter ?int $modelId; retain other parameters/defaults. Preserve aggregate's model/folder/files array and null-on-missing behavior.

  • Write CatalogCoreTest cases for empty models, independent same-title models and required home folder. Legacy ensureModel creates on zero matches, reuses one and throws DomainException on multiple matches. Reject blank titles and titles exceeding 255 characters without trimming stored titles or adding an extension dependency.
  • Test nullable registration, canonical path validation, strict nullable ownership comparisons, same owner/hash/size idempotence and conflicting path rejection. Preserve existing status/hash behavior.
  • Run php vendor/phpunit/phpunit/phpunit --filter CatalogCoreTest; confirm intended failures.
  • Implement atomic using the existing savepoint helper. Both creation methods acquire the existing root-folder writer lock before lookup/insert inside a transaction. Explicit creation always inserts; legacy lookup counts matches instead of using ON CONFLICT on the removed constraint. Reject empty home-folder input; retain existing slash-root/folder conventions.
  • Implement nullable registration and shared path validation before writes; retain recordFile's writer lock and database-backed path identity. Never adopt another owner's record implicitly.
  • Adapt interface doubles found by search; run CatalogCoreTest, RepositoryTest, ImportPipelineTest and lint. Review checkpoint before authorized commit.

Task 3 — Atomic associations and service boundary

Repository interface: changeFileOwner(array $fileIds, ?int $expectedOwnerId, ?int $targetOwnerId): void. Require a nonempty list of distinct positive integers; non-null owner IDs must be positive. Invalid arguments throw InvalidArgumentException; missing models/files or stale ownership throw DomainException. SQL/FK/lock failures remain PDOException; no automatic retry.

Service interface: Constructor takes CatalogRepositoryInterface. Expose createModel(string $folderPath, string $modelName): int, aggregate(int $modelId): ?array, assignFiles(int $modelId, array $fileIds): void (expected null), reassignFiles(int $expectedModelId, int $targetModelId, array $fileIds): void, detachFiles(int $expectedModelId, array $fileIds): void. Use repository atomic for command orchestration, never a separate PDO/transaction manager.

  • Test aggregate with two STL parts, selected image/document and files spanning folders. Reassignment leaves empty models valid; home folders stay unchanged. Compare all file fields except model_id, physical bytes, paths and mtimes across association operations.
  • Test empty/duplicate/noninteger/nonpositive selections, missing expected/target model, missing file mixed with valid IDs, stale/null expected ownership and same-owner no-op.
  • Test nested success leaves the caller transaction active and caller rollback undoes changes. Caught failure preserves unrelated outer writes. Use test-only database triggers to fail a later update and prove earlier updates roll back too.
  • Run CatalogCoreTest; confirm intended red tests.
  • Implement transactional validation: check referenced models, sort IDs numerically, acquire each file row's FOR UPDATE lock in ascending order on PostgreSQL; omit lock syntax on SQLite. Check every selected row/owner under lock before any update. Update only model_id. Same-owner no-op still locks/checks; concurrent target deletion is constrained by FK locking or causes rollback.
  • Implement service and DI registration, with container resolution coverage following existing tests. Keep importer using compatible repository methods, not a fictitious confirmed-manifest workflow.
  • Run CatalogCoreTest, RepositoryTest, ImportPipelineTest and lint; review checkpoint before authorized commit.

Task 4 — PostgreSQL contention and acceptance evidence

Follow PostgresRepositoryTest: generated schema, two connections, lock_timeout 250ms, statement_timeout 5s, finally rollback/cleanup. Configured failures must error rather than skip.

  • Write contest tests: A changes ownership in an uncommitted transaction; B times out with SQLSTATE 55P03 and no changes. After A commits, B's stale owner fails rather than overwrites. If A rolls back instead, B may succeed.
  • Hold FOR SHARE in A; B's same-owner no-op must block/fail. Nested lock failure preserves B's caller transaction and unrelated writes.
  • Verify ascending lock parameters for reversed input with a test-only PDOStatement execution recorder. Hold the higher-ID row in A; B must time out without partial updates or leaked lower-row locks after savepoint rollback. Timeout alone does not prove ordering.
  • Test root-lock contention for explicit and legacy creation; after release ensureModel reuses one match or rejects ambiguity. Preserve lock/deadlock SQLSTATE; no automatic retries or claims of reproduced deadlocks.
  • Run new PostgreSQL tests red, fix demonstrated gaps, then full composer test and composer lint locally. Record fresh totals/skips.
  • Run sh bin/test-postgres.sh from a fresh isolated checkout containing these changes on the Docker host. Never touch dirty /tmp/MeshVault or development data. Record results/evidence path; inability to run is a blocker, not a pass.
  • Run git diff --check, inspect/reread changed files and update migration guidance/status with verified results only. Broader #28 metadata/lifecycle contracts and unresolved M2/M3 acceptance remain open.
  • Whole-slice review before authorized commit/push or tracker acceptance changes.

Self-review and handoff

Coverage checked: migration/path safety (Task 1), identity/home folder/legacy compatibility (Task 2), multipart associations/nested rollback (Task 3), PostgreSQL concurrency (Task 4). Nullable-owner signatures agree. Import confirmation, removal service, UI and other non-goals remain deferred.

Execution status: implemented with subagents and approved by task-level and independent whole-slice review. Local verification: 377 tests, 2032 assertions, 28 skips; isolated PostgreSQL verification: 377 tests, 2160 assertions, 7 skips; foundation and lint passed. The checklist above preserves the original execution prescription, not a current pending-work list. PostgreSQL tests exposed and drove a trigger portability fix; the concurrency cases passed the existing implementation without a mutation-red run, and no deadlock was induced. The user authorized local integration into main; no push is authorized. Broader #28 and M2/M3 acceptance remain open.