update the workspace and skills flow

This commit is contained in:
2026-08-20 11:04:13 +05:30
parent d3f7f439f6
commit 161b237695
8 changed files with 1100 additions and 204 deletions

View File

@@ -798,6 +798,591 @@ for (const skill of reg.SKILLS) {
`${rehydrated.id} / ${rehydrated.name}`);
}
/* ── 8. Markdown ↔ manual equivalence ─────────────────────────────────────
The two authoring paths, asserted to produce one artefact.
A form that cannot express what the format can is not a shortcut to it: the
manual Owliver editor carried a single `source` shared by every selected
capability, so a capability chosen before a source was picked composed no
`responses:` block at all — and a definition whose capabilities all drop
registers, shows as active on its page, and contributes no chip. These check
that both doors compose the same definition, and that the one that cannot be
configured is refused rather than saved empty. */
console.log('\n── Markdown ↔ manual equivalence ──');
const dataResolver = await server.ssrLoadModule('/src/lib/skills/dataResolver.js');
const positionsContext = Object.entries(placement.PLACEMENT_ROUTES)
.find(([route]) => reg.pageKeyForRoute(route) === 'positions')?.[1];
/* The canonical model is what `parseSkill` makes of a definition. Comparing the
whole record would compare its Markdown too, which is the one thing the two
paths are allowed to differ on — so the runtime configuration is compared,
which is exactly what every consumer reads. */
const canonical = (skill) => JSON.parse(JSON.stringify({
id: skill.id,
name: skill.name,
description: skill.description,
status: skill.status,
pages: skill.pages,
kind: skill.kind,
triggers: skill.triggers,
ui: skill.ui,
owliver: skill.owliver,
}));
/* TEST A / B — the Board card, written both ways. */
const BOARD_MD = `---
id: board
name: Board
description: Helps Owliver understand, analyze, and act on the current task board.
pages:
- positions
status: active
ui:
type: card
placement: after-position-list-summary
title: Board
source: candidates.activity
periods:
- today
- last-7-days
- previous-month
---
# Board
`;
const boardManual = templates.uiSkillTemplate({
id: 'board',
name: 'Board',
description: 'Helps Owliver understand, analyze, and act on the current task board.',
pages: ['positions'],
type: 'card',
placement: 'after-position-list-summary',
title: 'Board',
source: 'candidates.activity',
periods: ['today', 'last-7-days', 'previous-month'],
});
record('TEST A: Markdown Board saves', reg.validateSkillSource(BOARD_MD) === null,
reg.validateSkillSource(BOARD_MD) || 'accepted');
record('TEST B: manual Board saves', reg.validateSkillSource(boardManual) === null,
reg.validateSkillSource(boardManual) || 'accepted');
const boardFromMd = reg.parseSkill(BOARD_MD, { custom: true });
const boardFromForm = reg.parseSkill(boardManual, { custom: true });
const boardSection = (skill) => Object.values(skill.ui || {}).flatMap((pg) => pg.sections || [])[0];
record('TEST A: one section, above the position list',
Object.values(boardFromMd.ui).flatMap((pg) => pg.sections).length === 1
&& boardSection(boardFromMd).placement === 'after-position-list-summary',
`${boardSection(boardFromMd).type}@${boardSection(boardFromMd).placement}`);
record('TEST A: the Board card contributes no Owliver capability',
boardFromMd.owliver.enabled === false && boardFromMd.owliver.capabilities.length === 0);
record('TEST A: no unresolvable section', reg.unresolvableSections(boardFromMd).length === 0,
reg.unresolvableSections(boardFromMd)[0]?.message || 'none');
record('TEST B: manual Board renders the identical section',
JSON.stringify(boardSection(boardFromForm)) === JSON.stringify(boardSection(boardFromMd)),
JSON.stringify(boardSection(boardFromForm)));
record('TEST B: manual Board keeps its title', boardSection(boardFromForm).title === 'Board',
String(boardSection(boardFromForm).title));
/* TEST C / D — the conversational skill, written both ways. */
const OWLIVER_MD = `---
id: owliver-conversation-test
name: Hiring Activity Test
description: Shows hiring activity from the Positions page.
pages:
- positions
status: active
triggers:
- hiring activity test
owliver:
enabled: true
suggestions:
- Show hiring activity
- Summarize hiring activity
capabilities:
- summary
responses:
summary:
source: candidates.activity
periods:
- today
- last-7-days
- previous-month
---
# Hiring Activity Test
`;
const owliverManual = templates.owliverSkillTemplate({
id: 'owliver-conversation-test',
name: 'Hiring Activity Test',
description: 'Shows hiring activity from the Positions page.',
pages: ['positions'],
suggestions: ['Show hiring activity', 'Summarize hiring activity'],
capabilities: ['summary'],
responses: {
summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] },
},
});
record('TEST C: Markdown Owliver skill saves', reg.validateSkillSource(OWLIVER_MD) === null,
reg.validateSkillSource(OWLIVER_MD) || 'accepted');
record('TEST D: manual Owliver skill saves', reg.validateSkillSource(owliverManual) === null,
reg.validateSkillSource(owliverManual) || 'accepted');
for (const [label, source] of [['TEST C', OWLIVER_MD], ['TEST D', owliverManual]]) {
const custom = [{ path: 'custom/owliver-conversation-test.md', raw: source }];
const skill = reg.parseSkill(source, { custom: true });
record(`${label}: the capability keeps its source`,
skill.owliver.responses.summary?.source === 'candidates.activity',
skill.owliver.responses.summary?.source || 'dropped');
record(`${label}: the capability keeps its periods`,
JSON.stringify(skill.owliver.responses.summary?.periods)
=== JSON.stringify(['today', 'last-7-days', 'previous-month']),
JSON.stringify(skill.owliver.responses.summary?.periods));
record(`${label}: it draws no page card`,
Object.keys(skill.ui).length === 0, `${Object.keys(skill.ui).length} pages`);
const chips = resolver.owliverSuggestions(positionsContext, [], custom, {});
const mine = chips.filter((c) => c.skillId === 'owliver-conversation-test');
record(`${label}: its suggestion reaches the panel`,
mine.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(chips.map((c) => c.label)));
record(`${label}: the suggestion answers without asking for a record`,
mine.every((c) => !c.deferred));
const owlSkills = resolver.owliverSkillsForContext(positionsContext, [], custom);
const matched = resolver.matchOwliverSkill('Show hiring activity', owlSkills);
record(`${label}: clicking it routes to this skill's summary`,
matched?.skill.id === 'owliver-conversation-test' && matched?.capability === 'summary' && matched?.exact,
`${matched?.skill.id}:${matched?.capability} exact=${matched?.exact}`);
const answered = resolver.resolveOwliverResponse({
skill: matched.skill,
capability: matched.capability,
question: 'Show hiring activity',
context: {
positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }],
applications: [{ id: 'a1', job_posting_id: 'p1', created_date: new Date().toISOString() }],
},
now: new Date(),
});
record(`${label}: it answers with the configured reading`,
answered?.missing === null && resolver.summaryLines(answered.data).length === 3,
JSON.stringify(resolver.summaryLines(answered?.data || {})));
}
/* TEST H — the same skill through both doors, normalized, compared. */
record('TEST H: Markdown and manual normalize identically (Board)',
JSON.stringify(canonical(boardFromMd)) === JSON.stringify(canonical(boardFromForm)),
JSON.stringify(canonical(boardFromForm)).slice(0, 120));
record('TEST H: Markdown and manual normalize identically (Owliver)',
JSON.stringify(canonical(reg.parseSkill(OWLIVER_MD, { custom: true })))
=== JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))),
JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))).slice(0, 120));
/* TEST E — a capability with no source is refused, never saved empty. */
const noSourceFields = {
id: 'no-source-test',
name: 'No Source Test',
description: 'A capability with nothing to read.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
capabilities: ['summary'],
responses: {},
};
const noSource = templates.owliverSkillTemplate(noSourceFields);
record('TEST E: an unconfigured capability is named by the form',
JSON.stringify(fields.unconfiguredCapabilities(noSourceFields)) === '["summary"]',
JSON.stringify(fields.unconfiguredCapabilities(noSourceFields)));
record('TEST E: and the save is refused',
/owliver\.responses\.summary/.test(reg.validateSkillSource(noSource) || ''),
reg.validateSkillSource(noSource) || 'ACCEPTED — a skill with no capability would register');
record('TEST E: the refused definition would have had zero capabilities',
reg.parseSkill(noSource, { custom: true }).owliver.capabilities.length === 0,
'which is why it is refused rather than stored');
/* TEST F — two capabilities, two different sources, neither inheriting. */
const twoSources = templates.owliverSkillTemplate({
id: 'two-source-test',
name: 'Two Source Test',
description: 'Two capabilities reading two sources.',
pages: ['positions'],
suggestions: ['Summarize workspace applications', 'List the open roles'],
capabilities: ['summary', 'list'],
responses: {
summary: { source: 'candidates.activity', periods: ['today'] },
list: { source: 'positions.demand', limit: 5 },
},
});
const twoSkill = reg.parseSkill(twoSources, { custom: true });
record('TEST F: manual save accepted', reg.validateSkillSource(twoSources) === null,
reg.validateSkillSource(twoSources) || 'accepted');
record('TEST F: each capability keeps its own source',
twoSkill.owliver.responses.summary?.source === 'candidates.activity'
&& twoSkill.owliver.responses.list?.source === 'positions.demand',
`summary→${twoSkill.owliver.responses.summary?.source}, list→${twoSkill.owliver.responses.list?.source}`);
record('TEST F: neither inherits the other\'s options',
JSON.stringify(twoSkill.owliver.responses.summary.periods) === '["today"]'
&& twoSkill.owliver.responses.list.limit === 5
&& twoSkill.owliver.responses.list.periods.length === 0,
`summary periods=${JSON.stringify(twoSkill.owliver.responses.summary.periods)}, list limit=${twoSkill.owliver.responses.list.limit}`);
record('TEST F: both are offered as suggestions',
resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/two.md', raw: twoSources }], {})
.filter((c) => c.skillId === 'two-source-test').length === 2);
/* TEST G — editing one field leaves every other configuration alone. */
const edited = fields.patchFrontmatter(
twoSources,
fields.owliverPatch(
{ ...fields.owliverFieldsFromSource(twoSources), description: 'A new description.' },
{ existing: twoSources }
)
);
const editedSkill = reg.parseSkill(edited, { custom: true });
record('TEST G: the edit lands', editedSkill.description === 'A new description.', editedSkill.description);
record('TEST G: every capability source survives the edit',
JSON.stringify(canonical(editedSkill).owliver.responses)
=== JSON.stringify(canonical(twoSkill).owliver.responses),
`summary→${editedSkill.owliver.responses.summary?.source}, list→${editedSkill.owliver.responses.list?.source}`);
record('TEST G: suggestions survive the edit',
JSON.stringify(editedSkill.owliver.suggestions) === JSON.stringify(twoSkill.owliver.suggestions));
const boardEdited = fields.patchFrontmatter(
boardManual,
fields.boardPatch(
{ ...fields.boardFieldsFromSource(boardManual), description: 'A new description.' },
{ existing: boardManual }
)
);
record('TEST G: Board UI configuration survives the edit',
JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true })))
=== JSON.stringify(boardSection(boardFromForm)),
JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true }))));
/* TEST 6 — one source/shape resolver, shared by the pickers and the validator. */
record('shape compatibility: the resolver refuses what the normalizer refuses',
surfaces.sourcesForShape('list').every((src) => surfaces.sourceSupportsShape(src.id, 'list'))
&& !surfaces.sourcesForShape('list').some((src) => src.id === 'candidates.activity'),
`list sources: ${surfaces.sourcesForShape('list').map((s) => s.id).join(', ')}`);
record('shape compatibility: a list against candidates.activity is refused on save',
/cannot be shown as/.test(reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'bad-shape-test',
name: 'Bad Shape Test',
description: 'A list of something with no list in it.',
pages: ['positions'],
capabilities: ['list'],
responses: { list: { source: 'candidates.activity' } },
})) || ''),
'the picker cannot offer it, and the save refuses it');
record('summary is prose, so every source suits it',
surfaces.shapeForCapability('summary') === null
&& surfaces.sourcesForShape(null).length === surfaces.DATA_SOURCES.length);
/* ── 9. The reported failure, reproduced end to end ───────────────────────── */
console.log('\n── Regression: manual Owliver skill on Positions ──');
/* Exactly the configuration from the report: Pages = Positions, one suggestion,
Summary, `candidates.activity`, three periods — composed the way the form
composes it, then read the way the panel reads it. */
const REGRESSION = templates.owliverSkillTemplate({
id: 'owliver-conversation-test',
name: 'Hiring Activity Test',
description: 'Shows hiring activity from the Positions page.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
capabilities: ['summary'],
responses: {
summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] },
},
});
const stored = [{ path: 'custom/owliver-conversation-test.md', raw: REGRESSION }];
record('the manual definition saves', reg.validateSkillSource(REGRESSION) === null,
reg.validateSkillSource(REGRESSION) || 'accepted');
record('it registers', reg.allSkills(stored).some((s) => s.id === 'owliver-conversation-test'));
record('it is attached to Positions',
reg.skillsForContext(positionsContext, [], stored).some((s) => s.id === 'owliver-conversation-test'));
record('it keeps a capability — the step that used to empty',
reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities.length === 1,
JSON.stringify(reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities));
record('it reaches owliverSkillsForContext',
resolver.owliverSkillsForContext(positionsContext, [], stored)
.some((s) => s.id === 'owliver-conversation-test'));
const regressionChips = resolver.owliverSuggestions(positionsContext, [], stored, {});
record('"Show hiring activity" is offered as a chip',
regressionChips.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(regressionChips.map((c) => c.label)));
const clicked = resolver.matchOwliverSkill(
'Show hiring activity',
resolver.owliverSkillsForContext(positionsContext, [], stored)
);
record('clicking it invokes this skill\'s summary',
clicked?.skill.id === 'owliver-conversation-test' && clicked?.capability === 'summary',
`${clicked?.skill.id}:${clicked?.capability}`);
const now = new Date();
const answer = resolver.resolveOwliverResponse({
skill: clicked.skill,
capability: clicked.capability,
question: 'Show hiring activity',
context: {
positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }],
applications: [
{ id: 'a1', job_posting_id: 'p1', created_date: now.toISOString() },
{ id: 'a2', job_posting_id: 'p1', created_date: new Date(now - 3 * 86400000).toISOString() },
],
},
now,
});
record('it answers with the configured source and periods',
answer?.missing === null && answer.section.source === 'candidates.activity'
&& answer.section.periods.length === 3,
`${answer?.section.source} over ${JSON.stringify(answer?.section.periods)}`);
record('the answer carries real figures',
resolver.summaryLines(answer.data).length === 3,
JSON.stringify(resolver.summaryLines(answer.data)));
record('and no `position.activity needs a position` is possible',
dataResolver.resolveSkillData(answer.section, { applications: [], positions: [] }, now).unavailable !== true,
'the source declares no context requirement');
/* Reload: what is stored is the Markdown, so the round trip through storage is
the round trip through the parser. */
const reloaded = reg.parseSkill(
fields.patchFrontmatter(REGRESSION, {}),
{ custom: true }
);
record('the source survives a reload',
reloaded.owliver.responses.summary?.source === 'candidates.activity',
reloaded.owliver.responses.summary?.source || 'lost');
record('the form reads it back unchanged',
JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses)
=== JSON.stringify({ summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'], limit: null } }),
JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses));
/* ── 10. Storage round trip ───────────────────────────────────────────────
The editors do not hand the registry a record; they write Markdown into
`preferences.customSkills`, which `base44Client` persists to localStorage and
reads back on the next load. "The source disappeared after reload" is a claim
about *that* path, so it is exercised here rather than assumed: the same
module the app runs, against a localStorage the same shape the browser's is. */
console.log('\n── Storage round trip ──');
const store = new Map();
globalThis.localStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
const { base44 } = await server.ssrLoadModule('/src/api/base44Client.js');
const custom = await server.ssrLoadModule('/src/lib/skills/customSkills.js');
/* Saving is what the editor's Save button does: compose, validate, upsert,
write the preference. */
const saved = custom.upsertCustomSkill(base44.auth.preferences().customSkills || [], REGRESSION);
const written = await base44.auth.updatePreferences({ customSkills: saved.next });
record('the definition is persisted', written.persisted, written.error?.message || 'written to storage');
/* Reload: a fresh read of the same key, exactly as a page load does. */
const rehydratedSkills = base44.auth.preferences().customSkills || [];
record('it survives the reload', rehydratedSkills.length === 1
&& String(rehydratedSkills[0].raw).includes('candidates.activity'),
`${rehydratedSkills.length} stored`);
const afterReload = reg.parseSkill(rehydratedSkills[0].raw, { custom: true });
record('its capability survives the reload',
afterReload.owliver.responses.summary?.source === 'candidates.activity',
afterReload.owliver.responses.summary?.source || 'lost');
record('and it still offers its chip after the reload',
resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {})
.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {}).map((c) => c.label)));
record('the editor reopens it with its source intact',
fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses.summary?.source === 'candidates.activity',
JSON.stringify(fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses));
/* The Board skill through the same path, stored beside it — the two subsystems
are independent, so both must survive one write. */
const bothStored = custom.upsertCustomSkill(rehydratedSkills, boardManual).next;
await base44.auth.updatePreferences({ customSkills: bothStored });
const finalStored = base44.auth.preferences().customSkills || [];
const finalRegistry = reg.readSkillRegistry(finalStored);
record('Board and Owliver skills coexist in storage', finalStored.length === 2,
`${finalStored.length} stored`);
record('neither produces a registry diagnostic',
!finalRegistry.diagnostics.some((d) => d.level === 'error'),
finalRegistry.diagnostics.filter((d) => d.level === 'error').map((d) => d.message).join(' | ') || 'clean');
record('the Board card draws once on Positions, and offers no chip',
reg.getSkillsForPage('positions', { customSources: finalStored })
.filter((sk) => sk.id === 'board')
.every((sk) => Object.values(sk.ui).flatMap((pg) => pg.sections).length === 1
&& sk.owliver.capabilities.length === 0));
record('the Owliver skill offers its chip, and draws no card',
resolver.owliverSuggestions(positionsContext, [], finalStored, {})
.some((c) => c.skillId === 'owliver-conversation-test')
&& Object.keys(reg.parseSkill(REGRESSION, { custom: true }).ui).length === 0);
/* ── 11. A fresh Add Owliver Skill screen ─────────────────────────────────
What the form starts as, asserted at the state that drives the checkbox
rather than at the checkbox. `checked` is
`draft.capabilities.includes(capability.id)` and the per-capability
configuration is rendered under `{checked && …}`, so the array below is the
whole of "what is ticked" and "what is shown". */
console.log('\n── Fresh Add Owliver Skill ──');
const CAPS = surfaces.OWLIVER_CAPABILITIES.map((c) => c.id);
/* 1. NEW skill. `emptyDraft` is `EMPTY_OWLIVER_FIELDS`, which is what the
editor initializes to when there is no `:id` and nothing handed over. */
record('new skill: initial capabilities are []',
Array.isArray(fields.EMPTY_OWLIVER_FIELDS.capabilities)
&& fields.EMPTY_OWLIVER_FIELDS.capabilities.length === 0,
JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.capabilities));
record('new skill: no response is pre-configured either',
Object.keys(fields.EMPTY_OWLIVER_FIELDS.responses).length === 0,
JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.responses));
record(`new skill: all ${CAPS.length} capability checkboxes start unchecked`,
CAPS.every((id) => !fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)),
CAPS.filter((id) => fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)).join(', ') || 'none ticked');
/* The template the fresh screen composes must not declare capabilities either:
an author who types a name before ticking anything patches *that* file, and a
`capabilities:` key in it would tick boxes nobody chose. */
const freshTemplate = templates.owliverSkillTemplate({});
record('new skill: the composed template declares no capabilities',
!/^\s*capabilities:/m.test(freshTemplate),
freshTemplate.split('\n').filter((l) => /capabilit/i.test(l)).join(' / ') || 'no capabilities key');
record('new skill: reading that template back still yields []',
fields.owliverFieldsFromSource(freshTemplate).capabilities.length === 0,
JSON.stringify(fields.owliverFieldsFromSource(freshTemplate).capabilities));
/* 2. One capability, chosen. The editor's `update({ capabilities })` is a plain
state merge, so the assertion is on what that array then means. */
const pickedSummary = { ...fields.EMPTY_OWLIVER_FIELDS, capabilities: ['summary'] };
record('selecting Summary ticks exactly Summary',
JSON.stringify(pickedSummary.capabilities) === '["summary"]');
record('and only Summary renders a configuration block',
CAPS.filter((id) => pickedSummary.capabilities.includes(id)).length === 1,
CAPS.filter((id) => pickedSummary.capabilities.includes(id)).join(', '));
record('an unsourced Summary is invalid, and only Summary is reported',
JSON.stringify(fields.unconfiguredCapabilities(pickedSummary)) === '["summary"]',
JSON.stringify(fields.unconfiguredCapabilities(pickedSummary)));
/* 3. Choosing its source clears the error, and the definition saves. */
const sourcedSummary = {
...pickedSummary,
id: 'fresh-summary-test',
name: 'Fresh Summary Test',
description: 'One capability, configured.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
responses: { summary: { source: 'candidates.activity', periods: ['today'] } },
};
record('choosing a source clears the validation error',
fields.unconfiguredCapabilities(sourcedSummary).length === 0,
JSON.stringify(fields.unconfiguredCapabilities(sourcedSummary)));
const freshSaved = templates.owliverSkillTemplate(sourcedSummary);
record('and the save succeeds', reg.validateSkillSource(freshSaved) === null,
reg.validateSkillSource(freshSaved) || 'accepted');
record('the saved definition declares only the chosen capability',
JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities) === '["summary"]',
JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities));
record('and its suggestion reaches the Positions panel',
resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/fresh.md', raw: freshSaved }], {})
.some((c) => c.skillId === 'fresh-summary-test'));
/* 4. Several, chosen by hand — only those, each validated on its own. */
const pickedTwo = {
...fields.EMPTY_OWLIVER_FIELDS,
capabilities: ['summary', 'list'],
responses: { summary: { source: 'candidates.activity' } },
};
record('selecting two capabilities renders exactly those two',
CAPS.filter((id) => pickedTwo.capabilities.includes(id)).join(',') === 'summary,list');
record('and only the unconfigured one is reported',
JSON.stringify(fields.unconfiguredCapabilities(pickedTwo)) === '["list"]',
JSON.stringify(fields.unconfiguredCapabilities(pickedTwo)));
/* 5. An EXISTING skill hydrates from its own definition, and from nothing else.
Including the capability that did *not* resolve: it is the one the author
opened the editor to fix, and dropping it from the form would delete it
from the file on the next save. */
const DECLARED_ONE = `---
id: declared-one
name: Declared One
description: Declares a single capability.
pages:
- positions
status: active
owliver:
enabled: true
suggestions:
- Show hiring activity
capabilities:
- summary
responses:
summary:
source: candidates.activity
---
# Declared One
`;
record('existing skill: hydrates exactly its declared capability',
JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities) === '["summary"]',
JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities));
record('existing skill: no new-skill default is applied on top',
CAPS.filter((id) => fields.owliverFieldsFromSource(DECLARED_ONE).capabilities.includes(id)).length === 1);
const HALF_BROKEN = DECLARED_ONE
.replace(' - summary\n', ' - summary\n - list\n')
.replace('id: declared-one', 'id: half-broken');
const halfFields = fields.owliverFieldsFromSource(HALF_BROKEN);
record('existing skill: a declared capability that lost its source still shows',
JSON.stringify(halfFields.capabilities) === '["summary","list"]',
JSON.stringify(halfFields.capabilities));
record('...unsourced, so it stays invalid rather than silently dropped',
JSON.stringify(fields.unconfiguredCapabilities(halfFields)) === '["list"]',
JSON.stringify(fields.unconfiguredCapabilities(halfFields)));
record('...and the configured one keeps its source through the round trip',
halfFields.responses.summary.source === 'candidates.activity',
halfFields.responses.summary.source);
record('...while the runtime still registers only what can answer',
JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities) === '["summary"]',
JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities));
/* A definition inheriting its reading from a `ui:` section still hydrates the
capability it actually offers — the union is declaration-first, not
declaration-only. */
const INHERITED = `---
id: inherited-cap
name: Inherited Cap
description: A capability reading the page section.
pages:
- positions
status: active
ui:
type: card
placement: after-position-list-summary
source: candidates.activity
owliver:
enabled: true
capabilities:
- card
---
# Inherited Cap
`;
record('a capability inheriting from `ui:` still hydrates with its source',
fields.owliverFieldsFromSource(INHERITED).responses.card?.source === 'candidates.activity',
fields.owliverFieldsFromSource(INHERITED).responses.card?.source || 'lost');
await server.close();
/* ── 8. Production bundle ─────────────────────────────────────────────────── */