Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 |
| Branch | master |
| Path or archive member | internal/modules/matching/handler.go |
| Original SHA-256 | 7e4dd542c8afc025c44e057a8e731a7fd81eea08bc1763ea55216e881d8746a5 |
| Git blob SHA | 080eaa83f799a4550ebdc385e2efbb9eaca868bf |
| Reading-copy SHA-256 | 7e4dd542c8afc025c44e057a8e731a7fd81eea08bc1763ea55216e881d8746a5 |
Every recorded occurrence
These 49 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/matching/handler.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓585 lines · 19.8 KB
package matching
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler exposes the matching/slate API. Routes are mounted under a tenant-scoped group.
type Handler struct{ svc *Service }
// NewHandler builds the HTTP handler.
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
// Routes registers EVERY matching endpoint on r at its flat path (already wrapped by the session
// resolver). It is the alias surface: it keeps the pre split paths (POST /match, /slates/..., etc.)
// working unchanged while the three tool routers mount the same handlers under their namespaces. It is
// the sum of the four tool route groups below, so a path appears in exactly one group and the flat and
// namespaced mounts share one handler. Every person scoring surface is an AEDT output and carries a
// capability check, so authorization is enforced at the API from the validated session, never hidden
// in the UI. Scoring and ranking need search.run; the slate write and the human decision need
// slate.write; reads need candidate.read.
func (h *Handler) Routes(r chi.Router) {
h.ScreenRoutes(r)
h.SlateRoutes(r)
h.ReadRoutes(r)
h.IntelRoutes(r)
h.MissionRoutes(r)
}
// ScreenRoutes are the screenMATCH endpoints: single candidate qualification, the pool ranked against
// one requirement, the Opportunity Fit for one candidate, the Mission Check resume flow, and the
// candidate writes that flow saves. POST /match is the original alias of POST /screen's raw result.
func (h *Handler) ScreenRoutes(r chi.Router) {
r.With(security.RequireCapability(security.CapSearchRun)).Post("/match", h.postMatch)
r.With(security.RequireCapability(security.CapSearchRun)).Post("/screen", h.postScreen)
r.With(security.RequireCapability(security.CapSearchRun)).Get("/requirements/{requirementID}/candidates", h.getRankedCandidates)
r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/{candidateID}/opportunities", h.getOpportunities)
// Mission Check (W1): scoring an in memory resume is a search.run AEDT output; the optional Save
// Candidate is a recruiter write. Both ride the same session auth, RBAC, and audit as every surface.
r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/opportunities", h.postMissionCheckOpportunities)
r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/screen", h.postMissionCheckJD)
r.With(security.RequireCapability(security.CapCandidateWrite)).Post("/candidates", h.postCreateCandidate)
r.With(security.RequireCapability(security.CapCandidateWrite)).Patch("/candidates/{candidateID}/clearance-provenance", h.patchClearanceProvenance)
}
// SlateRoutes are the slateMATCH endpoints: generate a slate, the human approve or reject of a member,
// and the frozen decision trace for a member.
func (h *Handler) SlateRoutes(r chi.Router) {
r.With(security.RequireCapability(security.CapSlateEdit)).Post("/slates/generate", h.postGenerate)
r.With(security.RequireCapability(security.CapSlateEdit)).Patch("/slates/{slateID}/members/{candidateID}/decision", h.postDecision)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}/members/{candidateID}/trace", h.getTrace)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements/{requirementID}/slates", h.getSlates)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}", h.getSlateDetail)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}/proposal-readiness", h.getProposalReadiness)
// The branded CSV is a bulk egress of candidate PII, so it needs export.run (owner, admin, recruiter),
// not the read capability every role holds. The egress is also recorded in the audit (ExportSlate).
r.With(security.RequireCapability(security.CapExportRun)).Get("/slates/{slateID}/export", h.exportSlate)
}
// ReadRoutes are the shared candidate and requirement pickers every tool needs to choose a target.
func (h *Handler) ReadRoutes(r chi.Router) {
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates", h.getCandidates)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates/{candidateID}", h.getCandidate)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements", h.getRequirements)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements/{requirementID}", h.getRequirement)
}
// IntelRoutes is the missionMATCH Candidates Intelligence View (the whole pool scored once, a
// screening stage AEDT output, so it needs search.run).
func (h *Handler) IntelRoutes(r chi.Router) {
r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/intelligence", h.getCandidateIntelligence)
}
// MissionRoutes are the missionMATCH reads that consume the stored artifacts: who can support a mission
// today, read from the persisted screenMATCH evaluations (never a rescore). It surfaces frozen scores,
// so it needs search.run like the other AEDT read surfaces.
func (h *Handler) MissionRoutes(r chi.Router) {
r.With(security.RequireCapability(security.CapSearchRun)).Get("/requirements/{requirementID}/support", h.getWhoCanSupport)
}
type matchRequest struct {
CandidateID uuid.UUID `json:"candidate_id"`
RequirementID uuid.UUID `json:"requirement_id"`
}
func (h *Handler) postMatch(w http.ResponseWriter, r *http.Request) {
var req matchRequest
if !decodeJSON(w, r, &req) {
return
}
if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required")
return
}
res, err := h.svc.ScreenCandidate(r.Context(), req.CandidateID, req.RequirementID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, matchResultView(res))
}
func (h *Handler) postScreen(w http.ResponseWriter, r *http.Request) {
var req matchRequest
if !decodeJSON(w, r, &req) {
return
}
if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required")
return
}
view, err := h.svc.Screen(r.Context(), req.CandidateID, req.RequirementID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, view)
}
func (h *Handler) getRankedCandidates(w http.ResponseWriter, r *http.Request) {
reqID, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
out, err := h.svc.RankCandidates(r.Context(), reqID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getOpportunities(w http.ResponseWriter, r *http.Request) {
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
limit := 10
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 100 {
limit = v
}
fits, err := h.svc.OpportunitiesForCandidate(r.Context(), candidateID, limit)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, fits)
}
type generateRequest struct {
RequirementID uuid.UUID `json:"requirement_id"`
Name string `json:"name"`
MaxCandidates int32 `json:"max_candidates"`
}
func (h *Handler) postGenerate(w http.ResponseWriter, r *http.Request) {
var req generateRequest
if !decodeJSON(w, r, &req) {
return
}
if req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "requirement_id is required")
return
}
out, err := h.svc.GenerateSlate(r.Context(), GenerateSlateInput{
RequirementID: req.RequirementID, Name: req.Name, MaxCandidates: req.MaxCandidates,
CreatedBy: principalID(r),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}
type decisionRequest struct {
Decision string `json:"decision"` // approved or rejected
Note string `json:"note,omitempty"` // optional reviewer note
}
func (h *Handler) postDecision(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
var req decisionRequest
if !decodeJSON(w, r, &req) {
return
}
if err := h.svc.DecideSlateMember(r.Context(), slateID, candidateID, req.Decision, req.Note); err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": req.Decision})
}
func (h *Handler) getTrace(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
m, err := h.svc.SlateMemberTrace(r.Context(), slateID, candidateID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, slateMemberTraceView(m))
}
func (h *Handler) getSlates(w http.ResponseWriter, r *http.Request) {
reqID, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
out, err := h.svc.ListSlates(r.Context(), reqID)
if err != nil {
writeServiceError(w, err)
return
}
if out == nil {
out = []SlateSummaryView{}
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getSlateDetail(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
out, err := h.svc.SlateDetail(r.Context(), slateID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getProposalReadiness(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
out, err := h.svc.ProposalReadiness(r.Context(), slateID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) exportSlate(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
csvText, err := h.svc.ExportSlate(r.Context(), slateID)
if err != nil {
writeServiceError(w, err)
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=\"slate-"+slateID.String()+".csv\"")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(csvText))
}
func (h *Handler) getWhoCanSupport(w http.ResponseWriter, r *http.Request) {
reqID, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
out, err := h.svc.WhoCanSupport(r.Context(), reqID)
if err != nil {
writeServiceError(w, err)
return
}
if out == nil {
out = []SupportView{}
}
writeJSON(w, http.StatusOK, out)
}
// principalID returns the validated session user id for the actor on a decision or generation.
func principalID(r *http.Request) *uuid.UUID {
if p, ok := tenant.UserFromContext(r.Context()); ok {
if id, err := uuid.Parse(p.UserID); err == nil {
return &id
}
}
return nil
}
func (h *Handler) getCandidateIntelligence(w http.ResponseWriter, r *http.Request) {
limit := 0
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil {
limit = v
}
out, err := h.svc.CandidateIntelligence(r.Context(), limit)
if err != nil {
writeServiceError(w, err)
return
}
if out == nil {
out = []CandidateIntelligenceView{}
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getCandidates(w http.ResponseWriter, r *http.Request) {
limit, offset := pageParams(r)
rows, err := h.svc.ListCandidates(r.Context(), limit, offset)
if err != nil {
writeServiceError(w, err)
return
}
out := make([]CandidateListView, len(rows))
for i, c := range rows {
out[i] = candidateListView(c)
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getRequirements(w http.ResponseWriter, r *http.Request) {
limit, offset := pageParams(r)
rows, err := h.svc.ListRequirements(r.Context(), limit, offset)
if err != nil {
writeServiceError(w, err)
return
}
out := make([]RequirementListView, len(rows))
for i, rq := range rows {
out[i] = requirementListView(rq)
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getCandidate(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
c, err := h.svc.GetCandidate(r.Context(), id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, candidateListView(c))
}
func (h *Handler) getRequirement(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
rq, err := h.svc.GetRequirement(r.Context(), id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, requirementListView(rq))
}
// ---------- Mission Check (W1) ----------
type missionCheckOpportunitiesRequest struct {
ResumeText string `json:"resume_text"`
Limit int `json:"limit"`
}
func (h *Handler) postMissionCheckOpportunities(w http.ResponseWriter, r *http.Request) {
var req missionCheckOpportunitiesRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.ResumeText) == "" {
writeError(w, http.StatusBadRequest, "resume_text is required")
return
}
out, err := h.svc.MissionCheckOpportunities(r.Context(), req.ResumeText, req.Limit)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
type missionCheckJDRequest struct {
ResumeText string `json:"resume_text"`
JobDescriptionText string `json:"job_description_text"`
}
func (h *Handler) postMissionCheckJD(w http.ResponseWriter, r *http.Request) {
var req missionCheckJDRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.ResumeText) == "" || strings.TrimSpace(req.JobDescriptionText) == "" {
writeError(w, http.StatusBadRequest, "resume_text and job_description_text are required")
return
}
out, err := h.svc.MissionCheckJD(r.Context(), req.ResumeText, req.JobDescriptionText)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
type createCandidateRequest struct {
FullName string `json:"full_name"`
ClearanceLevel string `json:"clearance_level"`
ClearancePoly string `json:"clearance_poly"`
ClearanceStatus string `json:"clearance_status"`
YearsExperience int `json:"years_experience"`
Location string `json:"location"`
Remote bool `json:"remote"`
Relocatable bool `json:"relocatable"`
Available bool `json:"available"`
Skills []string `json:"skills"`
Certifications []string `json:"certifications"`
CustomerExperience []string `json:"customer_experience"`
}
func (h *Handler) postCreateCandidate(w http.ResponseWriter, r *http.Request) {
var req createCandidateRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.FullName) == "" {
writeError(w, http.StatusBadRequest, "full_name is required")
return
}
id, err := h.svc.SaveCandidate(r.Context(), SaveCandidateInput(req))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, map[string]string{"id": id.String()})
}
type setProvenanceRequest struct {
Provenance string `json:"provenance"` // self_reported | sponsor_verified | document | continuous_vetting
VerifiedAt *string `json:"verified_at,omitempty"` // optional YYYY-MM-DD, the FSO verification date
}
func (h *Handler) patchClearanceProvenance(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
var req setProvenanceRequest
if !decodeJSON(w, r, &req) {
return
}
var va *time.Time
if req.VerifiedAt != nil && *req.VerifiedAt != "" {
t, perr := time.Parse("2006-01-02", *req.VerifiedAt)
if perr != nil {
writeError(w, http.StatusBadRequest, "verified_at must be YYYY-MM-DD")
return
}
va = &t
}
if err := h.svc.SetClearanceProvenance(r.Context(), id, req.Provenance, va); err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// ---------- helpers ----------
func pageParams(r *http.Request) (limit, offset int32) {
limit, offset = 50, 0
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 500 {
limit = int32(v)
}
if v, err := strconv.Atoi(r.URL.Query().Get("offset")); err == nil && v >= 0 {
offset = int32(v)
}
return limit, offset
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error())
return false
}
return true
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// writeServiceError maps service/tenant errors to HTTP status without leaking internals.
func writeServiceError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, tenant.ErrNoTenant):
writeError(w, http.StatusForbidden, "no validated tenant")
case errors.Is(err, ErrNotFound):
writeError(w, http.StatusNotFound, "not found")
case errors.Is(err, ErrBadRequest):
writeError(w, http.StatusBadRequest, "bad request")
case errors.Is(err, ErrNoDatabase):
writeError(w, http.StatusServiceUnavailable, "database not configured")
case errors.Is(err, ErrNoExtractor):
writeError(w, http.StatusServiceUnavailable, "mission check extraction not configured")
default:
writeError(w, http.StatusInternalServerError, "internal error")
}
}
// SlateMemberTraceView is the audit and candidate facing explanation of a frozen decision: the per
// component trace, the human rationale, the gap summary, the decision, and the model version that
// produced it. The trace and gap summary are passed through as the stored JSON.
type SlateMemberTraceView struct {
CandidateID string `json:"candidate_id"`
TotalScore string `json:"total_score"`
Band string `json:"band"`
Explanation string `json:"explanation"`
Trace json.RawMessage `json:"trace"`
GapSummary json.RawMessage `json:"gap_summary"`
Decision string `json:"decision"`
ModelVersion string `json:"model_version"`
}
func slateMemberTraceView(m dbgen.SlateMember) SlateMemberTraceView {
v := SlateMemberTraceView{
CandidateID: m.CandidateID.String(),
TotalScore: m.TotalScore.StringFixed(2),
Band: m.Band,
Explanation: m.Explanation,
Trace: json.RawMessage(m.Trace),
GapSummary: json.RawMessage(m.GapSummary),
Decision: m.Decision,
}
if m.ModelVersion != nil {
v.ModelVersion = *m.ModelVersion
}
return v
}