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.
162 lines
7.0 KiB
162 lines
7.0 KiB
/**
|
|
* SEMA_HOLDS_BY_INTENT — canonical lookup of perceptual hold + persistence
|
|
* by family + intent.
|
|
*
|
|
* PROVENANCE (corrected 2026-07-06; the earlier header cited a book section
|
|
* — "cap. 24 §6.2 (Holds por familia e intent)" — that DOES NOT EXIST, and
|
|
* claimed the tables were "verbatim"). The book *Diseñando lo que ocurre*
|
|
* gives NO milliseconds and no per-family hold table: its temporal doctrine
|
|
* is QUALITATIVE — cap. 12 (Tiempo) §4 duración · §6 "Persistencia y huella"
|
|
* ("no todo evento termina cuando termina su animación") · §8 tiempo e
|
|
* intent · §9 accesibilidad ("si un evento importante solo existe durante un
|
|
* instante, muchos usuarios no lo recibirán"), and cap. 32 TABLA 32.1/32.2
|
|
* ("regiones de diseño, no números sagrados" — e.g. commit.save+affirm
|
|
* "breve"; commit.complete+fulfill "breve-media, más resolutivo";
|
|
* signal.warn+risk "persistente hasta corrección"; signal.alert+threat
|
|
* "entrada rápida + persistencia hasta acción"; commit.delete+loss
|
|
* "breve + huella"). THIS TABLE is the framework's MATERIALIZATION of those
|
|
* regions onto the `SEMA_DURATIONS` anchors — authorship, not transcription.
|
|
*
|
|
* Two orthogonal concepts (book cap. 12 §6 distinguishes four — duración
|
|
* expresiva · persistencia · huella · caducidad; the runtime encodes two):
|
|
*
|
|
* - **hold** — perceptual MINIMUM display time. Independent of motion or
|
|
* animation timing; this is the duration the signal MUST be visible to
|
|
* register as a signal. (The expression itself is never cut short by the
|
|
* hold — see `VisualChannel.awaitExpression`.)
|
|
* - **persistence** — lifecycle policy. `transient` = auto-cleared after
|
|
* hold; `untilAction` / `untilFix` / `stateBound` = caller-managed
|
|
* lifecycle (the book's "hasta corrección/acción" is the EMITTER's
|
|
* responsibility — hence caller-managed).
|
|
*
|
|
* Used by:
|
|
* - the resolver (`resolveEffectiveSignature`) — THE single hold source per
|
|
* family + intent (the duplicated `SEMA_MAP.families[*].hold` table was
|
|
* removed 2026-07-06; it had drifted — signal 600 vs the book's "breve")
|
|
* - documentation tooling (lookup canonical defaults per family+intent)
|
|
* - authors as a reference when declaring `MorfoEvent.persistence`
|
|
*
|
|
* Persistence is NOT auto-applied by the runtime — `SomaRuntime.trigger`
|
|
* defaults to `'transient'` when the morfo doesn't declare it. This avoids
|
|
* silent behavior changes for components that don't yet opt in. Authors
|
|
* who want the canonical policy must declare it explicitly on the morfo
|
|
* event; `resolveHoldsByIntent(family, intent)` is provided so they don't
|
|
* have to guess.
|
|
*/
|
|
|
|
import type { Intent } from '../intent';
|
|
import type { SemaDurationSpec } from './durations';
|
|
import type { SemaFamily, SignalPersistence } from './types';
|
|
|
|
/**
|
|
* Canonical hold + persistence policy per family + intent.
|
|
*
|
|
* Encoded as a sparse map: each family's `_default` is the policy for
|
|
* absent / neutral intent; per-intent entries override. Intents NOT
|
|
* present fall back to `_default`.
|
|
*
|
|
* Reading: `SEMA_HOLDS_BY_INTENT.signal.threat = { hold: 'brief', persistence: 'untilAction' }`
|
|
* — a `signal.alert + threat` signal must be perceptible for at LEAST
|
|
* `brief` (240 ms) and persists in the DOM until the user acknowledges
|
|
* (caller clears it).
|
|
*
|
|
* Values are labels on the perceptual scale (never raw ms) so any future
|
|
* scale adjustment propagates automatically.
|
|
*/
|
|
export const SEMA_HOLDS_BY_INTENT = {
|
|
contact: {
|
|
// TABLA 32.1: "dentro de la ventana de atribución causal".
|
|
_default: { hold: 'glimpse', persistence: 'transient' }
|
|
},
|
|
emerge: {
|
|
// TABLA 32.1: dropdown "breve" · tooltip "breve-media" (per-event
|
|
// morfo `hold` override covers the longer cases).
|
|
_default: { hold: 'brief', persistence: 'transient' }
|
|
},
|
|
shift: {
|
|
// TABLA 32.1: "media, con orientación".
|
|
_default: { hold: 'noticed', persistence: 'transient' }
|
|
},
|
|
commit: {
|
|
// TABLA 32.1: commit.save + affirm "breve, sin interrumpir flujo".
|
|
_default: { hold: 'brief', persistence: 'transient' },
|
|
// TABLA 32.1: commit.complete + fulfill "breve-media, más
|
|
// resolutivo" → the settled (400) step (was `noticed` 600 — too
|
|
// long for "breve-media"; corrected 2026-07-06 with the new step).
|
|
fulfill: { hold: 'settled', persistence: 'transient' }
|
|
},
|
|
signal: {
|
|
// TABLA 32.2: signal.announce + neutral "breve o contextual".
|
|
_default: { hold: 'brief', persistence: 'transient' },
|
|
// TABLA 32.2: signal.warn + risk "hasta corrección/cierre" →
|
|
// untilFix. Hold = minimum display so a quickly-fixed warning
|
|
// still flashes for perceptibility.
|
|
risk: { hold: 'brief', persistence: 'untilFix' },
|
|
// TABLA 32.2: signal.alert + threat "hasta acción — alta, pero no
|
|
// infinita" → untilAction.
|
|
threat: { hold: 'brief', persistence: 'untilAction' },
|
|
// TABLA 32.1: commit.delete + loss "breve + huella" — loss is
|
|
// consumed grief: it registers and goes (`brief`, was `noticed`
|
|
// 600 against the book's "breve"; corrected 2026-07-06). The
|
|
// huella (trace) lives in undo/state, owned by the caller — not
|
|
// in a longer signal.
|
|
loss: { hold: 'brief', persistence: 'transient' }
|
|
},
|
|
handle: {
|
|
_default: { hold: 'brief', persistence: 'transient' }
|
|
},
|
|
sustain: {
|
|
// TABLA 32.1: "mientras dure el proceso" — state-bound, no fixed
|
|
// hold. The `hold` here is the minimum display for the perceptual
|
|
// transition into sustain mode.
|
|
_default: { hold: 'noticed', persistence: 'stateBound' }
|
|
},
|
|
delegate: {
|
|
// Cap. 29 — delegate is structural, signal weight similar to
|
|
// sustain. Transient by default; specific compositions
|
|
// (delegate + signal.warn, etc.) get their persistence from the
|
|
// signal partner.
|
|
_default: { hold: 'noticed', persistence: 'transient' }
|
|
}
|
|
} as const satisfies Record<
|
|
SemaFamily,
|
|
{ _default: HoldsPolicy } & Partial<Record<Intent, HoldsPolicy>>
|
|
>;
|
|
|
|
/**
|
|
* Hold + persistence pair resolved for a given family + intent. Authors
|
|
* declare this verbatim on `MorfoEvent` to opt into the canonical policy
|
|
* (the framework's materialization of the book's regions — see the header);
|
|
* otherwise `SomaRuntime` defaults to `'transient'`.
|
|
*/
|
|
export interface HoldsPolicy {
|
|
hold: SemaDurationSpec;
|
|
persistence: SignalPersistence;
|
|
}
|
|
|
|
/**
|
|
* Lookup the canonical hold + persistence for a given family + intent.
|
|
*
|
|
* - Family + specific intent declared → uses the intent's override.
|
|
* - Family + neutral / unspecified intent → uses the family's `_default`.
|
|
* - Family unknown → returns `undefined` (caller should fall back to the
|
|
* engine's default behavior).
|
|
*
|
|
* Pure function — no caching needed, lookup is constant time.
|
|
*/
|
|
export function resolveHoldsByIntent(
|
|
family: SemaFamily | undefined,
|
|
intent?: Intent | undefined
|
|
): HoldsPolicy | undefined {
|
|
if (!family) return undefined;
|
|
const entry = SEMA_HOLDS_BY_INTENT[family] as
|
|
| ({ _default: HoldsPolicy } & Partial<Record<Intent, HoldsPolicy>>)
|
|
| undefined;
|
|
if (!entry) return undefined;
|
|
if (intent && intent in entry) {
|
|
const perIntent = entry[intent];
|
|
if (perIntent) return perIntent;
|
|
}
|
|
return entry._default;
|
|
}
|