Replace FeedItem.priority with signals (#39)

* feat: replace FeedItem.priority with signals

Remove priority field from FeedItem and engine-level sorting.
Add FeedItemSignals with urgency and timeRelevance fields.
Update all source packages to emit signals instead of priority.

Ranking is now the post-processing layer's responsibility.
Urgency values are unchanged from the old priority values.

Co-authored-by: Ona <no-reply@ona.com>

* fix: use TimeRelevance enum in all tests

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-02-28 12:02:57 +00:00
committed by GitHub
parent 78b0ed94bd
commit 28d26b3c87
17 changed files with 278 additions and 145 deletions

View File

@@ -1,4 +1,4 @@
import { contextValue, type Context } from "@aris/core"
import { TimeRelevance, contextValue, type Context } from "@aris/core"
import { describe, expect, test } from "bun:test"
import type { ApiCalendarEvent, GoogleCalendarClient, ListEventsOptions } from "./types"
@@ -81,16 +81,17 @@ describe("GoogleCalendarSource", () => {
expect(allDayItems.length).toBe(1)
})
test("ongoing events get highest priority (1.0)", async () => {
test("ongoing events get highest urgency (1.0)", async () => {
const source = new GoogleCalendarSource({ client: defaultMockClient() })
const items = await source.fetchItems(createContext())
const ongoing = items.find((i) => i.data.eventId === "evt-ongoing")
expect(ongoing).toBeDefined()
expect(ongoing!.priority).toBe(1.0)
expect(ongoing!.signals!.urgency).toBe(1.0)
expect(ongoing!.signals!.timeRelevance).toBe(TimeRelevance.Imminent)
})
test("upcoming events get higher priority when sooner", async () => {
test("upcoming events get higher urgency when sooner", async () => {
const source = new GoogleCalendarSource({ client: defaultMockClient() })
const items = await source.fetchItems(createContext())
@@ -99,16 +100,17 @@ describe("GoogleCalendarSource", () => {
expect(soon).toBeDefined()
expect(later).toBeDefined()
expect(soon!.priority).toBeGreaterThan(later!.priority)
expect(soon!.signals!.urgency).toBeGreaterThan(later!.signals!.urgency!)
})
test("all-day events get flat priority (0.4)", async () => {
test("all-day events get flat urgency (0.4)", async () => {
const source = new GoogleCalendarSource({ client: defaultMockClient() })
const items = await source.fetchItems(createContext())
const allDay = items.find((i) => i.data.eventId === "evt-allday")
expect(allDay).toBeDefined()
expect(allDay!.priority).toBe(0.4)
expect(allDay!.signals!.urgency).toBe(0.4)
expect(allDay!.signals!.timeRelevance).toBe(TimeRelevance.Ambient)
})
test("generates unique IDs for each item", async () => {
@@ -280,7 +282,7 @@ describe("GoogleCalendarSource", () => {
})
})
describe("priority ordering", () => {
describe("urgency ordering", () => {
test("ongoing > upcoming > all-day", async () => {
const source = new GoogleCalendarSource({ client: defaultMockClient() })
const items = await source.fetchItems(createContext())
@@ -289,8 +291,8 @@ describe("GoogleCalendarSource", () => {
const upcoming = items.find((i) => i.data.eventId === "evt-soon")!
const allDay = items.find((i) => i.data.eventId === "evt-allday")!
expect(ongoing.priority).toBeGreaterThan(upcoming.priority)
expect(upcoming.priority).toBeGreaterThan(allDay.priority)
expect(ongoing.signals!.urgency).toBeGreaterThan(upcoming.signals!.urgency!)
expect(upcoming.signals!.urgency).toBeGreaterThan(allDay.signals!.urgency!)
})
})
})

View File

@@ -1,6 +1,6 @@
import type { ActionDefinition, Context, FeedSource } from "@aris/core"
import type { ActionDefinition, Context, FeedItemSignals, FeedSource } from "@aris/core"
import { UnknownActionError } from "@aris/core"
import { TimeRelevance, UnknownActionError } from "@aris/core"
import type {
ApiCalendarEvent,
@@ -35,10 +35,10 @@ import { DefaultGoogleCalendarClient } from "./google-calendar-api"
const DEFAULT_LOOKAHEAD_HOURS = 24
const PRIORITY_ONGOING = 1.0
const PRIORITY_UPCOMING_MAX = 0.9
const PRIORITY_UPCOMING_MIN = 0.3
const PRIORITY_ALL_DAY = 0.4
const URGENCY_ONGOING = 1.0
const URGENCY_UPCOMING_MAX = 0.9
const URGENCY_UPCOMING_MIN = 0.3
const URGENCY_ALL_DAY = 0.4
/**
* A FeedSource that provides Google Calendar events and next-event context.
@@ -171,9 +171,13 @@ function parseEvent(event: ApiCalendarEvent, calendarId: string): CalendarEventD
}
}
function computePriority(event: CalendarEventData, nowMs: number, lookaheadMs: number): number {
function computeSignals(
event: CalendarEventData,
nowMs: number,
lookaheadMs: number,
): FeedItemSignals {
if (event.isAllDay) {
return PRIORITY_ALL_DAY
return { urgency: URGENCY_ALL_DAY, timeRelevance: TimeRelevance.Ambient }
}
const startMs = event.startTime.getTime()
@@ -181,17 +185,23 @@ function computePriority(event: CalendarEventData, nowMs: number, lookaheadMs: n
// Ongoing: start <= now < end
if (startMs <= nowMs && nowMs < endMs) {
return PRIORITY_ONGOING
return { urgency: URGENCY_ONGOING, timeRelevance: TimeRelevance.Imminent }
}
// Upcoming: linear decay from PRIORITY_UPCOMING_MAX to PRIORITY_UPCOMING_MIN
// Upcoming: linear decay from URGENCY_UPCOMING_MAX to URGENCY_UPCOMING_MIN
const msUntilStart = startMs - nowMs
if (msUntilStart <= 0) {
return PRIORITY_UPCOMING_MIN
return { urgency: URGENCY_UPCOMING_MIN, timeRelevance: TimeRelevance.Ambient }
}
const ratio = Math.min(msUntilStart / lookaheadMs, 1)
return PRIORITY_UPCOMING_MAX - ratio * (PRIORITY_UPCOMING_MAX - PRIORITY_UPCOMING_MIN)
const urgency = URGENCY_UPCOMING_MAX - ratio * (URGENCY_UPCOMING_MAX - URGENCY_UPCOMING_MIN)
// Within 30 minutes = imminent, otherwise upcoming
const timeRelevance =
msUntilStart <= 30 * 60 * 1000 ? TimeRelevance.Imminent : TimeRelevance.Upcoming
return { urgency, timeRelevance }
}
function createFeedItem(
@@ -199,14 +209,13 @@ function createFeedItem(
nowMs: number,
lookaheadMs: number,
): CalendarFeedItem {
const priority = computePriority(event, nowMs, lookaheadMs)
const itemType = event.isAllDay ? CalendarFeedItemType.allDay : CalendarFeedItemType.event
return {
id: `calendar-${event.calendarId}-${event.eventId}`,
type: itemType,
priority,
timestamp: new Date(nowMs),
data: event,
signals: computeSignals(event, nowMs, lookaheadMs),
}
}