package staffing import ( "encoding/json" "time" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" ) // The wire contract for the W3 endpoints. This is the backend interface handoff the future frontend // consumes; the decision fields mirror slate_members so the existing approve and reject UI carries over. // GapRequest is the body of POST /recommendations/gap/{requirementID}. Both fields are optional per run // overrides; the values actually used are recorded in the run's parameters. type GapRequest struct { StaffingFloor *string `json:"staffing_floor,omitempty"` // decimal string in [0,100]; default 75 AdjacencyThreshold *string `json:"adjacency_threshold,omitempty"` // decimal string in [0,2]; default 0.40 } // PortfolioRequest is the body of POST /recommendations/portfolio. RequirementIDs is an optional explicit // subset; when empty the portfolio uses ALL open requirements. type PortfolioRequest struct { RequirementIDs []string `json:"requirement_ids,omitempty"` StaffingFloor *string `json:"staffing_floor,omitempty"` AdjacencyThreshold *string `json:"adjacency_threshold,omitempty"` } // DecisionRequest is the body of POST /recommendations/{recommendationID}/decision. type DecisionRequest struct { Decision string `json:"decision"` // approved | rejected | dismissed Note string `json:"note,omitempty"` } // RunView is the run metadata. type RunView struct { ID string `json:"id"` View string `json:"view"` ScopeRef string `json:"scope_ref,omitempty"` ModelVersion string `json:"model_version"` Parameters json.RawMessage `json:"parameters"` CreatedAt string `json:"created_at"` } // RecommendationView is one recommendation on the wire. type RecommendationView struct { ID string `json:"id"` RunID string `json:"run_id"` RequirementID string `json:"requirement_id"` CandidateID string `json:"candidate_id,omitempty"` Action string `json:"action"` Rank int `json:"rank"` Confidence string `json:"confidence"` ValueScore *int64 `json:"value_score,omitempty"` Rationale string `json:"rationale"` RationaleSource string `json:"rationale_source"` Evidence json.RawMessage `json:"evidence"` Status string `json:"status"` DecidedBy string `json:"decided_by,omitempty"` DecidedAt string `json:"decided_at,omitempty"` DecisionNote string `json:"decision_note,omitempty"` CreatedAt string `json:"created_at"` } // RunResponse is the generating endpoints' result: the run plus its recommendations in rank order. type RunResponse struct { Run RunView `json:"run"` Recommendations []RecommendationView `json:"recommendations"` } // ProactiveResponse is the bench watch: the current pending proactive recommendations, paginated. type ProactiveResponse struct { Recommendations []RecommendationView `json:"recommendations"` Limit int `json:"limit"` Offset int `json:"offset"` } func runView(r dbgen.RecommendationRun) RunView { v := RunView{ ID: r.ID.String(), View: r.View, ModelVersion: r.ModelVersion, Parameters: json.RawMessage(r.Parameters), CreatedAt: r.CreatedAt.Format(time.RFC3339), } if r.ScopeRef != nil { v.ScopeRef = r.ScopeRef.String() } if len(v.Parameters) == 0 { v.Parameters = json.RawMessage("{}") } return v } func recommendationView(r dbgen.StaffingRecommendation) RecommendationView { v := RecommendationView{ ID: r.ID.String(), RunID: r.RunID.String(), RequirementID: r.RequirementID.String(), Action: r.Action, Rank: int(r.Rank), Confidence: r.Confidence.String(), ValueScore: r.ValueScore, Rationale: r.Rationale, RationaleSource: r.RationaleSource, Evidence: json.RawMessage(r.Evidence), Status: r.Status, CreatedAt: r.CreatedAt.Format(time.RFC3339), } if r.CandidateID != nil { v.CandidateID = r.CandidateID.String() } if r.DecidedBy != nil { v.DecidedBy = r.DecidedBy.String() } if r.DecidedAt != nil { v.DecidedAt = r.DecidedAt.Format(time.RFC3339) } if r.DecisionNote != nil { v.DecisionNote = *r.DecisionNote } if len(v.Evidence) == 0 { v.Evidence = json.RawMessage("{}") } return v } func recommendationViews(rows []dbgen.StaffingRecommendation) []RecommendationView { out := make([]RecommendationView, len(rows)) for i, r := range rows { out[i] = recommendationView(r) } return out }