- Tier mapping on projects: configurable label→complexity mapping
- Presets: A/B/C, 1/2/3, Gold/Silver/Bronze
- Stored as JSON on project.tier_mapping
- ClientAsset.client_tier field for tracking which tier an asset belongs to
- GMAL family endpoint: GET /gmal/assets/{id}/family returns all complexity variants
- Looks up by asset_name (NOT by GMAL number increment)
- Verified: families share asset_name across non-sequential GMAL IDs
- Expand to Tiers: POST /projects/{id}/expand-tiers
- Splits each matched asset into N tier variants (one per tier)
- Finds correct GMAL variant by asset_name + complexity_level query
- Creates new ClientAsset + Match per tier with correct GMAL
- Removes original un-tiered asset after expansion
- Frontend: tier preset buttons + expand button on Match Review tab
- Tier tags shown with label → complexity mapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
5.2 KiB
Python
143 lines
5.2 KiB
Python
"""Expand matched assets into complexity tier variants."""
|
|
|
|
import json
|
|
import logging
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.gmal import GmalAsset
|
|
from app.models.project import Project, ClientAsset, Match, MatchConfidence
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Map complexity names to levels
|
|
COMPLEXITY_MAP = {
|
|
"simple": 1,
|
|
"medium": 2,
|
|
"mid": 2,
|
|
"complex": 3,
|
|
}
|
|
|
|
|
|
async def expand_to_tiers(db: AsyncSession, project: Project) -> dict:
|
|
"""Expand each matched asset into complexity variants based on the project's tier mapping.
|
|
|
|
For each selected match:
|
|
1. Find the GMAL family (same asset_name, different complexity_level)
|
|
2. For each tier in the mapping, create a new ClientAsset + Match
|
|
3. Remove the original single-match asset
|
|
|
|
Returns summary of what was done.
|
|
"""
|
|
if not project.tier_mapping:
|
|
return {"error": "No tier mapping configured for this project"}
|
|
|
|
tier_config = json.loads(project.tier_mapping)
|
|
tiers = tier_config.get("tiers", [])
|
|
if not tiers:
|
|
return {"error": "Tier mapping is empty"}
|
|
|
|
# Load client assets with selected matches
|
|
assets_result = await db.execute(
|
|
select(ClientAsset).where(ClientAsset.project_id == project.id).order_by(ClientAsset.sort_order)
|
|
)
|
|
client_assets = assets_result.scalars().all()
|
|
|
|
# Skip assets that already have a tier (already expanded)
|
|
unexpanded = [ca for ca in client_assets if not ca.client_tier]
|
|
if not unexpanded:
|
|
return {"message": "All assets already have tier assignments", "expanded": 0}
|
|
|
|
expanded_count = 0
|
|
skipped_count = 0
|
|
|
|
for ca in unexpanded:
|
|
# Get the selected match
|
|
match_result = await db.execute(
|
|
select(Match).where(Match.client_asset_id == ca.id, Match.is_selected == True)
|
|
)
|
|
selected_match = match_result.scalar_one_or_none()
|
|
if not selected_match:
|
|
skipped_count += 1
|
|
continue
|
|
|
|
# Get the matched GMAL asset
|
|
gmal_result = await db.execute(
|
|
select(GmalAsset).where(GmalAsset.id == selected_match.gmal_asset_id)
|
|
)
|
|
matched_gmal = gmal_result.scalar_one_or_none()
|
|
if not matched_gmal or not matched_gmal.asset_name:
|
|
skipped_count += 1
|
|
continue
|
|
|
|
# Find the GMAL family by asset_name
|
|
family_result = await db.execute(
|
|
select(GmalAsset).where(
|
|
GmalAsset.asset_name == matched_gmal.asset_name,
|
|
GmalAsset.has_hour_routes == True,
|
|
).order_by(GmalAsset.complexity_level)
|
|
)
|
|
family = {g.complexity_level: g for g in family_result.scalars().all()}
|
|
|
|
if len(family) < 2:
|
|
# Only one variant exists, skip expansion
|
|
skipped_count += 1
|
|
continue
|
|
|
|
# Create a new ClientAsset + Match for each tier
|
|
for tier in tiers:
|
|
tier_label = tier["label"]
|
|
complexity_name = tier["complexity"].lower()
|
|
complexity_level = COMPLEXITY_MAP.get(complexity_name)
|
|
|
|
if complexity_level is None:
|
|
logger.warning(f"Unknown complexity '{complexity_name}' in tier mapping")
|
|
continue
|
|
|
|
gmal_variant = family.get(complexity_level)
|
|
if not gmal_variant:
|
|
# This complexity level doesn't exist for this asset
|
|
logger.warning(f"No {complexity_name} variant for '{matched_gmal.asset_name}'")
|
|
continue
|
|
|
|
# Create new client asset with tier label
|
|
new_ca = ClientAsset(
|
|
project_id=project.id,
|
|
raw_name=f"{ca.raw_name} - {tier_label}",
|
|
raw_description=ca.raw_description,
|
|
client_tier=tier_label,
|
|
volume=ca.volume,
|
|
sort_order=(ca.sort_order or 0) * 10 + (complexity_level or 0),
|
|
)
|
|
db.add(new_ca)
|
|
await db.flush()
|
|
|
|
# Create match pointing to the correct complexity variant
|
|
new_match = Match(
|
|
client_asset_id=new_ca.id,
|
|
gmal_asset_id=gmal_variant.id,
|
|
confidence=MatchConfidence.EXACT,
|
|
confidence_score=0.95,
|
|
ai_reasoning=f"Tier expansion: {tier_label} → {gmal_variant.complexity_name} variant of {matched_gmal.asset_name} ({gmal_variant.gmal_id})",
|
|
caveat_text=f"Auto-mapped from tier '{tier_label}' to {gmal_variant.complexity_name} complexity.",
|
|
is_selected=True,
|
|
rank=1,
|
|
)
|
|
db.add(new_match)
|
|
expanded_count += 1
|
|
|
|
# Delete the original un-tiered asset and its matches
|
|
orig_matches = await db.execute(select(Match).where(Match.client_asset_id == ca.id))
|
|
for m in orig_matches.scalars().all():
|
|
await db.delete(m)
|
|
await db.delete(ca)
|
|
|
|
await db.commit()
|
|
|
|
return {
|
|
"message": f"Expanded {len(unexpanded) - skipped_count} assets into {expanded_count} tier variants. {skipped_count} skipped (no match or single variant).",
|
|
"expanded_assets": len(unexpanded) - skipped_count,
|
|
"new_variants": expanded_count,
|
|
"skipped": skipped_count,
|
|
}
|