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
5.3 KiB
162 lines
5.3 KiB
|
5 months ago
|
/**
|
||
|
|
* resolveSignature(signal) — turns a `SemanticSignal` into the per-channel
|
||
|
|
* `EffectiveSignature` used by every channel.
|
||
|
|
*
|
||
|
|
* The engine resolves once per `emit(...)` and dispatches `(signal,
|
||
|
|
* effective)` to each registered channel. Channels read their own slice
|
||
|
|
* from `effective` (e.g. `SoundChannel` reads `effective.sound`) and skip
|
||
|
|
* if their slice is missing or their channel id is not in
|
||
|
|
* `activeChannels`.
|
||
|
|
*
|
||
|
|
* Resolution rule:
|
||
|
|
* 1. Look up `family` in `SEMA_MAP.families` to get the `base`
|
||
|
|
* signatures + `activeChannels`.
|
||
|
|
* 2. If the family is valenced and the signal carries an `intent`,
|
||
|
|
* apply the intent's `deltas` per channel (numeric add, op-based
|
||
|
|
* replace/multiply/add, recursive merge).
|
||
|
|
* 3. If the family has a `soundPack` entry for the signal name, attach
|
||
|
|
* the sample URL to `effective.sound.sampleUrl`.
|
||
|
|
* 4. Always return an `EffectiveSignature`. If `family` is missing,
|
||
|
|
* return one with empty channels — channels skip rather than throw.
|
||
|
|
*
|
||
|
|
* Pure function, no state. Override behaviour (runtime overrides, CSEM
|
||
|
|
* selectors) lived in the prior `Resolver` class — out of scope for this
|
||
|
|
* iteration; can be re-added without breaking the function signature.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type { SemanticSignal } from './signal'
|
||
|
|
import type { SemaFamily, SemaIntent } from './types'
|
||
|
|
import {
|
||
|
|
SEMA_MAP,
|
||
|
|
SEMA_VALENCED_FAMILY_LIST,
|
||
|
|
type ColorSignature,
|
||
|
|
type DeltaOp,
|
||
|
|
type DeltaValue,
|
||
|
|
type MotionSignature,
|
||
|
|
type PresenceSignature,
|
||
|
|
type SemaActiveChannel,
|
||
|
|
type SemaMap,
|
||
|
|
type SoundSignature
|
||
|
|
} from './sema-map'
|
||
|
|
|
||
|
|
export type {
|
||
|
|
ColorSignature,
|
||
|
|
MotionSignature,
|
||
|
|
PresenceSignature,
|
||
|
|
SemaActiveChannel,
|
||
|
|
SoundSignature
|
||
|
|
} from './sema-map'
|
||
|
|
|
||
|
|
export interface EffectiveSignature {
|
||
|
|
family: SemaFamily | undefined
|
||
|
|
intent: SemaIntent | undefined
|
||
|
|
activeChannels: readonly SemaActiveChannel[]
|
||
|
|
motion?: MotionSignature
|
||
|
|
sound?: SoundSignature
|
||
|
|
color?: ColorSignature
|
||
|
|
presence?: PresenceSignature
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Empty signature returned when the input has no family. Channels see an
|
||
|
|
* `EffectiveSignature` with no active channels and skip their work.
|
||
|
|
*/
|
||
|
|
const EMPTY_SIGNATURE: EffectiveSignature = {
|
||
|
|
family: undefined,
|
||
|
|
intent: undefined,
|
||
|
|
activeChannels: []
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveSignature(
|
||
|
|
signal: SemanticSignal,
|
||
|
|
map: SemaMap = SEMA_MAP
|
||
|
|
): EffectiveSignature {
|
||
|
|
if (!signal.family) return EMPTY_SIGNATURE
|
||
|
|
|
||
|
|
const familyEntry = map.families[signal.family]
|
||
|
|
if (!familyEntry) return EMPTY_SIGNATURE
|
||
|
|
|
||
|
|
let signature: EffectiveSignature = {
|
||
|
|
family: signal.family,
|
||
|
|
intent: signal.intent,
|
||
|
|
activeChannels: [...familyEntry.activeChannels],
|
||
|
|
motion: familyEntry.base.motion ? deepClone(familyEntry.base.motion) : undefined,
|
||
|
|
sound: familyEntry.base.sound ? deepClone(familyEntry.base.sound) : undefined,
|
||
|
|
color: familyEntry.base.color ? deepClone(familyEntry.base.color) : undefined,
|
||
|
|
presence: familyEntry.base.presence ? deepClone(familyEntry.base.presence) : undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
// Intent deltas only apply to valenced families.
|
||
|
|
if (signal.intent && SEMA_VALENCED_FAMILY_LIST.includes(signal.family as never)) {
|
||
|
|
const intentEntry = map.intents[signal.intent]
|
||
|
|
if (intentEntry) {
|
||
|
|
signature = applyIntentDeltas(signature, intentEntry.deltas)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Optional per-event sample URL pack (e.g. `'commit-affirm'` → '/sounds/...').
|
||
|
|
const eventLabel = composeEventLabel(signal.family, signal.intent)
|
||
|
|
const sampleUrl = map.soundPack[eventLabel]
|
||
|
|
if (sampleUrl && signature.sound) {
|
||
|
|
signature.sound.sampleUrl = sampleUrl
|
||
|
|
}
|
||
|
|
|
||
|
|
return signature
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Internals ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function composeEventLabel(family: SemaFamily, intent?: SemaIntent): string {
|
||
|
|
return intent ? `${family}-${intent}` : family
|
||
|
|
}
|
||
|
|
|
||
|
|
function deepClone<T>(value: T): T {
|
||
|
|
return structuredClone(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
function isDeltaOp(value: unknown): value is DeltaOp {
|
||
|
|
return isRecord(value) && typeof value.op === 'string'
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyLeaf(base: unknown, delta: DeltaValue): unknown {
|
||
|
|
if (typeof delta === 'number') {
|
||
|
|
return typeof base === 'number' ? base + delta : delta
|
||
|
|
}
|
||
|
|
if (typeof delta === 'string' || typeof delta === 'boolean' || delta === null) {
|
||
|
|
return delta
|
||
|
|
}
|
||
|
|
if (isDeltaOp(delta)) {
|
||
|
|
if (delta.op === 'replace') return delta.value
|
||
|
|
if (typeof base !== 'number') return base
|
||
|
|
if (delta.op === 'multiply') return base * delta.factor
|
||
|
|
return base + delta.value
|
||
|
|
}
|
||
|
|
if (!isRecord(delta)) return base
|
||
|
|
if (!isRecord(base)) return base
|
||
|
|
|
||
|
|
const out: Record<string, unknown> = deepClone(base)
|
||
|
|
for (const [key, nextDelta] of Object.entries(delta)) {
|
||
|
|
out[key] = applyLeaf(out[key], nextDelta as DeltaValue)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyIntentDeltas(
|
||
|
|
signature: EffectiveSignature,
|
||
|
|
deltas: Partial<Record<SemaActiveChannel, Record<string, DeltaValue>>>
|
||
|
|
): EffectiveSignature {
|
||
|
|
const next: EffectiveSignature = deepClone(signature)
|
||
|
|
for (const channel of next.activeChannels) {
|
||
|
|
const delta = deltas[channel]
|
||
|
|
if (!delta) continue
|
||
|
|
const current = next[channel]
|
||
|
|
if (!current) continue
|
||
|
|
;(next as unknown as Record<string, unknown>)[channel] = applyLeaf(current, delta)
|
||
|
|
}
|
||
|
|
return next
|
||
|
|
}
|