From 3a5203eb471aabf1634d0bfb866d02a0f0682f53 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Tue, 3 Nov 2026 12:58:15 +0100 Subject: [PATCH] some renaming of variables --- README.md | 10 +++---- .../model/scheduling/ScheduleRules.java | 7 ++--- .../model/scheduling/SchedulingInput.java | 9 +++++-- .../service/SchedulingInputs.java | 5 ++-- .../service/SchoolManagementService.java | 3 ++- .../agiledancers/service/ShowSchedules.java | 24 ++++++++++------- .../service/model/SchoolSnapshot.java | 7 ++--- .../solver/SchedulingObjectives.java | 2 +- .../agiledancers/S18LifecycleTest.java | 4 +-- agiledancers-fe/e2e/school-management.spec.ts | 2 +- .../src/app/schools/school-page.html | 3 ++- .../src/app/shared/entity-editor.spec.ts | 26 ++++++++++++++++--- .../src/app/shared/entity-editor.ts | 14 ++++++++-- .../src/app/shared/schedule-editor.ts | 2 +- docs/s18-validation.md | 4 +-- show-scheduling-rules.md | 16 ++++++------ ...rules-for-plotting-dance-groups-in-show.md | 14 +++++----- 17 files changed, 98 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 5d9b56c..c8ea59f 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,8 @@ dancers; deleting a school also deletes its dancers, groups and shows. Shows have a name, zero to six dated dayparts (`MORNING`, `AFTERNOON`, `EVENING`), with unique date/type combinations. The displayed date range comes from these -dayparts. Each show stores `preferredDayParts` (x), defaulting to two or one for -one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly x +dayparts. Each show stores `preferredDayParts`, defaulting to two or one for +one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly preferredDayParts dayparts; preschool groups once; showcases every daypart. See [show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints, coach exceptions, ordering and optimization rules. @@ -178,10 +178,10 @@ status. The UI supports ordering and moving performances, and displays violation the same body to link an existing coach. `DELETE .../coaches/{coachId}/dancer` removes the identity link. The dancers screen supports maintaining these links. -School JSON exports use format version **3**, including x, schedule status and +School JSON exports use format version **3**, including preferredDayParts, schedule status and coach-to-dancer links. Version 2 remains importable; its schedules migrate to -drafts with default x. Version 1 is unsupported. Legacy `appearances` fields are -retained for file compatibility; S18 scheduling uses x and fixed type exceptions. +drafts with default preferredDayParts. Version 1 is unsupported. Legacy `appearances` fields are +retained for file compatibility; S18 scheduling uses preferredDayParts and fixed type exceptions. The REST `appearances` input is deprecated; use `preferredDayParts`. Run the HTTP integration tests with: diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/ScheduleRules.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/ScheduleRules.java index 5e2b95d..0dd6c23 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/ScheduleRules.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/ScheduleRules.java @@ -14,7 +14,7 @@ public final class ScheduleRules { public static List violations(final SchedulingInput input, final List> parts) { final List errors = new ArrayList<>(); if (input.dayParts() == 0 || input.preferredDayParts() < 1 || input.preferredDayParts() > input.dayParts()) { - errors.add("Configure dayparts and x between 1 and the number of dayparts."); + errors.add("Configure dayparts and preferredDayParts between 1 and the number of dayparts."); } if (parts == null || parts.size() != input.dayParts() || parts.stream().anyMatch(java.util.Objects::isNull)) { errors.add("Assignments must contain one list for every daypart."); @@ -86,7 +86,7 @@ public final class ScheduleRules { private static void attendance( final SchedulingInput input, final List> parts, final List errors) { - final Set exempt = new HashSet<>(input.coachIds()); + final Set exempt = new HashSet<>(input.coachDancerIds()); input.groups().stream() .filter(GroupRequirement::showcase) .forEach((final GroupRequirement g) -> exempt.addAll(g.dancerIds())); @@ -96,7 +96,8 @@ public final class ScheduleRules { } attendance.forEach((final Long dancer, final Set dayparts) -> { if (!exempt.contains(dancer) && dayparts.size() > input.preferredDayParts()) { - errors.add("Dancer " + dancer + " exceeds x=" + input.preferredDayParts() + " dayparts."); + errors.add( + "Dancer " + dancer + " exceeds preferredDayParts=" + input.preferredDayParts() + " dayparts."); } }); } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/SchedulingInput.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/SchedulingInput.java index 7c30a35..e341c96 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/SchedulingInput.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/scheduling/SchedulingInput.java @@ -3,16 +3,21 @@ package nl.agilecc.agiledancers.model.scheduling; import java.util.List; import java.util.Set; +/** + * Immutable solver snapshot. + * + * @param coachDancerIds dancer registration IDs linked to coaches, used for participation exemptions + */ public record SchedulingInput( long showId, long revision, int dayParts, List groups, int preferredDayParts, - Set coachIds) { + Set coachDancerIds) { public SchedulingInput { groups = List.copyOf(groups); - coachIds = Set.copyOf(coachIds); + coachDancerIds = Set.copyOf(coachDancerIds); } public SchedulingInput( diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchedulingInputs.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchedulingInputs.java index 6ce3b14..1130b96 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchedulingInputs.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchedulingInputs.java @@ -26,7 +26,7 @@ public final class SchedulingInputs { } public static SchedulingInput of(final DanceSchool school, final Show show, final long revision) { - final int x = preferred(show); + final int preferredDayParts = preferred(show); final LocalDate date = show.getDayParts().stream() .map(DayPart::getDate) .min(LocalDate::compareTo) @@ -40,7 +40,8 @@ public final class SchedulingInputs { .filter(java.util.Objects::nonNull) .map(Dancer::getId) .collect(Collectors.toSet()); - return new SchedulingInput(show.getId(), revision, show.getDayParts().size(), groups, x, coaches); + return new SchedulingInput( + show.getId(), revision, show.getDayParts().size(), groups, preferredDayParts, coaches); } private static GroupRequirement requirement(final DanceGroup group, final Show show, final LocalDate date) { diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java index d90143c..88eebe4 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java @@ -60,7 +60,8 @@ public class SchoolManagementService { "Scheduling requires at least one school group and one daypart."); } if (SchedulingInputs.preferred(show) > show.getDayParts().size()) { - throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "Correct x before scheduling."); + throw new ManagementException( + ManagementExceptionKind.INVALID_INPUT, "Correct preferredDayParts before scheduling."); } running.add(showId); return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L)); diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ShowSchedules.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ShowSchedules.java index d89bd9a..3383c10 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ShowSchedules.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ShowSchedules.java @@ -21,25 +21,26 @@ public class ShowSchedules { this.schools = schools; } - /** Retains assignments by date/type; removing dayparts may retain an invalid x in the draft. */ + /** Retains assignments by date/type; removing dayparts may retain an invalid preferredDayParts in the draft. */ public Show configure(final long schoolId, final Long showId, final Show settings) { synchronized (schools) { ShowConfiguration.validateParts(settings.getDayParts()); final Show existing = showId == null ? null : schools.show(schoolId, showId); - final int x = settings.getPreferredDayParts() == null + final int preferredDayParts = settings.getPreferredDayParts() == null ? (existing == null ? (settings.getDayParts().size() == 1 ? 1 : 2) : SchedulingInputs.preferred(existing)) : settings.getPreferredDayParts(); - validateX(x, existing, settings.getDayParts().size()); + validatePreferredDayParts( + preferredDayParts, existing, settings.getDayParts().size()); final Show show = existing == null ? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null) : existing; - final boolean changed = x != SchedulingInputs.preferred(show) + final boolean changed = preferredDayParts != SchedulingInputs.preferred(show) || !keys(show.getDayParts()).equals(keys(settings.getDayParts())); show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts())); show.setName(settings.getName()); - show.setPreferredDayParts(x); + show.setPreferredDayParts(preferredDayParts); if (changed) { schools.touch(show); } @@ -53,13 +54,16 @@ public class ShowSchedules { .toList(); } - private static void validateX(final int x, final Show existing, final int parts) { - if (x < 1 - || x > 6 - || (parts > 0 && x > parts && (existing == null || x != SchedulingInputs.preferred(existing)))) { + private static void validatePreferredDayParts(final int preferredDayParts, final Show existing, final int parts) { + if (preferredDayParts < 1 + || preferredDayParts > 6 + || (parts > 0 + && preferredDayParts > parts + && (existing == null || preferredDayParts != SchedulingInputs.preferred(existing)))) { throw new ManagementException( ManagementExceptionKind.INVALID_INPUT, - "x must be between 1 and the number of dayparts. Retained x must be corrected before scheduling."); + "preferredDayParts must be between 1 and the number of dayparts. Retained preferredDayParts must" + + " be corrected before scheduling."); } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/model/SchoolSnapshot.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/model/SchoolSnapshot.java index 8ef7bc9..ba9d952 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/model/SchoolSnapshot.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/model/SchoolSnapshot.java @@ -205,8 +205,9 @@ public record SchoolSnapshot( .build()); } ShowConfiguration.validateParts(parts); - final int x = show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts(); - check(x >= 1 && x <= 6, "x moet tussen 1 en 6 liggen"); + final int preferredDayParts = + show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts(); + check(preferredDayParts >= 1 && preferredDayParts <= 6, "preferredDayParts moet tussen 1 en 6 liggen"); final Show entity = Show.builder() .name(show.name()) .dayParts(parts) @@ -214,7 +215,7 @@ public record SchoolSnapshot( show.appearances() == null ? ShowConfiguration.defaults(parts.size()) : Map.copyOf(show.appearances())) - .preferredDayParts(x) + .preferredDayParts(preferredDayParts) .finalized(false) .build(); entity.setId((long) showIds.size()); diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/solver/SchedulingObjectives.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/solver/SchedulingObjectives.java index a51560e..73b4863 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/solver/SchedulingObjectives.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/solver/SchedulingObjectives.java @@ -24,7 +24,7 @@ public final class SchedulingObjectives { input.groups().stream() .filter(GroupRequirement::showcase) .forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds())); - dancers.removeAll(input.coachIds()); + dancers.removeAll(input.coachDancerIds()); final LinearExprBuilder penalty = LinearExpr.newBuilder(); for (final long dancer : dancers) { final IntVar[] parts = dancerParts(model, input, vars, dancer); diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/S18LifecycleTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/S18LifecycleTest.java index 71baf0d..8f9dbc9 100644 --- a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/S18LifecycleTest.java +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/S18LifecycleTest.java @@ -40,11 +40,11 @@ class S18LifecycleTest { .toList(); } - private Show settings(final int n, final Integer x) { + private Show settings(final int n, final Integer preferredDayParts) { return Show.builder() .name("Show") .dayParts(parts(n)) - .preferredDayParts(x) + .preferredDayParts(preferredDayParts) .build(); } diff --git a/agiledancers-fe/e2e/school-management.spec.ts b/agiledancers-fe/e2e/school-management.spec.ts index 64ee27c..1f3ed07 100644 --- a/agiledancers-fe/e2e/school-management.spec.ts +++ b/agiledancers-fe/e2e/school-management.spec.ts @@ -242,7 +242,7 @@ test('links a coach and finalizes performances spread across multiple dates', as data: { assignments: groupIds.map((id) => [id]) }, }); await page.goto(`/schools/${school.id}/shows`); - await expect(page.getByText(/exceeds x=1/)).toBeVisible(); + await expect(page.getByText(/exceeds preferredDayParts=1/)).toBeVisible(); await tab(page, 'Dansers'); await page.getByText('Coaches koppelen aan dansers', { exact: true }).click(); await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' }); diff --git a/agiledancers-fe/src/app/schools/school-page.html b/agiledancers-fe/src/app/schools/school-page.html index b6f3b29..c7e3d41 100644 --- a/agiledancers-fe/src/app/schools/school-page.html +++ b/agiledancers-fe/src/app/schools/school-page.html @@ -352,7 +352,8 @@

{{ show.name }}

- {{ show.finalized ? 'Definitief' : 'Concept' }} · x = {{ show.preferredDayParts }} + {{ show.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts = + {{ show.preferredDayParts }}

@for (violation of show.violations; track $index) {

{{ violation }}

diff --git a/agiledancers-fe/src/app/shared/entity-editor.spec.ts b/agiledancers-fe/src/app/shared/entity-editor.spec.ts index bb0689e..0ac18a5 100644 --- a/agiledancers-fe/src/app/shared/entity-editor.spec.ts +++ b/agiledancers-fe/src/app/shared/entity-editor.spec.ts @@ -80,7 +80,7 @@ describe('EntityEditor', () => { appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 }, }; - it('defaults x to two and rejects duplicate dated dayparts and fractional x', () => { + it('defaults preferredDayParts to two and rejects duplicate dated dayparts and fractional preferredDayParts', () => { const component = editor({ kind: 'show' }); component.name = 'Show'; expect(component.validDetails()).toBe(true); @@ -99,7 +99,27 @@ describe('EntityEditor', () => { expect(component.validDetails()).toBe(false); }); - it('retains x when dayparts are removed so it can be explicitly corrected', () => { + it('preserves an explicitly chosen preferredDayParts when adding dayparts to a new show', async () => { + api.saveShow.mockReturnValue(of(show)); + const component = editor({ kind: 'show' }); + component.name = 'Show'; + component.addPart(); + component.dayParts[0].date = '2027-06-01'; + component.addPart(); + component.dayParts[1].date = '2027-06-02'; + component.preferredDayParts = 1; + component.addPart(); + component.dayParts[2].date = '2027-06-03'; + expect(component.preferredDayParts).toBe(1); + await component.save(); + expect(api.saveShow).toHaveBeenCalledWith( + 7, + expect.objectContaining({ preferredDayParts: 1 }), + undefined, + ); + }); + + it('retains preferredDayParts when dayparts are removed so it can be explicitly corrected', () => { const component = editor({ kind: 'show', value: show }); component.addPart(); component.dayParts[2].date = '2027-06-03'; @@ -144,7 +164,7 @@ describe('EntityEditor', () => { } }); - it('displays one editable x field with a default of one for a one-daypart show', async () => { + it('displays one editable preferredDayParts field with a default of one for a one-daypart show', async () => { // jsdom does not implement native dialog opening. const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal'); Object.defineProperty(HTMLDialogElement.prototype, 'showModal', { diff --git a/agiledancers-fe/src/app/shared/entity-editor.ts b/agiledancers-fe/src/app/shared/entity-editor.ts index 189a84c..26d9168 100644 --- a/agiledancers-fe/src/app/shared/entity-editor.ts +++ b/agiledancers-fe/src/app/shared/entity-editor.ts @@ -185,7 +185,16 @@ export class EntityEditor implements OnInit { readonly today = new Date().toLocaleDateString('en-CA'); private readonly api = inject(SchoolApi); dayParts: DayPart[] = []; - preferredDayParts = 2; + private preferredDayPartsValue = 2; + private preferredDayPartsEdited = false; + + get preferredDayParts(): number { + return this.preferredDayPartsValue; + } + set preferredDayParts(value: number) { + this.preferredDayPartsValue = value; + this.preferredDayPartsEdited = true; + } name = ''; date = ''; type: GroupType = 'REGULAR'; @@ -218,7 +227,8 @@ export class EntityEditor implements OnInit { addPart(): void { if (this.dayParts.length >= 6) return; this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] }); - if (!this.target().value) this.preferredDayParts = Math.min(2, this.dayParts.length); + if (!this.target().value && !this.preferredDayPartsEdited) + this.preferredDayPartsValue = Math.min(2, this.dayParts.length); } removePart(index: number): void { this.dayParts.splice(index, 1); diff --git a/agiledancers-fe/src/app/shared/schedule-editor.ts b/agiledancers-fe/src/app/shared/schedule-editor.ts index a157880..88c7f50 100644 --- a/agiledancers-fe/src/app/shared/schedule-editor.ts +++ b/agiledancers-fe/src/app/shared/schedule-editor.ts @@ -16,7 +16,7 @@ import { DialogFrame } from './dialog-frame'; (dismissed)="closed.emit()" >

- {{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · x = + {{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts = {{ show().preferredDayParts }}

diff --git a/docs/s18-validation.md b/docs/s18-validation.md index 7e65026..0e09ab4 100644 --- a/docs/s18-validation.md +++ b/docs/s18-validation.md @@ -17,7 +17,7 @@ Validated on 2026-11-03 against the confirmed S18 specification. Focused solver tests cover showcase bookends (including extra showcases), single and absent showcases, changing gaps, coach exemptions, infeasibility, the strict priority of showcase attendance over balance, and style/age scores. Lifecycle -and HTTP tests cover skipped empty groups, first-show-date ages, retained x after +and HTTP tests cover skipped empty groups, first-show-date ages, retained preferredDayParts after daypart removal, atomic structural validation, invalid drafts, final status invalidation, stale solver results, timeout preservation, coach identity links, and ordered JSON round trips including version-2 migration. @@ -36,7 +36,7 @@ assessed against `CODING_STANDARDS.md` and `docs/backend-review.md`: - `ScheduleRules` has cyclomatic signals for independent configuration checks and showcase cases. Each rule family is isolated in a short method and shared between finalization and solver-result validation. -- `ShowSchedules` retains branching for nullable initial settings, retained x, +- `ShowSchedules` retains branching for nullable initial settings, retained preferredDayParts, and structural draft validation. It stages complete replacement lists before mutation and uses the existing aggregate lock. It separates schedule lifecycle from the already broad `SchoolManagementService` aggregate owner. diff --git a/show-scheduling-rules.md b/show-scheduling-rules.md index 5911248..a94a16f 100644 --- a/show-scheduling-rules.md +++ b/show-scheduling-rules.md @@ -5,12 +5,12 @@ calendar dates. Counts do not restart on a new date. ## Required performances -The manager chooses **x**, the desired number of distinct dayparts, per show. +The manager chooses **preferredDayParts**, the desired number of distinct dayparts, per show. It defaults to two, or one for a show with only one daypart. A show can be saved -without dayparts but cannot be scheduled. If dayparts are removed, x is retained +without dayparts but cannot be scheduled. If dayparts are removed, preferredDayParts is retained and must be corrected before scheduling when it exceeds the remaining count. -Every nonempty regular, selection and pre-education group performs in exactly x +Every nonempty regular, selection and pre-education group performs in exactly preferredDayParts dayparts, once in each. Each preschool group performs exactly once in the whole show. Every showcase performs once in every daypart. Groups without dancers are skipped completely, including for showcase positions and balancing. @@ -22,17 +22,17 @@ twice in the same daypart. Without showcases there is no opening/closing rule. ## People and changing clothes -An ordinary dancer must perform in no more than x distinct dayparts across all +An ordinary dancer must perform in no more than preferredDayParts distinct dayparts across all of their groups. The same dancer registration must be used in every group. Showcase participants necessarily perform in every daypart. For their other -performances, exceeding x dayparts is allowed but discouraged. We count their -non-showcase dayparts, subtract x, and count only positive excesses. The planner's +performances, exceeding preferredDayParts dayparts is allowed but discouraged. We count their +non-showcase dayparts, subtract preferredDayParts, and count only positive excesses. The planner's first preference is to minimize the sum of these excesses across dancers. A coach registration must be explicitly linked to the corresponding dancer; names are not used to guess identity. Coaches are available throughout the show -and are exempt from both the x limit and the excess penalty. A coach only performs +and are exempt from both the preferredDayParts limit and the excess penalty. A coach only performs with a group if included in its dancer membership. Being a group's coach alone never adds a performance or forces that group into extra dayparts. @@ -85,5 +85,5 @@ Preferences alone never block finalization. Manual edits and relevant membership, dancer-age, coach-link or show-setting changes return final schedules to draft. Existing schedules and imported schedules are drafts. Import retains assignment order and recalculates violations. Export -is available for both statuses and records status, x and coach identity links; +is available for both statuses and records status, preferredDayParts and coach identity links; reimport always requires a fresh finalization. diff --git a/specs/in_review/S18_new-rules-for-plotting-dance-groups-in-show.md b/specs/in_review/S18_new-rules-for-plotting-dance-groups-in-show.md index c447d2a..899702d 100644 --- a/specs/in_review/S18_new-rules-for-plotting-dance-groups-in-show.md +++ b/specs/in_review/S18_new-rules-for-plotting-dance-groups-in-show.md @@ -16,11 +16,11 @@ See [validation evidence](../../docs/s18-validation.md) and - A show consists of dayparts and can span multiple dates, typically two days. All counts below apply across the entire show, not separately per date. -- Each show has one configurable integer `x`, between 1 and the number of +- Each show has one configurable integer `preferredDayParts`, between 1 and the number of dayparts inclusive. Its default is 2, or 1 for a one-daypart show. - This replaces the existing configurable appearance counts per group type. - A show without dayparts can be stored but cannot be scheduled. -- If reducing the number of dayparts makes `x` invalid, require the user to +- If reducing the number of dayparts makes `preferredDayParts` invalid, require the user to correct it before scheduling again; do not silently change it. - Groups without dancers are excluded from scheduling and from all performance counts and showcase-position requirements. @@ -28,9 +28,9 @@ See [validation evidence](../../docs/s18-validation.md) and ## Required group appearances - Each nonempty REGULAR, SELECTION and PRE_EDUCATION group performs in exactly - `x` distinct dayparts, once within each selected daypart. + `preferredDayParts` distinct dayparts, once within each selected daypart. - Each nonempty PRE_SCHOOLER group performs exactly once in the complete show, - regardless of `x`. At most two such groups are expected. They can be assigned + regardless of `preferredDayParts`. At most two such groups are expected. They can be assigned freely to dayparts; no additional placement restriction applies. - Each nonempty SHOW_CASE group performs exactly once in every daypart. - With at least two showcase groups, each daypart starts with one showcase and @@ -42,12 +42,12 @@ See [validation evidence](../../docs/s18-validation.md) and ## Dancer participation and coaches -- A dancer without a showcase membership performs in at most `x` distinct +- A dancer without a showcase membership performs in at most `preferredDayParts` distinct dayparts across the complete show. This is a hard constraint. - A showcase participant necessarily performs in all dayparts. Their other - group performances may span more than `x` dayparts, but this is undesirable. + group performances may span more than `preferredDayParts` dayparts, but this is undesirable. - For each non-coach showcase participant, count the distinct dayparts in which - they perform outside showcases. Their excess is `max(0, count - x)`. + they perform outside showcases. Their excess is `max(0, count - preferredDayParts)`. Minimize the sum of these excesses as the first optimization priority. - A person dancing in several groups must use the same dancer registration in every group, including when accompanying preschoolers. Up to five group