/** * 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); }