update the workspace and skills flow
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { parseSkill } from './registry';
|
||||
import { boardPatch, owliverPatch, patchFrontmatter } from './skillFields';
|
||||
|
||||
/**
|
||||
* Account-authored skills, as stored.
|
||||
@@ -28,6 +29,22 @@ const frontMatter = ({ id, name, description, pages, fallback }) => [
|
||||
'status: active',
|
||||
].join('\n');
|
||||
|
||||
/**
|
||||
* The identity block every definition opens with, and nothing else.
|
||||
*
|
||||
* The configuration that follows it — the `ui:` section, the `owliver:` block —
|
||||
* is written by `boardPatch` and `owliverPatch`, which is the same writer the
|
||||
* editors use on an existing file. A template that composed its own YAML was a
|
||||
* second writer with a second field set, and the fields one knew about were not
|
||||
* the fields the other did.
|
||||
*/
|
||||
const skeleton = (fields, fallback, body) => `---
|
||||
${frontMatter({ ...fields, fallback })}
|
||||
---
|
||||
|
||||
${body}
|
||||
`;
|
||||
|
||||
/** A definition that draws a section on the pages it names. */
|
||||
export const uiSkillTemplate = ({
|
||||
id = '', name = '', description = '', pages = [],
|
||||
@@ -40,26 +57,18 @@ export const uiSkillTemplate = ({
|
||||
* record the page never had. A template must produce something that works
|
||||
* before it is edited, so the default reads a source that needs nothing.
|
||||
*/
|
||||
type = 'flow', placement = '', source = 'candidates.activity', periods = [],
|
||||
} = {}) => `---
|
||||
${frontMatter({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
pages,
|
||||
fallback: {
|
||||
id: 'my-ui-skill',
|
||||
name: 'My UI Skill',
|
||||
description: 'What this skill adds to the page.',
|
||||
},
|
||||
})}
|
||||
ui:
|
||||
type: ${type}
|
||||
${placement ? ` placement: ${placement}\n` : ''} title: ${name || 'My UI Skill'}
|
||||
source: ${source}
|
||||
${periods.length ? ` periods:\n${periods.map((p) => ` - ${p}`).join('\n')}\n` : ''}---
|
||||
type = 'flow', placement = '', title = '', source = 'candidates.activity', periods = [],
|
||||
} = {}) => {
|
||||
const identity = { id, name, description, pages };
|
||||
const fallback = {
|
||||
id: 'my-ui-skill',
|
||||
name: 'My UI Skill',
|
||||
description: 'What this skill adds to the page.',
|
||||
};
|
||||
const heading = name || fallback.name;
|
||||
|
||||
# ${name || 'My UI Skill'}
|
||||
return patchFrontmatter(
|
||||
skeleton(identity, fallback, `# ${heading}
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -68,59 +77,36 @@ Describe what this section shows, and why it belongs on these pages.
|
||||
## Capabilities
|
||||
|
||||
- Describe one thing the section reports.
|
||||
- Add more as needed.
|
||||
`;
|
||||
- Add more as needed.`),
|
||||
boardPatch({
|
||||
...identity,
|
||||
type,
|
||||
placement,
|
||||
/* A card with no title of its own is headed by the skill's name, which is
|
||||
what the previous template wrote out. Kept, so a fresh draft reads the
|
||||
same as it always did. */
|
||||
title: title || heading,
|
||||
source,
|
||||
periods,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/** A definition that teaches Owliver what it can be asked for. */
|
||||
export const owliverSkillTemplate = ({
|
||||
id = '', name = '', description = '', pages = [],
|
||||
triggers = [], suggestions = [], capabilities = [], source = '', periods = [],
|
||||
triggers = [], suggestions = [], capabilities = [], responses = {},
|
||||
} = {}) => {
|
||||
const label = name || 'My Owliver Skill';
|
||||
const lines = [`---
|
||||
${frontMatter({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
pages,
|
||||
fallback: {
|
||||
id: 'my-owliver-skill',
|
||||
name: label,
|
||||
description: 'What this skill helps Owliver answer.',
|
||||
},
|
||||
})}`];
|
||||
const identity = { id, name, description, pages };
|
||||
const fallback = {
|
||||
id: 'my-owliver-skill',
|
||||
name: 'My Owliver Skill',
|
||||
description: 'What this skill helps Owliver answer.',
|
||||
};
|
||||
const label = name || fallback.name;
|
||||
|
||||
lines.push('triggers:');
|
||||
lines.push((triggers.length ? triggers : [label.toLowerCase()]).map((t) => ` - ${t}`).join('\n'));
|
||||
|
||||
lines.push('owliver:');
|
||||
lines.push(' enabled: true');
|
||||
|
||||
if (suggestions.length) {
|
||||
lines.push(' suggestions:');
|
||||
lines.push(suggestions.map((s) => ` - ${s}`).join('\n'));
|
||||
}
|
||||
|
||||
if (capabilities.length) {
|
||||
lines.push(' capabilities:');
|
||||
lines.push(capabilities.map((c) => ` - ${c}`).join('\n'));
|
||||
|
||||
if (source) {
|
||||
lines.push(' responses:');
|
||||
for (const capability of capabilities) {
|
||||
lines.push(` ${capability}:`);
|
||||
lines.push(` source: ${source}`);
|
||||
if (periods.length) {
|
||||
lines.push(' periods:');
|
||||
lines.push(periods.map((p) => ` - ${p}`).join('\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`---
|
||||
|
||||
# ${label}
|
||||
return patchFrontmatter(
|
||||
skeleton(identity, fallback, `# ${label}
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -129,10 +115,17 @@ Describe what Owliver should be able to answer on these pages.
|
||||
## Capabilities
|
||||
|
||||
- Describe one thing Owliver can be asked for.
|
||||
- Add more as needed.
|
||||
`);
|
||||
|
||||
return lines.join('\n');
|
||||
- Add more as needed.`),
|
||||
owliverPatch({
|
||||
...identity,
|
||||
/* A definition that claims no phrase of its own still answers to its
|
||||
name — written out so the author can see what it will match on. */
|
||||
triggers: triggers.length ? triggers : [label.toLowerCase()],
|
||||
suggestions,
|
||||
capabilities,
|
||||
responses,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { hasFrontmatter, normalizeDefinition, parseFrontmatter, parseSkill } from './registry';
|
||||
import { SUPPORTED_OWLIVER_CAPABILITIES, sourceSupportsOption } from './surfaces';
|
||||
|
||||
/**
|
||||
* A definition, read into editor fields — and edited fields, written back.
|
||||
@@ -62,13 +63,33 @@ export const isReadableDefinition = (source) => hasFrontmatter(source);
|
||||
/** The Board (UI) editor's fields, before a definition is loaded into them. */
|
||||
export const EMPTY_BOARD_FIELDS = {
|
||||
id: '', name: '', description: '', pages: [],
|
||||
type: 'flow', placement: '', source: 'candidates.activity', periods: [],
|
||||
type: 'flow', placement: '', title: '', source: 'candidates.activity', periods: [],
|
||||
};
|
||||
|
||||
/** The Owliver editor's fields, before a definition is loaded into them. */
|
||||
/**
|
||||
* The Owliver editor's fields, before a definition is loaded into them.
|
||||
*
|
||||
* `responses` is keyed by capability, because that is how the format is keyed
|
||||
* and how the runtime reads it. It used to be one `source` and one `periods`
|
||||
* shared by every selected capability, and that single field is the whole of
|
||||
* the bug this shape exists to remove:
|
||||
*
|
||||
* - A capability selected before a source was picked composed **no**
|
||||
* `responses:` block at all. `normalizeSkillOwliver` then dropped every
|
||||
* capability for want of a source, `owliver.capabilities` normalized to
|
||||
* `[]`, and `owliverSkillsForContext` excludes a skill with none — so the
|
||||
* definition registered, showed as active on its page, and contributed no
|
||||
* suggestion chip.
|
||||
* - Two capabilities could not read different sources, and one global source
|
||||
* that suited the first often could not be drawn as the second: `list`
|
||||
* against `candidates.activity` is refused by `normalizeSection`, so
|
||||
* picking it silently cost the author that capability.
|
||||
*
|
||||
* A form that cannot express what the format can is not a shortcut to it.
|
||||
*/
|
||||
export const EMPTY_OWLIVER_FIELDS = {
|
||||
id: '', name: '', description: '', pages: [],
|
||||
triggers: [], suggestions: [], capabilities: [], source: '', periods: [],
|
||||
triggers: [], suggestions: [], capabilities: [], responses: {},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,6 +115,10 @@ export function boardFieldsFromSource(source) {
|
||||
pages: skill.pages,
|
||||
type: section?.type || EMPTY_BOARD_FIELDS.type,
|
||||
placement: section?.placement || '',
|
||||
/* The heading the card draws. Composed from the skill's name when it is
|
||||
left out, but it is a field of its own in the format and a definition
|
||||
that set it must survive a round trip through the form. */
|
||||
title: section?.title || '',
|
||||
source: section?.source || EMPTY_BOARD_FIELDS.source,
|
||||
periods: section?.periods || [],
|
||||
};
|
||||
@@ -113,8 +138,61 @@ export function owliverFieldsFromSource(source) {
|
||||
try {
|
||||
const skill = readSkill(source);
|
||||
if (!skill) throw new Error('no frontmatter');
|
||||
const first = skill.owliver.capabilities[0];
|
||||
const response = first ? skill.owliver.responses[first] : null;
|
||||
|
||||
/**
|
||||
* Every capability the definition *declares*, not only the ones that
|
||||
* resolved.
|
||||
*
|
||||
* `skill.owliver.capabilities` is filtered to those that found a reading,
|
||||
* which is right for the runtime and wrong for a form: a stored definition
|
||||
* declaring `summary` and `list` where `list` lost its source would open
|
||||
* with `list` simply absent, and the next save would delete a capability
|
||||
* its author never removed. The one that cannot answer is exactly the one
|
||||
* they opened the editor to fix, so it is shown — checked, with no source,
|
||||
* and refused by the same validation as before.
|
||||
*
|
||||
* Read from the frontmatter the definition actually carries, and narrowed
|
||||
* to the closed vocabulary, so a typo is still not a capability.
|
||||
*/
|
||||
const declared = (() => {
|
||||
let data;
|
||||
try {
|
||||
({ data } = parseFrontmatter(String(source)));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const block = data?.owliver;
|
||||
if (!block || typeof block !== 'object' || Array.isArray(block)) return [];
|
||||
const listed = Array.isArray(block.capabilities) ? block.capabilities : [];
|
||||
const keyed = block.responses && typeof block.responses === 'object' && !Array.isArray(block.responses)
|
||||
? Object.keys(block.responses)
|
||||
: [];
|
||||
return [...listed, ...keyed]
|
||||
.map((capability) => String(capability).trim())
|
||||
.filter((capability) => SUPPORTED_OWLIVER_CAPABILITIES.includes(capability));
|
||||
})();
|
||||
|
||||
/* Declaration order first, then anything that resolved without being
|
||||
listed — a definition whose capability came from its `ui:` section. */
|
||||
const capabilities = [...new Set([...declared, ...skill.owliver.capabilities])];
|
||||
|
||||
/**
|
||||
* Every capability's own reading, not the first one's.
|
||||
*
|
||||
* Reading one and showing it against all of them is how a two-capability
|
||||
* definition lost the second's source the first time any field was edited:
|
||||
* the form wrote back what it had read, and it had read half the block.
|
||||
*/
|
||||
const responses = Object.fromEntries(
|
||||
capabilities.map((capability) => {
|
||||
const response = skill.owliver.responses[capability];
|
||||
return [capability, {
|
||||
source: response?.source || '',
|
||||
periods: response?.periods || [],
|
||||
limit: response?.limit ?? null,
|
||||
}];
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: skill.id,
|
||||
@@ -133,9 +211,8 @@ export function owliverFieldsFromSource(source) {
|
||||
*/
|
||||
triggers: skill.declaredTriggers ? skill.triggers : [],
|
||||
suggestions: skill.owliver.suggestions.map((s) => s.label),
|
||||
capabilities: skill.owliver.capabilities,
|
||||
source: response?.source || '',
|
||||
periods: response?.periods || [],
|
||||
capabilities,
|
||||
responses,
|
||||
};
|
||||
} catch {
|
||||
return EMPTY_OWLIVER_FIELDS;
|
||||
@@ -404,3 +481,115 @@ export function patchFrontmatter(source, patch = {}) {
|
||||
|
||||
return `---\n${lines.join('\n')}\n---${raw.slice(match[0].length)}`;
|
||||
}
|
||||
|
||||
/* ── Writing fields back as one canonical patch ─────────────────────────── */
|
||||
|
||||
/**
|
||||
* The frontmatter a set of Board fields means.
|
||||
*
|
||||
* Both composing paths go through this: a fresh draft applies it to an empty
|
||||
* definition, an existing one applies it to the file already open. That is what
|
||||
* makes "typed into the form" and "pasted as Markdown" the same artefact — the
|
||||
* fields have one writer, so there is no second field set that only one path
|
||||
* knows how to express.
|
||||
*
|
||||
* `existing` is the definition being edited, and only two questions are asked of
|
||||
* it: whether its `pages:` are derived from its `ui:` entries, and whether its
|
||||
* `ui:` block is one this form can represent at all. Both are refusals to
|
||||
* overwrite what the fields cannot hold, never a second shape.
|
||||
*/
|
||||
export function boardPatch(fields, { existing = '' } = {}) {
|
||||
const derived = existing ? pagesAreDerived(existing) : false;
|
||||
const editable = existing ? uiIsEditableFromFields(existing) : true;
|
||||
|
||||
const section = editable ? {
|
||||
'ui.type': fields.type || undefined,
|
||||
'ui.placement': fields.placement || undefined,
|
||||
'ui.title': fields.title || undefined,
|
||||
'ui.source': fields.source || undefined,
|
||||
/* Periods only where the reading has any. A source that counts nothing over
|
||||
time keeps no period list, so switching to one cannot leave the previous
|
||||
source's windows behind as a block that validates and does nothing. */
|
||||
'ui.periods': fields.periods?.length && sourceSupportsOption(fields.source, 'periods')
|
||||
? fields.periods
|
||||
: REMOVE,
|
||||
} : {};
|
||||
|
||||
return {
|
||||
id: fields.id || undefined,
|
||||
name: fields.name || undefined,
|
||||
description: fields.description || undefined,
|
||||
pages: fields.pages?.length && !derived ? fields.pages : undefined,
|
||||
...section,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One capability's response, as the format writes it.
|
||||
*
|
||||
* Returns null when the capability has no source. That is deliberate and is the
|
||||
* point of the whole change: an unconfigured capability is left out of
|
||||
* `responses:` so `normalizeSkillOwliver` reports it by name and
|
||||
* `validateSkillSource` refuses the save. The alternative — inventing a source
|
||||
* to make the block well-formed — is how a definition gets saved reading data
|
||||
* its author never chose.
|
||||
*/
|
||||
function responseFor(fields, capability) {
|
||||
const response = fields.responses?.[capability];
|
||||
const source = response?.source || '';
|
||||
if (!source) return null;
|
||||
|
||||
const entry = { source };
|
||||
if (response.periods?.length && sourceSupportsOption(source, 'periods')) {
|
||||
entry.periods = response.periods;
|
||||
}
|
||||
if (response.limit && sourceSupportsOption(source, 'limit')) {
|
||||
entry.limit = Number(response.limit);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontmatter a set of Owliver fields means.
|
||||
*
|
||||
* Every selected capability that has a source is written with its *own*
|
||||
* `source:`, and its own periods or limit where the reading takes them. Nothing
|
||||
* is inherited from a `ui:` block here: this editor owns definitions that have
|
||||
* no `ui:` block at all, and a response that relies on inheriting one silently
|
||||
* loses its reading the day the section is edited.
|
||||
*/
|
||||
export function owliverPatch(fields, { existing = '' } = {}) {
|
||||
const derived = existing ? pagesAreDerived(existing) : false;
|
||||
|
||||
const responses = {};
|
||||
for (const capability of fields.capabilities || []) {
|
||||
const response = responseFor(fields, capability);
|
||||
if (response) responses[capability] = response;
|
||||
}
|
||||
|
||||
return {
|
||||
id: fields.id || undefined,
|
||||
name: fields.name || undefined,
|
||||
description: fields.description || undefined,
|
||||
pages: fields.pages?.length && !derived ? fields.pages : undefined,
|
||||
triggers: fields.triggers?.length ? fields.triggers : REMOVE,
|
||||
'owliver.enabled': true,
|
||||
'owliver.suggestions': fields.suggestions?.length ? fields.suggestions : REMOVE,
|
||||
'owliver.capabilities': fields.capabilities?.length ? fields.capabilities : REMOVE,
|
||||
/* An empty mapping is not a mapping the parser will take — `owliver.responses`
|
||||
must be a mapping of capability names — so nothing configured removes the
|
||||
key rather than writing a header with nothing under it. */
|
||||
'owliver.responses': Object.keys(responses).length ? responses : REMOVE,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Which selected capabilities are not configured, by name.
|
||||
*
|
||||
* The editor shows these against the capability rather than only as a refusal
|
||||
* on save, so "Summary needs a source" is read where the source is chosen. The
|
||||
* *refusal* is still `validateSkillSource`'s, on the same definition the
|
||||
* registry reads — this only says the same thing earlier.
|
||||
*/
|
||||
export const unconfiguredCapabilities = (fields) =>
|
||||
(fields.capabilities || []).filter((capability) => !responseFor(fields, capability));
|
||||
|
||||
@@ -438,6 +438,7 @@ export const DATA_SOURCES = [
|
||||
context: 'positionId',
|
||||
summary: 'Applications to this position, counted over time.',
|
||||
shapes: ['flow', 'stats', 'timeline', 'table', 'insight', 'card'],
|
||||
options: ['periods'],
|
||||
},
|
||||
{
|
||||
id: 'position.pipeline',
|
||||
@@ -452,6 +453,7 @@ export const DATA_SOURCES = [
|
||||
context: 'positionId',
|
||||
summary: 'Candidates matched to this position, best first.',
|
||||
shapes: ['list', 'table', 'stats', 'card'],
|
||||
options: ['limit'],
|
||||
},
|
||||
{
|
||||
/**
|
||||
@@ -469,6 +471,7 @@ export const DATA_SOURCES = [
|
||||
context: 'positionId',
|
||||
summary: 'The candidate pool scored against this position, best first.',
|
||||
shapes: ['list', 'table', 'stats', 'card', 'insight'],
|
||||
options: ['limit'],
|
||||
},
|
||||
{
|
||||
id: 'position.requirements',
|
||||
@@ -504,6 +507,7 @@ export const DATA_SOURCES = [
|
||||
context: null,
|
||||
summary: 'Applications across the workspace, counted over time.',
|
||||
shapes: ['flow', 'stats', 'timeline', 'table', 'card'],
|
||||
options: ['periods'],
|
||||
},
|
||||
{
|
||||
id: 'positions.demand',
|
||||
@@ -511,6 +515,7 @@ export const DATA_SOURCES = [
|
||||
context: null,
|
||||
summary: 'Open positions and what they still need.',
|
||||
shapes: ['list', 'table', 'stats', 'card'],
|
||||
options: ['limit'],
|
||||
},
|
||||
{
|
||||
id: 'workforce.training',
|
||||
@@ -518,6 +523,7 @@ export const DATA_SOURCES = [
|
||||
context: null,
|
||||
summary: 'Training paths and progress against them.',
|
||||
shapes: ['progress', 'list', 'stats', 'table', 'card'],
|
||||
options: ['limit'],
|
||||
},
|
||||
{
|
||||
/* The vetting weights a position is being specified with. `positionId`
|
||||
@@ -548,6 +554,7 @@ export const DATA_SOURCES = [
|
||||
context: null,
|
||||
summary: 'Who was hired, for which role, and when.',
|
||||
shapes: ['list', 'table', 'timeline', 'stats', 'card'],
|
||||
options: ['limit'],
|
||||
},
|
||||
{
|
||||
id: 'hires.performance',
|
||||
@@ -562,6 +569,7 @@ export const DATA_SOURCES = [
|
||||
context: null,
|
||||
summary: 'What has happened across the workspace, most recent first.',
|
||||
shapes: ['timeline', 'list', 'table', 'stats', 'card'],
|
||||
options: ['limit'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -569,6 +577,60 @@ export const SUPPORTED_DATA_SOURCES = DATA_SOURCES.map((s) => s.id);
|
||||
|
||||
export const dataSourceFor = (id) => DATA_SOURCES.find((s) => s.id === id) || null;
|
||||
|
||||
/* ── Source / shape compatibility ───────────────────────────────────────────
|
||||
One resolver, three consumers: `normalizeSection` refuses on it, the Board
|
||||
editor's source picker filters on it, and the Owliver editor's per-capability
|
||||
picker filters on it. They used to be three readings of `source.shapes` —
|
||||
inline in the normalizer, absent from one editor and approximated in the
|
||||
other — which is how a form could compose `list` against a source that has no
|
||||
list in it and only find out at save. */
|
||||
|
||||
/**
|
||||
* The section type a capability is drawn with, or null when it is prose.
|
||||
*
|
||||
* `summary` is the one capability with no component, so it is compatible with
|
||||
* every source: the figures are read back as sentences rather than drawn.
|
||||
*/
|
||||
export const shapeForCapability = (capability) => owliverCapabilityFor(capability)?.shape || null;
|
||||
|
||||
/** Can this source be drawn as this shape? A null shape is prose, and always can. */
|
||||
export function sourceSupportsShape(sourceId, shape) {
|
||||
const source = dataSourceFor(sourceId);
|
||||
if (!source) return false;
|
||||
if (!shape) return true;
|
||||
return Boolean(source.shapes?.includes(shape));
|
||||
}
|
||||
|
||||
/**
|
||||
* The sources that can fill this shape, optionally narrowed to what a placement
|
||||
* can supply context for.
|
||||
*
|
||||
* `context` is the list a placement provides — `contextSuppliedBy(pages,
|
||||
* placement)`. Passing it is how the Board editor offers only sources that can
|
||||
* actually resolve where the section is mounted; the Owliver editor passes
|
||||
* nothing, because a response with an unmet need asks which record is meant
|
||||
* rather than rendering dead.
|
||||
*/
|
||||
export function sourcesForShape(shape, { context = null } = {}) {
|
||||
return DATA_SOURCES.filter((source) => {
|
||||
if (!sourceSupportsShape(source.id, shape)) return false;
|
||||
if (!context) return true;
|
||||
return !source.context || context.includes(source.context);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a source reads an option a form can offer — `periods` or `limit`.
|
||||
*
|
||||
* Declared per source rather than inferred from its shapes, because the two do
|
||||
* not line up: `positions.demand` is a list that honours `limit` and has no
|
||||
* periods at all, while `candidates.activity` is the reverse. The editors used
|
||||
* to guess from the shape list and offered period checkboxes that changed
|
||||
* nothing.
|
||||
*/
|
||||
export const sourceSupportsOption = (sourceId, option) =>
|
||||
Boolean(dataSourceFor(sourceId)?.options?.includes(option));
|
||||
|
||||
/**
|
||||
* Can this reading be written back?
|
||||
*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
SUPPORTED_DATA_SOURCES, SUPPORTED_PERIODS, SUPPORTED_SECTION_TYPES,
|
||||
SUPPORTED_SKILL_PAGES, canonicalPage, dataSourceFor, isSourceWritable, placementFor,
|
||||
surfaceFor,
|
||||
sourceSupportsShape, surfaceFor,
|
||||
} from './surfaces';
|
||||
|
||||
/**
|
||||
@@ -151,7 +151,10 @@ export function normalizeSection(raw, {
|
||||
summary is prose, so there is no shape for a source to be incompatible
|
||||
with. Every drawn type maps to itself. */
|
||||
const shape = shapeFor(type);
|
||||
if (shape && definition.shapes && !definition.shapes.includes(shape)) {
|
||||
/* `sourceSupportsShape` rather than a reading of `definition.shapes` here:
|
||||
the editors' source pickers ask the same question, and the answer has to
|
||||
come from one place or a form can compose what the normalizer refuses. */
|
||||
if (!sourceSupportsShape(source, shape)) {
|
||||
errors.push(
|
||||
`${at || `${where}.${id}`}: \`${source}\` cannot be shown as \`${type}\`. It supports: ${definition.shapes.join(', ')}.`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user