candidates and board ui agent issue
Some checks failed
CI / check (push) Failing after 4m58s

This commit is contained in:
2026-09-05 10:46:06 +05:30
parent e02a0c23d4
commit 6249e00a3a
78 changed files with 15071 additions and 1998 deletions

83
src/lib/ui/skillNodes.js Normal file
View File

@@ -0,0 +1,83 @@
/**
* A skill's declared section, as a node in the page tree.
*
* This is the join between the two halves of the UI system. A Board skill's
* `ui:` block is still parsed by `uiConfig.normalizeSection`, still validated
* against the closed vocabulary, and still drawn by the same nine components —
* nothing about definitions changes. What this adds is *identity*: the section
* becomes an addressable node, so it can be hidden, moved and reordered by the
* same operations that move a built-in.
*
* **Nothing here writes back to Markdown.** A node carries `origin: 'skill'`,
* and a change to it is stored as an operation in the person's own layout
* patch. The definition on disk, and the definition in the account's custom
* skills, are read-only from here — which is what keeps a layout preference
* from silently editing something another user also sees.
*/
import { dataSourceFor, sourceSupportsOption } from '@/lib/skills/surfaces';
import { makeNode } from './node';
/** Ids are `skill-<skill>-<section>`, so provenance is legible in the DOM. */
export const skillNodeId = (skillId, sectionId) => [
'skill', slug(skillId), slug(sectionId),
].filter(Boolean).join('-');
const slug = (value) => String(value || '')
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
/**
* One normalized section, adapted.
*
* Only options the source actually declares are carried across. The skill
* format checks that a period is a real period; this checks that the reading
* takes periods at all — a stricter rule, and one a definition written against
* the looser one could fail. Dropping the option is right where refusing the
* node would not be: a Board card must never disappear because of a parameter
* that was doing nothing anyway.
*/
export function skillSectionNode(skill, section) {
const params = {};
if (section.periods?.length && sourceSupportsOption(section.source, 'periods')) {
params.periods = [...section.periods];
}
if (section.limit && sourceSupportsOption(section.source, 'limit')) {
params.limit = section.limit;
}
return makeNode({
id: skillNodeId(skill.id, section.id),
type: section.type,
origin: 'skill',
data: { source: section.source, params },
props: {
/* The same fallback the surface uses, so a section with no title of its
own is still named by the skill that contributed it. */
title: section.title || skill.name,
...(section.description ? { description: section.description } : {}),
/* Attribution is what lets a reader tell an extension from a built-in
panel, and which skill to switch off. */
attribution: skill.name,
...(section.editable ? { editable: true } : {}),
},
});
}
/**
* Every section a page's definitions contribute, grouped by placement.
*
* Grouped because that is how the composition consumes them: each
* `skill-surface` node names one placement and takes the sections that declared
* it. A placement no slot offers simply has nowhere to render, which is the
* same outcome as today.
*/
export function skillNodesByPlacement(sections = []) {
const out = {};
for (const { skill, section } of sections) {
if (!dataSourceFor(section.source)) continue;
const key = section.placement || '';
if (!out[key]) out[key] = [];
out[key].push(skillSectionNode(skill, section));
}
return out;
}