Built: repo layout per SPEC §11, both Dockerfiles, compose stack, toolchain.
Notes for next milestone:
entrypoint.sh for api supports migrate mode and calls vendor/bin/phinx.api_tokens table first)../scripts/ci.sh (Docker-based, no host PHP/Node needed). No GitHub Actions workflow per project decision.composer.json config pins platform.php to 8.3 in both subprojects so dependency resolution matches the FrankenPHP runtime image even when the build host's composer:2 image ships a newer PHP.Deviations from SPEC: none. Added dependencies beyond SPEC §2: none.
Built: all SPEC §4 tables; idempotent seeds; IP/CIDR value objects.
Schema notes for next milestone:
users.password_hash is NOT in the schema (per SPEC §4; UI owns local-admin credentials).api_tokens.kind enum values: reporter, consumer, admin, service (CHECK constraint enforced on both SQLite and MySQL: kind=reporter→reporter_id set & consumer_id null; kind=consumer→consumer_id set & reporter_id null; kind∈{admin,service}→both null).DATETIME(6) on MySQL. Default CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(6) accordingly.ip_bin always 16 bytes; v4 mapped to ::ffff:0:0/96. Use App\Domain\Ip\IpAddress::fromString() for normalization and Cidr::fromString() for subnets. Internally CIDRs store v4 prefixes as 96 + originalPrefix for unified containment math.Connection is wired through App\App\Container::build() and applies the four SQLite PRAGMAs (journal_mode=WAL, synchronous=NORMAL, busy_timeout=5000, foreign_keys=ON) on every new SQLite connection.App\Infrastructure\Db\Migrations\BaseMigration for adapter-aware timestamp/binary column helpers. The phinxlog table is unaffected.Decisions made:
ON DELETE semantics:
policy_category_thresholds.policy_id → CASCADE (thresholds belong to policy).policy_category_thresholds.category_id → RESTRICT (cannot drop a category in active use).consumers.policy_id → RESTRICT (cannot drop a policy in active use).reporters/consumers/manual_blocks/allowlist.created_by_user_id → SET NULL (preserve provenance after user delete).api_tokens.{reporter_id,consumer_id} → CASCADE (deleting a reporter/consumer revokes its tokens).reports.{category_id,reporter_id} → RESTRICT (preserve audit trail per SPEC hint).ip_scores.category_id → CASCADE (scores meaningless without their category).api_tokens is created via raw CREATE TABLE per adapter so the CHECK constraint on kind works on SQLite (which cannot ADD CHECK via ALTER TABLE) and on MySQL.BINARY(16) on MySQL is implemented as Phinx's portable binary type with limit => 16 (yields VARBINARY(16)); this is functionally identical for our fixed-width 16-byte payload and avoids per-adapter raw SQL.config/phinx.php where rtrim($path, '.sqlite') mangled the SQLite path because rtrim's second arg is a character set; switched to passing the full path verbatim with empty suffix.Deviations from SPEC: none. Added dependencies: none beyond SPEC §2.