puck / frontend /src /engine /wire.ts
vu1n's picture
Puck — desktop fairy familiar (HF Build Small)
3c124f3
Raw
History Blame Contribute Delete
3.68 kB
// 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: "🪶",
},
};
}