From fc8d7eec528522ff06a83b6a59fd4ce94c1e8cba Mon Sep 17 00:00:00 2001 From: Aravind Date: Fri, 18 Sep 2026 15:20:41 +0530 Subject: [PATCH] =?UTF-8?q?refactor(ts-migration):=20Phase=2011=20batch=20?= =?UTF-8?q?3=20=E2=80=94=20the=20rest=20of=20the=20skills=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remaining fifteen modules under `src/lib/skills/`, including the three under `flows/`. `src/lib/skills` now holds no JavaScript. Renaming them raised 134 errors, which came from nineteen values, not 134 places. Eleven were accumulators or parameters written `= {}`, whose type is then `{}` — an object with no properties — so every later read of a key looked like a mistake. Five were `Object.entries`/`values` on a dynamic value, which yields `unknown` rather than `any` because inference into their union parameter does not distribute. The rest were `reduce` accumulators in the same position. Annotating the nineteen sources cleared all 134. Where the keys were knowable they are written down rather than waved away: both `prefill` accumulators in `actions.ts` name the fields their own following lines assign, and `dataResolver`'s two event tallies are `Record`, which is what they are. Where the value is genuinely whatever an author wrote — a parsed YAML mapping, a skill context — it stays `any`. The one structural addition is `Frontmatter`, the return of `parseFrontmatter`. Its no-frontmatter early return hands back a literal `{}`, so TypeScript took the common shape of the two returns, which has no properties; that single empty object is what made twenty-five later readings of `data` look wrong. Typing the return also resolved four pre-existing errors in this file and four more that had cascaded into `lib/agents/registry.js`, so the project total is 16, below the 20 this phase started from. Nothing was suppressed to get there. Measured against `e73929f`: typecheck 16 errors, down from 20; 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 33/33 byte-identical, all of Phase 11 so far CORRECTION to the previous two commits. Both claim the migrated files emit "byte-identical minified JavaScript". That check was broken when it ran and proved nothing: it passed `--loader=js`/`--loader=ts` to esbuild on named files, and esbuild accepts `--loader` without an extension only for stdin. Both sides errored, both outputs were empty, and `cmp` found two empty files equal. Eighteen "IDENTICAL" lines meant eighteen pairs of nothing. Repaired here and re-run over all 33 files. Two further things had to change for the check to mean anything. It now proves it can detect a difference before it is trusted, against a pair of files differing in one character. And it compares with `--minify-whitespace --minify-syntax` rather than `--minify`: full minification renames locals, and esbuild's choice of names shifts with token counts, so twelve files differed only in whether a binding was called `g` or `u` — alpha-equivalent, at identical byte counts. Stripping comments and whitespace while keeping identifiers is the comparison that answers the actual question. The result is that the substantive claim was true throughout, and is now actually evidenced: all 33 files erase to byte-identical JavaScript. It was never the only evidence either — the production bundle hash and the 1691-check suite were compared in every batch, both valid, and both unchanged. No baseline artifact touched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- src/lib/skills/{actions.js => actions.ts} | 11 ++++++-- src/lib/skills/{catalog.js => catalog.ts} | 2 +- .../{customSkills.js => customSkills.ts} | 0 .../{dataResolver.js => dataResolver.ts} | 8 +++--- .../{employeeRole.js => employeeRole.ts} | 4 +-- src/lib/skills/flows/{index.js => index.ts} | 0 .../skills/flows/{position.js => position.ts} | 4 +-- .../{owliverConfig.js => owliverConfig.ts} | 2 +- ...{owliverResolver.js => owliverResolver.ts} | 6 ++-- .../skills/{pageSkills.js => pageSkills.ts} | 2 +- .../{positionFlow.js => positionFlow.ts} | 0 src/lib/skills/{registry.js => registry.ts} | 28 +++++++++++++++++-- .../skills/{skillFields.js => skillFields.ts} | 4 +-- src/lib/skills/{tools.js => tools.ts} | 0 .../{usePageSkills.js => usePageSkills.ts} | 0 15 files changed, 50 insertions(+), 21 deletions(-) rename src/lib/skills/{actions.js => actions.ts} (98%) rename src/lib/skills/{catalog.js => catalog.ts} (99%) rename src/lib/skills/{customSkills.js => customSkills.ts} (100%) rename src/lib/skills/{dataResolver.js => dataResolver.ts} (99%) rename src/lib/skills/flows/{employeeRole.js => employeeRole.ts} (99%) rename src/lib/skills/flows/{index.js => index.ts} (100%) rename src/lib/skills/flows/{position.js => position.ts} (99%) rename src/lib/skills/{owliverConfig.js => owliverConfig.ts} (99%) rename src/lib/skills/{owliverResolver.js => owliverResolver.ts} (99%) rename src/lib/skills/{pageSkills.js => pageSkills.ts} (99%) rename src/lib/skills/{positionFlow.js => positionFlow.ts} (100%) rename src/lib/skills/{registry.js => registry.ts} (97%) rename src/lib/skills/{skillFields.js => skillFields.ts} (99%) rename src/lib/skills/{tools.js => tools.ts} (100%) rename src/lib/skills/{usePageSkills.js => usePageSkills.ts} (100%) diff --git a/src/lib/skills/actions.js b/src/lib/skills/actions.ts similarity index 98% rename from src/lib/skills/actions.js rename to src/lib/skills/actions.ts index d1316d6..c5633dc 100644 --- a/src/lib/skills/actions.js +++ b/src/lib/skills/actions.ts @@ -168,7 +168,11 @@ export function buildPositionPrefill(question, categories = []) { const english = extractEnglish(question); const certifications = extractCertifications(question); - const prefill = {}; + const prefill: { + role_category?: any; title?: any; location?: any; + pay_range_min?: any; pay_range_max?: any; min_experience_years?: any; + english_required?: any; certifications_required?: any; + } = {}; if (role) { /* A known category fills the category field; anything else is a title, so a one-off role is not silently filed under the wrong category. Either way @@ -378,7 +382,10 @@ export function buildSkillPrefill(question, categories = []) { const outline = template?.outline || []; const challengeType = template?.proof || null; - const prefill = {}; + const prefill: { + title?: any; category?: any; difficulty?: any; + outline?: any; challengeType?: any; + } = {}; if (title) prefill.title = title; if (category || template?.category) prefill.category = category || template.category; if (difficulty) prefill.difficulty = difficulty; diff --git a/src/lib/skills/catalog.js b/src/lib/skills/catalog.ts similarity index 99% rename from src/lib/skills/catalog.js rename to src/lib/skills/catalog.ts index 39667d2..db2df72 100644 --- a/src/lib/skills/catalog.js +++ b/src/lib/skills/catalog.ts @@ -295,7 +295,7 @@ export const TYPE_LABEL = { owliver: 'Owliver', board: 'Board', both: 'Owliver + /** The sections a Board skill declares, flattened with the page each sits on. */ function sectionsOf(skill) { - return Object.entries(skill.ui || {}).flatMap(([page, config]) => + return Object.entries(skill.ui || {}).flatMap(([page, config]) => (config.sections || []).map((section) => ({ id: section.id, title: section.title || sectionTypeLabel(section.type), diff --git a/src/lib/skills/customSkills.js b/src/lib/skills/customSkills.ts similarity index 100% rename from src/lib/skills/customSkills.js rename to src/lib/skills/customSkills.ts diff --git a/src/lib/skills/dataResolver.js b/src/lib/skills/dataResolver.ts similarity index 99% rename from src/lib/skills/dataResolver.js rename to src/lib/skills/dataResolver.ts index 8339b04..f839637 100644 --- a/src/lib/skills/dataResolver.js +++ b/src/lib/skills/dataResolver.ts @@ -563,7 +563,7 @@ const RESOLVERS = { /* Deduped: overlapping windows — `today` inside `last-7-days` — would otherwise count the same application twice. */ - const unique = [...new Map(pool.map((a) => [a.id, a])).values()]; + const unique: any[] = [...new Map(pool.map((a) => [a.id, a])).values()]; const scored = unique.filter((a) => a.ai_score > 0); const bands = [ @@ -797,11 +797,11 @@ const RESOLVERS = { ).values()] : activity; - const byType = events.reduce((acc, e) => { + const byType = events.reduce>((acc, e) => { acc[e.event_type] = (acc[e.event_type] || 0) + 1; return acc; }, {}); - const byAccount = events.reduce((acc, e) => { + const byAccount = events.reduce>((acc, e) => { acc[e.user_name || e.user_email] = (acc[e.user_name || e.user_email] || 0) + 1; return acc; }, {}); @@ -1189,7 +1189,7 @@ const RESOLVERS = { * is missing returns `unavailable`, which the renderer states rather than * filling in. */ -export function resolveSkillData(section, context = {}, now = new Date()) { +export function resolveSkillData(section, context: any = {}, now = new Date()) { const resolve = RESOLVERS[section?.source]; if (!resolve) return { unavailable: true, emptyNote: `No resolver for ${section?.source}.` }; diff --git a/src/lib/skills/flows/employeeRole.js b/src/lib/skills/flows/employeeRole.ts similarity index 99% rename from src/lib/skills/flows/employeeRole.js rename to src/lib/skills/flows/employeeRole.ts index c625964..a731771 100644 --- a/src/lib/skills/flows/employeeRole.js +++ b/src/lib/skills/flows/employeeRole.ts @@ -191,7 +191,7 @@ const fields = { * `@workers` is the worker profiles the panel has already loaded for this * caller — org-scoped by the API, and nothing here widens that view. */ -const resolve = (token, { roles = [], postings = null }, draft = {}) => { +const resolve = (token, { roles = [], postings = null }, draft: any = {}) => { switch (token) { case '@roles': return roles; case '@english': return ENGLISH_LEVELS.map((l) => l.label); @@ -224,7 +224,7 @@ const resolve = (token, { roles = [], postings = null }, draft = {}) => { * how a role gets filed against the wrong person. */ function extract(said, ctx, fieldNames) { - const patch = {}; + const patch: any = {}; const touched = new Set(); if (fieldNames.has('role_category')) { diff --git a/src/lib/skills/flows/index.js b/src/lib/skills/flows/index.ts similarity index 100% rename from src/lib/skills/flows/index.js rename to src/lib/skills/flows/index.ts diff --git a/src/lib/skills/flows/position.js b/src/lib/skills/flows/position.ts similarity index 99% rename from src/lib/skills/flows/position.js rename to src/lib/skills/flows/position.ts index 00c68f9..0f37a10 100644 --- a/src/lib/skills/flows/position.js +++ b/src/lib/skills/flows/position.ts @@ -143,7 +143,7 @@ const fields = { * provision a TENANT the operator cannot then see, because every read is * predicated on the session's own org_id. */ -const resolve = (token, { roles = [], companies = [], postings = null }, draft = {}) => { +const resolve = (token, { roles = [], companies = [], postings = null }, draft: any = {}) => { switch (token) { case '@roles': return roles; case '@companies': return companies; @@ -160,7 +160,7 @@ const resolve = (token, { roles = [], companies = [], postings = null }, draft = /** One sentence read against every field of a posting at once. */ function extract(said, { roles = [] }, fieldNames) { const { prefill } = buildPositionPrefill(said, roles); - const patch = {}; + const patch: any = {}; const touched = new Set(); if (fieldNames.has('role_category') && prefill.title) { diff --git a/src/lib/skills/owliverConfig.js b/src/lib/skills/owliverConfig.ts similarity index 99% rename from src/lib/skills/owliverConfig.js rename to src/lib/skills/owliverConfig.ts index 81dc9e7..838b970 100644 --- a/src/lib/skills/owliverConfig.js +++ b/src/lib/skills/owliverConfig.ts @@ -59,7 +59,7 @@ const shapeOf = (capability) => owliverCapabilityFor(capability)?.shape || null; * section wins, in declaration order. */ function inheritedSection(ui) { - for (const page of Object.values(ui || {})) { + for (const page of Object.values(ui || {})) { const section = (page.sections || [])[0]; if (section) return section; } diff --git a/src/lib/skills/owliverResolver.js b/src/lib/skills/owliverResolver.ts similarity index 99% rename from src/lib/skills/owliverResolver.js rename to src/lib/skills/owliverResolver.ts index 3d12365..45ce3ac 100644 --- a/src/lib/skills/owliverResolver.js +++ b/src/lib/skills/owliverResolver.ts @@ -66,7 +66,7 @@ const TOTAL = 4; * A suggestion naming a capability the skill does not offer is dropped rather * than shown and then refused. */ -export function owliverSuggestions(contextId, disabled = [], customSources = [], context = {}) { +export function owliverSuggestions(contextId, disabled = [], customSources = [], context: any = {}) { const chips = []; for (const skill of owliverSkillsForContext(contextId, disabled, customSources)) { @@ -263,7 +263,7 @@ export function matchOwliverSkill(question, skills = []) { * says which role they mean has said so, and answering about a different one * because a drawer happened to be open would be worse than asking. */ -export function resolveEntity(section, question, context = {}) { +export function resolveEntity(section, question, context: any = {}) { const need = section.context; if (!need) return { context, ok: true }; @@ -338,7 +338,7 @@ export const responseTitle = (skill, section) => * Returns `{ skill, capability, section, data, missing }`. `missing` names what * the caller must ask for; when it is null the reading is real and complete. */ -export function resolveOwliverResponse({ skill, capability, question, context = {}, now = new Date() }) { +export function resolveOwliverResponse({ skill, capability, question, context = {}, now = new Date() }: any) { const section = skill.owliver.responses[capability]; if (!section) return null; diff --git a/src/lib/skills/pageSkills.js b/src/lib/skills/pageSkills.ts similarity index 99% rename from src/lib/skills/pageSkills.js rename to src/lib/skills/pageSkills.ts index 669ab68..9ac16b7 100644 --- a/src/lib/skills/pageSkills.js +++ b/src/lib/skills/pageSkills.ts @@ -85,7 +85,7 @@ export function skillStatesForPage(pageId, courses = [], profile = null, options * Same definitions, same graph, same derivation as the page-scoped version * above: this is a second question asked of one dataset, not a second dataset. */ -export function workforceSkillStates(courses = [], profile = null, options = {}) { +export function workforceSkillStates(courses = [], profile = null, options: any = {}) { const { customSources = [], disabled = [] } = options; return allSkills(customSources) .filter((s) => s.kind === 'workforce' && s.status === 'active' && !disabled.includes(s.id)) diff --git a/src/lib/skills/positionFlow.js b/src/lib/skills/positionFlow.ts similarity index 100% rename from src/lib/skills/positionFlow.js rename to src/lib/skills/positionFlow.ts diff --git a/src/lib/skills/registry.js b/src/lib/skills/registry.ts similarity index 97% rename from src/lib/skills/registry.js rename to src/lib/skills/registry.ts index 188a956..d97a817 100644 --- a/src/lib/skills/registry.js +++ b/src/lib/skills/registry.ts @@ -57,7 +57,29 @@ export const normalizeDefinition = (raw) => String(raw ?? '') export const hasFrontmatter = (raw) => /^---[ \t]*\n[\s\S]*?\n---[ \t]*(?=\n|$)/ .test(normalizeDefinition(raw)); -export function parseFrontmatter(raw) { +/** + * A definition file split into its YAML head and its Markdown body. + * + * `data` is `any` because it is whatever the author wrote. It comes back from + * `parseYaml`, which reads an arbitrary mapping out of the frontmatter, and the + * keys it may carry are the whole of the skill format — `pages:`, `owliver:`, + * `actions:`, `ui:` and the rest, each optional and each shaped differently. + * Describing it properly means describing that format as a type, which is a + * much larger piece of work than a rename and belongs with the format, not + * here. + * + * Without this annotation the two `return` statements unify to `{}`: the + * no-frontmatter branch hands back a literal `{}`, and TypeScript takes the + * common shape of the two, which has no properties at all. That is how one + * empty object in an early return came to make twenty-five readings of `data` + * further down look like mistakes. + */ +export interface Frontmatter { + data: any; + body: string; +} + +export function parseFrontmatter(raw): Frontmatter { const text = normalizeDefinition(raw); const match = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?=\n|$)/.exec(text); if (!match) return { data: {}, body: text }; @@ -249,7 +271,7 @@ export const SKILL_FACETS = ['ui', 'owliver']; * and are managed in Skill Development, so they carry no facet and appear on * neither list. */ -export function skillFacets({ data = {}, kind, ui = {}, owliver, conversation = [] }) { +export function skillFacets({ data = {}, kind, ui = {}, owliver, conversation = [] }: any) { if (kind === 'workforce') return []; const extendsPage = Object.keys(ui).length > 0; @@ -643,7 +665,7 @@ export function skillDiagnostics(customSources = []) { export function unresolvableSections(skill) { const problems = []; - for (const [page, config] of Object.entries(skill?.ui || {})) { + for (const [page, config] of Object.entries(skill?.ui || {})) { for (const section of config.sections || []) { if (!section.context) continue; if (placementProvides(page, section.placement).includes(section.context)) continue; diff --git a/src/lib/skills/skillFields.js b/src/lib/skills/skillFields.ts similarity index 99% rename from src/lib/skills/skillFields.js rename to src/lib/skills/skillFields.ts index 175f6ef..bffac75 100644 --- a/src/lib/skills/skillFields.js +++ b/src/lib/skills/skillFields.ts @@ -107,7 +107,7 @@ export function boardFieldsFromSource(source) { can show. `pages` still carries every page the definition reaches, and `uiIsEditableFromFields` is what stops those fields writing over the ones this record cannot represent. */ - const section = Object.values(skill.ui || {}).flatMap((p) => p.sections || [])[0]; + const section = Object.values(skill.ui || {}).flatMap((p) => p.sections || [])[0]; return { id: skill.id, name: skill.name, @@ -564,7 +564,7 @@ function responseFor(fields, capability) { const source = response?.source || ''; if (!source) return null; - const entry = { source }; + const entry: any = { source }; if (response.periods?.length && sourceSupportsOption(source, 'periods')) { entry.periods = response.periods; } diff --git a/src/lib/skills/tools.js b/src/lib/skills/tools.ts similarity index 100% rename from src/lib/skills/tools.js rename to src/lib/skills/tools.ts diff --git a/src/lib/skills/usePageSkills.js b/src/lib/skills/usePageSkills.ts similarity index 100% rename from src/lib/skills/usePageSkills.js rename to src/lib/skills/usePageSkills.ts