agent build

This commit is contained in:
2026-08-28 12:21:44 +05:30
parent b6f8655909
commit f7df96c973
138 changed files with 24164 additions and 207 deletions

View File

@@ -98,6 +98,22 @@ type Intent struct {
// but the caller's own account, and is therefore available to anyone signed
// in.
Reads []Need
// Signal is how loudly the organization's current state asks for this
// reading, from the counts in Context. Zero — and a nil Signal — mean "not
// worth raising unprompted", which is what an intent whose subject nothing
// in the database is doing should say.
//
// It is only consulted by Highlights, the no-query path. Suggest never
// looks at it: a reading the user typed the words for is wanted whether or
// not the data is remarkable, and letting a count veto a typed query would
// make the panel refuse to answer questions it can answer.
//
// Declared here rather than in a table beside the catalogue so that an
// intent and the thing that makes it relevant stay one entry. An intent
// that names no count is never offered unprompted, which is the honest
// default for a reading with nothing to measure.
Signal func(Context) int
}
// permitted reports whether a role may be offered this intent. Every reading
@@ -158,42 +174,48 @@ var catalogue = map[string][]Intent{
Subject: "platform health", Shapes: []string{"stats", "card", "insight"},
Terms: []string{"health", "healthy", "platform", "integrity", "data quality",
"status", "wrong", "broken", "degraded"},
Reads: []Need{postings, applications, interviews, profiles},
Reads: []Need{postings, applications, interviews, profiles},
Signal: func(c Context) int { return when(c.StarvedPositions+c.FlaggedRisks, 9) },
},
{
ID: "workforce-summary", Text: "Summarize the workforce across the platform",
Subject: "the workforce", Shapes: []string{"stats", "table", "progress", "card"},
Terms: []string{"workforce", "scale", "how big", "composition", "headcount",
"people", "how many"},
Reads: []Need{postings, profiles, staff},
Reads: []Need{postings, profiles, staff},
Signal: func(c Context) int { return when(c.Staff, 3) },
},
{
ID: "hiring-operations", Text: "How is hiring operating overall?",
Subject: "hiring activity", Shapes: []string{"flow", "stats", "timeline", "table", "card"},
Terms: []string{"hiring", "operations", "activity", "velocity", "speed",
"throughput", "how fast", "time to hire"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
ID: "pipeline-health", Text: "Where is the hiring pipeline getting stuck?",
Subject: "the hiring pipeline", Shapes: []string{"flow", "stats", "progress", "table", "card"},
Terms: []string{"pipeline", "funnel", "bottleneck", "stuck", "blocked",
"conversion", "stage", "stages", "drop off", "dropoff"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened+c.Shortlisted+c.Interviewing, 5) },
},
{
ID: "attention-required", Text: "What needs attention right now?",
Subject: "what needs attention", Shapes: []string{"list", "table", "insight"},
Terms: []string{"attention", "urgent", "priority", "action", "unusual",
"anomaly", "anomalous", "risk", "risks", "problem", "problems", "issue"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.StarvedPositions+c.Unscreened, 8) },
},
{
ID: "recommendations", Text: "What should I do next?",
Subject: "the recommendations", Shapes: []string{"list", "insight"},
Terms: []string{"recommend", "recommendation", "recommendations", "suggest",
"should", "advice", "next step", "next"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.DraftPositions+c.StarvedPositions+c.Unscreened, 6) },
},
},
@@ -205,7 +227,8 @@ var catalogue = map[string][]Intent{
Subject: "the unfinished drafts", Shapes: []string{"list", "table"},
Terms: []string{"draft", "drafts", "unfinished", "incomplete", "unpublished",
"not posted", "half", "position", "positions", "role", "roles"},
Reads: []Need{postings},
Reads: []Need{postings},
Signal: func(c Context) int { return when(c.DraftPositions, 7) },
},
{
ID: "position-strength", Text: "Which position has the strongest pipeline?",
@@ -213,7 +236,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"pipeline", "strength", "strongest", "healthiest", "best",
"conversion", "which position", "compare", "position", "positions",
"role", "roles"},
Reads: []Need{postings, applications},
Reads: []Need{postings, applications},
Signal: func(c Context) int { return when(atLeastTwo(c.ActivePositions), 3) },
},
{
ID: "positions-attention", Text: "Which positions need attention?",
@@ -221,7 +245,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"attention", "risk", "risks", "at risk", "stalled", "stale",
"ageing", "aging", "neglected", "urgent", "slipping", "behind",
"position", "positions", "role", "roles"},
Reads: []Need{postings, applications},
Reads: []Need{postings, applications},
Signal: func(c Context) int { return when(c.StarvedPositions, 9) },
},
{
ID: "hiring-priority", Text: "Which position should I fill first?",
@@ -229,21 +254,24 @@ var catalogue = map[string][]Intent{
Terms: []string{"priority", "prioritise", "prioritize", "fill", "fill first",
"first", "most important", "which position", "vacancy", "vacancies",
"position", "positions", "role", "roles"},
Reads: []Need{postings, applications},
Reads: []Need{postings, applications},
Signal: func(c Context) int { return when(c.UnderfilledActive, 5) },
},
{
ID: "pipeline-health", Text: "Where are the hiring bottlenecks?",
Subject: "the hiring bottlenecks", Shapes: []string{"flow", "stats", "progress", "table"},
Terms: []string{"bottleneck", "bottlenecks", "funnel", "stuck", "blocked",
"stage", "stages", "waiting", "backlog", "pipeline"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened+c.Interviewing, 4) },
},
{
ID: "hiring-operations", Text: "Summarize hiring activity across all positions",
Subject: "hiring activity", Shapes: []string{"flow", "stats", "timeline", "table", "card"},
Terms: []string{"hiring", "activity", "operations", "throughput", "velocity",
"how many", "posting", "postings", "role", "roles", "position", "positions"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
/* Deliberately NOT carrying "pipeline". This is the Positions page's
@@ -257,7 +285,8 @@ var catalogue = map[string][]Intent{
Subject: "the candidates waiting", Shapes: []string{"list", "table", "stats"},
Terms: []string{"waiting", "candidate", "candidates", "applicant", "applicants",
"review", "screen", "screening", "decision", "queue"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened, 6) },
},
},
@@ -270,7 +299,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"attention", "waiting", "stalled", "overdue", "action",
"decision", "decide", "urgent", "candidate", "candidates", "applicant",
"applicants"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened+c.Shortlisted, 6) },
},
{
ID: "top-candidates", Text: "Who are the strongest candidates?",
@@ -278,7 +308,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"strongest", "top", "best", "compare", "shortlist", "rank",
"ranking", "highest", "score", "scores", "scored", "scoring", "who",
"candidate", "candidates", "applicant", "applicants"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Applications-c.Unscreened, 3) },
},
{
ID: "interview-ready", Text: "Who is ready to interview?",
@@ -286,7 +317,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"interview", "interviews", "interviewed", "ready", "schedule",
"next round", "shortlist", "candidate", "candidates", "applicant",
"applicants"},
Reads: []Need{applications, interviews},
Reads: []Need{applications, interviews},
Signal: func(c Context) int { return when(c.Shortlisted, 5) },
},
{
ID: "screening-gaps", Text: "Which candidates have not been scored yet?",
@@ -294,14 +326,16 @@ var catalogue = map[string][]Intent{
Terms: []string{"unscored", "score", "scores", "scoring", "screening", "screen",
"gap", "gaps", "missing", "incomplete", "coverage", "candidate",
"candidates", "applicant", "applicants"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened, 7) },
},
{
ID: "pipeline-summary", Text: "Summarize the candidate pipeline",
Subject: "the candidate pipeline", Shapes: []string{"flow", "stats", "progress", "table", "card"},
Terms: []string{"pipeline", "funnel", "stage", "stages", "breakdown",
"how many", "where are", "conversion"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
ID: "candidate-risk", Text: "Which candidates carry risk flags?",
@@ -309,7 +343,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"risk", "risks", "risky", "flag", "flags", "flagged", "concern",
"concerns", "integrity", "doubt", "decision", "candidate", "candidates",
"applicant", "applicants"},
Reads: []Need{applications, interviews},
Reads: []Need{applications, interviews},
Signal: func(c Context) int { return when(c.FlaggedRisks, 9) },
},
},
@@ -321,35 +356,40 @@ var catalogue = map[string][]Intent{
Subject: "candidate risk", Shapes: []string{"table", "stats", "insight"},
Terms: []string{"risk", "risks", "flag", "flags", "flagged", "concern",
"integrity", "concentrated"},
Reads: []Need{applications, interviews},
Reads: []Need{applications, interviews},
Signal: func(c Context) int { return when(c.FlaggedRisks, 9) },
},
{
ID: "recruitment-insights", Text: "What do the recruitment numbers show?",
Subject: "the recruitment insights", Shapes: []string{"stats", "table", "insight", "card"},
Terms: []string{"insight", "insights", "recruitment", "quality", "pool",
"stands out", "numbers", "trend", "trends"},
Reads: []Need{applications, profiles},
Reads: []Need{applications, profiles},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
ID: "screening-gaps", Text: "Where are the screening gaps?",
Subject: "the screening gaps", Shapes: []string{"table", "stats", "progress"},
Terms: []string{"screening", "screen", "gap", "gaps", "unscored", "coverage",
"missing", "incomplete", "score", "scores"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened, 7) },
},
{
ID: "hiring-recommendations", Text: "Who should we hire?",
Subject: "the hiring recommendations", Shapes: []string{"list", "table", "insight"},
Terms: []string{"recommend", "recommendation", "recommendations", "hire",
"hiring", "should", "advice", "decision", "who"},
Reads: []Need{applications, profiles},
Reads: []Need{applications, profiles},
Signal: func(c Context) int { return when(c.Shortlisted, 5) },
},
{
ID: "top-candidates", Text: "Compare the top candidates",
Subject: "the top candidates", Shapes: []string{"table", "list", "stats"},
Terms: []string{"compare", "comparison", "top", "best", "strongest",
"shortlist", "rank", "ranking", "side by side"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Applications-c.Unscreened, 3) },
},
},
@@ -361,42 +401,48 @@ var catalogue = map[string][]Intent{
Subject: "the hiring trend", Shapes: []string{"timeline", "flow", "stats", "table"},
Terms: []string{"trend", "trends", "trending", "over time", "month", "monthly",
"week", "weekly", "history", "growth", "change"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
ID: "department-performance", Text: "How is each department performing?",
Subject: "department performance", Shapes: []string{"table", "stats", "progress"},
Terms: []string{"department", "departments", "team", "teams", "category",
"categories", "performance", "performing", "breakdown", "compare"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(atLeastTwo(c.ActivePositions), 3) },
},
{
ID: "pipeline-health", Text: "Where are the hiring bottlenecks?",
Subject: "the hiring bottlenecks", Shapes: []string{"flow", "stats", "progress", "table"},
Terms: []string{"bottleneck", "bottlenecks", "funnel", "pipeline", "conversion",
"stuck", "stage", "stages"},
Reads: []Need{applications},
Reads: []Need{applications},
Signal: func(c Context) int { return when(c.Unscreened+c.Interviewing, 5) },
},
{
ID: "position-conversion", Text: "Which positions convert best?",
Subject: "position conversion", Shapes: []string{"table", "stats", "list"},
Terms: []string{"conversion", "convert", "converts", "position", "positions",
"role", "roles", "rate", "rates", "ratio", "yield"},
Reads: []Need{postings, applications},
Reads: []Need{postings, applications},
Signal: func(c Context) int { return when(atLeastTwo(c.ActivePositions)+c.Hired, 4) },
},
{
ID: "hiring-operations", Text: "How is hiring performing overall?",
Subject: "hiring performance", Shapes: []string{"stats", "flow", "timeline", "card"},
Terms: []string{"hiring", "performance", "operations", "velocity", "speed",
"average", "averages", "time to hire", "throughput"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.Applications, 2) },
},
{
ID: "attention-required", Text: "What needs attention in the numbers?",
Subject: "what needs attention", Shapes: []string{"list", "insight", "table"},
Terms: []string{"attention", "outlier", "outliers", "anomaly", "unusual",
"risk", "risks", "worst", "falling"},
Reads: []Need{applications, postings},
Reads: []Need{applications, postings},
Signal: func(c Context) int { return when(c.StarvedPositions, 8) },
},
},
@@ -408,14 +454,16 @@ var catalogue = map[string][]Intent{
Subject: "the audit log", Shapes: []string{"stats", "table", "timeline", "card"},
Terms: []string{"audit", "log", "logs", "event", "events", "activity",
"trail", "record", "records", "how many"},
Reads: []Need{orgActivity},
Reads: []Need{orgActivity},
Signal: func(c Context) int { return when(c.ActivityEvents, 3) },
},
{
ID: "user-activity", Text: "Who has been most active?",
Subject: "activity by user", Shapes: []string{"table", "list", "stats"},
Terms: []string{"user", "users", "who", "account", "accounts", "busiest",
"most active", "behaviour", "behavior", "person"},
Reads: []Need{orgActivity},
Reads: []Need{orgActivity},
Signal: func(c Context) int { return when(c.ActivityEvents, 2) },
},
{
ID: "unusual-activity", Text: "Has anything unusual happened?",
@@ -441,28 +489,32 @@ var catalogue = map[string][]Intent{
Subject: "the talent priorities", Shapes: []string{"list", "table", "stats"},
Terms: []string{"prioritise", "prioritize", "priority", "priorities", "who",
"best", "top", "strongest", "elite", "star", "shortlist", "score", "scores"},
Reads: []Need{profiles},
Reads: []Need{profiles},
Signal: func(c Context) int { return when(c.Profiles, 3) },
},
{
ID: "talent-summary", Text: "Summarize the talent pool",
Subject: "the talent pool", Shapes: []string{"stats", "table", "progress", "card"},
Terms: []string{"pool", "talent", "worker", "workers", "profile", "profiles",
"segment", "segments", "composition", "supply", "how many"},
Reads: []Need{profiles},
Reads: []Need{profiles},
Signal: func(c Context) int { return when(c.Profiles, 2) },
},
{
ID: "talent-verification", Text: "Which profiles are missing verification?",
Subject: "the verification gaps", Shapes: []string{"list", "table", "progress", "stats"},
Terms: []string{"verification", "verify", "verified", "unverified", "gap",
"gaps", "missing", "credential", "credentials", "proof", "evidence"},
Reads: []Need{profiles, evidence},
Reads: []Need{profiles, evidence},
Signal: func(c Context) int { return when(c.UnverifiedProfiles, 7) },
},
{
ID: "talent-availability", Text: "Who is available to start?",
Subject: "availability", Shapes: []string{"list", "table", "stats"},
Terms: []string{"available", "availability", "unavailable", "free", "start",
"capacity", "when", "now", "notice"},
Reads: []Need{profiles},
Reads: []Need{profiles},
Signal: func(c Context) int { return when(c.Profiles, 2) },
},
},
@@ -474,28 +526,32 @@ var catalogue = map[string][]Intent{
Subject: "the hiring outcomes", Shapes: []string{"stats", "table", "progress", "card"},
Terms: []string{"outcome", "outcomes", "result", "results", "quality",
"retention", "worked out", "performance", "department", "departments"},
Reads: []Need{staff, applications},
Reads: []Need{staff, applications},
Signal: func(c Context) int { return when(c.Staff, 3) },
},
{
ID: "hiring-strongest", Text: "Who are our strongest hires?",
Subject: "the strongest hires", Shapes: []string{"list", "table", "stats"},
Terms: []string{"strongest", "best", "top", "star", "highest", "score",
"scores", "standout"},
Reads: []Need{staff, applications},
Reads: []Need{staff, applications},
Signal: func(c Context) int { return when(c.Staff, 2) },
},
{
ID: "hiring-patterns", Text: "What stands out about who we hire?",
Subject: "the hiring patterns", Shapes: []string{"table", "stats", "insight"},
Terms: []string{"pattern", "patterns", "stands out", "trend", "trends",
"common", "typical", "breakdown", "profile"},
Reads: []Need{staff, applications},
Reads: []Need{staff, applications},
Signal: func(c Context) int { return when(c.Staff, 2) },
},
{
ID: "hiring-recent", Text: "Who did we hire recently?",
Subject: "the recent hires", Shapes: []string{"list", "timeline", "table"},
Terms: []string{"recent", "recently", "latest", "last", "new hire", "new hires",
"who did we hire", "this month", "hired"},
Reads: []Need{staff},
Reads: []Need{staff},
Signal: func(c Context) int { return when(c.Hired, 5) },
},
},
@@ -507,14 +563,16 @@ var catalogue = map[string][]Intent{
Subject: "the skill library", Shapes: []string{"stats", "table", "list", "card"},
Terms: []string{"library", "skill", "skills", "catalogue", "catalog", "course",
"courses", "challenge", "challenges", "what do we have", "how many"},
Reads: []Need{courses},
Reads: []Need{courses},
Signal: func(c Context) int { return when(c.Courses, 3) },
},
{
ID: "forge-published", Text: "Which skills are published?",
Subject: "the published skills", Shapes: []string{"list", "table", "stats"},
Terms: []string{"published", "publish", "live", "draft", "drafts", "archived",
"archive", "status", "in service"},
Reads: []Need{courses},
Reads: []Need{courses},
Signal: func(c Context) int { return when(c.Courses, 2) },
},
{
ID: "forge-evaluation", Text: "How is submitted proof evaluated?",
@@ -529,7 +587,8 @@ var catalogue = map[string][]Intent{
Subject: "workforce usage", Shapes: []string{"stats", "progress", "table"},
Terms: []string{"workforce", "usage", "using", "uptake", "adoption", "progress",
"completion", "completed", "training", "learning"},
Reads: []Need{courses, evidence, profiles},
Reads: []Need{courses, evidence, profiles},
Signal: func(c Context) int { return when(c.Profiles, 2) },
},
{
ID: "forge-gaps", Text: "Which skill gaps are still open?",
@@ -556,7 +615,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"compare", "comparison", "benchmark", "benchmarks", "similar",
"typical", "average", "pay", "rate", "rates", "salary", "experience",
"market"},
Reads: []Need{postings},
Reads: []Need{postings},
Signal: func(c Context) int { return when(c.ActivePositions, 3) },
},
{
ID: "position-requirements", Text: "Which credentials are already in use?",
@@ -564,7 +624,8 @@ var catalogue = map[string][]Intent{
Terms: []string{"credential", "credentials", "certification", "certifications",
"requirement", "requirements", "qualification", "qualifications",
"licence", "license", "skill", "skills"},
Reads: []Need{postings, certs},
Reads: []Need{postings, certs},
Signal: func(c Context) int { return when(c.ActivePositions, 2) },
},
{
ID: "position-spec-steps", Text: "How does this form work?",
@@ -602,7 +663,8 @@ var catalogue = map[string][]Intent{
Subject: "my recent activity", Shapes: []string{"timeline", "list", "table"},
Terms: []string{"activity", "recent", "recently", "history", "what have i",
"my actions", "audit", "did i"},
Reads: []Need{ownActivity},
Reads: []Need{ownActivity},
Signal: func(c Context) int { return when(c.ActivityEvents, 3) },
},
{
ID: "profile-actions", Text: "What can I do on this page?",

View 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
}

View File

@@ -0,0 +1,286 @@
package owliver
import (
"testing"
"github.com/krow/krow-backend/go-api/internal/domain"
)
// Highlights — what is worth asking when nothing has been typed.
//
// Everything here is a pure function of a Context written out by hand, so the
// assertions are about the ranking itself rather than about a fixture. The
// database read that produces a real Context is exercised over HTTP, in
// internal/httpserver.
// ids is the intents a result names, in order.
func ids(list []Suggestion) []string {
out := make([]string, len(list))
for i, s := range list {
out[i] = s.Intent
}
return out
}
// has reports whether an intent was offered.
func has(list []Suggestion, intent string) bool {
for _, s := range list {
if s.Intent == intent {
return true
}
}
return false
}
// A workspace with nothing in it is asked nothing.
//
// The alternative — three questions about empty sets — is the padding this
// whole path is supposed to refuse. "You have no positions, would you like to
// know which position has the strongest pipeline?" is worse than silence.
func TestHighlightsOfAnEmptyOrganizationAreEmpty(t *testing.T) {
for _, page := range []string{"positions", "candidates", "control-center", "analytics",
"talent-pool", "hired-history", "krow-forge", "activity"} {
if got := Highlights(page, Context{}, domain.RoleAdmin); len(got) != 0 {
t.Errorf("%s offered %v for an empty organization", page, ids(got))
}
}
}
// The counts decide, and the loudest one leads.
func TestHighlightsRankByTheData(t *testing.T) {
cases := []struct {
name string
page string
ctx Context
wantTop string
}{
{
name: "unfinished drafts dominate a quiet workspace",
page: "positions",
ctx: Context{DraftPositions: 4, ActivePositions: 1},
wantTop: "position-drafts",
},
{
name: "a role nobody applied to outranks the drafts",
page: "positions",
ctx: Context{DraftPositions: 1, ActivePositions: 3, StarvedPositions: 2},
wantTop: "positions-attention",
},
{
// The tier, not the pile. This is the case weight × count gets
// wrong: forty applications outnumber one abandoned role, and the
// abandoned role is still the finding.
name: "one starved role outranks a large pile of everything else",
page: "positions",
ctx: Context{ActivePositions: 9, StarvedPositions: 1, Applications: 40, Unscreened: 22},
wantTop: "positions-attention",
},
{
// And the thing that just happened is heard, however small.
name: "a single unfinished draft is heard over a busy funnel",
page: "positions",
ctx: Context{ActivePositions: 9, DraftPositions: 1, Applications: 40, Unscreened: 22},
wantTop: "position-drafts",
},
{
name: "unscored candidates are the screening gap",
page: "candidates",
ctx: Context{Applications: 12, Unscreened: 9},
wantTop: "screening-gaps",
},
{
name: "a flagged interview is a risk before it is anything else",
page: "candidates",
ctx: Context{Applications: 12, Unscreened: 2, FlaggedRisks: 4},
wantTop: "candidate-risk",
},
{
name: "unverified profiles are the talent pool's gap",
page: "talent-pool",
ctx: Context{Profiles: 20, UnverifiedProfiles: 8},
wantTop: "talent-verification",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := Highlights(c.page, c.ctx, domain.RoleAdmin)
if len(got) == 0 {
t.Fatalf("%s offered nothing", c.page)
}
if got[0].Intent != c.wantTop {
t.Fatalf("led with %q, want %q (whole list %v)", got[0].Intent, c.wantTop, ids(got))
}
})
}
}
// A comparison needs two things to compare.
//
// "Which position has the strongest pipeline?" is not a question about a
// workspace holding one position: the answer is a table of one row, which is
// the same padding as a question about an empty set.
func TestHighlightsDoNotOfferAComparisonOfOne(t *testing.T) {
one := Highlights("positions", Context{ActivePositions: 1, Applications: 3}, domain.RoleAdmin)
if has(one, "position-strength") {
t.Fatalf("offered a pipeline comparison across one position: %v", ids(one))
}
two := Highlights("positions", Context{ActivePositions: 2, Applications: 3}, domain.RoleAdmin)
if !has(two, "position-strength") {
t.Fatalf("two positions is a comparison and was not offered: %v", ids(two))
}
}
// Never more than three, and never the same reading twice.
func TestHighlightsAreCappedAndDistinct(t *testing.T) {
loud := Context{
ActivePositions: 9, DraftPositions: 7, StarvedPositions: 5, UnderfilledActive: 6,
Applications: 40, Unscreened: 22, Shortlisted: 9, Interviewing: 6, Hired: 11,
FlaggedRisks: 4, Staff: 30, Profiles: 60, UnverifiedProfiles: 25, Courses: 14,
ActivityEvents: 900,
}
for page := range catalogue {
got := Highlights(page, loud, domain.RoleAdmin)
if len(got) > MaxSuggestions {
t.Errorf("%s returned %d, the cap is %d", page, len(got), MaxSuggestions)
}
seen := map[string]bool{}
for _, s := range got {
if seen[s.Intent] {
t.Errorf("%s repeated %q", page, s.Intent)
}
seen[s.Intent] = true
if s.Text == "" || s.Intent == "" {
t.Errorf("%s returned an incomplete suggestion: %+v", page, s)
}
// Nothing was typed, so nothing asked for a rendering.
if s.Capability != "" {
t.Errorf("%s carried a shape nobody asked for: %+v", page, s)
}
}
}
}
// Permission is decided before relevance, exactly as it is in Suggest.
//
// A count cannot promote a reading the caller may not perform: talent's rows
// are narrowed by the policy table, so an org-wide figure is not theirs to be
// told even as a ranking input.
func TestHighlightsRefuseWhatARoleCannotRead(t *testing.T) {
loud := Context{ActivePositions: 9, DraftPositions: 7, Applications: 40, Unscreened: 22,
Staff: 30, Profiles: 60}
for _, page := range []string{"positions", "candidates", "hired-history", "control-center"} {
if got := Highlights(page, loud, domain.RoleTalent); len(got) != 0 {
t.Errorf("%s offered talent %v", page, ids(got))
}
if got := Highlights(page, loud, domain.RoleAdmin); len(got) == 0 {
t.Errorf("%s offered an admin nothing — the assertion above proves nothing", page)
}
}
}
// An unrecognised role is offered nothing, as everywhere else.
func TestHighlightsDenyAnUnknownRole(t *testing.T) {
loud := Context{ActivePositions: 9, Applications: 40, Unscreened: 22}
for _, role := range []domain.Role{"", "root", "superuser", "Admin"} {
if got := Highlights("positions", loud, role); len(got) != 0 {
t.Errorf("role %q was offered %v", role, ids(got))
}
}
}
// A page the catalogue does not hold answers with an empty slice, not nil.
func TestHighlightsOfAnUnknownPageAreAnEmptySlice(t *testing.T) {
loud := Context{ActivePositions: 9, Applications: 40}
for _, page := range []string{"", "nowhere", "POSITIONS", "settings"} {
got := Highlights(page, loud, domain.RoleAdmin)
if got == nil {
t.Fatalf("page %q returned nil, which a client cannot range over", page)
}
if len(got) != 0 {
t.Errorf("page %q returned %v", page, ids(got))
}
}
}
// The same state always produces the same three in the same order.
func TestHighlightsAreDeterministic(t *testing.T) {
ctx := Context{ActivePositions: 5, DraftPositions: 3, StarvedPositions: 2,
Applications: 20, Unscreened: 7, Shortlisted: 4}
first := ids(Highlights("positions", ctx, domain.RoleAdmin))
for range 25 {
again := ids(Highlights("positions", ctx, domain.RoleAdmin))
if len(again) != len(first) {
t.Fatalf("got %v, first run was %v", again, first)
}
for i := range first {
if again[i] != first[i] {
t.Fatalf("got %v, first run was %v", again, first)
}
}
}
}
// Every declared Signal names an intent the catalogue actually holds, and is
// monotonic: more of the thing it counts can never make the reading less
// relevant. A signal that fell as its subject grew would be a sign error, and
// the symptom would be a suggestion that disappears exactly when it matters.
func TestSignalsAreMonotonic(t *testing.T) {
small := Context{
ActivePositions: 2, DraftPositions: 1, StarvedPositions: 1, UnderfilledActive: 1,
Applications: 5, Unscreened: 2, Shortlisted: 1, Interviewing: 1, Hired: 1,
FlaggedRisks: 1, Staff: 2, Profiles: 3, UnverifiedProfiles: 1, Courses: 1,
ActivityEvents: 10,
}
large := Context{
ActivePositions: 20, DraftPositions: 10, StarvedPositions: 10, UnderfilledActive: 10,
Applications: 50, Unscreened: 20, Shortlisted: 10, Interviewing: 10, Hired: 10,
FlaggedRisks: 10, Staff: 20, Profiles: 30, UnverifiedProfiles: 10, Courses: 10,
ActivityEvents: 100,
}
for page, intents := range catalogue {
for _, intent := range intents {
if intent.Signal == nil {
continue
}
lo, hi := intent.Signal(small), intent.Signal(large)
if lo < 0 || hi < 0 {
t.Errorf("%s/%s: a negative signal (%d, %d)", page, intent.ID, lo, hi)
}
if hi < lo {
t.Errorf("%s/%s: signal fell as the workspace grew (%d → %d)", page, intent.ID, lo, hi)
}
}
}
}
// A tier decides before a count does, everywhere.
//
// Stated as a property rather than as a list of pairs, because it is the whole
// design: a signal is "how much does this matter when it is happening at all",
// and a count that could climb into the tier above would make a large
// organization's biggest pile outrank its most urgent finding.
func TestATierAlwaysOutranksACount(t *testing.T) {
for tier := 1; tier <= 9; tier++ {
lowestAbove := when(1, tier+1)
highestWithin := when(maxTiebreak*10, tier) // deliberately over the cap
if highestWithin >= lowestAbove {
t.Fatalf("tier %d saturates into tier %d: %d >= %d",
tier, tier+1, highestWithin, lowestAbove)
}
}
// Nothing happening scores nothing, whatever the tier claims.
for tier := 1; tier <= 9; tier++ {
for _, count := range []int{0, -1, -1000} {
if got := when(count, tier); got != 0 {
t.Fatalf("when(%d, %d) = %d, want 0", count, tier, got)
}
}
}
}