gmal-scope-builder/backend/app/schemas/project.py
DJP 668ea44ea2 Client tier mapping + GMAL complexity variant expansion
- 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>
2026-04-09 15:02:45 -04:00

117 lines
2.5 KiB
Python

from pydantic import BaseModel
from datetime import datetime
class ProjectCreate(BaseModel):
name: str
client_name: str | None = None
description: str | None = None
model_type: str = "current_oplus"
class ProjectUpdate(BaseModel):
name: str | None = None
client_name: str | None = None
description: str | None = None
model_type: str | None = None
class ProjectOut(BaseModel):
id: int
name: str
client_name: str | None
description: str | None
model_type: str
status: str
source_filename: str | None
parse_stage: str | None = None
has_brief_analysis: bool = False
has_tier_mapping: bool = False
ai_input_tokens: int = 0
ai_output_tokens: int = 0
ai_cost_usd: float = 0
ai_call_count: int = 0
created_at: datetime
updated_at: datetime
asset_count: int = 0
class Config:
from_attributes = True
class ClientAssetOut(BaseModel):
id: int
project_id: int
raw_name: str | None
raw_description: str | None
client_tier: str | None = None
volume: int
sort_order: int | None
class Config:
from_attributes = True
class ClientAssetUpdate(BaseModel):
raw_name: str | None = None
raw_description: str | None = None
volume: int | None = None
class MatchOut(BaseModel):
id: int
client_asset_id: int
gmal_asset_id: int
gmal_id: str | None = None
gmal_name: str | None = None
gmal_unique_name: str | None = None
confidence: str
confidence_score: float | None
ai_reasoning: str | None
caveat_text: str | None
is_selected: bool
rank: int
class Config:
from_attributes = True
class MatchSelectRequest(BaseModel):
is_selected: bool = True
class ManualMatchRequest(BaseModel):
gmal_asset_id: int
class RatecardLineOut(BaseModel):
id: int
client_asset_id: int
client_asset_name: str | None = None
gmal_asset_id: int
gmal_id: str | None = None
role_id: int
role_title: str | None = None
discipline: str | None = None
base_hours: float | None
volume: int
total_hours: float | None
manual_override: float | None
notes: str | None
class Config:
from_attributes = True
class RatecardLineUpdate(BaseModel):
manual_override: float | None = None
notes: str | None = None
class RatecardSummary(BaseModel):
project_id: int
project_name: str
model_type: str
total_assets: int
total_hours: float
lines: list[RatecardLineOut]