Cleanup
ci / checks (push) Failing after 23m22s

This commit is contained in:
khodowany
2026-10-08 02:13:45 -05:00
parent 300097a37e
commit bc7baea7c4
88 changed files with 18844 additions and 1335 deletions
+21 -18
View File
@@ -1,13 +1,14 @@
"""Typed, target-driven harmonic expression against written harmony and explicit musical relationships."""
from __future__ import annotations
import dataclasses
import functools
import fractions
import functools
import generator.harmony
import generator.model
import generator.pitch
import generator.vocabulary
import generator.solo.types
import generator.vocabulary
@functools.lru_cache(maxsize=512)
def parsed_chord(symbol):
@@ -90,8 +91,9 @@ def _eligible_color(rule, span, harmony):
return False
return True
def validate(notes, contracts, harmony):
def validate(notes, contracts, harmony, *, composition=None):
"""Return event permissions only after checking complete actual gestures."""
composition = composition or generator.vocabulary.current()
actual = {n.event_id: n for n in notes}
approved = {}
@@ -115,7 +117,7 @@ def validate(notes, contracts, harmony):
if not previous or previous.chord.strip('()') == span.chord.strip('()'):
return {}, c.id + ': anticipation requires a harmonic change'
if c.kind in ('approach', 'enclosure', 'connection'):
rule = next((r for r in generator.vocabulary.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None)
rule = next((r for r in composition.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None)
if not rule or len(c.events) != len(rule['semitones']):
return {}, c.id + ': unknown approach formula'
arrival = c.boundary if c.boundary is not None else target.start
@@ -129,7 +131,7 @@ def validate(notes, contracts, harmony):
return {}, c.id + ': invalid internal approach gaps'
gap = fractions.Fraction(rule['gap'])
cursor = target.start - gap - sum((fractions.Fraction(d) for d in rule['durations']), fractions.Fraction()) - sum(gaps, fractions.Fraction())
if target.start - cursor > fractions.Fraction(generator.vocabulary.data('lead_ins')['max_approach']):
if target.start - cursor > fractions.Fraction(composition.data('lead_ins')['max_approach']):
return {}, c.id + ': approach too long'
for i, (n, step, duration) in enumerate(zip(c.events, rule['semitones'], rule['durations'])):
if n.start != cursor or n.end != cursor + fractions.Fraction(duration) or n.pitch.number() != target.pitch.number() + step:
@@ -144,7 +146,7 @@ def validate(notes, contracts, harmony):
if c.kind == 'connection' and (not c.preparation or c.preparation.end != c.events[0].start or abs(c.preparation.pitch.number() - c.events[0].pitch.number()) > 2):
return {}, c.id + ': chromatic connection lacks its incoming motion'
elif c.kind == 'passing':
rule = generator.vocabulary.data('lead_ins')['passing']
rule = composition.data('lead_ins')['passing']
if c.rule != rule['id'] or len(c.events) != 1 or not c.preparation:
return {}, c.id + ': unsupported passing figure'
n, = c.events
@@ -160,7 +162,7 @@ def validate(notes, contracts, harmony):
if not supported(before) or not supported(target):
return {}, c.id + ': unsupported passing endpoints'
elif c.kind == 'delayed_approach':
rule = generator.vocabulary.data('lead_ins').get('delayed_approach', {})
rule = composition.data('lead_ins').get('delayed_approach', {})
if c.rule != rule.get('id') or len(c.events) != 1:
return {}, c.id + ': unknown delayed approach'
n, = c.events
@@ -171,7 +173,7 @@ def validate(notes, contracts, harmony):
if target.pitch.number() % 12 not in harmony.members(target.start, target.end) or target.pitch.number() % 12 not in harmony.permitted(target.start, target.end):
return {}, c.id + ': invalid delayed destination'
elif c.kind == 'color':
rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None)
rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None)
if not rule:
return {}, c.id + ': unknown color policy'
colored = False
@@ -193,10 +195,10 @@ def validate(notes, contracts, harmony):
return {}, c.id + ': no interval expresses the declared color'
last = c.events[-1]
if rule.get('resolve'):
if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end):
if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(composition.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end):
return {}, c.id + ': unresolved color'
elif c.kind == 'suspension':
rule = next((r for r in generator.vocabulary.data('harmony').get('suspensions', []) if r['id'] == c.rule), None)
rule = next((r for r in composition.data('harmony').get('suspensions', []) if r['id'] == c.rule), None)
if not rule or len(c.events) != 1 or c.boundary is None:
return {}, c.id + ': unsupported suspension'
held, = c.events
@@ -211,23 +213,23 @@ def validate(notes, contracts, harmony):
if held.pitch.number() % 12 not in harmony.permitted(held.start, held.end):
return {}, c.id + ': suspension outside source-key/chord motion'
elif c.kind == 'anticipated_color':
rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None)
rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None)
if not rule or len(c.events) != 1 or c.boundary != span.start:
return {}, c.id + ': unsupported anticipated color'
n, = c.events
if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']):
if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']):
return {}, c.id + ': invalid anticipated color interval'
arrived = dataclasses.replace(n, start=c.boundary)
target = arrived if c.target == n else c.target
inner = generator.solo.types.ExpressionContract(c.id + ':arrived', 'color', rule['id'], (arrived,), target, span.chord)
_, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony)
_, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony, composition=composition)
if error:
return {}, error
elif c.kind == 'anticipation':
if c.rule not in ('early_chord_tone', 'early_reattack') or len(c.events) != 1 or span.start != c.boundary:
return {}, c.id + ': unknown anticipation or non-boundary target'
n, = c.events
if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']):
if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']):
return {}, c.id + ': invalid anticipation interval'
if c.rule == 'early_chord_tone' and (n != target or not c.boundary < n.end):
return {}, c.id + ': invalid held anticipation'
@@ -238,7 +240,7 @@ def validate(notes, contracts, harmony):
elif c.kind == 'blues':
if len(c.events) != 1:
return {}, c.id + ': blues treatment requires one inflected event'
rule = next((r for r in generator.vocabulary.data('lead_ins')['blues'] if r['id'] == c.rule), None)
rule = next((r for r in composition.data('lead_ins')['blues'] if r['id'] == c.rule), None)
n, = c.events
chord = parsed_chord(span.chord)
if not rule or n.end - n.start > fractions.Fraction(1, 2) or n.end != target.start or target.pitch.number() % 12 not in span.roles.basic:
@@ -262,15 +264,16 @@ def validate(notes, contracts, harmony):
approved[n.event_id] = c
return approved, ''
def validate_frontier(notes, contracts, harmony, until, complete):
def validate_frontier(notes, contracts, harmony, until, complete, *, composition=None):
"""Validate heard prefixes and bounded future obligations separately.
Future contract events are used ONLY to establish rule feasibility. They do
not count as heard resolution; the contract stays pending until its entire
target hold is present. Missing past events and expired deadlines fail.
"""
composition = composition or generator.vocabulary.current()
if complete:
approved, error = validate(notes, contracts, harmony)
approved, error = validate(notes, contracts, harmony, composition=composition)
return approved, (), error
actual = {n.event_id: n for n in notes}
future = dict(actual)
@@ -290,5 +293,5 @@ def validate_frontier(notes, contracts, harmony, until, complete):
return {}, (), c.id + ': missing/changed event before resolution deadline'
if incomplete:
pending.append(c)
approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony)
approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony, composition=composition)
return {key: c for key, c in approved.items() if key in actual}, tuple(pending), error