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
183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { parseSkill } from './registry';
|
|
import { boardPatch, owliverPatch, patchFrontmatter } from './skillFields';
|
|
|
|
/**
|
|
* Account-authored skills, as stored.
|
|
*
|
|
* A custom skill is its Markdown source and nothing else — the same artefact a
|
|
* file in `src/skills/` is, read back by the same parser. These helpers exist so
|
|
* the Add Skill dialog and the Skills page write that list identically; two
|
|
* writers with two shapes would be a second skill system by accident.
|
|
*/
|
|
|
|
/**
|
|
* The starting definitions offered to an author.
|
|
*
|
|
* Two templates, because there are two jobs and one of them was being learned
|
|
* from the other's example. A UI skill's first draft declares a section; an
|
|
* Owliver skill's declares triggers and the shapes of an answer. Both are the
|
|
* same format, read by the same parser — what differs is which half of it the
|
|
* author is being handed.
|
|
*/
|
|
|
|
const frontMatter = ({ id, name, description, pages, fallback }) => [
|
|
`id: ${id || fallback.id}`,
|
|
`name: ${name || fallback.name}`,
|
|
`description: ${description || fallback.description}`,
|
|
'pages:',
|
|
(pages?.length ? pages : ['positions']).map((p) => ` - ${p}`).join('\n'),
|
|
'status: active',
|
|
].join('\n');
|
|
|
|
/**
|
|
* The identity block every definition opens with, and nothing else.
|
|
*
|
|
* The configuration that follows it — the `ui:` section, the `owliver:` block —
|
|
* is written by `boardPatch` and `owliverPatch`, which is the same writer the
|
|
* editors use on an existing file. A template that composed its own YAML was a
|
|
* second writer with a second field set, and the fields one knew about were not
|
|
* the fields the other did.
|
|
*/
|
|
const skeleton = (fields, fallback, body) => `---
|
|
${frontMatter({ ...fields, fallback })}
|
|
---
|
|
|
|
${body}
|
|
`;
|
|
|
|
/** A definition that draws a section on the pages it names. */
|
|
export const uiSkillTemplate = ({
|
|
id = '', name = '', description = '', pages = [],
|
|
/**
|
|
* `candidates.activity`, not `position.activity`.
|
|
*
|
|
* The default placement on the default page is `after-position-list-summary`
|
|
* — above the grid, with no position in context — so the old default composed
|
|
* a definition that drew a titled card and then reported that it needed a
|
|
* record the page never had. A template must produce something that works
|
|
* before it is edited, so the default reads a source that needs nothing.
|
|
*/
|
|
type = 'flow', placement = '', title = '', source = 'candidates.activity', periods = [],
|
|
} = {}) => {
|
|
const identity = { id, name, description, pages };
|
|
const fallback = {
|
|
id: 'my-ui-skill',
|
|
name: 'My UI Skill',
|
|
description: 'What this skill adds to the page.',
|
|
};
|
|
const heading = name || fallback.name;
|
|
|
|
return patchFrontmatter(
|
|
skeleton(identity, fallback, `# ${heading}
|
|
|
|
## Purpose
|
|
|
|
Describe what this section shows, and why it belongs on these pages.
|
|
|
|
## Capabilities
|
|
|
|
- Describe one thing the section reports.
|
|
- Add more as needed.`),
|
|
boardPatch({
|
|
...identity,
|
|
type,
|
|
placement,
|
|
/* A card with no title of its own is headed by the skill's name, which is
|
|
what the previous template wrote out. Kept, so a fresh draft reads the
|
|
same as it always did. */
|
|
title: title || heading,
|
|
source,
|
|
periods,
|
|
})
|
|
);
|
|
};
|
|
|
|
/** A definition that teaches Owliver what it can be asked for. */
|
|
export const owliverSkillTemplate = ({
|
|
id = '', name = '', description = '', pages = [],
|
|
triggers = [], suggestions = [], capabilities = [], responses = {},
|
|
} = {}) => {
|
|
const identity = { id, name, description, pages };
|
|
const fallback = {
|
|
id: 'my-owliver-skill',
|
|
name: 'My Owliver Skill',
|
|
description: 'What this skill helps Owliver answer.',
|
|
};
|
|
const label = name || fallback.name;
|
|
|
|
return patchFrontmatter(
|
|
skeleton(identity, fallback, `# ${label}
|
|
|
|
## Purpose
|
|
|
|
Describe what Owliver should be able to answer on these pages.
|
|
|
|
## Capabilities
|
|
|
|
- Describe one thing Owliver can be asked for.
|
|
- Add more as needed.`),
|
|
owliverPatch({
|
|
...identity,
|
|
/* A definition that claims no phrase of its own still answers to its
|
|
name — written out so the author can see what it will match on. */
|
|
triggers: triggers.length ? triggers : [label.toLowerCase()],
|
|
suggestions,
|
|
capabilities,
|
|
responses,
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* The template the Add Skill dialog offers.
|
|
*
|
|
* That dialog opens from Owliver's own header, mid-conversation, so what it
|
|
* hands the author is an Owliver skill. Kept under its original name because
|
|
* it is what the dialog already imports.
|
|
*/
|
|
export const skillTemplate = owliverSkillTemplate;
|
|
|
|
/** Parses a stored entry, tolerating the bare-string form. */
|
|
const sourceOf = (entry) => (typeof entry === 'string' ? entry : entry?.raw ?? '');
|
|
|
|
/**
|
|
* The stored list with `source` added or replaced.
|
|
*
|
|
* Matching is by skill id, so editing a skill overwrites its own entry rather
|
|
* than adding a near-duplicate beside it.
|
|
*/
|
|
export function upsertCustomSkill(existing = [], source) {
|
|
const skill = parseSkill(source, { custom: true });
|
|
const rest = existing.filter((entry) => {
|
|
try {
|
|
return parseSkill(sourceOf(entry), { custom: true }).id !== skill.id;
|
|
} catch {
|
|
return true;
|
|
}
|
|
});
|
|
return { skill, next: [...rest, { path: `custom/${skill.id}.md`, raw: source }] };
|
|
}
|
|
|
|
/** The stored list without the skill of this id. */
|
|
export function removeCustomSkill(existing = [], id) {
|
|
return existing.filter((entry) => {
|
|
try {
|
|
return parseSkill(sourceOf(entry), { custom: true }).id !== id;
|
|
} catch {
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** The stored Markdown for one custom skill, or null if the account has none. */
|
|
export function customSkillSource(existing = [], id) {
|
|
for (const entry of existing) {
|
|
try {
|
|
if (parseSkill(sourceOf(entry), { custom: true }).id === id) return sourceOf(entry);
|
|
} catch {
|
|
/* An unparseable entry cannot be the one being edited. */
|
|
}
|
|
}
|
|
return null;
|
|
}
|