Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/backend/prompts/phase2_system.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ LIVE 12 OUTPUT POLICY
- trackContext to one of:
- an exact trackLayout.name value
- Master
- Return:<return name>
- Return:<name> where <name> is the EXACT string from routingBlueprint.returns[].name
- CRITICAL: Every Return:<name> trackContext MUST use a name that appears verbatim in your routingBlueprint.returns array. Do not abbreviate, shorten, or paraphrase return names. If you declared a return named "Long Reverb", use Return:Long Reverb — never Return:Reverb.
- For return tracks, use the exact format Return:<name> with no space after the colon.
- workflowStage to one of:
- PROJECT_SETUP
Expand Down
84 changes: 84 additions & 0 deletions apps/backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3427,6 +3427,71 @@ def _normalize_track_context_value(
]


def _declared_return_names(data: dict[str, Any]) -> list[str]:
routing_blueprint = _as_record(data.get("routingBlueprint"))
returns = routing_blueprint.get("returns") if routing_blueprint else None
if not isinstance(returns, list):
return []
names: list[str] = []
for item in returns:
record = _as_record(item)
if record and _is_str(record.get("name")):
names.append(record["name"])
return names


def _repair_return_track_context(
value: str,
*,
path: str,
declared_returns: list[str],
) -> tuple[str, list[dict[str, Any]]]:
if not isinstance(value, str) or not value.startswith("Return:"):
return value, []
suffix = value[len("Return:"):]
if not suffix:
return value, []
exact_set = {f"Return:{name}" for name in declared_returns}
if value in exact_set:
return value, []
# Case-insensitive exact match
suffix_lower = suffix.lower()
ci_matches = [name for name in declared_returns if suffix_lower == name.lower()]
if len(ci_matches) == 1:
repaired = f"Return:{ci_matches[0]}"
return repaired, [
_build_phase2_validation_warning(
code="COERCED_TRACK_CONTEXT",
path=path,
message=(
f"Coerced trackContext '{value}' to '{repaired}' by matching "
"against declared routingBlueprint return names."
),
original_value=value,
coerced_value=repaired,
)
]
# Substring match (suffix is a substring of a declared return name)
sub_matches = [
name for name in declared_returns if suffix_lower in name.lower()
]
if len(sub_matches) == 1:
repaired = f"Return:{sub_matches[0]}"
return repaired, [
_build_phase2_validation_warning(
code="COERCED_TRACK_CONTEXT",
path=path,
message=(
f"Coerced trackContext '{value}' to '{repaired}' by matching "
"against declared routingBlueprint return names."
),
original_value=value,
coerced_value=repaired,
)
]
return value, []


def _salvage_phase2_array_items(
items: Any,
*,
Expand Down Expand Up @@ -3471,6 +3536,7 @@ def _normalize_and_salvage_phase2_result(
normalized = dict(data)
warnings: list[dict[str, Any]] = []
emptied_required_arrays: set[str] = set()
declared_returns = _declared_return_names(data)

recommendations = normalized.get("abletonRecommendations")
if isinstance(recommendations, list):
Expand All @@ -3486,9 +3552,15 @@ def _normalize_and_salvage_phase2_result(
normalized_record.get("trackContext"),
path=f"abletonRecommendations[{index}].trackContext",
)
track_context, repair_warnings = _repair_return_track_context(
track_context,
path=f"abletonRecommendations[{index}].trackContext",
declared_returns=declared_returns,
)
normalized_item = dict(normalized_record)
normalized_item["trackContext"] = track_context
warnings.extend(track_context_warnings)
warnings.extend(repair_warnings)
normalized_recommendations.append(normalized_item)
warnings.extend(item_warnings)
normalized["abletonRecommendations"] = normalized_recommendations
Expand All @@ -3506,9 +3578,15 @@ def _normalize_and_salvage_phase2_result(
normalized_item.get("trackContext"),
path=f"mixAndMasterChain[{index}].trackContext",
)
track_context, repair_warnings = _repair_return_track_context(
track_context,
path=f"mixAndMasterChain[{index}].trackContext",
declared_returns=declared_returns,
)
normalized_item["trackContext"] = track_context
normalized_mix_chain.append(normalized_item)
warnings.extend(track_context_warnings)
warnings.extend(repair_warnings)
normalized["mixAndMasterChain"] = normalized_mix_chain

secret_sauce = _as_record(normalized.get("secretSauce"))
Expand All @@ -3525,9 +3603,15 @@ def _normalize_and_salvage_phase2_result(
normalized_item.get("trackContext"),
path=f"secretSauce.workflowSteps[{index}].trackContext",
)
track_context, repair_warnings = _repair_return_track_context(
track_context,
path=f"secretSauce.workflowSteps[{index}].trackContext",
declared_returns=declared_returns,
)
normalized_item["trackContext"] = track_context
normalized_steps.append(normalized_item)
warnings.extend(track_context_warnings)
warnings.extend(repair_warnings)
normalized_secret_sauce = dict(secret_sauce)
normalized_secret_sauce["workflowSteps"] = normalized_steps
normalized["secretSauce"] = normalized_secret_sauce
Expand Down
89 changes: 89 additions & 0 deletions apps/backend/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,95 @@ def test_parse_phase2_result_debug_coerces_return_track_context_spacing(self) ->
self.assertIn("abletonRecommendations[0].trackContext", warning_paths)
self.assertIn("secretSauce.workflowSteps[0].trackContext", warning_paths)

def test_parse_phase2_result_debug_repairs_abbreviated_return_name(self) -> None:
payload = _valid_phase2_result()
payload["routingBlueprint"]["returns"][0]["name"] = "Long Reverb"
payload["mixAndMasterChain"][0]["trackContext"] = "Return:Reverb"
payload["abletonRecommendations"][0]["trackContext"] = "Return:Reverb"
payload["secretSauce"]["workflowSteps"][0]["trackContext"] = "Return:Reverb"

debug = server._parse_phase2_result_debug(json.dumps(payload))

self.assertIsNone(debug["skipReason"])
self.assertEqual(debug["result"]["mixAndMasterChain"][0]["trackContext"], "Return:Long Reverb")
self.assertEqual(debug["result"]["abletonRecommendations"][0]["trackContext"], "Return:Long Reverb")
self.assertEqual(
debug["result"]["secretSauce"]["workflowSteps"][0]["trackContext"],
"Return:Long Reverb",
)
coerced = [w for w in debug["validationWarnings"] if w["code"] == "COERCED_TRACK_CONTEXT"]
self.assertEqual(len(coerced), 3)

def test_parse_phase2_result_debug_repairs_case_insensitive_return_name(self) -> None:
payload = _valid_phase2_result()
payload["routingBlueprint"]["returns"][0]["name"] = "Long Reverb"
payload["mixAndMasterChain"][0]["trackContext"] = "Return:long reverb"

debug = server._parse_phase2_result_debug(json.dumps(payload))

self.assertIsNone(debug["skipReason"])
self.assertEqual(debug["result"]["mixAndMasterChain"][0]["trackContext"], "Return:Long Reverb")
coerced = [w for w in debug["validationWarnings"] if w["code"] == "COERCED_TRACK_CONTEXT"]
self.assertTrue(len(coerced) >= 1)

def test_parse_phase2_result_debug_no_repair_when_ambiguous_substring(self) -> None:
payload = _valid_phase2_result()
payload["routingBlueprint"]["returns"] = [
{
"name": "Long Reverb",
"purpose": "Spatial depth.",
"sendSources": ["Drum Group"],
"deviceFocus": "Hybrid Reverb",
"levelGuidance": "-18 dB send baseline",
},
{
"name": "Short Reverb",
"purpose": "Tight space.",
"sendSources": ["Drum Group"],
"deviceFocus": "Reverb",
"levelGuidance": "-12 dB send baseline",
},
]
payload["mixAndMasterChain"][0]["trackContext"] = "Return:Reverb"

debug = server._parse_phase2_result_debug(json.dumps(payload))

self.assertIsNone(debug["skipReason"])
# No repair — ambiguous match, value stays as-is
self.assertEqual(debug["result"]["mixAndMasterChain"][0]["trackContext"], "Return:Reverb")
coerced = [w for w in debug["validationWarnings"] if w["code"] == "COERCED_TRACK_CONTEXT"]
# No coercion warning from the repair pass for this field
coerced_mix = [w for w in coerced if "mixAndMasterChain" in w["path"]]
self.assertEqual(len(coerced_mix), 0)

def test_parse_phase2_result_debug_no_repair_when_exact_match_exists(self) -> None:
payload = _valid_phase2_result()
payload["routingBlueprint"]["returns"][0]["name"] = "Reverb"
payload["mixAndMasterChain"][0]["trackContext"] = "Return:Reverb"

debug = server._parse_phase2_result_debug(json.dumps(payload))

self.assertIsNone(debug["skipReason"])
self.assertEqual(debug["result"]["mixAndMasterChain"][0]["trackContext"], "Return:Reverb")
coerced = [w for w in debug["validationWarnings"] if w["code"] == "COERCED_TRACK_CONTEXT"]
coerced_mix = [w for w in coerced if "mixAndMasterChain" in w["path"]]
self.assertEqual(len(coerced_mix), 0)

def test_parse_phase2_result_debug_repairs_after_spacing_normalization(self) -> None:
payload = _valid_phase2_result()
payload["routingBlueprint"]["returns"][0]["name"] = "Long Reverb"
# Space after colon AND abbreviated — both normalizers should chain
payload["mixAndMasterChain"][0]["trackContext"] = "Return: Reverb"

debug = server._parse_phase2_result_debug(json.dumps(payload))

self.assertIsNone(debug["skipReason"])
self.assertEqual(debug["result"]["mixAndMasterChain"][0]["trackContext"], "Return:Long Reverb")
coerced = [w for w in debug["validationWarnings"] if w["code"] == "COERCED_TRACK_CONTEXT"]
coerced_mix = [w for w in coerced if "mixAndMasterChain" in w["path"]]
# Two coercion warnings: spacing fix + fuzzy repair
self.assertEqual(len(coerced_mix), 2)

def test_parse_phase2_result_drops_single_invalid_ableton_recommendation_item(self) -> None:
payload = _valid_phase2_result()
payload["abletonRecommendations"].append(
Expand Down
Loading