Files
krow_backend/go-api/internal/owliver/catalog.go
2026-08-28 12:21:44 +05:30

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
}