refactor(ts-migration): Phase 11 batch 3 — the rest of the skills layer
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<string, number>`, 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
@@ -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;
|
||||
@@ -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<any>(skill.ui || {}).flatMap(([page, config]) =>
|
||||
(config.sections || []).map((section) => ({
|
||||
id: section.id,
|
||||
title: section.title || sectionTypeLabel(section.type),
|
||||
@@ -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<Record<string, number>>((acc, e) => {
|
||||
acc[e.event_type] = (acc[e.event_type] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const byAccount = events.reduce((acc, e) => {
|
||||
const byAccount = events.reduce<Record<string, number>>((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}.` };
|
||||
|
||||
@@ -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')) {
|
||||
@@ -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) {
|
||||
@@ -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<any>(ui || {})) {
|
||||
const section = (page.sections || [])[0];
|
||||
if (section) return section;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
@@ -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<any>(skill?.ui || {})) {
|
||||
for (const section of config.sections || []) {
|
||||
if (!section.context) continue;
|
||||
if (placementProvides(page, section.placement).includes(section.context)) continue;
|
||||
@@ -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<any>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user