Date: 2026-06-16 Status: Proposed
In the grid-locked complement modes (rhythm_lock, register_contrast,
call_response) part B's pitch is chosen by hashing each onset's position into
the band's scale ladder (pitch_index(seed, i, ladder.len())). After ADR-0012
and the bottom-octave fix the ladder spans A's whole register band, but the
choice is all-or-nothing: a uniform draw over the entire band, with no control
over how far B wanders between a static line and a register-wide one. Users
asking for a "complementary guitar" want exactly that dial — "how much does B
depart from a static line / A's contour".
Three constraints frame the decision:
- Determinism is a hard rule. SPEC §6: generation is deterministic under a
fixed seed; the glossary defines a deterministic generator as
same seed/input → same result. Any knob must change the deterministic output, never introduce nondeterminism. - S6 already owns "controlled variability". Its strategies (constrained random walk with leap/repeat penalties, motif-transpose + variation) are seeded and style-bounded (density within corpus mean ± 1σ). Variation is not foreign to the generator; it is the generator's native idea, exposed as a parameter.
- Grid-locked means onset-locked. The grid modes deliberately do not
route through an S6 round-trip — "A's onsets already respect A's timeline;
regeneration could only misalign" (
decisions.log).rhythm_lock's acceptance criterion isB's onsets match A's onset gridandrhythm_similarity == 1.0. A variability knob that drifts B off the grid would break the mode's contract.
The repository already has the pattern for such a knob: the spec / fact /
provenance split of ADR-0012, carried by GestureControl / StructureControl
(ADR-0015) — a typed ask compiled over the S6 generator, run deterministically,
returning the produced is as provenance.
We add VariationControl — the pitch-variability ask, orthogonal to
RelationMode, a sibling of GestureControl and StructureControl.
-
One axis to start:
pitch_spread ∈ [0.0, 1.0]. The fraction of the band's scale ladder B may use.0.0pins every note to the band's anchor degree (a static line, still locked to A's grid);1.0uses the whole band — the unconstrained default, i.e. exactly today'sarrange_complement. -
Pitch only, never onsets. The knob narrows the modulo of the existing seeded pitch hash; it does not move a single onset. Grid-locked modes stay grid-locked —
rhythm_similaritystays1.0. It applies to the ladder-substitution modes (rhythm_lock,register_contrast,call_response).octave_doubleandsupport_layerhave no pitch degree-of-freedom to spread;counter_melody's variability is S6's own (mappingpitch_spreadonto the walk's leap penalty is a future axis). -
A separate entry point, not a
ComplementSpecchange.arrange_complement_varied(score, idx, spec, seed, control) -> VariedComplement.ComplementSpechas many construction sites; extending it would break every caller for a concern that composes cleanly on top — thegenerate_gesturedprecedent (a separate entry, not an extra request field).arrange_complementstays and is exactlyarrange_complement_varied(.., VariationControl::FULL). -
Deterministic (SPEC §6). The window only shrinks the range of the seeded hash; the same
(score, idx, spec, seed, control)always yields the same B. No new RNG is introduced. -
Provenance is ask-vs-is.
VariedComplement { complement, control, realized_spread }carries the control that asked and B's realized pitch ambitus as a fraction of the target band — theGesturedCandidateduality. An out-of-range control is the typedVariationError::InvalidControl, never a silent clamp. -
Future axes live on the same struct. Contour adherence (track A's up/down motion), a leap budget, and the
counter_melodywalk-penalty mapping are added as furtherVariationControlfields. Onset drift is explicitly not aVariationControlaxis — rhythmic independence already has a home incounter_melody(and any future blended mode), not in a knob bolted onto a grid-locked mode.
- griff gains a "how static ↔ how wandering" dial for complement pitch, without violating determinism or any mode's grid-lock contract.
- The default path is byte-identical (
pitch_spread = 1.0is the identity window), so the corpus schema, goldens, and CLI snapshots are unaffected; the feature is purely additive. - Composes with
GestureControl/StructureControl— the axes are orthogonal, as ADR-0015 anticipated ("a complement part can carry its own control"). - Accepted:
realized_spreadis a coarse ambitus-over-band ratio, not a per-degree histogram; the window anchors at the band floor (small spread hugs the low register) rather than centring on A's contour — both are refined by the future axes above. - Accepted:
counter_melodyis not wired to the knob in this increment; its variability stays S6's until the walk-penalty mapping lands. - A CLI
--variation <0..1>surface is the natural follow-up; it defaults to1.0, so existing snapshots continue to hold.