"""Summarize finished production decisions; no composition or reference reading.""" from __future__ import annotations import argparse,collections,json from fractions import Fraction as F from pathlib import Path from generator import vocabulary def main(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('--assets',type=Path,required=True);p.add_argument('--output',type=Path,required=True);args=p.parse_args();args.output.mkdir(parents=True,exist_ok=True) evidence={};text=['# Finished production evidence','', 'Times below are exact quarter-note beats relative to the solo. Counts describe the selected music; they are not targets or listening verdicts.',''] for path in sorted((args.assets/'analysis').glob('*.roles')): roles=json.loads(path.read_text()) for role,v in roles.items(): key=path.stem+'/'+role evidence[key]={'document_hash':vocabulary.fingerprint(v['document']),'trace':v['trace']} if not role.endswith('_solo'):continue d=v['details'];ps=d['phrases'];ns=[n for p in ps for n in p['draft']['notes']] operations=collections.Counter(t['operation'] for p in ps for t in p['draft']['transformations']) span=max(F(p['end']) for p in ps)-min(F(p['start']) for p in ps) sounding=sum((F(n['end'])-F(n['start']) for n in ns),F()) summary={'attacks':sum(not n['continuation'] for n in ns),'quarter_beats':str(span),'occupancy':float(sounding/span),'fretboard':d['fretboard'],'operations_selected':dict(operations),'operation_funnel':d['selection_trace'].get('operations',{}),'expressions_selected':dict(collections.Counter(e['kind'] for e in d['expressions'])),'expression_funnel':d['selection_trace'].get('expression_operations',{}),'evaluation_total':d['selection_trace']['selection_total'],'unresolved':d['unresolved_treatments'],'boundaries':[]} evidence[key].update(summary) text += ['## '+key,'',f"{summary['attacks']} attacks over {span} quarter beats; sounding occupancy {summary['occupancy']:.1%}; sounding MIDI range {d['fretboard']['range_midi']}. Harmonic valid: {d['harmonic_valid']}; unresolved obligations: {len(d['unresolved_treatments'])}.",'', '| Span | Behavior | Destination / actual hold | Home identity |','|---|---|---|---|'] for phrase in ps: anchors=[{k:a[k] for k in ('role','effect','pitch','actual_start','actual_end','completed')} for a in phrase['anchors']] summary['boundaries'].append({'start':phrase['start'],'end':phrase['end'],'closure':phrase['closure'],'anchors':anchors,'home':phrase['home']}) destination='; '.join(a['effect']+' '+a['pitch']+' '+str(a['actual_start'])+'–'+str(a['actual_end']) for a in anchors) or 'no forced arrival' text.append(f"| {phrase['start']}–{phrase['end']} | {phrase['closure']} / {phrase['ending']} | {destination} | `{phrase['home']}` |") text += ['', 'Selected actions: '+', '.join(f'{k}: {v}' for k,v in sorted(operations.items()))+'.','', 'Typed treatments: '+str(summary['expressions_selected'])+'.','', '### Representative evaluated phrase',''] phrase=next(p for p in ps if p['draft']['notes']);evaluation=phrase['evaluation'];assert abs(phrase['score']-sum(evaluation['contributions'].values()))<1e-10 text += [f"Selected `{phrase['id']}`; score {phrase['score']:.9f}. Identity is a hard feature contract.",'','| Contribution | Raw | Weight | Weighted |','|---|---:|---:|---:|'] for k,value in evaluation['raw'].items():text.append(f"| {k} | {value:.9f} | {evaluation['weights'][k]} | {evaluation['contributions'][k]:.9f} |") pool=d['selection_trace'].get('source_candidate_pools',{}).get(phrase['passage'],[]) alternatives=sorted((c for c in pool if c['id']!=phrase['draft']['id']),key=lambda c:(c['score'],c['id'])) text += ['', 'Other retained alternatives for this source window (possibly different incoming histories):'] for c in alternatives[:3]:text.append(f"- `{c['id']}`: {c['score']:.9f}, {c['attacks']} attacks; selected ending {c['ending']}.") text += ['', 'These are local alternatives, not a proof of a global optimum. The chorus frontier compares complete histories using duration-weighted phrase scores. The JSON retains every reported contribution, operation cutoff counts and source input trace.',''] evidence[key]['winner_evaluation']=evaluation;evidence[key]['local_alternatives']=alternatives[:3] (args.output/'evidence.json').write_text(json.dumps(evidence,indent=2)) (args.output/'phrase_traces.md').write_text('\n'.join(text)+'\n') if __name__=='__main__':main()