You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/sema/verbs.ts

205 lines
5.9 KiB

/**
* Canonical action verb vocabulary, grouped by family.
*
* Cross-component verb names that morfo `events[].semantic.verb` (and the
* `events[].name` head) should preferably align with. Lets sema/sound/haptic
* engines subscribe by verb instead of by component-specific event name,
* and lets eidos write transversal selectors like
* `[data-event^="dismiss"]`.
*
* Vocabulary is intentionally small. New verbs only added when at least
* two components share the action with the same semantic family.
*
* Composite event names follow the convention `{verb}-{variant}`, e.g.
* `commit-save` / `commit-cancel` / `dismiss-outside`. The verb is the
* head; everything after the first `-` is component-specific nuance.
*
* The validator (`validateEventName`) extracts the head and reports
* whether it matches a canonical verb. Advisory by design — it doesn't
* reject morfos, just flags drift for review.
*
* Doctrina (per src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md §1.3):
* verbs live under their family. Some verbs that look like one family
* actually belong to another in the doctrinal classification:
* - select / toggle → commit (fix state, not just contact)
* - acknowledge → commit (closes a pending decision)
* - edit → represented as shift.enter-mode (régime change)
*/
export const SEMA_VERBS = {
contact: ['press', 'tap', 'activate', 'focus', 'trigger', 'release'],
commit: [
'select',
'toggle',
'save',
'submit',
'confirm',
'cancel',
'complete',
'fail',
'delete',
'restore',
'reset',
'discard',
'expire',
'acknowledge',
'set',
'remove',
'reorder'
],
signal: ['announce', 'notify', 'warn', 'alert', 'emphasize', 'remind'],
handle: ['pick', 'carry', 'drop', 'drag', 'resize', 'reorder', 'rotate', 'scroll'],
emerge: [
'present',
'dismiss',
'open',
'close',
'expand',
'collapse',
'reveal',
'hide'
],
shift: ['enter-mode', 'exit-mode', 'navigate', 'route', 'step', 'return', 'context'],
sustain: [
'start',
'progress',
'loading',
'waiting',
'syncing',
'processing',
'streaming',
'pending',
'retrying',
'end'
]
} as const;
export type SemaFamilyName = keyof typeof SEMA_VERBS;
export type SemaVerb = (typeof SEMA_VERBS)[SemaFamilyName][number];
const VERB_SET: ReadonlySet<string> = new Set(
Object.values(SEMA_VERBS).flatMap((verbs) => [...verbs])
);
/**
* Reverse lookup: which family does a given verb belong to?
*
* Some verbs appear in multiple families (e.g. `reorder` is both a
* commit and a handle verb depending on context). The map returns the
* FIRST family in which the verb is declared per `SEMA_VERBS` ordering.
*/
const VERB_TO_FAMILY: ReadonlyMap<string, SemaFamilyName> = (() => {
const map = new Map<string, SemaFamilyName>();
for (const [family, verbs] of Object.entries(SEMA_VERBS) as [
SemaFamilyName,
readonly string[]
][]) {
for (const verb of verbs) {
if (!map.has(verb)) map.set(verb, family);
}
}
return map;
})();
export interface VerbValidation {
/** The morfo event name as authored. */
name: string;
/** The verb head — the part before the first `-`, or the whole name. */
head: string;
/** Whether the parsed head OR (family + first-tail-segment) is canonical. */
matchesCanonical: boolean;
/** Tail after the first `-`, if any. e.g. `'commit-save'` → `'save'`. */
variant: string | undefined;
/**
* The family resolved from the name. Three sources, in order:
* 1. head is a verb → family from `VERB_TO_FAMILY[head]`
* 2. head is a family name → that family
* 3. neither → `undefined`
*/
family: SemaFamilyName | undefined;
/**
* The canonical verb resolved from the name, when one can be
* determined. For `'commit-toggle'` (family-verb shape) this is
* `'toggle'`; for `'dismiss-outside'` (verb-variant shape) this is
* `'dismiss'`; for bare `'present'` this is `'present'`.
*/
verb: string | undefined;
}
/**
* Inspect a morfo `events[].name` against the canonical vocabulary.
*
* Recognises two naming shapes:
* - `{verb}` or `{verb}-{variant}` — head is the verb (e.g.
* `present`, `dismiss-outside`, `close-cancel`).
* - `{family}-{verb}` or `{family}-{verb}-{variant}` — head is a
* family name (e.g. `commit-toggle`, `commit-save`).
*
* Returns `matchesCanonical: true` for either shape. Used for auditing
* morfos and for `morfo:vocabulary`-style scripts to surface drift.
*/
export function validateEventName(name: string): VerbValidation {
const dashIdx = name.indexOf('-');
const head = dashIdx === -1 ? name : name.slice(0, dashIdx);
const tail = dashIdx === -1 ? '' : name.slice(dashIdx + 1);
const variant = dashIdx === -1 ? undefined : tail;
// Case 1: head is a canonical verb → family from VERB_TO_FAMILY.
if (VERB_SET.has(head)) {
return {
name,
head,
matchesCanonical: true,
variant,
family: VERB_TO_FAMILY.get(head),
verb: head
};
}
// Case 2: head is a family name → tail's first segment should be a
// verb in that family.
if (head in SEMA_VERBS) {
const family = head as SemaFamilyName;
const tailFirstDash = tail.indexOf('-');
const verbCandidate = tailFirstDash === -1 ? tail : tail.slice(0, tailFirstDash);
const familyVerbs = SEMA_VERBS[family] as readonly string[];
const verbCanonical = verbCandidate.length > 0 && familyVerbs.includes(verbCandidate);
return {
name,
head,
matchesCanonical: verbCanonical,
variant,
family,
verb: verbCanonical ? verbCandidate : undefined
};
}
// Case 3: neither verb nor family.
return {
name,
head,
matchesCanonical: false,
variant,
family: undefined,
verb: undefined
};
}
export function isSemaVerb(value: unknown): value is SemaVerb {
return typeof value === 'string' && VERB_SET.has(value);
}
/**
* Family the canonical verb belongs to, or `undefined` if not canonical.
*/
export function familyForVerb(verb: string): SemaFamilyName | undefined {
return VERB_TO_FAMILY.get(verb);
}

Powered by TurnKey Linux.