Skip to content

Commit ad38b52

Browse files
authored
Merge pull request #178 from PhysShell/claude/griff-task-erqa7z
feat(tonal): S15 Phase 2 — explicit scoped tonal context (accepted) Accepted, closed and frozen at 8799b55 after three independent review rounds; closure recorded in 1172b0d. Independent CI green on the PR: 15/15 checks. Carried, not consumed: generation, pitch selection, reranking, and cadence are unchanged, and the S6 chain baseline golden is untouched.
2 parents 0201fd1 + 1172b0d commit ad38b52

13 files changed

Lines changed: 2340 additions & 14 deletions

‎cli/src/main.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,10 @@ fn phrase_reasons(r: boundary::BoundaryReason) -> String {
908908
/// and novelty has nothing to measure against (all candidates read fully
909909
/// novel). With `--corpus`, rhythm templates, novelty references, and the
910910
/// burst/rest gesture ask come from the curated chunks.
911+
// `tonal: _` is deliberate over `..`: the field is named so a `RankedSet` shape
912+
// change still breaks this command's compilation, while the value is explicitly
913+
// not reported.
914+
#[allow(clippy::unneeded_field_pattern)]
911915
fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<(), CliError> {
912916
let GenerateOpts {
913917
seed,
@@ -960,6 +964,9 @@ fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<
960964
bars,
961965
variants_per_strategy: candidates,
962966
gesture: !no_gesture,
967+
// No `--tonal-scope` flag exists: the scope is a caller's choice
968+
// and `griff generate` does not make one (S15 Phase 2).
969+
tonal: None,
963970
},
964971
plan.as_ref().map(|p| p.templates.as_slice()),
965972
)?;
@@ -970,6 +977,10 @@ fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<
970977
rhythm_explicit,
971978
gesture,
972979
policy,
980+
// Carried through the pass but not reported: `griff generate` never
981+
// supplies a tonal context (there is no scope flag), and printing an
982+
// always-`None` line would be a CLI output change S15 Phase 2 forbids.
983+
tonal: _,
973984
} = &set;
974985

975986
if *rhythm_explicit {

‎cockpit/src/generation.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ impl GeneratePanel {
137137
bars: self.bars,
138138
variants_per_strategy: self.variants,
139139
gesture: self.gesture,
140+
// The Generate panel exposes no scope control yet (S15 Phase 2 is
141+
// the carriage, not the UI), so the ask carries no tonal context.
142+
tonal: None,
140143
}
141144
}
142145

‎core/src/candidate_chain.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,7 @@ mod tests {
20972097
rhythm_explicit: false,
20982098
gesture: None,
20992099
policy: s6_policy(),
2100+
tonal: None,
21002101
}
21012102
}
21022103

@@ -2428,6 +2429,7 @@ mod tests {
24282429
bars: 4,
24292430
variants_per_strategy: 2,
24302431
gesture: false,
2432+
tonal: None,
24312433
};
24322434
let set = ranked_candidates(&source, None, &ask, None).expect("the pass runs");
24332435
let a = plan_candidate_chain(&set).expect("plans");

‎core/src/generation_input.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::rerank;
1919
use crate::score::{AtomEvent, Score, Track};
2020
use crate::scoring::{Scored, WeightPolicy};
2121
use crate::slice;
22+
use crate::tonal::TonalContext;
2223

2324
/// Why building a generation input failed.
2425
#[derive(Debug)]
@@ -170,6 +171,18 @@ pub struct GenerationAsk {
170171
/// Carve the corpus's burst/rest gesture. Ignored without a corpus, and
171172
/// when no chunk carries gesture stats — a pass never invents a gesture.
172173
pub gesture: bool,
174+
/// An explicitly scoped tonal estimate to record with the pass, or `None`.
175+
///
176+
/// **Carried, not consumed** (S15 Phase 2): the pass reads no pitch, no
177+
/// palette, and no weight from it, so a set generated with a context is
178+
/// byte-identical to one generated without. It travels so a session, a
179+
/// candidate record, or a frontend can say which scope's estimate was on
180+
/// the table, and reproduce it.
181+
///
182+
/// The core never measures one on the caller's behalf — the scope is a
183+
/// human/frontend choice ([`TonalContext::measure`]), and `None` means the
184+
/// caller supplied no context, never that the source has no key.
185+
pub tonal: Option<TonalContext>,
173186
}
174187

175188
/// A ranked candidate set and the input it was generated from — enough to show
@@ -194,6 +207,10 @@ pub struct RankedSet {
194207
pub gesture: Option<GestureControl>,
195208
/// The rerank policy the aggregates were scored under.
196209
pub policy: WeightPolicy,
210+
/// The tonal context the ask carried, verbatim — `None` when it carried
211+
/// none. Provenance only: nothing in the pass consulted it, and a pass that
212+
/// was given none reports none rather than measuring a default.
213+
pub tonal: Option<TonalContext>,
197214
}
198215

199216
/// Generates and reranks the full candidate set for `score` under `ask`.
@@ -264,6 +281,8 @@ pub fn ranked_candidates(
264281
rhythm_explicit: explicit.is_some(),
265282
gesture,
266283
policy,
284+
// Echoed, never read: the pass above is the Phase-1 pass, unchanged.
285+
tonal: ask.tonal,
267286
})
268287
}
269288

0 commit comments

Comments
 (0)