refactor(ts-migration): Phase 11 batch 7 — the assistant's non-component modules
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
This commit is contained in:
172
src/components/ai-assistant/placement.ts
Normal file
172
src/components/ai-assistant/placement.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
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 }))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user