The eleven `.js` files under `src/components/ai-assistant/`: the blocks
format, contexts, routing, history, placement, viewport, the greeting
and prompt tables in `dynamic`, the derivations in `insights`, and
`uiEdit`, whose boundary batch 1 already typed.
79 errors, and two optional markers cleared 63 of them.
`plural(n, word, irregular)` is called with two arguments sixty times in
`dynamic.ts` and its own body reads `irregular || \`${word}s\``, so the
third parameter has always been optional in everything but the
signature. `heading(value, sub)` is the same: `sub` is spread into the
block and `undefined` is what most callers mean. Marking both optional
is a statement about the existing contract, and the markers erase — the
emitted signatures still read `plural=(n,word,irregular)` and
`heading=(value,sub)`, checked in the output rather than assumed.
Those three `heading` errors landed in `lib/skills/workforceFlow.ts`,
already migrated and untouched here. Worth noting how that works: a
function's arity only starts being enforced on its callers once the file
defining it is TypeScript. Migrating a leaf makes claims about every
file that imports it, which is why this phase moves bottom-up.
The remaining nine were two `reduce` accumulators inferring `{}`, so
`Object.values` over them produced `unknown`. Both are now stated —
`{ label, count }` for the score bands, and the five-field hire grouping
— which is more useful than `any` and exactly what the lines below them
build.
Measured against `dde4ba6`:
typecheck 6 errors, unchanged; no new error anywhere
lint exit 0, 0 errors, 289 warnings
npm test 1684/1691, the same 7 failures verbatim
build exit 0, identical bundle hash 74d17e2d…
type erasure 83/83 byte-identical across Phase 11 so far
No baseline artifact touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
173 lines
7.7 KiB
TypeScript
173 lines
7.7 KiB
TypeScript
import { ASSISTANT_CONTEXTS } from './contexts';
|
|
|
|
/**
|
|
* Where the Owliver dashboard panel appears.
|
|
*
|
|
* The assistant occupies a permanent column in the page layout, so enabling it
|
|
* on a page is a layout decision, not a feature flag — which is exactly why the
|
|
* decision lives in one table rather than being scattered across pages.
|
|
*
|
|
* Matching is exact, not prefix-based: `/candidates` must not leak an assistant
|
|
* onto a future `/candidates/:id`, and `/positions/:id` must not inherit one
|
|
* from `/positions`.
|
|
*/
|
|
const PLACEMENT = {
|
|
admin: {
|
|
'/admin': 'admin.controlCenter',
|
|
'/admin/positions': 'admin.positions',
|
|
/* The authoring form. Owliver belongs here for the same reason it belongs
|
|
on Positions: the questions asked while specifying a role — what do these
|
|
weights score, what do comparable roles ask for — are answerable from the
|
|
page and from what this workspace has already posted. */
|
|
'/admin/positions/new': 'admin.createPosition',
|
|
'/admin/candidates': 'admin.candidatesList',
|
|
'/admin/candidates-analysis': 'admin.candidates',
|
|
'/admin/analytics': 'admin.analytics',
|
|
'/admin/activity': 'admin.activity',
|
|
'/admin/university': 'admin.forge',
|
|
'/admin/talent-pool': 'admin.talentPool',
|
|
'/admin/hired': 'admin.hiredHistory',
|
|
'/admin/profile': 'admin.profile',
|
|
/* Agent management. Configuring an agent is a workspace task, not an
|
|
operational page — see `admin.agentConfigure` for what that means for
|
|
what Owliver may read here. `agents/new` is listed exactly so it is
|
|
never read as an agent whose id is "new". */
|
|
'/admin/workspace/agents/new': 'admin.agentConfigure',
|
|
|
|
/**
|
|
* Settings and the rest of the workspace.
|
|
*
|
|
* These pages have no agent written for them, and for a while that was read
|
|
* as "no Owliver here" — the panel did not mount at all, so a product that
|
|
* answered questions on every Admin page before specialists existed
|
|
* answered on eight of them afterwards.
|
|
*
|
|
* The absence of a specialist is not the absence of an assistant. Each of
|
|
* these carries the same panel, resolved by this same table, answering from
|
|
* the general agent. What they do *not* carry is operational data: no skill
|
|
* declares these surfaces, so a workforce question asked here is declined
|
|
* and pointed at the page that holds the records rather than answered from
|
|
* a configuration screen.
|
|
*/
|
|
'/admin/settings': 'admin.settings',
|
|
'/admin/workspace': 'admin.workspace',
|
|
'/admin/workspace/agents': 'admin.workspaceAgents',
|
|
'/admin/workspace/skills': 'admin.workspaceSkills',
|
|
'/admin/workspace/skill-development': 'admin.skillDevelopment',
|
|
/* The skill editor, listed exactly for the same reason `agents/new` is: so
|
|
`skills/new` is never read as a skill whose id is "new". This is also the
|
|
entry the page key is derived from — `pageKeyForRoute` reads the surface
|
|
table, and this route is the one the `workspace-skill-configure` surface
|
|
declares. The Owliver editor's addresses reach the same context through
|
|
the pattern table below; listing them here too would overwrite that key
|
|
with a path tail. */
|
|
'/admin/workspace/skills/new': 'admin.skillConfigure',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Pages that must never carry the assistant, listed explicitly so the intent is
|
|
* documented rather than implied by omission.
|
|
*
|
|
* Admin: login only. Every other surface — including Profile and Settings,
|
|
* where the questions are about the account rather than the workforce,
|
|
* and the workspace pages, where they are about the configuration —
|
|
* carries the panel.
|
|
* Employer: every page. The contextual panel is an Admin capability.
|
|
* Talent: every page. The talent portal has the standalone Owliver product,
|
|
* with its own voice experience, branding and workflow.
|
|
*/
|
|
export const EXCLUDED_ROUTES = [
|
|
// Every Employer and Talent route: the contextual panel is Admin-only.
|
|
'/', '/overview', '/positions', '/positions/new', '/positions/:id', '/candidates',
|
|
/* The Employer aliases only — the Admin `/admin/positions/new` carries the
|
|
panel, and is listed in the placement table above. */
|
|
'/hired', '/talent-pool', '/analytics', '/profile', '/apply',
|
|
'/university', '/university/:id', '/me', '/identity', '/employee',
|
|
'/owliver', '/design-system',
|
|
// Sign-in has no page data to reason about.
|
|
'/admin/login',
|
|
// The full candidate profile is one person's record, not a page-level question.
|
|
'/admin/candidates/:id',
|
|
];
|
|
|
|
/**
|
|
* The Admin route → context table, exported so the skill registry can derive
|
|
* page keys from it. One table, so a route added here is addressable by a skill
|
|
* without a second list to keep in step.
|
|
*/
|
|
export const PLACEMENT_ROUTES = PLACEMENT.admin;
|
|
|
|
/**
|
|
* Routes whose address carries a record id.
|
|
*
|
|
* The table above is matched exactly, which is deliberate and stays that way:
|
|
* `/candidates` must not leak an assistant onto `/candidates/:id`. But an agent
|
|
* is configured at `/admin/workspace/agents/<id>`, and no exact table can list
|
|
* an address that contains an id nobody has created yet.
|
|
*
|
|
* So a second, much smaller table is consulted **only after the exact lookup
|
|
* misses**. Every one of the eight operational pages is an exact match and
|
|
* never reaches this code, so their placement is unchanged by construction
|
|
* rather than by care.
|
|
*
|
|
* `exclude` keeps a sibling literal route — `agents/new` is the same screen and
|
|
* is listed exactly — from being read as an id.
|
|
*/
|
|
const PLACEMENT_PATTERNS = {
|
|
admin: [
|
|
{
|
|
/* Agent configuration: one path segment after `agents/`, and not a
|
|
nested route beneath it. */
|
|
test: (pathname) => /^\/admin\/workspace\/agents\/[^/]+$/.test(pathname),
|
|
contextId: 'admin.agentConfigure',
|
|
},
|
|
{
|
|
/* The Owliver skill editor — `skills/owliver/new` and
|
|
`skills/owliver/<id>`. Two segments, so it is matched before the
|
|
one-segment pattern below and can never be read as a skill whose id is
|
|
"owliver". */
|
|
test: (pathname) => /^\/admin\/workspace\/skills\/owliver\/[^/]+$/.test(pathname),
|
|
contextId: 'admin.skillConfigure',
|
|
},
|
|
{
|
|
/* The Board skill editor: one segment after `skills/`, and no deeper.
|
|
`owliver` is excluded because it is a prefix rather than a skill, and
|
|
the product has no page at that address. */
|
|
test: (pathname) => /^\/admin\/workspace\/skills\/(?!owliver$)[^/]+$/.test(pathname),
|
|
contextId: 'admin.skillConfigure',
|
|
},
|
|
],
|
|
};
|
|
|
|
/**
|
|
* The dynamic routes, as literal examples.
|
|
*
|
|
* Exported so the registry and the checks can reason about a pattern without
|
|
* re-implementing it. These are addresses the pattern genuinely matches.
|
|
*/
|
|
export const PLACEMENT_PATTERN_ROUTES = {
|
|
'/admin/workspace/agents/:id': 'admin.agentConfigure',
|
|
'/admin/workspace/skills/:id': 'admin.skillConfigure',
|
|
'/admin/workspace/skills/owliver/new': 'admin.skillConfigure',
|
|
'/admin/workspace/skills/owliver/:id': 'admin.skillConfigure',
|
|
};
|
|
|
|
/** Returns the context for a role and path, or `null` to render no assistant. */
|
|
export function resolveAssistantContext(role, pathname) {
|
|
/* Exact first, always. The eight operational pages resolve here and never
|
|
reach the patterns below. */
|
|
const id = PLACEMENT[role]?.[pathname];
|
|
if (id) return ASSISTANT_CONTEXTS[id] ?? null;
|
|
|
|
const pattern = (PLACEMENT_PATTERNS[role] || []).find((p) => p.test(pathname));
|
|
return pattern ? ASSISTANT_CONTEXTS[pattern.contextId] ?? null : null;
|
|
}
|
|
|
|
/** Every enabled role/route pair — used by the placement verification. */
|
|
export function enabledRoutes() {
|
|
return Object.entries(PLACEMENT).flatMap(([role, routes]) =>
|
|
Object.entries(routes).map(([path, contextId]) => ({ role, path, contextId }))
|
|
);
|
|
}
|