Spaces:
Running
Running
| // Wire events: what external watchers POST to the daemon (see /schema/event.schema.json). | |
| // This module is the only place wire shapes become engine EventDefs — the daemon | |
| // stays dumb pipes; mapping wire types onto scoring templates is policy, so it lives here. | |
| import { EVENTS } from "./events"; | |
| import type { EventDef, EventScores, SourceId } from "./types"; | |
| export interface WireEvent { | |
| id?: string; | |
| ts?: string; | |
| source: "claude" | "build" | "mail" | "discord" | "calendar" | "browser"; | |
| type: string; | |
| title: string; | |
| summary?: string; | |
| payload?: Record<string, unknown>; | |
| } | |
| /** Keep in lockstep with event.schema.json — pinned by a parity test. */ | |
| export const WIRE_SOURCES = ["claude", "build", "mail", "discord", "calendar", "browser"] as const; | |
| export const WIRE_REQUIRED = ["source", "type", "title"] as const; | |
| export const WIRE_OPTIONAL = ["id", "ts", "summary", "payload"] as const; | |
| export function isWireEvent(v: unknown): v is WireEvent { | |
| if (typeof v !== "object" || v === null) return false; | |
| const o = v as Record<string, unknown>; | |
| return ( | |
| typeof o.source === "string" && | |
| (WIRE_SOURCES as readonly string[]).includes(o.source) && | |
| typeof o.type === "string" && | |
| o.type.length > 0 && | |
| typeof o.title === "string" && | |
| o.title.length > 0 | |
| ); | |
| } | |
| const SOURCE_IDS: Record<WireEvent["source"], SourceId> = { | |
| claude: "Claude", | |
| build: "Build", | |
| mail: "Mail", | |
| discord: "Discord", | |
| calendar: "Calendar", | |
| browser: "Browser", | |
| }; | |
| // (source, type) → the sim-deck event whose base scores fit. Real events borrow | |
| // the deck's tuned scoring + memory seeds; only the words change. | |
| // Exported for the drift test: every value must name a real deck event. | |
| export const TEMPLATES: Record<string, string> = { | |
| "claude/task_completed": "claude_done", | |
| "claude/stop": "claude_done", | |
| "claude/notification": "claude_done", | |
| "claude/permission_needed": "claude_permission", | |
| // a failed Claude turn borrows the failing-build urgency profile | |
| "claude/failed": "build_fail", | |
| "build/passed": "build_done", | |
| "build/failed": "build_fail", | |
| "build/tests_failed": "build_fail", | |
| "mail/important": "mail_important", | |
| "mail/statement": "mail_finance", | |
| "discord/mention": "discord_mention", | |
| "discord/noise": "discord_noise", | |
| "calendar/soon": "calendar_soon", | |
| "browser/stale_tab": "stale_tab", | |
| "browser/new_page": "browser_newpage", | |
| "browser/rabbit_hole": "browser_rabbithole", | |
| }; | |
| // unknown (source, type): middling everything — surfaces as a scroll/glow, never an interrupt | |
| const FALLBACK_SCORES: EventScores = { urgency: 50, relevance: 60, novelty: 55, annoyance: 30, interest: 60 }; | |
| /** Wrap a wire event in an EventDef the engine can score and voice. */ | |
| export function toEventDef(w: WireEvent): EventDef { | |
| const template = EVENTS.find((e) => e.id === TEMPLATES[`${w.source}/${w.type}`]); | |
| const line = w.summary ? `${w.title} — ${w.summary}` : w.title; | |
| return { | |
| id: w.id ?? `wire_${w.source}_${w.type}`, | |
| source: SOURCE_IDS[w.source], | |
| // real events have no sim window to fly to; the shell decides where Puck goes | |
| target: null, | |
| glyph: template?.glyph ?? "✦", | |
| title: w.title, | |
| base: template?.base ?? FALLBACK_SCORES, | |
| effect: "none", | |
| // real titles speak for themselves; tier flavor comes from the template's framing | |
| lines: { | |
| plain: line, | |
| playful: template ? `Word from the ${w.source} realm: ${line}` : line, | |
| mythic: template ? `An omen from ${w.source}: ${line}` : line, | |
| }, | |
| memory: template?.memory ?? { | |
| text: `Vu gets real ${w.source} events now. Watch how he reacts.`, | |
| type: "pattern", | |
| icon: "🪶", | |
| }, | |
| }; | |
| } | |