|
|
|
|
import { isSemaEvent, isSemaEventLabel, isSemaIntent, isSemaIntentBinding } from './event'
|
|
|
|
|
import { SEMA_FAMILY_POLICY, type SemaActionEvent, type SemaIntentBinding } from './types'
|
|
|
|
|
|
|
|
|
|
export class SemaInvariantError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = 'SemaInvariantError'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateSemaIntentBinding(binding: SemaIntentBinding, ctx = 'sema.intent'): void {
|
|
|
|
|
if (!isSemaIntentBinding(binding)) {
|
|
|
|
|
throw new SemaInvariantError(`${ctx} is not a valid SemaIntentBinding`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const supported = binding.supported ?? []
|
|
|
|
|
if (supported.length === 0) return
|
|
|
|
|
|
|
|
|
|
for (const intent of supported) {
|
|
|
|
|
if (!isSemaIntent(intent)) {
|
|
|
|
|
throw new SemaInvariantError(`${ctx}: intent "${String(intent)}" is not a valid SemaIntent`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!supported.includes(binding.default)) {
|
|
|
|
|
throw new SemaInvariantError(
|
|
|
|
|
`${ctx}: default intent "${binding.default}" must be included in supported intents`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateSemaEvent(event: SemaActionEvent, ctx = 'sema.event'): void {
|
|
|
|
|
if (isSemaEventLabel(event)) return
|
|
|
|
|
|
|
|
|
|
if (!isSemaEvent(event)) {
|
|
|
|
|
throw new SemaInvariantError(`${ctx} is not a valid canonical semantic event`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const intent = 'intent' in event ? event.intent : undefined
|
|
|
|
|
|
|
|
|
|
// Policy enforcement: families marked `intentPolicy: 'expected'` MUST
|
|
|
|
|
// declare intent.
|
|
|
|
|
if (
|
|
|
|
|
intent === undefined &&
|
|
|
|
|
SEMA_FAMILY_POLICY[event.family].intentPolicy === 'expected'
|
|
|
|
|
) {
|
|
|
|
|
throw new SemaInvariantError(
|
|
|
|
|
`${ctx}: family "${event.family}" requires intent (intentPolicy: 'expected')`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bindings need structural validation; literals + absence are fine.
|
|
|
|
|
if (intent === undefined || typeof intent === 'string') return
|
|
|
|
|
|
|
|
|
|
validateSemaIntentBinding(intent, `${ctx}.intent`)
|
|
|
|
|
}
|