688 lines
32 KiB
Go
688 lines
32 KiB
Go
// Package owliver answers "what could I usefully ask here?".
|
|
//
|
|
// It is the suggestion side of the Owliver panel and nothing else. It does not
|
|
// answer questions, does not reach a database, does not call a model, and holds
|
|
// no state: a request names a page and what the user has typed so far, and this
|
|
// package ranks a static catalogue against it. That is deliberate — the panel
|
|
// calls the endpoint on every keystroke, so the work per call has to be a few
|
|
// string comparisons over a table built once at process start.
|
|
//
|
|
// WHERE THE CATALOGUE COMES FROM. Owliver's capabilities are declared in the
|
|
// frontend, one manifest per page context, in
|
|
// src/components/ai-assistant/capabilities/. Each entry there is an id, a label
|
|
// and the function that answers it. This file transcribes the id and the page
|
|
// it is offered on, and adds the two things a manifest does not carry because
|
|
// the browser never needed them: the words that mean a user is reaching for
|
|
// that reading, and the records it reads.
|
|
//
|
|
// Transcription rather than a second registry, on the pattern already set by
|
|
// internal/definition/vocabulary.go: the frontend owns the vocabulary, this
|
|
// side names keys from it, and TestIntentIDsAreFrontendCapabilities keeps the
|
|
// two honest. An Intent.ID that no manifest declares is a bug here, not a new
|
|
// capability — the id in a response is the id the panel dispatches on.
|
|
//
|
|
// WHAT MAKES A SUGGESTION SAFE TO SHOW. Offering someone a question is a claim
|
|
// that they could ask it. Reads is that claim written down: the resources the
|
|
// capability actually reads, checked against the one authorization table in
|
|
// internal/domain/policy.go. No role list is repeated here, and no rule is
|
|
// re-derived — a capability that reads staff is unavailable to talent because
|
|
// policy.go says talent may not list staff, and for no other reason.
|
|
package owliver
|
|
|
|
import "github.com/krow/krow-backend/go-api/internal/domain"
|
|
|
|
// Need is one record set a capability reads, and how much of it.
|
|
//
|
|
// OrgWide is the half a role check alone cannot express. Every role may list
|
|
// job applications; a talent caller sees only their own, because the policy
|
|
// attaches a row predicate rather than a refusal. So "which position has the
|
|
// strongest pipeline?" is not forbidden to talent — it is unanswerable for
|
|
// them, and offering it would promise a reading across records they will never
|
|
// be shown. OrgWide asks policy.ScopeFor for exactly that: is this caller's
|
|
// view of this resource narrowed, or is it the whole organization?
|
|
type Need struct {
|
|
Resource string // domain resource path, e.g. "job-applications"
|
|
Op domain.Op
|
|
OrgWide bool
|
|
}
|
|
|
|
// permitted reports whether a role may perform this reading.
|
|
//
|
|
// Three gates, all sourced from the descriptors: the API must expose the
|
|
// operation at all, the policy must allow the role, and — for an org-wide
|
|
// reading — the policy must not narrow the role's rows.
|
|
func (n Need) permitted(role domain.Role) bool {
|
|
res, ok := domain.ResourceByPath[n.Resource]
|
|
if !ok || !res.Supports(n.Op) {
|
|
return false
|
|
}
|
|
if !res.Policy.Allows(n.Op, role) {
|
|
return false
|
|
}
|
|
if n.OrgWide && res.Policy.ScopeFor(role).Kind != domain.ScopeNone {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Intent is one reading Owliver can offer on one page.
|
|
type Intent struct {
|
|
// ID is the frontend capability id, verbatim. It is what the panel
|
|
// dispatches on, which is why it is not invented here.
|
|
ID string
|
|
|
|
// Text is the suggestion as the user reads it: a question in their words,
|
|
// not a label. Unique within a page.
|
|
Text string
|
|
|
|
// Subject names what the reading is about — "hiring activity", "the
|
|
// candidate pipeline" — and exists so a shaped variant can be phrased
|
|
// ("Summarize hiring activity", "Show hiring activity as a flow") without
|
|
// writing every combination out. An intent with no Subject is offered only
|
|
// as its Text.
|
|
Subject string
|
|
|
|
// Shapes are the section types this reading can be drawn as, named from the
|
|
// closed OWLIVER_CAPABILITIES vocabulary in src/lib/skills/surfaces.js.
|
|
// Empty means prose only. `summary` is never listed: it has no component,
|
|
// so it applies to anything with a Subject.
|
|
Shapes []string
|
|
|
|
// Terms are the words that mean a user is reaching for this reading. About
|
|
// the subject, never about the shape — "as a flow" belongs to the shape
|
|
// table, and putting it here would offer every page's intents to anyone who
|
|
// typed "chart".
|
|
Terms []string
|
|
|
|
// Reads is what the capability actually reads. Empty means it reads nothing
|
|
// 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
|
|
// must be permitted: a suggestion that is half-answerable is not answerable.
|
|
func (i Intent) permitted(role domain.Role) bool {
|
|
for _, n := range i.Reads {
|
|
if !n.permitted(role) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/* ── The readings each resource stands for ──────────────────────────────── */
|
|
//
|
|
// Named rather than repeated so that "this capability reads the organization's
|
|
// applications" is written once and reads the same everywhere it appears.
|
|
|
|
var (
|
|
postings = Need{Resource: "job-postings", Op: domain.OpList, OrgWide: true}
|
|
applications = Need{Resource: "job-applications", Op: domain.OpList, OrgWide: true}
|
|
interviews = Need{Resource: "ai-interviews", Op: domain.OpList, OrgWide: true}
|
|
profiles = Need{Resource: "worker-profiles", Op: domain.OpList, OrgWide: true}
|
|
staff = Need{Resource: "staff", Op: domain.OpList, OrgWide: true}
|
|
orgActivity = Need{Resource: "user-activity", Op: domain.OpList, OrgWide: true}
|
|
courses = Need{Resource: "courses", Op: domain.OpList, OrgWide: true}
|
|
certs = Need{Resource: "certifications", Op: domain.OpList, OrgWide: true}
|
|
evidence = Need{Resource: "evidence", Op: domain.OpList, OrgWide: true}
|
|
|
|
// The caller's own audit trail rather than the organization's — the Profile
|
|
// page reads what *you* did, which every role may do for themselves.
|
|
ownActivity = Need{Resource: "user-activity", Op: domain.OpList}
|
|
)
|
|
|
|
/* ── The catalogue ──────────────────────────────────────────────────────── */
|
|
|
|
// catalogue is every intent, keyed by canonical page id.
|
|
//
|
|
// Keys are canonical SKILL_SURFACES ids — the same vocabulary
|
|
// internal/definition validates a definition's `pages:` against. A page that is
|
|
// a real surface but has no entry here is not an error: it answers with an
|
|
// empty list, which is the honest reply for a screen holding no readings.
|
|
//
|
|
// The seven workspace and configuration surfaces are deliberately absent.
|
|
// Their manifests explain the screen rather than read workforce records — they
|
|
// have no data behind them to rank a typed query against, and the panel there
|
|
// answers from the registries directly.
|
|
//
|
|
// Declaration order is the tie-break when two intents score equally, so the
|
|
// order within a page is the order the frontend manifest lists them in.
|
|
var catalogue = map[string][]Intent{
|
|
|
|
/* ── Control Center — the platform read as a whole ─────────────────── */
|
|
|
|
"control-center": {
|
|
{
|
|
ID: "platform-health", Text: "How healthy is the platform right now?",
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.DraftPositions+c.StarvedPositions+c.Unscreened, 6) },
|
|
},
|
|
},
|
|
|
|
/* ── Positions — the roles being filled ────────────────────────────── */
|
|
|
|
"positions": {
|
|
{
|
|
ID: "position-drafts", Text: "Which positions are still unfinished drafts?",
|
|
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},
|
|
Signal: func(c Context) int { return when(c.DraftPositions, 7) },
|
|
},
|
|
{
|
|
ID: "position-strength", Text: "Which position has the strongest pipeline?",
|
|
Subject: "pipeline strength by position", Shapes: []string{"table", "stats", "list", "card"},
|
|
Terms: []string{"pipeline", "strength", "strongest", "healthiest", "best",
|
|
"conversion", "which position", "compare", "position", "positions",
|
|
"role", "roles"},
|
|
Reads: []Need{postings, applications},
|
|
Signal: func(c Context) int { return when(atLeastTwo(c.ActivePositions), 3) },
|
|
},
|
|
{
|
|
ID: "positions-attention", Text: "Which positions need attention?",
|
|
Subject: "positions needing attention", Shapes: []string{"list", "table", "insight"},
|
|
Terms: []string{"attention", "risk", "risks", "at risk", "stalled", "stale",
|
|
"ageing", "aging", "neglected", "urgent", "slipping", "behind",
|
|
"position", "positions", "role", "roles"},
|
|
Reads: []Need{postings, applications},
|
|
Signal: func(c Context) int { return when(c.StarvedPositions, 9) },
|
|
},
|
|
{
|
|
ID: "hiring-priority", Text: "Which position should I fill first?",
|
|
Subject: "what to fill first", Shapes: []string{"list", "table"},
|
|
Terms: []string{"priority", "prioritise", "prioritize", "fill", "fill first",
|
|
"first", "most important", "which position", "vacancy", "vacancies",
|
|
"position", "positions", "role", "roles"},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Applications, 2) },
|
|
},
|
|
{
|
|
/* Deliberately NOT carrying "pipeline". This is the Positions page's
|
|
reading of who is waiting on a decision, and it belongs here — the
|
|
frontend manifest declares it on this page. But "pipeline" on
|
|
Positions is a question about how the ROLES are converting, and
|
|
answering it with a list of people is the page context being
|
|
right and the ranking being wrong. Its own words are what it
|
|
answers to. */
|
|
ID: "candidates-waiting", Text: "Which candidates are waiting on a decision?",
|
|
Subject: "the candidates waiting", Shapes: []string{"list", "table", "stats"},
|
|
Terms: []string{"waiting", "candidate", "candidates", "applicant", "applicants",
|
|
"review", "screen", "screening", "decision", "queue"},
|
|
Reads: []Need{applications},
|
|
Signal: func(c Context) int { return when(c.Unscreened, 6) },
|
|
},
|
|
},
|
|
|
|
/* ── Candidates — the people in the funnel, as a triage queue ──────── */
|
|
|
|
"candidates": {
|
|
{
|
|
ID: "candidates-attention", Text: "Which candidates need attention?",
|
|
Subject: "the candidates needing attention", Shapes: []string{"list", "table", "insight"},
|
|
Terms: []string{"attention", "waiting", "stalled", "overdue", "action",
|
|
"decision", "decide", "urgent", "candidate", "candidates", "applicant",
|
|
"applicants"},
|
|
Reads: []Need{applications},
|
|
Signal: func(c Context) int { return when(c.Unscreened+c.Shortlisted, 6) },
|
|
},
|
|
{
|
|
ID: "top-candidates", Text: "Who are the strongest candidates?",
|
|
Subject: "the strongest candidates", Shapes: []string{"list", "table", "stats", "card"},
|
|
Terms: []string{"strongest", "top", "best", "compare", "shortlist", "rank",
|
|
"ranking", "highest", "score", "scores", "scored", "scoring", "who",
|
|
"candidate", "candidates", "applicant", "applicants"},
|
|
Reads: []Need{applications},
|
|
Signal: func(c Context) int { return when(c.Applications-c.Unscreened, 3) },
|
|
},
|
|
{
|
|
ID: "interview-ready", Text: "Who is ready to interview?",
|
|
Subject: "the interview-ready candidates", Shapes: []string{"list", "table", "stats"},
|
|
Terms: []string{"interview", "interviews", "interviewed", "ready", "schedule",
|
|
"next round", "shortlist", "candidate", "candidates", "applicant",
|
|
"applicants"},
|
|
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?",
|
|
Subject: "the screening gaps", Shapes: []string{"list", "table", "stats", "progress"},
|
|
Terms: []string{"unscored", "score", "scores", "scoring", "screening", "screen",
|
|
"gap", "gaps", "missing", "incomplete", "coverage", "candidate",
|
|
"candidates", "applicant", "applicants"},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Applications, 2) },
|
|
},
|
|
{
|
|
ID: "candidate-risk", Text: "Which candidates carry risk flags?",
|
|
Subject: "the candidate risks", Shapes: []string{"list", "table", "insight"},
|
|
Terms: []string{"risk", "risks", "risky", "flag", "flags", "flagged", "concern",
|
|
"concerns", "integrity", "doubt", "decision", "candidate", "candidates",
|
|
"applicant", "applicants"},
|
|
Reads: []Need{applications, interviews},
|
|
Signal: func(c Context) int { return when(c.FlaggedRisks, 9) },
|
|
},
|
|
},
|
|
|
|
/* ── Candidates Analysis — the same records read as a pool ─────────── */
|
|
|
|
"candidates-analysis": {
|
|
{
|
|
ID: "candidate-risk", Text: "Where is candidate risk concentrated?",
|
|
Subject: "candidate risk", Shapes: []string{"table", "stats", "insight"},
|
|
Terms: []string{"risk", "risks", "flag", "flags", "flagged", "concern",
|
|
"integrity", "concentrated"},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Applications-c.Unscreened, 3) },
|
|
},
|
|
},
|
|
|
|
/* ── Analytics — performance over time ─────────────────────────────── */
|
|
|
|
"analytics": {
|
|
{
|
|
ID: "hiring-trend", Text: "How has hiring trended over time?",
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.StarvedPositions, 8) },
|
|
},
|
|
},
|
|
|
|
/* ── Activity — the audit log ──────────────────────────────────────── */
|
|
|
|
"activity": {
|
|
{
|
|
ID: "audit-summary", Text: "Summarize the audit log",
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.ActivityEvents, 2) },
|
|
},
|
|
{
|
|
ID: "unusual-activity", Text: "Has anything unusual happened?",
|
|
Subject: "the unusual activity", Shapes: []string{"list", "timeline", "insight"},
|
|
Terms: []string{"unusual", "anomaly", "anomalies", "anomalous", "suspicious",
|
|
"spike", "spikes", "burst", "odd", "strange", "out of hours"},
|
|
Reads: []Need{orgActivity},
|
|
},
|
|
{
|
|
ID: "security-insights", Text: "Are there any security concerns?",
|
|
Subject: "the security findings", Shapes: []string{"list", "insight", "table"},
|
|
Terms: []string{"security", "secure", "breach", "compliance", "compliant",
|
|
"integrity", "trace", "traceable", "attributable", "risk", "risks"},
|
|
Reads: []Need{orgActivity},
|
|
},
|
|
},
|
|
|
|
/* ── Talent Pool — supply, before anyone applies ───────────────────── */
|
|
|
|
"talent-pool": {
|
|
{
|
|
ID: "talent-priorities", Text: "Who should I prioritize in the talent pool?",
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Profiles, 2) },
|
|
},
|
|
},
|
|
|
|
/* ── Hired History — what happened after the hire ───────────────────── */
|
|
|
|
"hired-history": {
|
|
{
|
|
ID: "hiring-outcomes", Text: "How have our hires worked out?",
|
|
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},
|
|
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},
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Hired, 5) },
|
|
},
|
|
},
|
|
|
|
/* ── KROW Forge — the skill library and the proof behind it ────────── */
|
|
|
|
"krow-forge": {
|
|
{
|
|
ID: "forge-library", Text: "What is in the skill library?",
|
|
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},
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Courses, 2) },
|
|
},
|
|
{
|
|
ID: "forge-evaluation", Text: "How is submitted proof evaluated?",
|
|
Subject: "the evaluation criteria", Shapes: []string{"list", "table", "insight"},
|
|
Terms: []string{"evaluate", "evaluated", "evaluation", "rubric", "rubrics",
|
|
"criteria", "criterion", "grading", "graded", "proof", "verify",
|
|
"verification", "assess"},
|
|
Reads: []Need{courses, evidence},
|
|
},
|
|
{
|
|
ID: "forge-workforce", Text: "How is the workforce using the Forge?",
|
|
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},
|
|
Signal: func(c Context) int { return when(c.Profiles, 2) },
|
|
},
|
|
{
|
|
ID: "forge-gaps", Text: "Which skill gaps are still open?",
|
|
Subject: "the skill gaps", Shapes: []string{"list", "table", "progress"},
|
|
Terms: []string{"gap", "gaps", "missing", "uncovered", "close", "coverage",
|
|
"needed", "shortfall", "certification", "certifications"},
|
|
Reads: []Need{courses, certs, profiles},
|
|
},
|
|
},
|
|
|
|
/* ── Create Position — the authoring form ──────────────────────────── */
|
|
|
|
"create-position": {
|
|
{
|
|
ID: "vetting-weights", Text: "What do the vetting weights score?",
|
|
Subject: "the vetting weights", Shapes: []string{"table", "insight"},
|
|
Terms: []string{"weight", "weights", "weighting", "weightings", "vetting",
|
|
"criteria", "criterion", "scoring", "score", "balance", "importance"},
|
|
Reads: []Need{postings},
|
|
},
|
|
{
|
|
ID: "position-benchmarks", Text: "How does this compare with similar roles?",
|
|
Subject: "the benchmarks", Shapes: []string{"table", "stats", "insight"},
|
|
Terms: []string{"compare", "comparison", "benchmark", "benchmarks", "similar",
|
|
"typical", "average", "pay", "rate", "rates", "salary", "experience",
|
|
"market"},
|
|
Reads: []Need{postings},
|
|
Signal: func(c Context) int { return when(c.ActivePositions, 3) },
|
|
},
|
|
{
|
|
ID: "position-requirements", Text: "Which credentials are already in use?",
|
|
Subject: "the credentials in use", Shapes: []string{"list", "table", "stats"},
|
|
Terms: []string{"credential", "credentials", "certification", "certifications",
|
|
"requirement", "requirements", "qualification", "qualifications",
|
|
"licence", "license", "skill", "skills"},
|
|
Reads: []Need{postings, certs},
|
|
Signal: func(c Context) int { return when(c.ActivePositions, 2) },
|
|
},
|
|
{
|
|
ID: "position-spec-steps", Text: "How does this form work?",
|
|
Terms: []string{"form", "field", "fields", "step", "steps", "section",
|
|
"sections", "how do i", "what do i", "explain", "fill in", "required"},
|
|
},
|
|
},
|
|
|
|
/* ── Profile — the account, not the workforce ──────────────────────── */
|
|
|
|
"profile": {
|
|
{
|
|
ID: "profile-permissions", Text: "What am I permitted to do?",
|
|
Terms: []string{"permission", "permissions", "permitted", "allowed", "can i",
|
|
"scope", "access", "role", "rights", "privilege", "privileges"},
|
|
},
|
|
{
|
|
ID: "profile-identity", Text: "What are my account details?",
|
|
Terms: []string{"account", "details", "name", "email", "identity", "profile",
|
|
"who am i", "my details"},
|
|
},
|
|
{
|
|
ID: "profile-security", Text: "How is my account secured?",
|
|
Terms: []string{"security", "secure", "secured", "password", "two-factor",
|
|
"two factor", "2fa", "session", "sessions", "sign out", "log out",
|
|
"protect", "protected"},
|
|
},
|
|
{
|
|
ID: "profile-preferences", Text: "Which preferences are set?",
|
|
Terms: []string{"preference", "preferences", "setting", "settings", "digest",
|
|
"density", "owliver", "default", "defaults", "toggle"},
|
|
},
|
|
{
|
|
ID: "profile-activity", Text: "What have I done recently?",
|
|
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},
|
|
Signal: func(c Context) int { return when(c.ActivityEvents, 3) },
|
|
},
|
|
{
|
|
ID: "profile-actions", Text: "What can I do on this page?",
|
|
Terms: []string{"do here", "what can i", "action", "actions", "edit", "change",
|
|
"change my", "update", "manage"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Pages is every page the catalogue holds intents for, for tests and
|
|
// diagnostics. It is not the set of valid pages — that is
|
|
// definition.SupportedPages, and a valid page absent from here answers with an
|
|
// empty list rather than a validation error.
|
|
func Pages() []string {
|
|
out := make([]string, 0, len(catalogue))
|
|
for page := range catalogue {
|
|
out = append(out, page)
|
|
}
|
|
return out
|
|
}
|