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:
2026-09-18 15:35:15 +05:30
parent dde4ba62c6
commit 3f835eee93
11 changed files with 6 additions and 4 deletions

View 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 }))
);
}