// 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": { { /** * Creating a position, offered as a chip. * * The only intent on this page that WRITES, which is why it reads * job-postings with OpCreate: the permission gate ahead of ranking * then answers "may this caller create one?" from the same policy * table the endpoint uses, and a talent caller is never offered it. * * Terms are PHRASES ONLY, deliberately. A bare "position" or "role" * term would join the score-10 tie every reading on this page is in * and evict one of them from the exact ordered result * TestPositionsSuggestions asserts — a create chip would arrive by * pushing a reading out, which is not a trade this page should make * silently. * * No Subject and no Shapes, on the precedent of position-spec-steps: * a Subject would let the bare query "summarize" match this through * matchShape and survive filterOnTopic, offering "Summarize creating * a position" to somebody who asked for an overview of the page. * * OrgWide stays false. ScopeFor is the READ predicate; it says * nothing about a write and asking it here would be a category * error that happens to return the right answer. * * No Signal: never offered unprompted. An empty composer should * report what the organization needs, not propose paperwork. */ ID: "create-company-position", Text: "Create a company position", Terms: []string{"create position", "create a position", "create new position", "create a new position", "new position", "post a job", "post a new job", "create a company position", "open a role", "add a position", "create"}, Reads: []Need{{Resource: "job-postings", Op: domain.OpCreate}}, }, { /** * The supply-side twin, offered here as well as on Talent Pool * because "create" on Positions is ambiguous between the two and * showing both is how the reader tells them apart. The wording is * what disambiguates: "company" and "employee" carry it, and the * chip text is what the panel dispatches, so the choice the reader * makes is the one that routes. */ ID: "create-employee-role", Text: "Create an employee role", Terms: []string{"create employee role", "create an employee role", "add an employee role", "new employee role", "create worker role", "add a worker role", "employee role", "worker role"}, Reads: []Need{{Resource: "employee-roles", Op: domain.OpCreate}}, }, { 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": { { /** * Recording what a worker does, offered as a chip. * * The write on this page. Same construction as its twin on * Positions — phrases only, no Subject, no Signal — and the same * permission gate: employee-roles grants Create to operators, so a * talent caller is never offered it even though they may read their * own. */ ID: "create-employee-role", Text: "Create an employee role", Terms: []string{"create employee role", "create an employee role", "add an employee role", "new employee role", "create worker role", "add a worker role", "employee role", "worker role", "add a worker"}, Reads: []Need{{Resource: "employee-roles", Op: domain.OpCreate}}, }, { 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 }