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/event.ts

198 lines
5.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import {
SEMA_FAMILY_POLICY,
type SemaActionEvent,
type SemaEvent,
type SemaEventKey,
type SemaEventLabel,
type SemaFamily,
type IntentBinding,
type SemaTransitionalFamily,
type SemaValencedFamily
} from './types'
import { INTENTS, type Intent } from '../intent'
export const SEMA_VALENCED_FAMILIES = [
'contact',
'commit',
'signal',
'handle'
] as const satisfies readonly SemaValencedFamily[]
export const SEMA_TRANSITIONAL_FAMILIES = [
'emerge',
'shift',
'sustain'
] as const satisfies readonly SemaTransitionalFamily[]
export const SEMA_FAMILIES = [
...SEMA_VALENCED_FAMILIES,
...SEMA_TRANSITIONAL_FAMILIES
] as const satisfies readonly SemaFamily[]
/**
* All valid `SemaEventKey` values, generated from the family × intent
* product plus family-only forms. Open template-literal types can't be
* enumerated by the type system; we materialise the array at module load
* for runtime membership checks.
*/
export const SEMA_EVENT_LABELS: readonly SemaEventKey[] = [
...SEMA_FAMILIES,
...SEMA_FAMILIES.flatMap((family) =>
INTENTS.map((intent) => `${family}-${intent}` as SemaEventKey)
)
]
const FAMILY_SET = new Set<SemaFamily>(SEMA_FAMILIES)
const VALENCED_FAMILY_SET = new Set<SemaValencedFamily>(SEMA_VALENCED_FAMILIES)
const TRANSITIONAL_FAMILY_SET = new Set<SemaTransitionalFamily>(SEMA_TRANSITIONAL_FAMILIES)
const INTENT_SET = new Set<Intent>(INTENTS)
const LABEL_SET = new Set<SemaEventKey>(SEMA_EVENT_LABELS)
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
export function isSemaFamily(value: unknown): value is SemaFamily {
return typeof value === 'string' && FAMILY_SET.has(value as SemaFamily)
}
export function isSemaValencedFamily(value: unknown): value is SemaValencedFamily {
return typeof value === 'string' && VALENCED_FAMILY_SET.has(value as SemaValencedFamily)
}
export function isSemaTransitionalFamily(value: unknown): value is SemaTransitionalFamily {
return typeof value === 'string' && TRANSITIONAL_FAMILY_SET.has(value as SemaTransitionalFamily)
}
export function isIntent(value: unknown): value is Intent {
return typeof value === 'string' && INTENT_SET.has(value as Intent)
}
export function isSemaEventLabel(value: unknown): value is SemaEventLabel {
return typeof value === 'string' && LABEL_SET.has(value as SemaEventKey)
}
export function isIntentBinding(value: unknown): value is IntentBinding {
if (!isRecord(value)) return false
if (!isIntent(value.default)) return false
if ('fromProp' in value && value.fromProp !== undefined && typeof value.fromProp !== 'string') return false
if ('supported' in value && value.supported !== undefined) {
if (!Array.isArray(value.supported)) return false
if (!value.supported.every(isIntent)) return false
}
return true
}
/**
* Validate the runtime shape of a `SemaEvent` against the per-family
* `SEMA_FAMILY_POLICY`:
*
* - `expected` policy → intent MUST be a valid literal or binding.
* - `allowed` / `optional` policy → intent may be absent; if present,
* must still be a valid literal or binding.
*
* Replaces the previous valenced/transitional split (which forbade
* intent on transitional families). Post-canon doctrine: any family can
* carry intent unless its policy says otherwise.
*/
export function isSemaEvent(value: unknown): value is SemaEvent {
if (!isRecord(value) || !isSemaFamily(value.family)) return false
const policy = SEMA_FAMILY_POLICY[value.family].intentPolicy
const intentValue = 'intent' in value ? value.intent : undefined
if (intentValue === undefined) {
return policy !== 'expected'
}
return isIntent(intentValue) || isIntentBinding(intentValue)
}
/**
* Parsed shape of a `SemaEventKey`. With the policy reform, ANY family
* may carry an intent (or none) — the parsed shape only distinguishes
* "intent present" from "intent absent" without locking the family kind.
*/
export type ParsedSemaEventLabel =
| { family: SemaFamily }
| { family: SemaFamily; intent: Intent }
export function parseSemaEventLabel(label: SemaEventLabel): ParsedSemaEventLabel {
const [family, rawIntent] = label.split('-')
if (rawIntent === undefined) {
return { family: family as SemaFamily }
}
return {
family: family as SemaFamily,
intent: rawIntent as Intent
}
}
export function resolveIntent(
intent: Intent | IntentBinding,
props?: Record<string, unknown>
): Intent {
if (isIntent(intent)) return intent
const supported = intent.supported?.filter(isIntent) ?? [...INTENTS]
const fallback = supported[0] ?? intent.default
const candidate = intent.fromProp ? props?.[intent.fromProp] : undefined
if (isIntent(candidate) && supported.includes(candidate)) {
return candidate
}
return supported.includes(intent.default) ? intent.default : fallback
}
/**
* Normalize a `SemaActionEvent` (label or structured event) into its
* canonical resolved form. Preserves intent on ANY family that carries
* one — the previous "transitional families never carry intent" rule
* was the bug Phase 2 of the codex refactor fixes.
*/
export function normalizeSemaEvent(
event: SemaActionEvent,
props?: Record<string, unknown>
): {
family: SemaFamily
intent: Intent | null
label: SemaEventLabel
} {
if (typeof event === 'string') {
const parsed = parseSemaEventLabel(event)
if ('intent' in parsed) {
return {
family: parsed.family,
intent: parsed.intent,
label: event
}
}
return {
family: parsed.family,
intent: null,
label: event
}
}
const intentValue = 'intent' in event ? event.intent : undefined
if (intentValue === undefined) {
return {
family: event.family,
intent: null,
label: event.family as SemaEventLabel
}
}
const intent = resolveIntent(intentValue, props)
return {
family: event.family,
intent,
label: `${event.family}-${intent}` as SemaEventLabel
}
}
export function toSemaEventLabel(event: SemaActionEvent, props?: Record<string, unknown>): SemaEventLabel {
return normalizeSemaEvent(event, props).label
}

Powered by TurnKey Linux.