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 | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | internal/modules/matching/service.go |
| Original SHA-256 | 2de54c6a6660a1fc2c54f97a67eae201b6cc5939bde6253ee70c8c3b96697902 |
| Git blob SHA | 9cccb5ee0bfab7513ee2013d87d2d719a245d1d4 |
| Reading-copy SHA-256 | 2de54c6a6660a1fc2c54f97a67eae201b6cc5939bde6253ee70c8c3b96697902 |
Recovered source transcript
Download complete source text ↓694 lines · 27.2 KB
package matching
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/shopspring/decimal"
"github.com/missionworkforce/missionmatch/internal/aioe"
"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/extraction"
"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")
ErrNoExtractor = errors.New("matching: no extractor configured")
)
const defaultMaxCandidates = 500
// maxSlateCandidates is the HARD upper bound on how many candidates a single slate generation may score and
// insert as members in one transaction. A caller supplied MaxCandidates above this is clamped, so an oversized
// or hostile request (for example MaxCandidates far above the pool size) can never balloon the transaction or
// the slate. It is generous enough for any real proposal pool while bounding the resource cost.
const maxSlateCandidates = 2000
// clampMaxCandidates resolves the caller's requested cap to a safe value: a non positive request defaults, and
// a request above the hard bound is clamped. It is a pure function so the bound is unit tested without a
// database.
func clampMaxCandidates(n int32) int32 {
if n <= 0 {
return defaultMaxCandidates
}
if n > maxSlateCandidates {
return maxSlateCandidates
}
return n
}
// 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)
extractor extraction.Extractor // Mission Check resume/JD extractor; may be nil (Mission Check then 503)
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}
}
// WithExtractor injects the Mission Check structured extractor and returns the service for chaining. It
// is a separate setter so the NewService signature (and its existing callers, including the integration
// tests) stay unchanged. A service with no extractor returns ErrNoExtractor on the Mission Check paths.
func (s *Service) WithExtractor(ext extraction.Extractor) *Service {
s.extractor = ext
return s
}
// 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, the band, and the weight profile version that produced the score
// 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 {
profile := res.WeightProfileVersion
if profile == "" {
profile = engine.GlobalDefaultProfileVersion
}
payload, _ := json.Marshal(map[string]any{
"tool": tool,
"score": res.Total.StringFixed(2),
"band": res.Band.Code(),
"weight_profile": profile,
})
return audit.Event{Action: "aedt.score.served", EntityType: "candidate", EntityID: candidateID.String(), NewValues: payload}
}
// scorerOnTx returns the per transaction scoring closure: it loads the tenant's active AIOE weight
// profile ONCE on the tx (falling back to the frozen global default) and scores every candidate in
// the transaction under that one profile, stamping the profile version into each result. This is
// the single seam between the adaptive layer and the frozen engine: the engine mechanics never
// change, only the injected weights do, and every result names the weights that produced it.
//
// It returns an error when an ACTIVE profile exists but cannot be read, and the whole transaction then
// rolls back. That is the only safe answer: the alternative is scoring the request on default weights
// while stamping every result and every audit row with the global.default label, which is a score the
// tenant did not configure recorded under a profile version that was not in effect.
func (s *Service) scorerOnTx(ctx context.Context, tx pgx.Tx) (func(ctx context.Context, ec engine.Candidate, er engine.Requirement) engine.MatchResult, error) {
w, label, err := aioe.LoadActiveWeightsOnTx(ctx, tx)
if err != nil {
return nil, err
}
return func(ctx context.Context, ec engine.Candidate, er engine.Requirement) engine.MatchResult {
res := engine.ScoreCandidateWithWeights(ctx, ec, er, s.matcher, w)
res.WeightProfileVersion = label
return res
}, nil
}
// actorID returns the validated session user id, or nil when there is no principal (background jobs,
// tests). It is the evaluated_by/decided_by actor stamped on a persisted artifact.
func actorID(ctx context.Context) *uuid.UUID {
if p, ok := tenant.UserFromContext(ctx); ok {
if id, err := uuid.Parse(p.UserID); err == nil {
return &id
}
}
return nil
}
// persistEvaluationOnTx upserts the latest screenMATCH qualification for one candidate against one
// requirement on the caller's tenant tx, so missionMATCH reads a stored evaluation rather than
// recompute on read. It is a no op when no DB is configured. The served score is audited separately;
// this is the readable artifact. It runs inside the same transaction as the score and its audit, so a
// failed write rolls the screen back rather than serving a score the rollup will never see.
func (s *Service) persistEvaluationOnTx(ctx context.Context, tx pgx.Tx, orgID, candidateID, requirementID uuid.UUID, res engine.MatchResult) error {
params, err := ToCandidateEvaluationParams(orgID, candidateID, requirementID, res, actorID(ctx))
if err != nil {
return err
}
return dbgen.New(tx).UpsertCandidateEvaluation(ctx, params)
}
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
}
orgID, err := s.orgID(ctx)
if err != nil {
return engine.MatchResult{}, err
}
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
}
score, err := s.scorerOnTx(ctx, tx)
if err != nil {
return err
}
res = score(ctx, ec, er)
// 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.
if e := s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.match", candidateID, res)); e != nil {
return e
}
// Persist the qualification (upsert latest per pair) so missionMATCH reads the stored evaluation
// rather than recompute it on read.
return s.persistEvaluationOnTx(ctx, tx, orgID, candidateID, requirementID, 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")
}
// Clamp the requested candidate cap to the hard bound so an oversized MaxCandidates cannot balloon the
// transaction or the slate member count regardless of what the caller supplies.
in.MaxCandidates = clampMaxCandidates(in.MaxCandidates)
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 {
// Read the champion display order re ranker on the same tx (best effort; on absence the frozen engine
// order stands). A learned champion permutes the display order; the prior only v1 is a no op.
rerankWeights, rerankVersion := championRerankWeights(ctx, tx)
score, e := s.scorerOnTx(ctx, tx)
if e != nil {
return e
}
out, e = generateSlate(ctx, dbgen.New(tx), orgID, in, score, asOf, rerankWeights, rerankVersion)
if e != nil {
return e
}
payload, _ := json.Marshal(map[string]any{
"requirement_id": in.RequirementID.String(),
"members": len(out.Members),
"display_order_model": rerankModelKey,
"display_order_version": rerankVersion,
})
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, note 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
}
var notePtr *string
if n := strings.TrimSpace(note); n != "" {
notePtr = &n
}
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, DecisionNote: notePtr,
})
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
// The note is part of the human in the loop record but is NOT echoed into the audit payload: a
// reviewer note can carry candidate PII, which never belongs in an audit event.
payload, _ := json.Marshal(map[string]any{"decision": decision, "band": row.Band, "has_note": notePtr != nil})
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
}
// GetCandidate returns one candidate by id, scoped to the caller's org by RLS. A missing id or one in
// another org both surface as ErrNotFound: RLS makes the two indistinguishable, which is the intended
// privacy floor (no cross org existence oracle). The by id read backs a deep linked evaluation, so the
// page no longer depends on the candidate being inside the first list page.
func (s *Service) GetCandidate(ctx context.Context, id uuid.UUID) (dbgen.Candidate, error) {
if s.db == nil {
return dbgen.Candidate{}, ErrNoDatabase
}
var out dbgen.Candidate
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
c, e := dbgen.New(tx).GetCandidate(ctx, id)
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
out = c
return nil
})
return out, err
}
// GetRequirement returns one requirement by id, scoped to the caller's org by RLS. Missing and cross
// org both surface as ErrNotFound, mirroring GetCandidate.
func (s *Service) GetRequirement(ctx context.Context, id uuid.UUID) (dbgen.Requirement, error) {
if s.db == nil {
return dbgen.Requirement{}, ErrNoDatabase
}
var out dbgen.Requirement
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rq, e := dbgen.New(tx).GetRequirement(ctx, id)
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
out = rq
return nil
})
return out, err
}
// generateSlate is the pure orchestration over a Querier, testable with a fake, no live DB. The
// score closure carries the tenant's active AIOE weight profile (or the default engine scorer in
// tests), so slate scoring runs under the same profile the rest of the transaction uses.
func generateSlate(ctx context.Context, q dbgen.Querier, orgID uuid.UUID, in GenerateSlateInput, score func(context.Context, engine.Candidate, engine.Requirement) engine.MatchResult, asOf time.Time, rerankWeights map[string]float64, rerankVersion int) (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: score(ctx, ec, er)})
}
// Deterministic ranking: total score descending, candidate id ascending as the unique tiebreaker. This is
// the frozen engine order, the base display order and the tiebreak for any re ranking.
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()
})
// A LEARNED display order re ranker (a promoted champion, version above 1) permutes the display order so the
// candidates a human is likely to approve surface first. It is a STABLE reorder over the frozen engine order,
// so an equal utility preserves the engine order exactly, and it changes only the ORDER, never a score or a
// band (the frozen engine Total stays THE score). The prior only champion (version 1) holds the engine
// weights and is skipped, so the order is byte identical to the engine order until a learned model is
// promoted. A human still approves every member.
if rerankVersion > 1 && len(rerankWeights) > 0 {
sort.SliceStable(results, func(i, j int) bool {
return rerankUtility(rerankWeights, results[i].res) > rerankUtility(rerankWeights, results[j].res)
})
}
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
}
orgID, err := s.orgID(ctx)
if err != nil {
return ScreenView{}, err
}
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
}
score, err := s.scorerOnTx(ctx, tx)
if err != nil {
return err
}
res := score(ctx, ec, er)
view = assembleScreenView(ec, er, res, ClearanceFactsFrom(c, cust), asOf)
if e := s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.screen", candidateID, res)); e != nil {
return e
}
// Persist the qualification (upsert latest per pair) so missionMATCH reads the stored evaluation.
return s.persistEvaluationOnTx(ctx, tx, orgID, candidateID, requirementID, 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
}
score, err := s.scorerOnTx(ctx, tx)
if err != nil {
return err
}
ranked := make([]scored, 0, len(cands))
for _, c := range cands {
ec, err := loadCandidate(ctx, q, c, asOf)
if err != nil {
return err
}
res := score(ctx, ec, er)
// 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
}
// The candidate id is the unique total order tiebreaker (the full name is not unique), so two
// candidates sharing a score rank byte identically on every run, the AEDT reproducibility mandate.
return ranked[i].view.CandidateID.String() < ranked[j].view.CandidateID.String()
})
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
}
out, err = s.rankOpenOpportunities(ctx, tx, ec, limit, func(res engine.MatchResult) error {
// Scoring this person against each opportunity is a screening stage AEDT output; record each
// served score so the screening stage analysis is complete.
return s.auditOnTx(ctx, tx, servedScoreEvent("screenMATCH.opportunities", candidateID, res))
})
return err
})
return out, err
}
// rankOpenOpportunities scores a ready engine.Candidate against every OPEN requirement, calls auditFn
// for each served score (so the saved path and the ephemeral Mission Check path each record under their
// own AEDT action), ranks total descending with the requirement id as the stable tiebreak, and
// truncates to limit. It is the single ranking path shared by OpportunitiesForCandidate and Mission
// Check Mode A, so an unsaved resume and a saved candidate with identical fields rank identically.
func (s *Service) rankOpenOpportunities(ctx context.Context, tx pgx.Tx, ec engine.Candidate, limit int, auditFn func(res engine.MatchResult) error) ([]OpportunityFitView, error) {
q := dbgen.New(tx)
reqs, err := q.ListOpenRequirements(ctx, dbgen.ListOpenRequirementsParams{Limit: 500, Offset: 0})
if err != nil {
return nil, fmt.Errorf("list open requirements: %w", err)
}
type scored struct {
view OpportunityFitView
total decimal.Decimal
}
score, err := s.scorerOnTx(ctx, tx)
if err != nil {
return nil, err
}
ranked := make([]scored, 0, len(reqs))
for _, rr := range reqs {
er, err := reqFromRow(ctx, q, rr)
if err != nil {
return nil, err
}
res := score(ctx, ec, er)
if err := auditFn(res); err != nil {
return nil, err
}
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, sc := range ranked {
out[i] = sc.view
}
return out, nil
}
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)
}