Files
doormilxpress_astryx/scripts/skill-check.mjs
2026-08-19 17:36:27 +05:30

836 lines
34 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Skill pipeline check — Owliver skills and Board (UI) skills.
*
* Runs the *real* module graph through Vite, so `import.meta.glob`, the `@/`
* alias and the Markdown loading all behave exactly as they do in the app. That
* matters: the reliability problem this script exists for was never in the
* Markdown, it was in what the pipeline did with a definition it could only
* partly read, and a mock of that pipeline would have reproduced none of it.
*
* node scripts/skill-check.mjs # against source, as the dev server sees it
* node scripts/skill-check.mjs --dist # also assert the built bundle carries every definition
*
* Exits non-zero on failure, so it can gate a build.
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'vite';
const ROOT = process.cwd();
const results = [];
const record = (name, pass, detail = '') => {
results.push({ name, pass, detail });
const mark = pass ? ' ok ' : ' FAIL ';
console.log(`[${mark}] ${name}${detail ? ` — ${detail}` : ''}`);
};
const server = await createServer({
root: ROOT,
server: { middlewareMode: true },
appType: 'custom',
logLevel: 'error',
});
const reg = await server.ssrLoadModule('/src/lib/skills/registry.js');
const placement = await server.ssrLoadModule('/src/components/ai-assistant/placement.js');
const contextFor = (pageKey) => Object.entries(placement.PLACEMENT_ROUTES)
.find(([route]) => reg.pageKeyForRoute(route) === pageKey)?.[1] ?? null;
/* ── 1. Discovery ─────────────────────────────────────────────────────────── */
console.log('\n── Discovery ──');
const onDisk = readdirSync(join(ROOT, 'src/skills'))
.flatMap((dir) => readdirSync(join(ROOT, 'src/skills', dir)).map((f) => `${dir}/${f}`))
.filter((f) => f.endsWith('.md'));
record(
'every .md under src/skills registers',
reg.SKILLS.length === onDisk.length,
`${reg.SKILLS.length} registered / ${onDisk.length} files`
);
record(
'no duplicate skill ids',
new Set(reg.SKILLS.map((s) => s.id)).size === reg.SKILLS.length,
`${new Set(reg.SKILLS.map((s) => s.id)).size} unique ids`
);
record(
'every skill declares at least one page',
reg.SKILLS.every((s) => s.pages.length > 0)
);
/* Both managed lists are non-empty — the Workspace → Skills tabs. */
const uiFacet = reg.skillsWithFacet(reg.SKILLS, 'ui');
const owliverFacet = reg.skillsWithFacet(reg.SKILLS, 'owliver');
record('Owliver Skills list is populated', owliverFacet.length > 0, `${owliverFacet.length} skills`);
record(
'Board Skills list resolves without error',
Array.isArray(uiFacet),
`${uiFacet.length} skills declare a \`ui:\` block`
);
/* ── 2. Registration integrity: nothing half-loads in silence ─────────────── */
console.log('\n── Registration integrity ──');
const clean = reg.readSkillRegistry([]);
record(
'shipped registry reports no diagnostics',
clean.diagnostics.length === 0,
clean.diagnostics.map((d) => d.message).join(' | ') || 'none'
);
record(
'every shipped skill registered every capability it declared',
reg.SKILLS.every((s) => !(s.owliverErrors?.length || s.uiErrors?.length))
);
/* An unreadable stored definition must be REPORTED, not swallowed. */
const brokenYaml = `---\nid: broken\n name: bad indent\n---\n# Broken\n`;
const withBroken = reg.readSkillRegistry([{ path: 'custom/broken.md', raw: brokenYaml }]);
record(
'unreadable stored skill is reported, not silently dropped',
withBroken.diagnostics.some((d) => d.kind === 'unreadable'),
withBroken.diagnostics.find((d) => d.kind === 'unreadable')?.message ?? 'NO DIAGNOSTIC'
);
/* A definition with one bad capability keeps the good one AND says so. */
const partial = `---
id: partial-check
name: Partial Check
description: One resolvable capability and one that names no source.
pages:
- positions
status: active
owliver:
enabled: true
capabilities:
- summary
- flow
responses:
summary:
source: position.activity
periods:
- today
---
# Partial Check
`;
const withPartial = reg.readSkillRegistry([{ path: 'custom/partial.md', raw: partial }]);
const partialSkill = withPartial.skills.find((s) => s.id === 'partial-check');
record(
'partly-resolvable skill still registers its good capability',
partialSkill?.owliver.capabilities.includes('summary'),
`capabilities = ${JSON.stringify(partialSkill?.owliver.capabilities)}`
);
record(
'and the capability it LOST is reported',
withPartial.diagnostics.some((d) => d.kind === 'incomplete' && d.skillId === 'partial-check'),
withPartial.diagnostics.find((d) => d.kind === 'incomplete')?.message ?? 'NO DIAGNOSTIC'
);
/* Shadowing a built-in is allowed, but must be visible. */
const shadow = `---\nid: create-position\nname: Shadow\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n# Shadow\n`;
const withShadow = reg.readSkillRegistry([{ path: 'custom/shadow.md', raw: shadow }]);
record(
'a custom skill overriding a built-in is reported',
withShadow.diagnostics.some((d) => d.kind === 'shadowed' && d.skillId === 'create-position'),
withShadow.diagnostics.find((d) => d.kind === 'shadowed')?.message ?? 'NO DIAGNOSTIC'
);
/* ── 3. Markdown parsing: no silent content loss ──────────────────────────── */
console.log('\n── Markdown parsing ──');
const cp = reg.SKILLS.find((s) => s.id === 'create-position');
record(
'wrapped bullets are read whole',
cp.capabilities.some((c) => c.endsWith('out of a single sentence.')),
cp.capabilities.find((c) => c.includes('single sentence')) ?? 'TRUNCATED'
);
record(
'conversation steps all parse',
cp.conversation.length === 7 && cp.conversation.every((s) => s.field && s.question),
`${cp.conversation.length} steps`
);
const numbered = reg.parseSkill(
`---\nid: numbered\nname: Numbered\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n1. First instruction.\n2. Second instruction.\n3. Third instruction.\n`,
{ custom: true }
);
record(
'numbered instructions are preserved',
numbered.capabilities.length === 3,
JSON.stringify(numbered.capabilities)
);
/* A section ends at the next `##`, and a heading that merely starts with the
same word is a different heading. Both halves matter: the first is what stops
`Capabilities` swallowing the rest of the file, the second is what stops a
near-miss heading being read as the real one. */
const bounded = reg.parseSkill(
`---\nid: bounded\nname: Bounded\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n- one\n- two\n\n## Capabilities (v2)\n\n- three\n\n## Actions\n\n- four\n`,
{ custom: true }
);
record(
'a section stops at the next heading',
bounded.capabilities.length === 2,
JSON.stringify(bounded.capabilities)
);
/* Sub-headings (`###`) belong to the section they sit under, and must not end
it — `\n##\s` would match `### ` if the space were not required. */
const nested = reg.parseSkill(
`---\nid: nested\nname: Nested\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n- one\n\n### Detail\n\n- two\n\n## Actions\n\n- three\n`,
{ custom: true }
);
record(
'a `###` sub-heading does not truncate its section',
nested.capabilities.length === 2,
JSON.stringify(nested.capabilities)
);
/* Frontmatter of every shipped file parses and carries its required fields. */
let frontmatterOk = true;
const frontmatterDetail = [];
for (const s of reg.SKILLS) {
if (!s.id || !s.name || !s.pages.length) {
frontmatterOk = false;
frontmatterDetail.push(s.path);
}
}
record('every shipped skill has id, name and pages', frontmatterOk, frontmatterDetail.join(', '));
/* ── 4. Trigger / matching ────────────────────────────────────────────────── */
console.log('\n── Trigger matching ──');
const positions = contextFor('positions');
record('positions context resolves', Boolean(positions), String(positions));
const MATRIX = [
['create a position', 'create-position'],
['i want to hire', 'create-position'],
['post a job', 'create-position'],
['add a client', 'create-position'],
['show hiring activity', 'hiring-activity-assistant'],
['hiring flow', 'hiring-activity-assistant'],
['applications over time', 'hiring-activity-assistant'],
];
for (const [question, expected] of MATRIX) {
const got = reg.matchSkill(question, positions)?.id ?? null;
record(`trigger "${question}" -> ${expected}`, got === expected, got ?? 'no match');
}
/* Every claimant is enumerable, so a contested phrase is not invisible. */
const contender = `---
id: zz-contender
name: ZZ Contender
description: Also claims a phrase create-position owns.
pages:
- positions
status: active
triggers:
- create a position
owliver:
enabled: true
capabilities:
- summary
responses:
summary:
source: position.activity
periods:
- today
---
# ZZ Contender
`;
const contested = [{ path: 'custom/zz.md', raw: contender }];
const claimants = reg.matchSkills('create a position', positions, [], contested);
record(
'all trigger claimants are enumerable',
claimants.length === 2,
claimants.map((s) => s.id).join(', ')
);
record(
'a contested trigger is reported as a collision',
reg.readSkillRegistry(contested).diagnostics.some((d) => d.kind === 'trigger-collision'),
reg.readSkillRegistry(contested).diagnostics.find((d) => d.kind === 'trigger-collision')?.message ?? 'NO DIAGNOSTIC'
);
/* ── 5. Owliver suggestions ───────────────────────────────────────────────── */
console.log('\n── Suggestions ──');
const resolver = await server.ssrLoadModule('/src/lib/skills/owliverResolver.js');
const declared = resolver.owliverSuggestions(positions, [], [], {});
record(
'every declared suggestion names a capability the skill offers',
declared.every((c) => !c.skillCapability
|| reg.SKILLS.find((s) => s.id === c.skillId)?.owliver.capabilities.includes(c.skillCapability))
);
const byIntent = declared.map((c) => `${c.skillId}:${c.skillCapability}`);
record(
'no two declared suggestions resolve to one capability',
new Set(byIntent).size === byIntent.length,
byIntent.join(' | ')
);
const dynamic = await server.ssrLoadModule('/src/components/ai-assistant/dynamic.js');
/* The generator is exercised at three different data shapes: the labels must
track the data, and must never appear when the count behind them is zero. */
const factsFor = (n) => ({
postings: [], starvedPositions: new Array(n).fill({}), unscreened: new Array(n).fill({}),
scored: [], stalled: [], ranked: [], activity: [], missingCredentials: [],
});
const shapes = [0, 1, 7, 12];
let labelsOk = true;
const offenders = [];
for (const n of shapes) {
for (const chip of dynamic.buildPrompts('admin.positions', factsFor(n), null)) {
/* A complete intent asks or instructs. A raw fragment — "4 ready for
interview" — does neither, and is what this check exists to catch. */
const isIntent = /\?$/.test(chip.label) || /^(show|summari[sz]e|create|continue|compare|find|explain|what|which|who)\b/i.test(chip.label);
if (!isIntent) { labelsOk = false; offenders.push(`n=${n}: "${chip.label}"`); }
if (n === 0 && /\b0\b/.test(chip.label)) { labelsOk = false; offenders.push(`n=0 leaked a zero count: "${chip.label}"`); }
}
}
record('every generated label is a complete intent', labelsOk, offenders.join(' ; ') || 'all labels are intents');
/* Counts must move with the data rather than being baked in. */
const at7 = dynamic.buildPrompts('admin.positions', factsFor(7), null).map((c) => c.label);
const at12 = dynamic.buildPrompts('admin.positions', factsFor(12), null).map((c) => c.label);
record(
'labels are dynamic, not hardcoded',
at7.some((l) => l.includes('7')) && at12.some((l) => l.includes('12')) && !at7.some((l) => l.includes('12')),
`7 -> ${at7.find((l) => l.includes('7'))} | 12 -> ${at12.find((l) => l.includes('12'))}`
);
const atZero = dynamic.buildPrompts('admin.positions', factsFor(0), null).map((c) => c.label);
record(
'no count-bearing suggestion when the count is zero',
!atZero.some((l) => /\d/.test(l)),
atZero.join(' | ')
);
/**
* The workforce branch — the one the fragments came from.
*
* "4 ready for interview" and "2 strong for Event Server" were generated here,
* so a suggestion check that passes `null` for workforce proves nothing about
* them. Applications are dated today so the "applied today" branch is live too.
*/
const workforceAt = (readyCount) => {
const position = { id: 'p1', title: 'Event Server – Fine Dining', status: 'active', headcount: 5 };
const today = new Date();
const applications = Array.from({ length: readyCount }, (_, i) => ({
id: `a${i}`,
job_posting_id: 'p1',
email: `c${i}@example.com`,
applicant_name: `Candidate ${i}`,
status: i % 2 ? 'ai_screened' : 'shortlisted',
ai_score: 90,
created_date: today.toISOString(),
updated_date: today.toISOString(),
}));
return {
positions: [position],
currentPositionId: null,
context: { applications, today, profiles: [], staff: [], assignments: [] },
};
};
for (const n of [1, 4, 9]) {
const labels = dynamic
.buildPrompts('admin.positions', factsFor(2), workforceAt(n))
.map((c) => c.label);
const ready = labels.find((l) => /ready for interview/i.test(l));
record(
`workforce branch at ${n}: "ready for interview" is a complete question`,
Boolean(ready) && /\?$/.test(ready) && ready.includes(String(n)) && /^Which\b/.test(ready),
ready ?? 'NOT GENERATED'
);
/* Singular/plural has to track the count, or the fix trades one awkward
label for another. */
if (n === 1) {
record(
'a single candidate reads as one candidate',
ready === 'Which 1 candidate is ready for interview?',
ready ?? 'NOT GENERATED'
);
}
const fragment = labels.find((l) => /^\d+\s/.test(l));
record(
`workforce branch at ${n}: no bare status fragments`,
!fragment,
fragment ?? 'none'
);
}
/* ── 6. Authoring: what the editors save, and what they refuse ────────────── */
console.log('\n── Authoring ──');
const fields = await server.ssrLoadModule('/src/lib/skills/skillFields.js');
const templates = await server.ssrLoadModule('/src/lib/skills/customSkills.js');
const surfaces = await server.ssrLoadModule('/src/lib/skills/surfaces.js');
/* A template must produce something that works before it is edited. The Board
template used to default to a source its own default placement cannot read. */
record(
'the Board template validates as written',
reg.validateSkillSource(templates.uiSkillTemplate({
id: 'template-check', name: 'Template Check', pages: ['positions'],
})) === null,
reg.validateSkillSource(templates.uiSkillTemplate({
id: 'template-check', name: 'Template Check', pages: ['positions'],
})) || 'valid'
);
/* An Owliver block that can answer nothing is refused rather than saved and
then reported as a skill that does not work. */
record(
'an Owliver skill with no capabilities is refused',
Boolean(reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'],
}))),
reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'],
})) || 'ACCEPTED — should have been refused'
);
/* The section-can-never-resolve check, in both directions. */
const boardOn = (page, placement) => `---
id: context-check
name: Context Check
description: Reads position activity.
pages:
- ${page}
status: active
ui:
type: flow
placement: ${placement}
source: position.activity
---
# Context Check
`;
record(
'a position source on a page with no position is refused',
Boolean(reg.validateSkillSource(boardOn('analytics', 'after-header'))),
reg.validateSkillSource(boardOn('analytics', 'after-header')) || 'ACCEPTED — should have been refused'
);
record(
'the same source is refused above the Positions grid',
Boolean(reg.validateSkillSource(boardOn('positions', 'after-position-list'))),
reg.validateSkillSource(boardOn('positions', 'after-position-list')) || 'ACCEPTED — should have been refused'
);
record(
'and accepted inside a position card',
reg.validateSkillSource(boardOn('positions', 'after-position-card')) === null,
reg.validateSkillSource(boardOn('positions', 'after-position-card')) || 'accepted'
);
/* Owliver responses are deliberately NOT subject to that rule: an unmet need is
a question back, not a dead card. `hiring-activity-assistant` depends on it. */
record(
'a shipped Owliver skill reading one position still validates',
reg.validateSkillSource(
reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown
) === null
);
record(
'every shipped definition still validates',
reg.SKILLS.every((s) => reg.validateSkillSource(s.markdown) === null),
reg.SKILLS.filter((s) => reg.validateSkillSource(s.markdown)).map((s) => s.id).join(', ') || 'all valid'
);
/* Reading a definition into the editor fields — the upload path. */
for (const skill of reg.SKILLS.filter((s) => s.kind === 'assistant')) {
const read = fields.owliverFieldsFromSource(skill.markdown);
record(
`\`${skill.id}\` reads back into the Owliver fields`,
read.id === skill.id && read.name === skill.name
&& read.pages.join(',') === skill.pages.join(','),
`${read.name} / ${read.pages.join(', ')}`
);
}
record(
'an unparseable file leaves the fields empty rather than throwing',
fields.owliverFieldsFromSource('not a definition').id === ''
);
/* Writing a field back — the half that used to do nothing. */
const original = reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown;
const renamed = fields.patchFrontmatter(original, { name: 'Renamed Assistant' });
record(
'patching a field changes what the registry reads',
reg.parseSkill(renamed, { custom: true }).name === 'Renamed Assistant',
reg.parseSkill(renamed, { custom: true }).name
);
record(
'patching a field leaves the body untouched',
reg.parseSkill(renamed, { custom: true }).body === reg.parseSkill(original, { custom: true }).body
);
record(
'patching a field preserves frontmatter comments',
renamed.includes('# One suggestion per capability.')
);
record(
'patching a field changes nothing else',
original.split('\n').filter((l) => !l.startsWith('name:')).join('\n')
=== renamed.split('\n').filter((l) => !l.startsWith('name:')).join('\n')
);
record(
'a patched definition still validates',
reg.validateSkillSource(renamed) === null,
reg.validateSkillSource(renamed) || 'valid'
);
const repaged = fields.patchFrontmatter(original, { pages: ['analytics', 'activity'] });
record(
'patching a list replaces the whole block',
reg.parseSkill(repaged, { custom: true }).pages.join(',') === 'analytics,activity',
reg.parseSkill(repaged, { custom: true }).pages.join(',')
);
const restatused = fields.patchFrontmatter(original, { status: 'inactive' });
record(
'patching a key the file never declared adds it',
reg.parseSkill(restatused, { custom: true }).status === 'inactive'
);
/* Both keys, because that is what the editor's own field handler writes: the
registry reads capabilities as the union of the declared list and the keys of
`responses:`, so patching one without the other changes nothing. */
const recapped = fields.patchFrontmatter(original, {
'owliver.capabilities': ['summary'],
'owliver.responses': { summary: { source: 'position.activity', periods: ['today'] } },
});
record(
'patching a nested block rewrites only that block',
reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',') === 'summary',
reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',')
);
/* Asserted on the text, not on the parse: with `flow` no longer offered, the
registry correctly drops the suggestion that names it — which is the rule
working, not the patch reaching a sibling key it should not have. */
record(
'and leaves its siblings in the same block alone',
recapped.includes('Summarize hiring activity for this position')
&& recapped.includes('Show hiring activity as a flow')
&& recapped.includes(' enabled: true')
);
/* The two `ui:` shapes, told apart — the per-page form must not be overwritten
from four single-valued fields. */
record(
'the shorthand `ui:` form is recognised',
fields.uiShape(boardOn('positions', 'after-position-card')) === 'shorthand'
);
record(
'a definition with no `ui:` block reports none',
fields.uiShape(original) === 'none'
);
/* The picker and the validator read the same table. */
record(
'a position card supplies a position',
surfaces.contextSuppliedBy(['positions'], 'after-position-card').includes('positionId')
);
record(
'the Positions list supplies nothing',
surfaces.contextSuppliedBy(['positions'], 'after-position-list').length === 0
);
record(
'Analytics supplies nothing',
surfaces.contextSuppliedBy(['analytics'], 'after-header').length === 0
);
record(
'every surface declares what its placements provide',
surfaces.SKILL_SURFACES.every((s) => s.provides && typeof s.provides === 'object'),
surfaces.SKILL_SURFACES.filter((s) => !s.provides).map((s) => s.id).join(', ') || 'all declared'
);
/* ── 7. Upload hydration: a file, into the fields ─────────────────────────── */
console.log('\n── Upload hydration ──');
/**
* The shapes people actually upload.
*
* Every case here is a definition that arrived from outside the editors, which
* is the only way most definitions arrive. What is being asserted is not that
* the parser is lenient — it is that the three identity fields, the pages and
* the section a reader can see in the file are the ones the form shows.
*/
const COMPLETE = `---
id: hiring-activity-assistant
name: Hiring Activity Assistant
description: Answer questions about recent hiring activity on a position.
type: board
ui:
- page: Positions
placement: grid-card
source: position.activity
---
# Hiring Activity Assistant
`;
const NO_ID = `---
name: Hiring Activity Assistant
description: Answer questions about recent hiring activity.
pages:
- positions
---
# Hiring Activity Assistant
`;
const MULTI_PAGE = `---
id: hiring-activity
name: Hiring Activity
description: Hiring activity across the workspace.
ui:
- page: Positions
placement: grid-card
source: candidates.activity
- page: Analytics
placement: panel
source: hires.performance
---
# Hiring Activity
`;
/* Case 1 — a complete definition hydrates every field it declares. */
{
const f = fields.boardFieldsFromSource(COMPLETE);
record('upload: name hydrates', f.name === 'Hiring Activity Assistant', f.name || 'EMPTY');
record('upload: id hydrates', f.id === 'hiring-activity-assistant', f.id || 'EMPTY');
record(
'upload: description hydrates',
f.description === 'Answer questions about recent hiring activity on a position.',
f.description || 'EMPTY'
);
record('upload: pages come from the `ui:` entries', f.pages.join(',') === 'positions', f.pages.join(',') || 'EMPTY');
record('upload: `grid-card` resolves to a real placement', f.placement === 'after-position-card', f.placement || 'EMPTY');
record('upload: source hydrates', f.source === 'position.activity', f.source || 'EMPTY');
record('upload: an undeclared type is inferred from the source', f.type === 'flow', f.type || 'EMPTY');
record('upload: the definition validates as written', reg.validateSkillSource(COMPLETE) === null,
reg.validateSkillSource(COMPLETE) || 'valid');
record('upload: it is classified as a Board skill',
fields.facetsFromSource(COMPLETE).join(',') === 'ui', fields.facetsFromSource(COMPLETE).join(','));
/* The same file read by the other editor's reader — one pipeline, two views. */
const o = fields.owliverFieldsFromSource(COMPLETE);
record('upload: the Owliver reader hydrates the same identity',
o.id === f.id && o.name === f.name && o.description === f.description);
}
/* Case 2 — no `id:`, so it is slugged from the name and never invented. */
{
const f = fields.boardFieldsFromSource(NO_ID);
record('upload: a missing id slugs the name', f.id === 'hiring-activity-assistant', f.id || 'EMPTY');
record('upload: a missing id does not become the placeholder path', f.id !== 'custom');
record('upload: an explicit id is never replaced by a generated one',
fields.boardFieldsFromSource(COMPLETE).id === 'hiring-activity-assistant');
record('upload: a file with no frontmatter hydrates nothing',
fields.boardFieldsFromSource('# Just a heading\n').id === '');
}
/* Case 3 — several pages, and every entry survives a field edit. */
{
const f = fields.boardFieldsFromSource(MULTI_PAGE);
record('upload: every page in the list is reported', f.pages.join(',') === 'positions,analytics', f.pages.join(','));
const parsed = reg.parseSkill(MULTI_PAGE, { custom: true });
record('upload: every entry becomes a section',
parsed.ui.positions.sections.length === 1 && parsed.ui.analytics.sections.length === 1);
record('upload: each entry keeps its own source',
parsed.ui.positions.sections[0].source === 'candidates.activity'
&& parsed.ui.analytics.sections[0].source === 'hires.performance');
record('upload: a multi-page definition validates', reg.validateSkillSource(MULTI_PAGE) === null,
reg.validateSkillSource(MULTI_PAGE) || 'valid');
/* The fields must not be able to flatten it. */
record('upload: the section fields are read-only against a list', !fields.uiIsEditableFromFields(MULTI_PAGE));
record('upload: a single-section definition stays field-editable',
fields.uiIsEditableFromFields(templates.uiSkillTemplate({ id: 'x', name: 'X', pages: ['positions'] })));
/* Renaming is identity, not structure: it must still work, and must not
touch either entry. */
const renamedMulti = fields.patchFrontmatter(MULTI_PAGE, { name: 'Renamed Multi' });
const after = reg.parseSkill(renamedMulti, { custom: true });
record('upload: renaming a multi-page definition keeps both entries',
after.ui.positions?.sections.length === 1 && after.ui.analytics?.sections.length === 1,
Object.keys(after.ui).join(','));
record('upload: ...and actually renames it', after.name === 'Renamed Multi', after.name);
record('upload: ...and leaves the `ui:` text byte-identical',
renamedMulti.slice(renamedMulti.indexOf('ui:')) === MULTI_PAGE.slice(MULTI_PAGE.indexOf('ui:')));
}
/* Files as they actually arrive: from Windows, from a download, from paste. */
{
const dirty = {
'a byte-order mark': `${COMPLETE}`,
'CRLF line endings': COMPLETE.replace(/\n/g, '\r\n'),
'a BOM and CRLF': `${COMPLETE.replace(/\n/g, '\r\n')}`,
'a blank line above the fence': `\n\n${COMPLETE}`,
'trailing spaces on the fence': COMPLETE.replace(/^---$/gm, '--- '),
};
for (const [what, md] of Object.entries(dirty)) {
const f = fields.boardFieldsFromSource(md);
record(
`upload: a file with ${what} still hydrates`,
f.name === 'Hiring Activity Assistant' && f.id === 'hiring-activity-assistant'
&& f.pages.join(',') === 'positions',
`${f.name || 'EMPTY'} / ${f.id || 'EMPTY'} / ${f.pages.join(',') || 'EMPTY'}`
);
record(
`upload: ...and the registry reads it the same way`,
reg.parseSkill(md, { custom: true }).name === 'Hiring Activity Assistant',
reg.parseSkill(md, { custom: true }).name
);
record(`upload: ...and it validates`, reg.validateSkillSource(md) === null,
reg.validateSkillSource(md) || 'valid');
}
/* Normalising on the way in is what keeps `patchFrontmatter` safe: an
unrecognised fence would have it write a second one above the first. */
const patchedDirty = fields.patchFrontmatter(fields.normalizeUpload(`${COMPLETE}`), { name: 'Clean' });
record('upload: patching a normalised file writes one frontmatter block',
(patchedDirty.match(/^---$/gm) || []).length === 2,
`${(patchedDirty.match(/^---$/gm) || []).length} fences`);
record('upload: ...and it still parses', reg.parseSkill(patchedDirty, { custom: true }).name === 'Clean');
/* A file with nothing to read must be refused, not read as a blank skill. */
record('upload: a file with no fence is not readable', !fields.isReadableDefinition('# Just prose\n'));
record('upload: a real definition is readable', fields.isReadableDefinition(COMPLETE));
}
/* Page names and placements as they are written in the product, not as the
vocabulary spells them internally. */
record('upload: `Positions` resolves to the positions surface', surfaces.canonicalPage('Positions') === 'positions');
record('upload: `Talent Pool` resolves to the talent-pool surface', surfaces.canonicalPage('Talent Pool') === 'talent-pool');
record('upload: `panel` on Analytics resolves to a real placement',
surfaces.placementFor('analytics', 'panel') === 'after-header');
record('upload: an alias never resolves onto a surface that lacks it',
surfaces.placementFor('analytics', 'grid-card') === null);
record('upload: a canonical placement still resolves to itself',
surfaces.placementFor('positions', 'after-position-card') === 'after-position-card');
/* Case 4 — an existing definition reopened for editing. */
for (const skill of reg.SKILLS) {
const f = skill.facets?.includes('ui')
? fields.boardFieldsFromSource(skill.markdown)
: fields.owliverFieldsFromSource(skill.markdown);
record(
`reopening \`${skill.id}\` loads its identity unchanged`,
f.id === skill.id && f.name === skill.name && f.description === skill.description
&& f.pages.join(',') === skill.pages.join(','),
`${f.id} / ${f.name} / ${f.pages.join(', ')}`
);
}
/* Case 5 — a manual edit is not reverted by later synchronisation. */
{
/* The editor's own handler, in miniature: hydrate from the file, edit one
field, then edit an unrelated one. The first edit must survive the second. */
let draft = fields.owliverFieldsFromSource(COMPLETE);
let src = COMPLETE;
const edit = (patch) => {
draft = { ...draft, ...patch };
src = fields.patchFrontmatter(src, {
id: draft.id || undefined,
name: draft.name || undefined,
description: draft.description || undefined,
pages: draft.pages?.length && !fields.pagesAreDerived(src) ? draft.pages : undefined,
});
};
edit({ name: 'My Own Name' });
record('edit: a manual name reaches the artefact',
reg.parseSkill(src, { custom: true }).name === 'My Own Name');
edit({ description: 'My own description.' });
record('edit: a later edit does not revert the earlier one',
reg.parseSkill(src, { custom: true }).name === 'My Own Name',
reg.parseSkill(src, { custom: true }).name);
record('edit: ...and applies itself',
reg.parseSkill(src, { custom: true }).description === 'My own description.');
record('edit: ...and the `ui:` block is untouched throughout',
src.slice(src.indexOf('ui:')) === COMPLETE.slice(COMPLETE.indexOf('ui:')));
/* A second upload replaces the draft outright — it is a new source. */
const rehydrated = fields.owliverFieldsFromSource(MULTI_PAGE);
record('edit: a derived `pages:` is never written back',
!src.includes('\npages:'), src.includes('\npages:') ? 'pages: was inserted' : 'not written');
record('edit: a declared `pages:` still patches normally',
reg.parseSkill(
fields.patchFrontmatter(NO_ID, { pages: ['analytics'] }), { custom: true }
).pages.join(',') === 'analytics');
record('edit: an inherited trigger is not written into the file',
!src.includes('triggers:'), src.includes('triggers:') ? 'triggers: was materialised' : 'not written');
record('edit: a declared trigger is still read into the fields',
fields.owliverFieldsFromSource(
reg.SKILLS.find((x) => x.id === 'hiring-activity-assistant').markdown
).triggers.includes('hiring activity'));
record('edit: a definition with no triggers reads none',
fields.owliverFieldsFromSource(COMPLETE).triggers.length === 0,
JSON.stringify(fields.owliverFieldsFromSource(COMPLETE).triggers));
record('edit: a second upload hydrates from the new file',
rehydrated.id === 'hiring-activity' && rehydrated.name === 'Hiring Activity',
`${rehydrated.id} / ${rehydrated.name}`);
}
await server.close();
/* ── 8. Production bundle ─────────────────────────────────────────────────── */
if (process.argv.includes('--dist')) {
console.log('\n── Production bundle ──');
const dir = join(ROOT, 'dist/assets');
if (!existsSync(dir)) {
record('dist/assets exists', false, 'run `npm run build` first');
} else {
const bundle = readdirSync(dir)
.filter((f) => f.endsWith('.js'))
.map((f) => readFileSync(join(dir, f), 'utf8'))
.join('\n');
for (const s of reg.SKILLS) {
record(`bundle carries \`${s.id}\``, bundle.includes(s.id));
}
/* Body content, not just the id — a manifest naming a skill whose Markdown
did not survive is the failure this is looking for. */
record(
'bundle carries skill body text',
bundle.includes('Which client is this role for'),
'create-position conversation step found in bundle'
);
}
}
/* ── Summary ──────────────────────────────────────────────────────────────── */
const failed = results.filter((r) => !r.pass);
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
if (failed.length) {
console.log('\nFailed:');
failed.forEach((r) => console.log(` - ${r.name}${r.detail ? ` (${r.detail})` : ''}`));
process.exit(1);
}