Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/service.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 39df94f92b2a33f852481fe1954dabed841524bb |
| Branch | w0-aedt-compliance |
| Path or archive member | internal/modules/matching/service.go |
| Original SHA-256 | e97d742f0bb0a36022a9596e997743a9a9c762ebf972bfe21404721c06337ab8 |
| Git blob SHA | 0dab5f56afa38dc3e3dccf5d29e7076ebed35913 |
| Reading-copy SHA-256 | e97d742f0bb0a36022a9596e997743a9a9c762ebf972bfe21404721c06337ab8 |
Recovered source transcript
Download complete source text ↓486 lines · 17.5 KB
package matching
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/shopspring/decimal"
"github.com/missionworkforce/missionmatch/internal/modules/matching/engine"
"github.com/missionworkforce/missionmatch/internal/modules/matching/intelligence"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrNoDatabase is returned when a DB-backed operation is attempted but no pool is configured.
var (
ErrNoDatabase = errors.New("matching: no database configured")
ErrNotFound = errors.New("matching: not found")
ErrBadRequest = errors.New("matching: bad request")
)
const defaultMaxCandidates = 500
// Service is the slate-loop application service. It runs every DB operation inside a tenant
// transaction (RLS-scoped) and delegates scoring to the pure engine.
type Service struct {
db *tenant.DB
matcher engine.SkillMatcher // injected semantic skill matcher; may be nil (exact-only)
aud *audit.Writer // AEDT audit trail; may be nil (tests skip audit)
clock func() time.Time // injectable for deterministic tests
}
// NewService builds the service. db may be nil (DB routes then return ErrNoDatabase). aud may be nil.
func NewService(db *tenant.DB, matcher engine.SkillMatcher, aud *audit.Writer) *Service {
return &Service{db: db, matcher: matcher, aud: aud, clock: time.Now}
}
// auditOnTx records an AEDT event on the caller's tenant tx if an audit writer is configured. The
// actor is the validated session principal; demographics are NEVER carried in an audit event. It
// returns the write error so the caller fails closed: an AEDT score or decision that cannot be
// recorded rolls back rather than being served unaudited (the audit package contract).
func (s *Service) auditOnTx(ctx context.Context, tx pgx.Tx, e audit.Event) error {
if s.aud == nil {
return nil
}
if p, ok := tenant.UserFromContext(ctx); ok {
if id, err := uuid.Parse(p.UserID); err == nil {
e.ActorUserID = &id
}
}
return s.aud.OnTx(ctx, tx, e)
}
// servedScoreEvent builds the aedt.score.served event for a single served candidate score. It carries
// the candidate, the tool, the score, and the band so the compliance pool can reconstruct a screening
// stage adverse impact analysis by joining to eeo, and NOTHING about demographics.
func servedScoreEvent(tool string, candidateID uuid.UUID, res engine.MatchResult) audit.Event {
payload, _ := json.Marshal(map[string]any{
"tool": tool,
"score": res.Total.StringFixed(2),
"band": res.Band.Code(),
})
return audit.Event{Action: "aedt.score.served", EntityType: "candidate", EntityID: candidateID.String(), NewValues: payload}
}
func (s *Service) orgID(ctx context.Context) (uuid.UUID, error) {
raw, ok := tenant.OrgFromContext(ctx)
if !ok {
return uuid.Nil, tenant.ErrNoTenant
}
id, err := uuid.Parse(raw)
if err != nil {
return uuid.Nil, fmt.Errorf("matching: invalid org id in context: %w", err)
}
return id, nil
}
// ScreenCandidate scores one candidate against one requirement (screenMATCH / POST /match). Read-only.
func (s *Service) ScreenCandidate(ctx context.Context, candidateID, requirementID uuid.UUID) (engine.MatchResult, error) {
if s.db == nil {
return engine.MatchResult{}, ErrNoDatabase
}
asOf := s.clock()
var res engine.MatchResult
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
er, err := loadRequirement(ctx, q, requirementID)
if err != nil {
return err
}
c, err := q.GetCandidate(ctx, candidateID)
if err != nil {
return fmt.Errorf("get candidate: %w", err)
}
ec, err := loadCandidate(ctx, q, c, asOf)
if err != nil {
return err
}
res = engine.ScoreCandidate(ec, er, s.matcher)
// A focused single candidate qualification is a screening stage AEDT output. Record it as a
// served score (candidate, tool, score, band, no demographics) so adverse impact at the
// screening stage stays visible even before any slate decision exists. Fail closed if it cannot
// be recorded.
return s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.match", candidateID, res))
})
return res, err
}
// GenerateSlate scores every candidate against a requirement, ranks them, and persists the slate
// plus the frozen decision records (slateMATCH / POST /slates/generate). One atomic transaction.
func (s *Service) GenerateSlate(ctx context.Context, in GenerateSlateInput) (SlateResult, error) {
if s.db == nil {
return SlateResult{}, ErrNoDatabase
}
if in.Name == "" {
return SlateResult{}, errors.New("matching: slate name is required")
}
if in.MaxCandidates <= 0 {
in.MaxCandidates = defaultMaxCandidates
}
orgID, err := s.orgID(ctx)
if err != nil {
return SlateResult{}, err
}
asOf := s.clock()
var out SlateResult
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = generateSlate(ctx, dbgen.New(tx), orgID, in, s.matcher, asOf)
if e != nil {
return e
}
payload, _ := json.Marshal(map[string]any{"requirement_id": in.RequirementID.String(), "members": len(out.Members)})
return s.auditOnTx(ctx, tx, audit.Event{Action: "aedt.slate.generated", EntityType: "slate", EntityID: out.SlateID.String(), NewValues: payload})
})
return out, err
}
// DecideSlateMember records the human approve or reject of a slate member. This is the human in the
// loop control: the engine never decides; a person with slate authority does, and the action is
// recorded with the actor and the time, plus an AEDT audit event. decision must be approved or
// rejected (pending is the un decided default and cannot be set here).
func (s *Service) DecideSlateMember(ctx context.Context, slateID, candidateID uuid.UUID, decision string) error {
if s.db == nil {
return ErrNoDatabase
}
if decision != "approved" && decision != "rejected" {
return ErrBadRequest
}
p, ok := tenant.UserFromContext(ctx)
if !ok {
return tenant.ErrNoTenant
}
decidedBy, err := uuid.Parse(p.UserID)
if err != nil {
return tenant.ErrNoTenant
}
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
row, e := q.SetSlateMemberDecision(ctx, dbgen.SetSlateMemberDecisionParams{
SlateID: slateID, CandidateID: candidateID, Decision: decision, DecidedBy: &decidedBy,
})
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
payload, _ := json.Marshal(map[string]any{"decision": decision, "band": row.Band})
return s.auditOnTx(ctx, tx, audit.Event{Action: "aedt.slate_member.decided", EntityType: "candidate", EntityID: candidateID.String(), NewValues: payload})
})
}
// SlateMemberTrace returns the persisted per component trace, rationale, and gap summary for a frozen
// decision, the audit and candidate facing explanation of what was assessed and why.
func (s *Service) SlateMemberTrace(ctx context.Context, slateID, candidateID uuid.UUID) (dbgen.SlateMember, error) {
if s.db == nil {
return dbgen.SlateMember{}, ErrNoDatabase
}
var m dbgen.SlateMember
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
row, e := dbgen.New(tx).GetSlateMember(ctx, dbgen.GetSlateMemberParams{SlateID: slateID, CandidateID: candidateID})
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
m = row
return nil
})
return m, err
}
// ListCandidates / ListRequirements are the read endpoints (GET /candidates, /requirements).
func (s *Service) ListCandidates(ctx context.Context, limit, offset int32) ([]dbgen.Candidate, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
var out []dbgen.Candidate
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = dbgen.New(tx).ListCandidates(ctx, dbgen.ListCandidatesParams{Limit: limit, Offset: offset})
return e
})
return out, err
}
func (s *Service) ListRequirements(ctx context.Context, limit, offset int32) ([]dbgen.Requirement, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
var out []dbgen.Requirement
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = dbgen.New(tx).ListRequirements(ctx, dbgen.ListRequirementsParams{Limit: limit, Offset: offset})
return e
})
return out, err
}
// generateSlate is the pure orchestration over a Querier — testable with a fake, no live DB.
func generateSlate(ctx context.Context, q dbgen.Querier, orgID uuid.UUID, in GenerateSlateInput, matcher engine.SkillMatcher, asOf time.Time) (SlateResult, error) {
er, err := loadRequirement(ctx, q, in.RequirementID)
if err != nil {
return SlateResult{}, err
}
cands, err := q.ListCandidates(ctx, dbgen.ListCandidatesParams{Limit: in.MaxCandidates, Offset: 0})
if err != nil {
return SlateResult{}, fmt.Errorf("list candidates: %w", err)
}
type scored struct {
id uuid.UUID
res engine.MatchResult
}
results := make([]scored, 0, len(cands))
for _, c := range cands {
ec, err := loadCandidate(ctx, q, c, asOf)
if err != nil {
return SlateResult{}, err
}
results = append(results, scored{id: c.ID, res: engine.ScoreCandidate(ec, er, matcher)})
}
// Deterministic ranking: total score descending, candidate id ascending as the unique tiebreaker.
sort.SliceStable(results, func(i, j int) bool {
if d := results[i].res.Total.Cmp(results[j].res.Total); d != 0 {
return d > 0
}
return results[i].id.String() < results[j].id.String()
})
slate, err := q.CreateSlate(ctx, dbgen.CreateSlateParams{
OrganizationID: orgID, RequirementID: in.RequirementID, Name: in.Name, Status: "draft", CreatedBy: in.CreatedBy,
})
if err != nil {
return SlateResult{}, fmt.Errorf("create slate: %w", err)
}
members := make([]SlateMemberView, 0, len(results))
for i, sc := range results {
rank := int32(i + 1)
params, err := ToSlateMemberParams(orgID, slate.ID, sc.id, sc.res, &rank)
if err != nil {
return SlateResult{}, err
}
if _, err := q.AddSlateMember(ctx, params); err != nil {
return SlateResult{}, fmt.Errorf("add slate member: %w", err)
}
members = append(members, slateMemberView(sc.id.String(), int(rank), sc.res))
}
return SlateResult{SlateID: slate.ID, RequirementID: in.RequirementID, Name: in.Name, Members: members}, nil
}
func loadRequirement(ctx context.Context, q dbgen.Querier, reqID uuid.UUID) (engine.Requirement, error) {
r, err := q.GetRequirement(ctx, reqID)
if err != nil {
return engine.Requirement{}, fmt.Errorf("get requirement: %w", err)
}
return reqFromRow(ctx, q, r)
}
func reqFromRow(ctx context.Context, q dbgen.Querier, r dbgen.Requirement) (engine.Requirement, error) {
rs, err := q.ListRequiredSkills(ctx, r.ID)
if err != nil {
return engine.Requirement{}, fmt.Errorf("list required skills: %w", err)
}
rc, err := q.ListRequiredCertifications(ctx, r.ID)
if err != nil {
return engine.Requirement{}, fmt.Errorf("list required certifications: %w", err)
}
return ToEngineRequirement(r, rs, rc)
}
// loadCandidateByID loads a candidate, its sub-lists, and the engine input in one place. It returns
// the DB row (for clearance dates), the engine candidate, and the customer agencies.
func loadCandidateByID(ctx context.Context, q dbgen.Querier, id uuid.UUID, asOf time.Time) (dbgen.Candidate, engine.Candidate, []string, error) {
c, err := q.GetCandidate(ctx, id)
if err != nil {
return dbgen.Candidate{}, engine.Candidate{}, nil, fmt.Errorf("get candidate: %w", err)
}
skills, err := q.ListCandidateSkills(ctx, id)
if err != nil {
return dbgen.Candidate{}, engine.Candidate{}, nil, fmt.Errorf("list candidate skills: %w", err)
}
certs, err := q.ListCandidateCertifications(ctx, id)
if err != nil {
return dbgen.Candidate{}, engine.Candidate{}, nil, fmt.Errorf("list candidate certifications: %w", err)
}
cust, err := q.ListCandidateCustomerExperience(ctx, id)
if err != nil {
return dbgen.Candidate{}, engine.Candidate{}, nil, fmt.Errorf("list candidate customer experience: %w", err)
}
ec, err := ToEngineCandidate(c, skills, certs, cust, asOf)
if err != nil {
return dbgen.Candidate{}, engine.Candidate{}, nil, err
}
return c, ec, cust, nil
}
// Screen returns the full screenMATCH™ intelligence view for one candidate vs one requirement —
// the explainable score plus the Mission Readiness panels. Read-only.
func (s *Service) Screen(ctx context.Context, candidateID, requirementID uuid.UUID) (ScreenView, error) {
if s.db == nil {
return ScreenView{}, ErrNoDatabase
}
asOf := s.clock()
var view ScreenView
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
er, err := loadRequirement(ctx, q, requirementID)
if err != nil {
return err
}
c, ec, cust, err := loadCandidateByID(ctx, q, candidateID, asOf)
if err != nil {
return err
}
res := engine.ScoreCandidate(ec, er, s.matcher)
view = assembleScreenView(ec, er, res, ClearanceFactsFrom(c, cust), asOf)
return s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.screen", candidateID, res))
})
return view, err
}
// RankCandidates scores EVERY candidate in the org against one requirement and returns them ranked
// (Mission Readiness descending) — the Candidate Screening Queue (Screen 1). Read-only: no slate is
// persisted (use GenerateSlate to freeze a slate).
func (s *Service) RankCandidates(ctx context.Context, requirementID uuid.UUID) ([]RankedCandidateView, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
asOf := s.clock()
var out []RankedCandidateView
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
er, err := loadRequirement(ctx, q, requirementID)
if err != nil {
return err
}
cands, err := q.ListCandidates(ctx, dbgen.ListCandidatesParams{Limit: 500, Offset: 0})
if err != nil {
return fmt.Errorf("list candidates: %w", err)
}
type scored struct {
view RankedCandidateView
total decimal.Decimal
}
ranked := make([]scored, 0, len(cands))
for _, c := range cands {
ec, err := loadCandidate(ctx, q, c, asOf)
if err != nil {
return err
}
res := engine.ScoreCandidate(ec, er, s.matcher)
// The Candidate Screening Queue scores everyone for a requirement, a screening stage AEDT
// output. Record each as a served score so the screening stage adverse impact denominator is
// complete, not biased toward the focused single candidate screens.
if e := s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.rank", c.ID, res)); e != nil {
return e
}
ranked = append(ranked, scored{view: rankedCandidateView(c, res), total: res.Total})
}
sort.SliceStable(ranked, func(i, j int) bool {
if d := ranked[i].total.Cmp(ranked[j].total); d != 0 {
return d > 0
}
return ranked[i].view.FullName < ranked[j].view.FullName
})
out = make([]RankedCandidateView, len(ranked))
for i, sc := range ranked {
out[i] = sc.view
}
return nil
})
return out, err
}
// OpportunitiesForCandidate scores one candidate against every open requirement in the org and
// returns the best fits, ranked — the Opportunity Fit Engine™ ("best opportunities for this person").
func (s *Service) OpportunitiesForCandidate(ctx context.Context, candidateID uuid.UUID, limit int) ([]OpportunityFitView, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
if limit <= 0 || limit > 100 {
limit = 10
}
asOf := s.clock()
var out []OpportunityFitView
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
_, ec, _, err := loadCandidateByID(ctx, q, candidateID, asOf)
if err != nil {
return err
}
reqs, err := q.ListRequirements(ctx, dbgen.ListRequirementsParams{Limit: 500, Offset: 0})
if err != nil {
return fmt.Errorf("list requirements: %w", err)
}
type scored struct {
view OpportunityFitView
total decimal.Decimal
}
ranked := make([]scored, 0, len(reqs))
for _, rr := range reqs {
er, err := reqFromRow(ctx, q, rr)
if err != nil {
return err
}
res := engine.ScoreCandidate(ec, er, s.matcher)
// Scoring this person against each opportunity is a screening stage AEDT output; record each
// served score so the screening stage analysis is complete.
if e := s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.opportunities", candidateID, res)); e != nil {
return e
}
ranked = append(ranked, scored{
view: OpportunityFitView{
RequirementID: rr.ID, Title: rr.Title, CustomerAgency: deref(rr.CustomerAgency),
Score: res.Total.StringFixed(2), Band: res.Band.Code(),
Status: intelligence.MissionStatusLabel(res.Band),
},
total: res.Total,
})
}
sort.SliceStable(ranked, func(i, j int) bool {
if d := ranked[i].total.Cmp(ranked[j].total); d != 0 {
return d > 0
}
return ranked[i].view.RequirementID.String() < ranked[j].view.RequirementID.String()
})
if len(ranked) > limit {
ranked = ranked[:limit]
}
out = make([]OpportunityFitView, len(ranked))
for i, s := range ranked {
out[i] = s.view
}
return nil
})
return out, err
}
func loadCandidate(ctx context.Context, q dbgen.Querier, c dbgen.Candidate, asOf time.Time) (engine.Candidate, error) {
skills, err := q.ListCandidateSkills(ctx, c.ID)
if err != nil {
return engine.Candidate{}, fmt.Errorf("list candidate skills: %w", err)
}
certs, err := q.ListCandidateCertifications(ctx, c.ID)
if err != nil {
return engine.Candidate{}, fmt.Errorf("list candidate certifications: %w", err)
}
cust, err := q.ListCandidateCustomerExperience(ctx, c.ID)
if err != nil {
return engine.Candidate{}, fmt.Errorf("list candidate customer experience: %w", err)
}
return ToEngineCandidate(c, skills, certs, cust, asOf)
}