@@ -16,6 +30,36 @@ export function App() {
{t("supportedFormats")}: {SUPPORTED_AUDIO_FORMATS.join(", ")}
+
+ {rehearsalSong.title}
+ {rehearsalSong.exportSummary.headline}
+
+ {rehearsalSong.sections.map((section) => (
+
+ {section.label}
+ {section.groove}
+
+ {t("sectionConfidence")}: {confidenceLabels[section.confidence.level]} ({provenanceLabels[section.confidence.source]})
+
+
+ {section.roles.map((role) => (
+ -
+ {role.name}
+ - {role.harmony.chord}
+ - {role.cue.value}
+ - {t("roleConfidence")}: {confidenceLabels[role.confidence.level]}
+ - {t("harmonySource")}: {provenanceLabels[role.harmony.source]}
+ {role.manualOverrides.map((override, index) => (
+
+ {" "}
+ - {t("manualOverride")}: {override.value.chord} ({provenanceLabels[override.source]})
+
+ ))}
+
+ ))}
+
+
+ ))}
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index 2f031557..0b2f836f 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -6,5 +6,14 @@
"chordsCard": "Chord analysis baseline is wired.",
"rangesCard": "Range analysis baseline is wired.",
"settingsCard": "Settings baseline is wired.",
- "supportedFormats": "Supported input formats"
+ "supportedFormats": "Supported input formats",
+ "sectionConfidence": "Section confidence",
+ "roleConfidence": "confidence",
+ "harmonySource": "harmony source",
+ "manualOverride": "manual override",
+ "confidenceLevelLow": "Low confidence",
+ "confidenceLevelMedium": "Needs ear check",
+ "confidenceLevelHigh": "Ready to trust",
+ "provenanceSourceModel": "Auto-detected",
+ "provenanceSourceUser": "User-confirmed"
}
diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json
index 4e20d6af..42545ce8 100644
--- a/apps/desktop/src/locales/ko/common.json
+++ b/apps/desktop/src/locales/ko/common.json
@@ -6,5 +6,14 @@
"chordsCard": "코드 분석 기준선이 연결되었습니다.",
"rangesCard": "음역 분석 기준선이 연결되었습니다.",
"settingsCard": "설정 기준선이 연결되었습니다.",
- "supportedFormats": "지원 입력 형식"
+ "supportedFormats": "지원 입력 형식",
+ "sectionConfidence": "구간 신뢰도",
+ "roleConfidence": "신뢰도",
+ "harmonySource": "화성 출처",
+ "manualOverride": "수동 수정",
+ "confidenceLevelLow": "확신이 낮음",
+ "confidenceLevelMedium": "귀로 한 번 더 확인",
+ "confidenceLevelHigh": "믿고 가져가도 됨",
+ "provenanceSourceModel": "자동 추정",
+ "provenanceSourceUser": "사용자 확인"
}
diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts
index 44779367..4410c5ce 100644
--- a/packages/shared-types/src/index.ts
+++ b/packages/shared-types/src/index.ts
@@ -7,6 +7,214 @@ export type ProjectSummary = {
supportedAudioFormats: readonly (typeof SUPPORTED_AUDIO_FORMATS)[number][];
};
+export type ConfidenceLevel = "low" | "medium" | "high";
+export type ProvenanceSource = "model" | "user";
+export type CueAnchorKind = "lyric" | "count" | "transition";
+export type RehearsalPriority = "low" | "medium" | "high";
+export type ExportFormat = "cue-sheet" | "chart-summary";
+
+export type ConfidenceMarker = {
+ level: ConfidenceLevel;
+ source: ProvenanceSource;
+ notes: string;
+};
+
+export type CueAnchor = {
+ kind: CueAnchorKind;
+ value: string;
+};
+
+export type RangeSummary = {
+ lowestNote: string;
+ highestNote: string;
+};
+
+export type RehearsalHarmony = {
+ chord: string;
+ functionLabel: string;
+ source: ProvenanceSource;
+};
+
+export type ManualOverride =
+ {
+ field: "harmony";
+ value: RehearsalHarmony & { source: "user" };
+ source: "user";
+ };
+
+export type RehearsalRole = {
+ id: string;
+ name: string;
+ roleType: "instrument" | "vocal" | "hand";
+ harmony: RehearsalHarmony;
+ cue: CueAnchor;
+ range: RangeSummary;
+ confidence: ConfidenceMarker;
+ rehearsalPriority: RehearsalPriority;
+ simplification: string;
+ setupNote: string;
+ manualOverrides: ManualOverride[];
+};
+
+export type RehearsalSection = {
+ id: string;
+ label: string;
+ groove: string;
+ confidence: ConfidenceMarker;
+ roles: RehearsalRole[];
+};
+
+export type ExportSummary = {
+ format: ExportFormat;
+ headline: string;
+ focusSections: string[];
+};
+
+export type RehearsalSong = {
+ id: string;
+ title: string;
+ sections: RehearsalSection[];
+ exportSummary: ExportSummary;
+};
+
+const CONFIDENCE_LEVELS = ["low", "medium", "high"] as const;
+const REHEARSAL_PRIORITIES = ["low", "medium", "high"] as const;
+const PROVENANCE_SOURCES = ["model", "user"] as const;
+const CUE_ANCHOR_KINDS = ["lyric", "count", "transition"] as const;
+const ROLE_TYPES = ["instrument", "vocal", "hand"] as const;
+const EXPORT_FORMATS = ["cue-sheet", "chart-summary"] as const;
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function isDenseArray(value: unknown): value is unknown[] {
+ return Array.isArray(value) && Array.from({ length: value.length }, (_, index) => index in value).every(Boolean);
+}
+
+function isOneOf(options: readonly T[], value: unknown): value is T {
+ return typeof value === "string" && options.includes(value as T);
+}
+
+function invalidField(path: string): string {
+ return `Invalid rehearsal song contract: invalid field '${path}'`;
+}
+
+const demoRehearsalSongSeed: RehearsalSong = {
+ id: "demo-song",
+ title: "Late Night Set",
+ sections: [
+ {
+ id: "verse-1",
+ label: "Verse 1",
+ groove: "Straight eighths with a late snare feel",
+ confidence: {
+ level: "medium",
+ source: "model",
+ notes: "Double-check the pickup into the chorus."
+ },
+ roles: [
+ {
+ id: "bass-guitar",
+ name: "Bass Guitar",
+ roleType: "instrument",
+ harmony: {
+ chord: "C#m7",
+ functionLabel: "vi pedal anchor",
+ source: "model"
+ },
+ cue: {
+ kind: "transition",
+ value: "Hold through the pickup before the downbeat.",
+ },
+ range: {
+ lowestNote: "C#2",
+ highestNote: "E3"
+ },
+ confidence: {
+ level: "medium",
+ source: "model",
+ notes: "Watch the slide into the turnaround."
+ },
+ rehearsalPriority: "high",
+ simplification: "Stay on roots if the chorus entrance gets muddy.",
+ setupNote: "Keep the attack short so the verse breathes.",
+ manualOverrides: []
+ },
+ {
+ id: "keys-right",
+ name: "Keyboard 1 Right Hand",
+ roleType: "hand",
+ harmony: {
+ chord: "Emaj7",
+ functionLabel: "Imaj7 color",
+ source: "model"
+ },
+ cue: {
+ kind: "count",
+ value: "Enter on beat 2 after the pickup."
+ },
+ range: {
+ lowestNote: "B3",
+ highestNote: "G#5"
+ },
+ confidence: {
+ level: "medium",
+ source: "model",
+ notes: "Top note voicing may need a quick ear check."
+ },
+ rehearsalPriority: "high",
+ simplification: "Drop the top extension if the chorus turnaround still feels busy.",
+ setupNote: "Keep the patch bright enough to stay over the guitars.",
+ manualOverrides: []
+ },
+ {
+ id: "lead-vocal",
+ name: "Lead Vocal",
+ roleType: "vocal",
+ harmony: {
+ chord: "C#m7",
+ functionLabel: "vi melodic pull",
+ source: "model"
+ },
+ cue: {
+ kind: "lyric",
+ value: "city lights"
+ },
+ range: {
+ lowestNote: "G#3",
+ highestNote: "C#5"
+ },
+ confidence: {
+ level: "high",
+ source: "user",
+ notes: "Singer confirmed the pickup phrasing in rehearsal notes."
+ },
+ rehearsalPriority: "medium",
+ simplification: "Keep the sustained note centered; skip the ad-lib on the first pass.",
+ setupNote: "Watch the breath before the last line of the verse.",
+ manualOverrides: [
+ {
+ field: "harmony",
+ value: {
+ chord: "C#m11",
+ functionLabel: "vi suspended lift",
+ source: "user"
+ },
+ source: "user"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ exportSummary: {
+ format: "cue-sheet",
+ headline: "Start with Verse 1 entrances before the chorus lift.",
+ focusSections: ["Verse 1"]
+ }
+};
+
export function createDefaultProjectSummary(input: {
id: string;
title: string;
@@ -18,3 +226,238 @@ export function createDefaultProjectSummary(input: {
supportedAudioFormats: SUPPORTED_AUDIO_FORMATS
};
}
+
+export function createDemoRehearsalSong(): RehearsalSong {
+ return structuredClone(demoRehearsalSongSeed);
+}
+
+function validateConfidenceMarker(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (!isOneOf(CONFIDENCE_LEVELS, value.level)) {
+ return invalidField(`${path}.level`);
+ }
+ if (!isOneOf(PROVENANCE_SOURCES, value.source)) {
+ return invalidField(`${path}.source`);
+ }
+ if (typeof value.notes !== "string") {
+ return invalidField(`${path}.notes`);
+ }
+
+ return null;
+}
+
+function validateCueAnchor(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (!isOneOf(CUE_ANCHOR_KINDS, value.kind)) {
+ return invalidField(`${path}.kind`);
+ }
+ if (typeof value.value !== "string") {
+ return invalidField(`${path}.value`);
+ }
+
+ return null;
+}
+
+function validateRangeSummary(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (typeof value.lowestNote !== "string") {
+ return invalidField(`${path}.lowestNote`);
+ }
+ if (typeof value.highestNote !== "string") {
+ return invalidField(`${path}.highestNote`);
+ }
+
+ return null;
+}
+
+function validateRehearsalHarmony(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (typeof value.chord !== "string") {
+ return invalidField(`${path}.chord`);
+ }
+ if (typeof value.functionLabel !== "string") {
+ return invalidField(`${path}.functionLabel`);
+ }
+ if (!isOneOf(PROVENANCE_SOURCES, value.source)) {
+ return invalidField(`${path}.source`);
+ }
+
+ return null;
+}
+
+function validateManualOverride(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (value.field !== "harmony") {
+ return invalidField(`${path}.field`);
+ }
+ if (value.source !== "user") {
+ return invalidField(`${path}.source`);
+ }
+
+ const harmonyError = validateRehearsalHarmony(value.value, `${path}.value`);
+ if (harmonyError) {
+ return harmonyError;
+ }
+ const harmonyValue = value.value as RehearsalHarmony;
+ if (harmonyValue.source !== "user") {
+ return invalidField(`${path}.value.source`);
+ }
+
+ return null;
+}
+
+function validateRehearsalRole(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (typeof value.id !== "string") {
+ return invalidField(`${path}.id`);
+ }
+ if (typeof value.name !== "string") {
+ return invalidField(`${path}.name`);
+ }
+ if (!isOneOf(ROLE_TYPES, value.roleType)) {
+ return invalidField(`${path}.roleType`);
+ }
+
+ const harmonyError = validateRehearsalHarmony(value.harmony, `${path}.harmony`);
+ if (harmonyError) {
+ return harmonyError;
+ }
+
+ const cueError = validateCueAnchor(value.cue, `${path}.cue`);
+ if (cueError) {
+ return cueError;
+ }
+
+ const rangeError = validateRangeSummary(value.range, `${path}.range`);
+ if (rangeError) {
+ return rangeError;
+ }
+
+ const confidenceError = validateConfidenceMarker(value.confidence, `${path}.confidence`);
+ if (confidenceError) {
+ return confidenceError;
+ }
+
+ if (!isOneOf(REHEARSAL_PRIORITIES, value.rehearsalPriority)) {
+ return invalidField(`${path}.rehearsalPriority`);
+ }
+ if (typeof value.simplification !== "string") {
+ return invalidField(`${path}.simplification`);
+ }
+ if (typeof value.setupNote !== "string") {
+ return invalidField(`${path}.setupNote`);
+ }
+ if (!isDenseArray(value.manualOverrides)) {
+ return invalidField(`${path}.manualOverrides`);
+ }
+ for (const [index, override] of value.manualOverrides.entries()) {
+ const overrideError = validateManualOverride(override, `${path}.manualOverrides[${index}]`);
+ if (overrideError) {
+ return overrideError;
+ }
+ }
+
+ return null;
+}
+
+function validateRehearsalSection(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (typeof value.id !== "string") {
+ return invalidField(`${path}.id`);
+ }
+ if (typeof value.label !== "string") {
+ return invalidField(`${path}.label`);
+ }
+ if (typeof value.groove !== "string") {
+ return invalidField(`${path}.groove`);
+ }
+
+ const confidenceError = validateConfidenceMarker(value.confidence, `${path}.confidence`);
+ if (confidenceError) {
+ return confidenceError;
+ }
+
+ if (!isDenseArray(value.roles)) {
+ return invalidField(`${path}.roles`);
+ }
+ for (const [index, role] of value.roles.entries()) {
+ const roleError = validateRehearsalRole(role, `${path}.roles[${index}]`);
+ if (roleError) {
+ return roleError;
+ }
+ }
+
+ return null;
+}
+
+function validateExportSummary(value: unknown, path: string): string | null {
+ if (!isRecord(value)) {
+ return invalidField(path);
+ }
+ if (!isOneOf(EXPORT_FORMATS, value.format)) {
+ return invalidField(`${path}.format`);
+ }
+ if (typeof value.headline !== "string") {
+ return invalidField(`${path}.headline`);
+ }
+ if (!isDenseArray(value.focusSections)) {
+ return invalidField(`${path}.focusSections`);
+ }
+ for (const [index, section] of value.focusSections.entries()) {
+ if (typeof section !== "string") {
+ return invalidField(`${path}.focusSections[${index}]`);
+ }
+ }
+
+ return null;
+}
+
+function validateRehearsalSong(value: unknown): string | null {
+ if (!isRecord(value)) {
+ return invalidField("root");
+ }
+ if (typeof value.id !== "string") {
+ return invalidField("id");
+ }
+ if (typeof value.title !== "string") {
+ return invalidField("title");
+ }
+ if (!isDenseArray(value.sections)) {
+ return invalidField("sections");
+ }
+ for (const [index, section] of value.sections.entries()) {
+ const sectionError = validateRehearsalSection(section, `sections[${index}]`);
+ if (sectionError) {
+ return sectionError;
+ }
+ }
+
+ return validateExportSummary(value.exportSummary, "exportSummary");
+}
+
+export function isRehearsalSong(value: unknown): value is RehearsalSong {
+ return validateRehearsalSong(value) === null;
+}
+
+export function parseRehearsalSong(value: unknown): RehearsalSong {
+ const validationError = validateRehearsalSong(value);
+ if (validationError) {
+ throw new Error(validationError);
+ }
+
+ return structuredClone(value as RehearsalSong);
+}
diff --git a/packages/shared-types/test/index.test.ts b/packages/shared-types/test/index.test.ts
index 2bfc40db..f7a4b5cf 100644
--- a/packages/shared-types/test/index.test.ts
+++ b/packages/shared-types/test/index.test.ts
@@ -1,4 +1,11 @@
-import { createDefaultProjectSummary, SUPPORTED_AUDIO_FORMATS } from "../src/index";
+import {
+ createDefaultProjectSummary,
+ createDemoRehearsalSong,
+ isRehearsalSong,
+ parseRehearsalSong,
+ type RehearsalSong,
+ SUPPORTED_AUDIO_FORMATS
+} from "../src/index";
describe("shared type helpers", () => {
it("creates a project summary for a fresh analysis job", () => {
@@ -14,4 +21,375 @@ describe("shared type helpers", () => {
supportedAudioFormats: SUPPORTED_AUDIO_FORMATS
});
});
+
+ it("creates a rehearsal song with section and role level guidance", () => {
+ const song = createDemoRehearsalSong();
+
+ expect(song).toMatchObject({
+ id: "demo-song",
+ title: "Late Night Set",
+ sections: [
+ {
+ id: "verse-1",
+ label: "Verse 1",
+ confidence: {
+ level: "medium",
+ source: "model"
+ },
+ roles: [
+ {
+ id: "bass-guitar",
+ name: "Bass Guitar",
+ roleType: "instrument"
+ },
+ {
+ id: "keys-right",
+ name: "Keyboard 1 Right Hand",
+ roleType: "hand",
+ harmony: {
+ chord: "Emaj7",
+ source: "model"
+ }
+ },
+ {
+ id: "lead-vocal",
+ name: "Lead Vocal",
+ roleType: "vocal",
+ cue: {
+ kind: "lyric",
+ value: "city lights"
+ }
+ }
+ ]
+ }
+ ],
+ exportSummary: {
+ format: "cue-sheet"
+ }
+ });
+
+ expect(song.sections[0]?.roles[2]?.harmony?.source).toBe("model");
+ expect(song.sections[0]?.roles[2]?.manualOverrides?.[0]).toMatchObject({
+ field: "harmony",
+ source: "user",
+ value: {
+ chord: "C#m11"
+ }
+ });
+ });
+
+ it("returns a fresh copy of the rehearsal song fixture", () => {
+ const first = createDemoRehearsalSong();
+ const second = createDemoRehearsalSong();
+
+ first.sections[0]?.roles[2]?.manualOverrides?.splice(0, 1);
+
+ expect(second).not.toBe(first);
+ expect(second.sections).not.toBe(first.sections);
+ expect(second.sections[0]?.roles).not.toBe(first.sections[0]?.roles);
+ expect(second.sections[0]?.roles[2]?.manualOverrides).toHaveLength(1);
+ });
+
+ it("validates and parses rehearsal song payloads", () => {
+ const song = createDemoRehearsalSong();
+ const malformedSong = createDemoRehearsalSong() as unknown as {
+ sections: Array<{ roles: unknown[] }>;
+ };
+ const sparseSong = createDemoRehearsalSong() as unknown as {
+ exportSummary: { focusSections: string[] };
+ sections: Array<{ roles: unknown[] }>;
+ };
+ const sparseSongWithProperty = createDemoRehearsalSong() as unknown as {
+ exportSummary: { focusSections: string[] & { label?: string } };
+ };
+ const arrayPayload = Object.assign([], {
+ id: "array-song",
+ title: "Array Song",
+ sections: [],
+ exportSummary: {
+ format: "cue-sheet",
+ headline: "Array payload",
+ focusSections: []
+ }
+ });
+ malformedSong.sections[0]!.roles = [{ id: "broken-role" }];
+ sparseSong.exportSummary.focusSections = new Array(1);
+ sparseSongWithProperty.exportSummary.focusSections = new Array(1) as string[] & {
+ label?: string;
+ };
+ sparseSongWithProperty.exportSummary.focusSections.label = "ghost";
+
+ expect(isRehearsalSong(song)).toBe(true);
+ expect(isRehearsalSong({ id: "bad" })).toBe(false);
+ expect(isRehearsalSong({
+ id: "bad",
+ title: "Bad",
+ sections: [],
+ exportSummary: {
+ format: 42,
+ headline: "oops"
+ }
+ })).toBe(false);
+ expect(isRehearsalSong(malformedSong)).toBe(false);
+ expect(isRehearsalSong(sparseSong)).toBe(false);
+ expect(isRehearsalSong(sparseSongWithProperty)).toBe(false);
+ expect(isRehearsalSong(arrayPayload)).toBe(false);
+
+ const parsed = parseRehearsalSong(song);
+ parsed.sections[0]?.roles.splice(0, 1);
+
+ expect(parsed.sections[0]?.roles).toHaveLength(2);
+ expect(song.sections[0]?.roles).toHaveLength(3);
+ expect(() => parseRehearsalSong(null)).toThrow("Invalid rehearsal song contract");
+ expect(() => parseRehearsalSong({
+ id: "bad",
+ title: "Bad",
+ sections: [],
+ exportSummary: {
+ format: 42,
+ headline: "oops"
+ }
+ })).toThrow("exportSummary.format");
+ });
+
+ it("reports the first invalid field path for nested contract failures", () => {
+ const roleSparse = createDemoRehearsalSong() as unknown as {
+ sections: Array<{ roles: unknown[] }>;
+ };
+ const badOverride = createDemoRehearsalSong() as unknown as {
+ sections: Array<{ roles: Array<{ manualOverrides: Array<{ value: { source: string } }> }> }>;
+ };
+ const badHeadline = createDemoRehearsalSong() as unknown as {
+ exportSummary: { headline: unknown };
+ };
+ const badFocusSection = createDemoRehearsalSong() as unknown as {
+ exportSummary: { focusSections: unknown[] };
+ };
+ const badExportSummary = createDemoRehearsalSong() as unknown as {
+ exportSummary: unknown;
+ };
+ const missingId = { ...createDemoRehearsalSong(), id: 42 };
+ const sparseSections = createDemoRehearsalSong() as unknown as { sections: RehearsalSong["sections"] };
+
+ roleSparse.sections[0]!.roles = new Array(1);
+ badOverride.sections[0]!.roles[2]!.manualOverrides[0]!.value.source = "model";
+ badHeadline.exportSummary.headline = 99;
+ badFocusSection.exportSummary.focusSections = ["Verse 1", 7];
+ badExportSummary.exportSummary = [];
+ sparseSections.sections = new Array(1) as RehearsalSong["sections"];
+
+ expect(() => parseRehearsalSong(roleSparse)).toThrow("sections[0].roles");
+ expect(() => parseRehearsalSong(badOverride)).toThrow("manualOverrides[0].value.source");
+ expect(() => parseRehearsalSong(badHeadline)).toThrow("exportSummary.headline");
+ expect(() => parseRehearsalSong(badFocusSection)).toThrow("exportSummary.focusSections[1]");
+ expect(() => parseRehearsalSong(badExportSummary)).toThrow("exportSummary");
+ expect(() => parseRehearsalSong(missingId)).toThrow("id");
+ expect(() => parseRehearsalSong(sparseSections)).toThrow("sections");
+ });
+
+ it("covers detailed validation branches", () => {
+ const createInvalidSong = (mutate: (song: RehearsalSong) => unknown) => {
+ const song = createDemoRehearsalSong();
+ mutate(song);
+ return song;
+ };
+
+ const cases: Array<{ message: string; payload: unknown }> = [
+ { message: "title", payload: { id: "song" } },
+ {
+ message: "sections[0]",
+ payload: { ...createDemoRehearsalSong(), sections: [null] }
+ },
+ {
+ message: "sections[0].id",
+ payload: createInvalidSong((song) => {
+ (song.sections[0] as RehearsalSong["sections"][number]).id = 4 as never;
+ })
+ },
+ {
+ message: "sections[0].label",
+ payload: createInvalidSong((song) => {
+ (song.sections[0] as RehearsalSong["sections"][number]).label = 4 as never;
+ })
+ },
+ {
+ message: "sections[0].groove",
+ payload: createInvalidSong((song) => {
+ (song.sections[0] as RehearsalSong["sections"][number]).groove = 4 as never;
+ })
+ },
+ {
+ message: "sections[0].confidence.level",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.confidence.level = "certain" as never;
+ })
+ },
+ {
+ message: "sections[0].confidence.source",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.confidence.source = "other" as never;
+ })
+ },
+ {
+ message: "sections[0].confidence.notes",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.confidence.notes = 1 as never;
+ })
+ },
+ {
+ message: "sections[0].confidence",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.confidence = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].id",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.id = 7 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0]",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0] = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].name",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.name = 7 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].roleType",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.roleType = "drums" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].harmony",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.harmony = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].harmony.chord",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.harmony.chord = 3 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].harmony.functionLabel",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.harmony.functionLabel = 3 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].harmony.source",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.harmony.source = "other" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].cue",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.cue = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].cue.kind",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.cue.kind = "bar" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].cue.value",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.cue.value = 2 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].range",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.range = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].range.lowestNote",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.range.lowestNote = 2 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].range.highestNote",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.range.highestNote = 2 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].confidence",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.confidence = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].rehearsalPriority",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.rehearsalPriority = "urgent" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].simplification",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.simplification = 2 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].setupNote",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.setupNote = 2 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[2].manualOverrides[0]",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[2]!.manualOverrides[0] = null as never;
+ })
+ },
+ {
+ message: "sections[0].roles[2].manualOverrides[0].field",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[2]!.manualOverrides[0]!.field = "cue" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[2].manualOverrides[0].source",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[2]!.manualOverrides[0]!.source = "model" as never;
+ })
+ },
+ {
+ message: "sections[0].roles[2].manualOverrides[0].value.chord",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[2]!.manualOverrides[0]!.value.chord = 5 as never;
+ })
+ },
+ {
+ message: "sections[0].roles[0].manualOverrides",
+ payload: createInvalidSong((song) => {
+ song.sections[0]!.roles[0]!.manualOverrides = new Array(1) as never;
+ })
+ },
+ {
+ message: "exportSummary.focusSections",
+ payload: createInvalidSong((song) => {
+ song.exportSummary.focusSections = new Array(1) as never;
+ })
+ }
+ ];
+
+ for (const testCase of cases) {
+ expect(() => parseRehearsalSong(testCase.payload)).toThrow(testCase.message);
+ }
+ });
});