Phase A diagnosed an empty SPA timetable as a controller filter bug. B1.1's
schema-verify gate proved the opposite: the seeder violates Model A, the
controllers are correct.
Canonical model (Model A) per:
- dev-docs/SCHEMA.md:1285 artist_engagements.event_id → festival OR flat event
- dev-docs/SCHEMA.md:1329 performances.event_id → sub-event OR flat event ("show host")
- dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:1247-1257 (§10.2 contract)
"performance.event_id must be flat event OR a sub-event of the
engagement.event_id festival"
- dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:455-477 (§D17)
"Friday + Saturday under one combined deal = 1 engagement, 2 performances"
— only works if engagement is at festival level
Controller audit (B1.2): all five filters in
api/app/Http/Controllers/Api/V1/Artist/{PerformanceController,
ArtistEngagementController, StageController}.php already match Model A.
No controller changes needed.
Seeder change (B1.3) — single consistent fix:
ArtistTimetableDevSeeder::seedForFestival now creates one engagement per
(artist, festival) instead of per (artist, sub-event). When the same artist
recurs across iterations on different sub-events, the existing engagement
is reused and another performance is added (the D17 multi-perf path).
Performances continue to carry event_id = sub-event.
Same model fix in seedForSeries (engagement at parent series, performance
at week sub-event).
seedForFlatEvent already conformed (engagement.event_id = performance.event_id
= the flat event itself).
Existence-check semantics shift from `where event_id = $subEvent->id` to
`where event_id = $festival->id` (or $parent->id for series). Numerically
the test counts hold because the bucket-cycling makes scheduled artists
distinct within the festival window.
Tests (B1.4) — new TimetableSeederControllerIntegrationTest with 7 assertions:
- engagement.event_id is at festival level (DB invariant)
- performance.event_id is at sub-event level (DB invariant)
- GET /performances?day={subEvent} returns non-empty + correct event_ids
- GET /performances unfiltered returns all sub-event performances
- GET /performances?stage_id=null returns the seeded parked perf
- GET /engagements returns engagements with event_id = festival
- GET /stages returns 5 stages with event_id = festival
This locks the visible-symptom regression from Session 4: an empty SPA
timetable on a freshly-seeded festival cannot land again silently.
Existing ArtistTimetableDevSeederTest (4 tests) and the broader Artist
suite (121 tests) all stay green. composer analyse + Pint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LaneCascadeService::move() now calls disableLogging() before every
save inside the transaction (locked performance + cascade-bumped
peers + park-path). The two explicit activity('performance')
->event('moved'|'parked') entries with cascade_count + cascaded_ids
properties are the only audit records per move, matching RFC §8's
"single parent entry summarising the cascade" requirement.
Park path additionally writes an explicit 'performance.parked'
entry per RFC §8 vocabulary instead of falling back to a generic
'updated' auto-log entry.
Two new tests verify:
- cascade move with N peers produces exactly 1 activity entry on
the moved subject and 0 on each cascade-bumped peer
- park writes exactly 1 'parked' entry
PerformanceObserver::saving (version bump) is unaffected:
disableLogging() suppresses only the activity log trait, not
Eloquent model events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
advance_submissions.advance_section_id FK changed from cascadeOnDelete
to nullOnDelete; column made nullable. Aligns implementation with
RFC v0.2 §5.4 audit-immutability ("submissions remain for retention
compliance") — when ArtistEngagementObserver::deleted hard-deletes a
section, its submissions persist as orphans rather than disappearing.
Migration edited in place (branch unpushed, dev-only). Observer
docblock + test assertion updated to match. Removed pre-existing
follow-up comment that documented the deviation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Phase C test files:
- tests/Unit/Models/Artist/ArtistDomainModelsTest.php — relationships,
casts, soft-delete trait presence, slug uniqueness within/across
organisations, isParked() helper, AdvanceSection's primary scope,
PURPOSE_SUBJECT_FQCN['artist'] resolves to instantiable class.
- tests/Feature/Artist/ArtistEngagementObserverTest.php — auto-fill
organisation_id from artist, cross-tenant guard throws, soft-delete
cascades to performances + hard-deletes advance_sections.
- tests/Feature/Artist/PerformanceObserverTest.php — version starts
at 0, increments by 1 per UPDATE, no bump on no-op save.
- tests/Feature/Artist/ArtistDomainScopeLeakageTest.php — 5 scoped
models (Artist/Genre/Engagement direct + Stage/Performance FK-chain)
isolate cross-org queries.
- tests/Feature/Artist/ArtistTimetableDevSeederTest.php — fixture-count
smoke (4 stages, 12 stage_days, 6 artists, 12 engagements,
13 performances incl. 1 parked).
Cross-cutting fixes that Phase C surfaced:
- AppServiceProvider: morph-map block 2 extended with the 8 new
artist-domain models (artist_engagement, artist_contact, genre,
stage, stage_day, performance, advance_section, advance_submission).
Block 1 'artist' alias was already wired via PurposeRegistry.
- 5 form-builder backfill tests bumped --step rollback counts by +10
to account for the 10 new May 8 migrations sitting at HEAD between
the test's calibration point and current head.
- phpstan-baseline.neon regenerated (1631 entries) — all errors are
same patterns existing baselined code already exhibits
(Factory generic typing, Model property docblock gaps). Tracked
systematically under TECH-LARASTAN-* in BACKLOG.
Tests: 1646 passing (was 1624 pre-Session-1 → +22 net, no losses).
Larastan: 0 errors over baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>