agent build
This commit is contained in:
177
go-api/internal/owliver/context.go
Normal file
177
go-api/internal/owliver/context.go
Normal file
@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user