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

310 lines
10 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"]`.
*
* Composite event names follow the convention `{family}-{verb}` or
* `{family}-{verb}-{variant}` (e.g. `commit-save`, `commit-toggle-visibility`,
* `signal-warn-count-overflow`). `validateEventName` extracts the head, checks
* it against the canon, and reports drift.
*
* ## Doctrina (libro «Diseñando lo que ocurre», cap. 8 + cap. 22-29)
*
* Eight families, each answering one perceptual question:
*
* - `contact` (cap. 22) — ¿el sistema ha sentido mi acción? Pre-evaluation
* reception of input. Intent is typically `neutral` or omitted; "el intent
* fuerte no debería vivir en el contacto, sino en la señal o consecuencia
* posterior" (cap. 22 §11).
* - `commit` (cap. 23) — ¿algo quedó fijado o tuvo consecuencia? Where state
* or outcome lands. Accepts intent fully.
* - `signal` (cap. 24) — ¿algo reclama mi atención? System-initiated attention
* orientation. Accepts intent fully.
* - `handle` (cap. 25) — ¿estoy controlando directamente este objeto? Direct
* manipulation. Intent concentrates on `drop`; `carry` stays neutral.
* - `emerge` (cap. 26) — ¿algo entró o salió del campo perceptivo? Presence
* transitions. Normally does NOT accept intent — what appears may carry
* it, the appearance itself doesn't.
* - `shift` (cap. 27) — ¿cambió el contexto? Context/regime change. Normally
* does NOT accept intent.
* - `sustain` (cap. 28) — ¿esto sigue ocurriendo? Operational continuity.
* Normally does NOT accept intent — pair with signal/commit when needed.
* - `delegate` (cap. 29) — ¿quién actúa ahora? Reparto de iniciativa entre
* usuario y sistema (workflows, IA, macros, aprobaciones). No intent by
* default; intent appears in the resulting signal/commit.
*
* ## Six intents (cap. 10): neutral, affirm, fulfill, risk, threat, loss
*
* threat ≠ loss: threat lives ANTES de la consecuencia (convoca acción),
* loss lives DESPUÉS (registra consecuencia consumada).
*
* ## Composition rules (cap. 30)
*
* 1. contact precede al resultado cuando hay acción directa
* 2. sustain precede a commit cuando hay espera
* 3. threat precede a loss (nunca `commit.delete + threat`)
* 4. emerge no absorbe intent del contenido
* 5. shift debe orientar (foco + título + landmark)
* 6. handle concentra evaluación en el drop, no en el carry
* 7. todo proceso abierto necesita salida (commit/cancel/fail)
*
* ## Extending the canon
*
* Adding a verb is justified when at least two components share the action
* with the same semantic family. The book (cap. 8 §1) authorises verbs as a
* level distinct from families: "no todo debe ser familia. Algunas diferencias
* pertenecen al verbo, a la fase, al intent o a la realización."
*/
export const SEMA_VERBS = {
contact: ['press', 'tap', 'activate', 'focus', 'trigger', 'release'],
commit: [
// Fijación de estado de selección — par natural canónico.
// `select` (libro Cap 23) + `unselect` (extensión del autor 2026-05-25):
// resultados aplicados sobre el estado de selección de un elemento.
// "este item queda seleccionado" / "este item deja de estar seleccionado".
// No es `remove` (no se elimina ni se saca de colección, solo cambia
// estado de selección). No es `toggle` (toggle describe inversión
// binaria genérica, no el resultado semántico de selección).
'select',
'unselect',
// Inversión binaria genérica (switch, checkbox, control on/off).
'toggle',
// Otros fijadores de estado.
'save',
// Resolución de proceso
'submit',
'confirm',
'complete',
'fail',
// Cancelación / reversión
'cancel',
'reset',
'discard',
// Consecuencia consumada
'delete',
'restore',
'expire',
// Cierre que cubre una decisión pendiente sin afirmar/rechazar
'acknowledge',
// Verbos contextuales que el libro usa en casos de estudio
// (cap. 29 + cap. 30 + apéndice C):
'apply',
'partial',
'block',
'move',
'set',
'remove',
'reorder',
'upload'
],
signal: [
'announce',
'notify',
'warn',
'alert',
'inform',
'emphasize',
'remind'
],
// `zoom` (extensión del autor 2026-06-10): manipulación directa de la escala
// del contenido (rueda / pinch), junto a drag / resize / rotate / scroll.
handle: ['pick', 'carry', 'drop', 'drag', 'resize', 'reorder', 'rotate', 'scroll', 'zoom'],
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',
'upload',
'end'
],
// Familia delegate (libro cap. 29). Eventos donde cambia quién lleva la
// iniciativa o ejecuta parte de la acción. No requiere IA — workflows,
// macros, reglas automáticas y aprobaciones también producen delegate.
delegate: [
'offer', // el sistema ofrece encargarse
'plan', // el sistema propone un curso
'authorize', // el usuario concede permiso
'act', // el sistema actúa por el usuario
'review', // el sistema espera revisión
'escalate', // el sistema pide intervención humana
'return' // el control vuelve al usuario
]
} 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])
);
/**
* Find the longest canonical verb that is a prefix of `text` (followed
* by end-of-string or a dash). Required because the canon includes
* multi-word verbs (`enter-mode`, `exit-mode`); a naive "split at first
* dash" would only see `enter` / `exit` and miss the actual verb.
*
* Returns the matched verb plus whatever remains of `text` after it
* (with the separating dash consumed), or `undefined` if no verb
* prefixes `text`. Sorts by length descending so `enter-mode` beats
* `enter` when both could match.
*/
function matchLongestVerb(
text: string,
verbs: Iterable<string>
): { verb: string; tail: string } | undefined {
const sorted = [...verbs].sort((a, b) => b.length - a.length);
for (const verb of sorted) {
if (text === verb) return { verb, tail: '' };
if (text.startsWith(verb + '-')) return { verb, tail: text.slice(verb.length + 1) };
}
return undefined;
}
/**
* 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; `return` is both a
* shift and a delegate verb; `upload` is both a commit outcome and a
* sustain phase). 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: name starts with a canonical verb (bare verb or verb-variant
// shape). Longest-prefix match against ALL canonical verbs so
// multi-word verbs like `enter-mode` win over `enter`.
const verbHeadMatch = matchLongestVerb(name, VERB_SET);
if (verbHeadMatch) {
return {
name,
head,
matchesCanonical: true,
variant,
family: VERB_TO_FAMILY.get(verbHeadMatch.verb),
verb: verbHeadMatch.verb
};
}
// Case 2: head is a family name → longest-prefix match against the
// family's verbs (covers multi-word verbs like `shift.enter-mode`).
if (head in SEMA_VERBS) {
const family = head as SemaFamilyName;
const familyVerbs = SEMA_VERBS[family] as readonly string[];
const verbMatch = matchLongestVerb(tail, familyVerbs);
return {
name,
head,
matchesCanonical: verbMatch !== undefined,
variant,
family,
verb: verbMatch?.verb
};
}
// 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.