update position page design
This commit is contained in:
@@ -91,6 +91,43 @@ function sectionSteps(body, heading) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* The prose under a `## Heading`, with its bullets and blank lines stripped to
|
||||
* one line of summary. Used for a level's description, which is a sentence
|
||||
* rather than a list.
|
||||
*/
|
||||
function sectionText(body, heading) {
|
||||
const section = new RegExp(`##\\s+${heading}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, 'i').exec(body);
|
||||
if (!section) return '';
|
||||
return section[1]
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/^\s*[-*]\s+/, '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* The rungs a workforce skill defines, read from its own body.
|
||||
*
|
||||
* A definition names its ladder as `## Beginner`, `## Intermediate` and so on,
|
||||
* each followed by what a person must be able to do at that level. Only the
|
||||
* headings that are actually present become rungs, so a skill that tops out at
|
||||
* Advanced has a three-rung ladder rather than a fourth empty one — the ladder
|
||||
* is what the author wrote, not a fixed shape they are padded into.
|
||||
*/
|
||||
const LEVEL_HEADINGS = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
|
||||
|
||||
function sectionLevels(body) {
|
||||
return LEVEL_HEADINGS
|
||||
.map((heading) => ({
|
||||
level: heading.toLowerCase(),
|
||||
label: heading,
|
||||
summary: sectionText(body, heading),
|
||||
}))
|
||||
.filter((rung) => rung.summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* The page key a route belongs to — `/admin/positions` → `positions`.
|
||||
*
|
||||
@@ -128,12 +165,44 @@ export function parseSkill(raw, { path = 'custom', custom = false } = {}) {
|
||||
{
|
||||
const { data, body } = parseFrontmatter(raw);
|
||||
const pages = Array.isArray(data.pages) ? data.pages : [];
|
||||
const id = data.id || path.split('/').pop().replace(/\.md$/, '');
|
||||
const levels = sectionLevels(body);
|
||||
|
||||
/**
|
||||
* Two things wear the same format.
|
||||
*
|
||||
* An *assistant* skill teaches Owliver to do something on a page. A
|
||||
* *workforce* skill defines a capability the workforce can hold, at levels,
|
||||
* and says which pages may surface it. A definition that names a ladder is
|
||||
* the second kind — nothing else distinguishes them, so an author declares a
|
||||
* workforce skill by writing one, not by setting a flag.
|
||||
*/
|
||||
const kind = data.kind || (levels.length ? 'workforce' : 'assistant');
|
||||
|
||||
return {
|
||||
id: data.id || path.split('/').pop().replace(/\.md$/, ''),
|
||||
id,
|
||||
kind,
|
||||
name: data.name || 'Untitled skill',
|
||||
description: data.description || '',
|
||||
status: data.status === 'inactive' ? 'inactive' : 'active',
|
||||
pages,
|
||||
/**
|
||||
* The capability in the skill graph this definition governs.
|
||||
*
|
||||
* Declared as `skill:`, or inferred by dropping a `-training` suffix and
|
||||
* swapping dashes for underscores — so `server-training.md` governs
|
||||
* `server` and `customer-service-training.md` governs `customer_service`
|
||||
* without the author restating it. Only meaningful for workforce skills.
|
||||
*/
|
||||
skillId: kind === 'workforce'
|
||||
? (data.skill || id.replace(/-training$/, '')).replace(/-/g, '_')
|
||||
: null,
|
||||
/* The ladder, in order, each rung carrying what it means to hold it. */
|
||||
levels,
|
||||
/* The definition as written. Forge edits this; every other page reads the
|
||||
parsed form, so there is exactly one artefact behind all of them. */
|
||||
markdown: raw,
|
||||
source: custom ? 'account' : 'repository',
|
||||
/* The page label Settings shows, taken from the context the page carries
|
||||
so the two never disagree. */
|
||||
actions: Array.isArray(data.actions) ? data.actions : [],
|
||||
@@ -214,18 +283,42 @@ export function contextIdsForSkill(skill) {
|
||||
return skill.pages.map((key) => ROUTE_BY_PAGE_KEY[key]?.contextId).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every skill attached to a page — the one answer to "what belongs here".
|
||||
*
|
||||
* This is what `pages:` is for. A definition listing `positions` is not
|
||||
* documenting itself; it is saying that the Positions experience may surface it,
|
||||
* and this function is the only place that question is answered. Pages call it
|
||||
* and render what comes back, which is what lets a skill authored tomorrow
|
||||
* appear on the right pages tonight without any page component being edited.
|
||||
*
|
||||
* Nothing downstream may test a page name against a skill id. The moment a page
|
||||
* asks "is this Server Training?" the attachment has stopped being data.
|
||||
*/
|
||||
export function getSkillsForPage(pageId, { disabled = [], customSources = [], kind } = {}) {
|
||||
if (!pageId) return [];
|
||||
return allSkills(customSources).filter(
|
||||
(s) => s.status === 'active'
|
||||
&& !disabled.includes(s.id)
|
||||
&& s.pages.includes(pageId)
|
||||
&& (!kind || s.kind === kind)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skills available on a page.
|
||||
*
|
||||
* `disabled` is the account's list of switched-off skill ids, so a skill can be
|
||||
* turned off from Settings without being deleted from disk.
|
||||
*
|
||||
* Assistant skills only: a workforce skill defines a capability, not something
|
||||
* Owliver can be asked to do, and offering one as a chat action would promise
|
||||
* behaviour that does not exist.
|
||||
*/
|
||||
export function skillsForContext(contextId, disabled = [], customSources = []) {
|
||||
const pageKey = PAGE_KEY_BY_CONTEXT[contextId];
|
||||
if (!pageKey) return [];
|
||||
return allSkills(customSources).filter(
|
||||
(s) => s.status === 'active' && !disabled.includes(s.id) && s.pages.includes(pageKey)
|
||||
);
|
||||
return getSkillsForPage(pageKey, { disabled, customSources, kind: 'assistant' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user