Files
Suriyakumarvijayanayagam dc785b917c
Some checks failed
CI / test (push) Failing after 4m41s
CI / fixture (push) Failing after 8s
Separate what a worker does from what a company needs filled
Owliver could offer neither create. The Create Position flow worked and no chip
anywhere suggested it, because the chip row is entirely the backend's static
catalogue and no intent in it wrote anything. The gap was never in the
frontend's trigger matching — every phrasing already routed.

`employee_roles` is the supply side of `job_postings`. A posting is what the
ORGANIZATION needs filled; this is what a WORKER says they do. They share a
vocabulary and almost nothing else: "3 years" on a posting is a minimum an
applicant must clear, and the same words here are what the person has. There is
deliberately no foreign key between them — supply and demand already meet
through `job_applications`, which carries the funnel, the interview and the
outcome, and a second weaker link would disagree with it the first time
somebody withdrew.

NO NEW COMPANY ENTITY, AND THAT IS THE LOAD-BEARING DECISION. "Create a company
position" reads like it needs a client record. `organizations` is the TENANT —
absent from the resource table, absent from the policy map, written only by the
seeder — so creating a row there from a chat flow would provision a new tenant,
and the position would carry an org_id the operator's session cannot see. The
operator could never view the record they just created. That breaks I5 and I1
to add a feature nobody asked for. The client stays free text on the posting,
per blueprint decision D2, and the flow simply offers the clients this
organization already staffs for as chips. No schema change, no endpoint change.

Create is operators-only, and that is an I1 decision rather than a deferral.
The worker is named explicitly on the row and is deliberately NOT derived from
the session, because an operator recording a role on somebody's behalf is the
whole point of the flow. Granting talent the same Create would let a talent
caller write a role under any worker_email in the tenant — the attribution hole
Phase 3D closed elsewhere. Talent reads its own via a ScopeEmail predicate,
which is in place now so the grant is one line when a talent console exists.

`created_by` is in gen_resources.py's SERVER_OWNED as well as the policy's
Derived list. Both are required and the pairing is easy to miss: Derived fills
the column from the session, SERVER_OWNED is what makes the descriptor ReadOnly
so a request body cannot set it in the first place. Without it,
TestDerivedColumnsAreReadOnlyOrTalentScoped fails — verified by mutation, not
by reading.

The two catalogue intents carry PHRASE terms only. A bare "position" or "role"
term scores 10, the same as every reading on that page, and wins the tie on
declaration order — so a create chip would have arrived by evicting
`positions-attention` from the exact ordered result TestPositionsSuggestions
asserts. An offer to create something must not displace the reading a person
actually asked for. Neither declares a Subject, on the precedent of
`position-spec-steps`: a Subject would let the bare query "summarize" match
through matchShape and survive filterOnTopic. Neither declares a Signal, so an
empty composer still reports what the organization needs rather than proposing
paperwork.

Chip text is the coupling with nothing else holding it together: no page
context declares `capabilities`, so every server suggestion dispatches as its
own TEXT and is answered by whichever skill's trigger that text matches. A
renamed chip would open nothing, silently. Asserted on the frontend side.

The down migration drops `employee_role_status` and keeps `english_level`,
which is shared with job_postings.english_required and
job_applications.english_level. Rolled back and re-applied against the
database to prove it, not asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-09-02 15:29:25 +05:30

753 lines
35 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": {
{
/**
* 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
}