package owliver import ( "sort" "github.com/krow/krow-backend/go-api/internal/domain" ) // Context is the state of one organization's hiring, as counts. // // It is the answer to "what is actually going on here right now", read from // PostgreSQL by service.SuggestionsService and handed to Highlights. Nothing in // this package fetches it: the ranking stays a pure function of its inputs, and // the one place that touches a database stays in the service layer where every // other query lives. // // Counts rather than records, deliberately. A suggestion is a question, and // deciding whether a question is worth asking needs to know that eleven // candidates are waiting on a decision — never who they are. Nothing here can // leak a name, and a zero-valued Context is a valid one: it means the reading // has nothing to report, and Highlights answers with nothing rather than with a // question about an empty set. type Context struct { // Positions. ActivePositions int // status = 'active' DraftPositions int // status = 'draft' StarvedPositions int // active postings nobody has applied to UnderfilledActive int // active postings with fewer hires than headcount // Candidates, by where they are in the funnel. Applications int Unscreened int // status = 'applied' — nobody has scored them Shortlisted int Interviewing int Hired int FlaggedRisks int // interviews carrying at least one ai_flag // Supply and the record behind it. Staff int Profiles int UnverifiedProfiles int // profiles with no evidence filed Courses int ActivityEvents int } // Empty reports that nothing in this organization is worth remarking on. // // Used to tell "the database says there is nothing here" apart from "the // database was not consulted": both produce no highlights, but only the second // is a reason to fall back to anything. func (c Context) Empty() bool { return c == Context{} } /* ── Highlights ─────────────────────────────────────────────────────────── */ // Highlights is what is worth asking on this page given the state of the data. // // The counterpart to Suggest, and deliberately a separate function rather than // a mode of it. Suggest answers "the user typed this, what did they mean" and // is a pure string match; this answers "the user typed nothing, what should // they know" and is a pure read of the organization. Merging them would make // every keystroke pay for a database round trip in order to serve the one // request per page that has nothing typed. // // The stages are the same and in the same order: page context, then permission, // then relevance, then the cap. An intent the caller may not perform is never // scored, so no ordering bug can surface one; an intent whose signal is zero is // dropped rather than padded in, so a quiet workspace is offered nothing rather // than three questions about empty sets. func Highlights(page string, ctx Context, role domain.Role) []Suggestion { out := []Suggestion{} // Deny by default, exactly as Suggest does. An intent that reads nothing is // permitted to every role, so without this an unparseable role would be // offered the account readings. if _, known := domain.ParseRole(string(role)); !known { return out } intents, ok := catalogue[page] if !ok { return out } candidates := make([]scored, 0, len(intents)) for order, intent := range intents { if !intent.permitted(role) { continue } if intent.Signal == nil { continue } signal := intent.Signal(ctx) if signal <= 0 { continue } candidates = append(candidates, scored{ suggestion: Suggestion{Text: intent.Text, Intent: intent.ID}, score: signal, order: order, onTopic: true, }) } // Strongest signal first; declaration order breaks every tie, so the same // database state always produces the same three in the same sequence. sort.SliceStable(candidates, func(a, b int) bool { if candidates[a].score != candidates[b].score { return candidates[a].score > candidates[b].score } return candidates[a].order < candidates[b].order }) seenIntent := make(map[string]bool, MaxSuggestions) for _, c := range candidates { if len(out) == MaxSuggestions { break } if seenIntent[c.suggestion.Intent] { continue } seenIntent[c.suggestion.Intent] = true out = append(out, c.suggestion) } return out } /* ── Signal helpers ─────────────────────────────────────────────────────── */ // maxTiebreak bounds the count half of a signal, so a very large organization // cannot let a tie-break spill into the tier above it. const maxTiebreak = 999 // when scores a reading as "how much does this matter when it is happening at // all", with the count breaking ties inside a tier. // // The obvious formulation — weight × count — is wrong here, and wrong in a way // that gets worse as an organization grows: a workspace with forty applications // and one role nobody has applied to would be asked about the forty, because // forty of anything outscores one of anything else. But the single starved role // is the finding. Nobody needs to be told there are applications. // // So the tier dominates and the count only orders readings within it. `tier` is // a judgement about the SUBJECT, made once where the intent is declared: // // 9 something is at risk and nobody is on it — a role with no applicants, // an interview carrying a flag // 7 work is queued on a person's decision — unscored candidates, unfinished // drafts, unverified profiles // 5 a state worth reviewing — a shortlist waiting, a role under-filled, // recent hires // 3 what exists, as a figure — headcounts, comparisons, trends // // A count of zero scores zero whatever the tier, which is what stops a page // being asked an urgent-sounding question about an empty set. func when(count, tier int) int { if count <= 0 { return 0 } if count > maxTiebreak { count = maxTiebreak } return tier*(maxTiebreak+1) + count } // atLeastTwo is the count, or zero below two. // // A comparison needs something to compare. "Which position has the strongest // pipeline?" is not a question about a workspace holding one position, and // "how is each department performing?" is not a question about one department — // both would answer with a table of a single row, which is the padding // Highlights exists to refuse. func atLeastTwo(n int) int { if n < 2 { return 0 } return n }