mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 09:01:19 +00:00
Compare commits
8 Commits
feat/post-
...
fix/feed-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
6520926aca
|
|||
| 4c9ac2c61a | |||
| be3fc41a00 | |||
| 2e9c600e93 | |||
| d616fd52d3 | |||
| 2d7544500d | |||
| 9dc0cc3d2f | |||
| fe1d261f56 |
43
.claude/skills/gpg-commit-signing/SKILL.md
Normal file
43
.claude/skills/gpg-commit-signing/SKILL.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: gpg-commit-signing
|
||||
description: Sign git commits with GPG in non-interactive environments. Use when committing code and the `GPG_PRIVATE_KEY_PASSPHRASE` environment variable is available. Triggers on "commit", "sign commit", "GPG", "git commit -S", or any git operation requiring signed commits.
|
||||
---
|
||||
|
||||
# GPG Commit Signing
|
||||
|
||||
Sign commits in headless/non-interactive environments where `/dev/tty` is unavailable.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check whether `GPG_PRIVATE_KEY_PASSPHRASE` is set:
|
||||
|
||||
```bash
|
||||
test -n "$GPG_PRIVATE_KEY_PASSPHRASE" && echo "available" || echo "not set"
|
||||
```
|
||||
|
||||
If not set, skip signing — commit without `-S`.
|
||||
|
||||
2. Try a direct signed commit first — the environment may already have loopback pinentry configured:
|
||||
|
||||
```bash
|
||||
git commit -S -m "message"
|
||||
```
|
||||
|
||||
If this succeeds, no further steps are needed.
|
||||
|
||||
3. If step 2 fails with a `/dev/tty` error, use `--pinentry-mode loopback` via a wrapper script:
|
||||
|
||||
```bash
|
||||
printf '#!/bin/sh\ngpg --batch --pinentry-mode loopback --passphrase "$GPG_PRIVATE_KEY_PASSPHRASE" "$@"\n' > /tmp/gpg-sign.sh
|
||||
chmod +x /tmp/gpg-sign.sh
|
||||
git -c gpg.program=/tmp/gpg-sign.sh commit -S -m "message"
|
||||
rm /tmp/gpg-sign.sh
|
||||
```
|
||||
|
||||
This passes the passphrase directly to gpg on each signing invocation, bypassing the need for a configured gpg-agent.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Do not echo or log `GPG_PRIVATE_KEY_PASSPHRASE`.
|
||||
- Do not commit without `-S` when the passphrase is available — the project expects signed commits.
|
||||
- Do not leave wrapper scripts on disk after committing.
|
||||
@@ -300,6 +300,7 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
|
||||
let currentItems = items
|
||||
const allGroupedItems: ItemGroup[] = []
|
||||
const allErrors = [...errors]
|
||||
const boostScores = new Map<string, number>()
|
||||
|
||||
for (const processor of this.postProcessors) {
|
||||
const snapshot = currentItems
|
||||
@@ -322,6 +323,12 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
|
||||
if (enhancement.groupedItems?.length) {
|
||||
allGroupedItems.push(...enhancement.groupedItems)
|
||||
}
|
||||
|
||||
if (enhancement.boost) {
|
||||
for (const [id, score] of Object.entries(enhancement.boost)) {
|
||||
boostScores.set(id, (boostScores.get(id) ?? 0) + score)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sourceId = processor.name || "anonymous"
|
||||
allErrors.push({
|
||||
@@ -332,6 +339,12 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply boost reordering: positive-boost first (desc), then zero, then negative (desc).
|
||||
// Stable sort within each tier preserves original relative order.
|
||||
if (boostScores.size > 0) {
|
||||
currentItems = applyBoostOrder(currentItems, boostScores)
|
||||
}
|
||||
|
||||
// Remove stale item IDs from groups and drop empty groups
|
||||
const itemIds = new Set(currentItems.map((item) => item.id))
|
||||
const validGroups = allGroupedItems.reduce<ItemGroup[]>((acc, group) => {
|
||||
@@ -487,6 +500,47 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
function applyBoostOrder<T extends FeedItem>(items: T[], boostScores: Map<string, number>): T[] {
|
||||
const positive: T[] = []
|
||||
const neutral: T[] = []
|
||||
const negative: T[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const raw = boostScores.get(item.id)
|
||||
if (raw === undefined || raw === 0) {
|
||||
neutral.push(item)
|
||||
} else {
|
||||
const clamped = clamp(raw, -1, 1)
|
||||
if (clamped > 0) {
|
||||
positive.push(item)
|
||||
} else if (clamped < 0) {
|
||||
negative.push(item)
|
||||
} else {
|
||||
neutral.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort positive descending by boost, negative descending (least negative first, most negative last)
|
||||
positive.sort((a, b) => {
|
||||
const aScore = clamp(boostScores.get(a.id) ?? 0, -1, 1)
|
||||
const bScore = clamp(boostScores.get(b.id) ?? 0, -1, 1)
|
||||
return bScore - aScore
|
||||
})
|
||||
|
||||
negative.sort((a, b) => {
|
||||
const aScore = clamp(boostScores.get(a.id) ?? 0, -1, 1)
|
||||
const bScore = clamp(boostScores.get(b.id) ?? 0, -1, 1)
|
||||
return bScore - aScore
|
||||
})
|
||||
|
||||
return [...positive, ...neutral, ...negative]
|
||||
}
|
||||
|
||||
function buildGraph(sources: FeedSource[]): SourceGraph {
|
||||
const byId = new Map<string, FeedSource>()
|
||||
for (const source of sources) {
|
||||
|
||||
@@ -209,6 +209,163 @@ describe("FeedPostProcessor", () => {
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// BOOST
|
||||
// =============================================================================
|
||||
|
||||
describe("boost", () => {
|
||||
test("positive boost moves item before non-boosted items", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({ boost: { w2: 0.8 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w2", "w1"])
|
||||
})
|
||||
|
||||
test("negative boost moves item after non-boosted items", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({ boost: { w1: -0.5 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w2", "w1"])
|
||||
})
|
||||
|
||||
test("multiple boosted items are sorted by boost descending", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(
|
||||
createWeatherSource([
|
||||
weatherItem("w1", 20),
|
||||
weatherItem("w2", 25),
|
||||
weatherItem("w3", 30),
|
||||
]),
|
||||
)
|
||||
.registerPostProcessor(async () => ({
|
||||
boost: { w3: 0.3, w1: 0.9 },
|
||||
}))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// w1 (0.9) first, w3 (0.3) second, w2 (no boost) last
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w1", "w3", "w2"])
|
||||
})
|
||||
|
||||
test("multiple processors accumulate boost scores", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({ boost: { w1: 0.3 } }))
|
||||
.registerPostProcessor(async () => ({ boost: { w1: 0.4 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// w1 accumulated boost = 0.7, moves before w2
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w1", "w2"])
|
||||
})
|
||||
|
||||
test("accumulated boost is clamped to [-1, 1]", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(
|
||||
createWeatherSource([
|
||||
weatherItem("w1", 20),
|
||||
weatherItem("w2", 25),
|
||||
weatherItem("w3", 30),
|
||||
]),
|
||||
)
|
||||
.registerPostProcessor(async () => ({ boost: { w1: 0.8, w2: 0.9 } }))
|
||||
.registerPostProcessor(async () => ({ boost: { w1: 0.8 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// w1 accumulated = 1.6 clamped to 1, w2 = 0.9 — w1 still first
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w1", "w2", "w3"])
|
||||
})
|
||||
|
||||
test("out-of-range boost values are clamped", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({ boost: { w1: 5.0 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// Clamped to 1, still boosted to front
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w1", "w2"])
|
||||
})
|
||||
|
||||
test("boosting a suppressed item is a no-op", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({
|
||||
suppress: ["w1"],
|
||||
boost: { w1: 1.0 },
|
||||
}))
|
||||
|
||||
const result = await engine.refresh()
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].id).toBe("w2")
|
||||
})
|
||||
|
||||
test("boosting a nonexistent item ID is a no-op", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20)]))
|
||||
.registerPostProcessor(async () => ({ boost: { nonexistent: 1.0 } }))
|
||||
|
||||
const result = await engine.refresh()
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0].id).toBe("w1")
|
||||
})
|
||||
|
||||
test("items with equal boost retain original relative order", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(
|
||||
createWeatherSource([
|
||||
weatherItem("w1", 20),
|
||||
weatherItem("w2", 25),
|
||||
weatherItem("w3", 30),
|
||||
]),
|
||||
)
|
||||
.registerPostProcessor(async () => ({
|
||||
boost: { w1: 0.5, w3: 0.5 },
|
||||
}))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// w1 and w3 have equal boost — original order preserved: w1 before w3
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w1", "w3", "w2"])
|
||||
})
|
||||
|
||||
test("negative boosts preserve relative order among demoted items", async () => {
|
||||
const engine = new FeedEngine()
|
||||
.register(
|
||||
createWeatherSource([
|
||||
weatherItem("w1", 20),
|
||||
weatherItem("w2", 25),
|
||||
weatherItem("w3", 30),
|
||||
]),
|
||||
)
|
||||
.registerPostProcessor(async () => ({
|
||||
boost: { w1: -0.3, w2: -0.3 },
|
||||
}))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// w3 (neutral) first, then w1 and w2 (equal negative) in original order
|
||||
expect(result.items.map((i) => i.id)).toEqual(["w3", "w1", "w2"])
|
||||
})
|
||||
|
||||
test("boost works alongside additionalItems and groupedItems", async () => {
|
||||
const extra = calendarItem("c1", "Meeting")
|
||||
|
||||
const engine = new FeedEngine()
|
||||
.register(createWeatherSource([weatherItem("w1", 20), weatherItem("w2", 25)]))
|
||||
.registerPostProcessor(async () => ({
|
||||
additionalItems: [extra],
|
||||
boost: { c1: 1.0 },
|
||||
groupedItems: [{ itemIds: ["w1", "c1"], summary: "Related" }],
|
||||
}))
|
||||
|
||||
const result = await engine.refresh()
|
||||
// c1 boosted to front
|
||||
expect(result.items[0].id).toBe("c1")
|
||||
expect(result.items).toHaveLength(3)
|
||||
expect(result.groupedItems).toEqual([{ itemIds: ["w1", "c1"], summary: "Related" }])
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// PIPELINE ORDERING
|
||||
// =============================================================================
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface FeedEnhancement {
|
||||
groupedItems?: ItemGroup[]
|
||||
/** Item IDs to remove from the feed */
|
||||
suppress?: string[]
|
||||
/** Map of item ID to boost score (-1 to 1). Positive promotes, negative demotes. */
|
||||
boost?: Record<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("WeatherKitDataSource", () => {
|
||||
credentials: mockCredentials,
|
||||
})
|
||||
|
||||
expect(dataSource.type).toBe(WeatherFeedItemType.current)
|
||||
expect(dataSource.type).toBe(WeatherFeedItemType.Current)
|
||||
})
|
||||
|
||||
test("throws error if neither client nor credentials provided", () => {
|
||||
@@ -130,9 +130,9 @@ describe("query() with mocked client", () => {
|
||||
const items = await dataSource.query(context)
|
||||
|
||||
expect(items.length).toBeGreaterThan(0)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.current)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.hourly)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.daily)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Current)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Hourly)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Daily)).toBe(true)
|
||||
})
|
||||
|
||||
test("applies hourly and daily limits", async () => {
|
||||
@@ -145,8 +145,8 @@ describe("query() with mocked client", () => {
|
||||
|
||||
const items = await dataSource.query(context)
|
||||
|
||||
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.hourly)
|
||||
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.daily)
|
||||
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly)
|
||||
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily)
|
||||
|
||||
expect(hourlyItems.length).toBe(3)
|
||||
expect(dailyItems.length).toBe(2)
|
||||
@@ -176,8 +176,8 @@ describe("query() with mocked client", () => {
|
||||
units: Units.imperial,
|
||||
})
|
||||
|
||||
const metricCurrent = metricItems.find((i) => i.type === WeatherFeedItemType.current)
|
||||
const imperialCurrent = imperialItems.find((i) => i.type === WeatherFeedItemType.current)
|
||||
const metricCurrent = metricItems.find((i) => i.type === WeatherFeedItemType.Current)
|
||||
const imperialCurrent = imperialItems.find((i) => i.type === WeatherFeedItemType.Current)
|
||||
|
||||
expect(metricCurrent).toBeDefined()
|
||||
expect(imperialCurrent).toBeDefined()
|
||||
@@ -203,7 +203,7 @@ describe("query() with mocked client", () => {
|
||||
expect(item.signals!.timeRelevance).toBeDefined()
|
||||
}
|
||||
|
||||
const currentItem = items.find((i) => i.type === WeatherFeedItemType.current)
|
||||
const currentItem = items.find((i) => i.type === WeatherFeedItemType.Current)
|
||||
expect(currentItem).toBeDefined()
|
||||
expect(currentItem!.signals!.urgency).toBeGreaterThanOrEqual(0.5)
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ export class WeatherKitDataSource implements DataSource<WeatherFeedItem, Weather
|
||||
private readonly DEFAULT_HOURLY_LIMIT = 12
|
||||
private readonly DEFAULT_DAILY_LIMIT = 7
|
||||
|
||||
readonly type = WeatherFeedItemType.current
|
||||
readonly type = WeatherFeedItemType.Current
|
||||
private readonly client: WeatherKitClient
|
||||
private readonly hourlyLimit: number
|
||||
private readonly dailyLimit: number
|
||||
@@ -228,7 +228,7 @@ function createCurrentWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-current-${timestamp.getTime()}`,
|
||||
type: WeatherFeedItemType.current,
|
||||
type: WeatherFeedItemType.Current,
|
||||
timestamp,
|
||||
data: {
|
||||
conditionCode: current.conditionCode,
|
||||
@@ -262,7 +262,7 @@ function createHourlyWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-hourly-${timestamp.getTime()}-${index}`,
|
||||
type: WeatherFeedItemType.hourly,
|
||||
type: WeatherFeedItemType.Hourly,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastTime: new Date(hourly.forecastStart),
|
||||
@@ -296,7 +296,7 @@ function createDailyWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-daily-${timestamp.getTime()}-${index}`,
|
||||
type: WeatherFeedItemType.daily,
|
||||
type: WeatherFeedItemType.Daily,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastDate: new Date(daily.forecastStart),
|
||||
@@ -323,7 +323,7 @@ function createWeatherAlertFeedItem(alert: WeatherAlert, timestamp: Date): Weath
|
||||
|
||||
return {
|
||||
id: `weather-alert-${alert.id}`,
|
||||
type: WeatherFeedItemType.alert,
|
||||
type: WeatherFeedItemType.Alert,
|
||||
timestamp,
|
||||
data: {
|
||||
alertId: alert.id,
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { FeedItem } from "@aris/core"
|
||||
import type { Certainty, ConditionCode, PrecipitationType, Severity, Urgency } from "./weatherkit"
|
||||
|
||||
export const WeatherFeedItemType = {
|
||||
current: "weather-current",
|
||||
hourly: "weather-hourly",
|
||||
daily: "weather-daily",
|
||||
alert: "weather-alert",
|
||||
Current: "weather-current",
|
||||
Hourly: "weather-hourly",
|
||||
Daily: "weather-daily",
|
||||
Alert: "weather-alert",
|
||||
} as const
|
||||
|
||||
export type WeatherFeedItemType = (typeof WeatherFeedItemType)[keyof typeof WeatherFeedItemType]
|
||||
@@ -28,7 +28,7 @@ export type CurrentWeatherData = {
|
||||
}
|
||||
|
||||
export interface CurrentWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.current,
|
||||
typeof WeatherFeedItemType.Current,
|
||||
CurrentWeatherData
|
||||
> {}
|
||||
|
||||
@@ -49,7 +49,7 @@ export type HourlyWeatherData = {
|
||||
}
|
||||
|
||||
export interface HourlyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.hourly,
|
||||
typeof WeatherFeedItemType.Hourly,
|
||||
HourlyWeatherData
|
||||
> {}
|
||||
|
||||
@@ -68,7 +68,7 @@ export type DailyWeatherData = {
|
||||
}
|
||||
|
||||
export interface DailyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.daily,
|
||||
typeof WeatherFeedItemType.Daily,
|
||||
DailyWeatherData
|
||||
> {}
|
||||
|
||||
@@ -86,7 +86,7 @@ export type WeatherAlertData = {
|
||||
}
|
||||
|
||||
export interface WeatherAlertFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.alert,
|
||||
typeof WeatherFeedItemType.Alert,
|
||||
WeatherAlertData
|
||||
> {}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { TimeRelevance, UnknownActionError } from "@aris/core"
|
||||
import { DAVClient } from "tsdav"
|
||||
|
||||
import type { CalDavDAVClient, CalDavEventData, CalDavFeedItem } from "./types.ts"
|
||||
import { CalDavEventStatus } from "./types.ts"
|
||||
|
||||
import { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts"
|
||||
import { parseICalEvents } from "./ical-parser.ts"
|
||||
import { CalDavEventStatus, CalDavFeedItemType } from "./types.ts"
|
||||
|
||||
// -- Source options --
|
||||
|
||||
@@ -340,7 +340,7 @@ export function computeSignals(
|
||||
function createFeedItem(event: CalDavEventData, now: Date, timeZone?: string): CalDavFeedItem {
|
||||
return {
|
||||
id: `caldav-event-${event.uid}${event.recurrenceId ? `-${event.recurrenceId}` : ""}`,
|
||||
type: "caldav-event",
|
||||
type: CalDavFeedItemType.Event,
|
||||
timestamp: now,
|
||||
data: event,
|
||||
signals: computeSignals(event, now, timeZone),
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
AttendeeRole,
|
||||
AttendeeStatus,
|
||||
CalDavEventStatus,
|
||||
CalDavFeedItemType,
|
||||
type CalDavAlarm,
|
||||
type CalDavAttendee,
|
||||
type CalDavDAVCalendar,
|
||||
|
||||
@@ -64,9 +64,17 @@ export interface CalDavEventData extends Record<string, unknown> {
|
||||
recurrenceId: string | null
|
||||
}
|
||||
|
||||
// -- Feed item type --
|
||||
|
||||
export const CalDavFeedItemType = {
|
||||
Event: "caldav-event",
|
||||
} as const
|
||||
|
||||
export type CalDavFeedItemType = (typeof CalDavFeedItemType)[keyof typeof CalDavFeedItemType]
|
||||
|
||||
// -- Feed item --
|
||||
|
||||
export type CalDavFeedItem = FeedItem<"caldav-event", CalDavEventData>
|
||||
export type CalDavFeedItem = FeedItem<typeof CalDavFeedItemType.Event, CalDavEventData>
|
||||
|
||||
// -- DAV client interface --
|
||||
|
||||
|
||||
@@ -3,19 +3,19 @@ import type { FeedItem } from "@aris/core"
|
||||
import type { CalendarEventData } from "./types"
|
||||
|
||||
export const CalendarFeedItemType = {
|
||||
event: "calendar-event",
|
||||
allDay: "calendar-all-day",
|
||||
Event: "calendar-event",
|
||||
AllDay: "calendar-all-day",
|
||||
} as const
|
||||
|
||||
export type CalendarFeedItemType = (typeof CalendarFeedItemType)[keyof typeof CalendarFeedItemType]
|
||||
|
||||
export interface CalendarEventFeedItem extends FeedItem<
|
||||
typeof CalendarFeedItemType.event,
|
||||
typeof CalendarFeedItemType.Event,
|
||||
CalendarEventData
|
||||
> {}
|
||||
|
||||
export interface CalendarAllDayFeedItem extends FeedItem<
|
||||
typeof CalendarFeedItemType.allDay,
|
||||
typeof CalendarFeedItemType.AllDay,
|
||||
CalendarEventData
|
||||
> {}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("GoogleCalendarSource", () => {
|
||||
const source = new GoogleCalendarSource({ client: defaultMockClient() })
|
||||
const items = await source.fetchItems(createContext())
|
||||
|
||||
const timedItems = items.filter((i) => i.type === CalendarFeedItemType.event)
|
||||
const timedItems = items.filter((i) => i.type === CalendarFeedItemType.Event)
|
||||
expect(timedItems.length).toBe(4)
|
||||
})
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("GoogleCalendarSource", () => {
|
||||
const source = new GoogleCalendarSource({ client: defaultMockClient() })
|
||||
const items = await source.fetchItems(createContext())
|
||||
|
||||
const allDayItems = items.filter((i) => i.type === CalendarFeedItemType.allDay)
|
||||
const allDayItems = items.filter((i) => i.type === CalendarFeedItemType.AllDay)
|
||||
expect(allDayItems.length).toBe(1)
|
||||
})
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ function createFeedItem(
|
||||
nowMs: number,
|
||||
lookaheadMs: number,
|
||||
): CalendarFeedItem {
|
||||
const itemType = event.isAllDay ? CalendarFeedItemType.allDay : CalendarFeedItemType.event
|
||||
const itemType = event.isAllDay ? CalendarFeedItemType.AllDay : CalendarFeedItemType.Event
|
||||
|
||||
return {
|
||||
id: `calendar-${event.calendarId}-${event.eventId}`,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export { NextEventKey, type NextEvent } from "./calendar-context"
|
||||
export {
|
||||
CalendarFeedItemType,
|
||||
type CalendarFeedItemType as CalendarFeedItemTypeType,
|
||||
type CalendarAllDayFeedItem,
|
||||
type CalendarEventFeedItem,
|
||||
type CalendarFeedItem,
|
||||
@@ -10,7 +9,6 @@ export { DefaultGoogleCalendarClient } from "./google-calendar-api"
|
||||
export { GoogleCalendarSource, type GoogleCalendarSourceOptions } from "./google-calendar-source"
|
||||
export {
|
||||
EventStatus,
|
||||
type EventStatus as EventStatusType,
|
||||
type ApiCalendarEvent,
|
||||
type ApiEventDateTime,
|
||||
type CalendarEventData,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
export { TflSource } from "./tfl-source.ts"
|
||||
export { TflApi } from "./tfl-api.ts"
|
||||
export type { TflLineId } from "./tfl-api.ts"
|
||||
export type {
|
||||
ITflApi,
|
||||
StationLocation,
|
||||
TflAlertData,
|
||||
TflAlertFeedItem,
|
||||
TflAlertSeverity,
|
||||
TflLineStatus,
|
||||
TflSourceOptions,
|
||||
export {
|
||||
TflFeedItemType,
|
||||
type ITflApi,
|
||||
type StationLocation,
|
||||
type TflAlertData,
|
||||
type TflAlertFeedItem,
|
||||
type TflAlertSeverity,
|
||||
type TflLineStatus,
|
||||
type TflSourceOptions,
|
||||
} from "./types.ts"
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from "./types.ts"
|
||||
|
||||
import { TflApi, lineId } from "./tfl-api.ts"
|
||||
import { TflFeedItemType } from "./types.ts"
|
||||
|
||||
const setLinesInput = lineId.array()
|
||||
|
||||
@@ -150,7 +151,7 @@ export class TflSource implements FeedSource<TflAlertFeedItem> {
|
||||
|
||||
return {
|
||||
id: `tfl-alert-${status.lineId}-${status.severity}`,
|
||||
type: "tfl-alert",
|
||||
type: TflFeedItemType.Alert,
|
||||
timestamp: context.time,
|
||||
data,
|
||||
signals,
|
||||
|
||||
@@ -20,7 +20,13 @@ export interface TflAlertData extends Record<string, unknown> {
|
||||
closestStationDistance: number | null
|
||||
}
|
||||
|
||||
export type TflAlertFeedItem = FeedItem<"tfl-alert", TflAlertData>
|
||||
export const TflFeedItemType = {
|
||||
Alert: "tfl-alert",
|
||||
} as const
|
||||
|
||||
export type TflFeedItemType = (typeof TflFeedItemType)[keyof typeof TflFeedItemType]
|
||||
|
||||
export type TflAlertFeedItem = FeedItem<typeof TflFeedItemType.Alert, TflAlertData>
|
||||
|
||||
export interface TflSourceOptions {
|
||||
apiKey?: string
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { FeedItem } from "@aris/core"
|
||||
import type { Certainty, ConditionCode, PrecipitationType, Severity, Urgency } from "./weatherkit"
|
||||
|
||||
export const WeatherFeedItemType = {
|
||||
current: "weather-current",
|
||||
hourly: "weather-hourly",
|
||||
daily: "weather-daily",
|
||||
alert: "weather-alert",
|
||||
Current: "weather-current",
|
||||
Hourly: "weather-hourly",
|
||||
Daily: "weather-daily",
|
||||
Alert: "weather-alert",
|
||||
} as const
|
||||
|
||||
export type WeatherFeedItemType = (typeof WeatherFeedItemType)[keyof typeof WeatherFeedItemType]
|
||||
@@ -28,7 +28,7 @@ export type CurrentWeatherData = {
|
||||
}
|
||||
|
||||
export interface CurrentWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.current,
|
||||
typeof WeatherFeedItemType.Current,
|
||||
CurrentWeatherData
|
||||
> {}
|
||||
|
||||
@@ -49,7 +49,7 @@ export type HourlyWeatherData = {
|
||||
}
|
||||
|
||||
export interface HourlyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.hourly,
|
||||
typeof WeatherFeedItemType.Hourly,
|
||||
HourlyWeatherData
|
||||
> {}
|
||||
|
||||
@@ -68,7 +68,7 @@ export type DailyWeatherData = {
|
||||
}
|
||||
|
||||
export interface DailyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.daily,
|
||||
typeof WeatherFeedItemType.Daily,
|
||||
DailyWeatherData
|
||||
> {}
|
||||
|
||||
@@ -86,7 +86,7 @@ export type WeatherAlertData = {
|
||||
}
|
||||
|
||||
export interface WeatherAlertFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.alert,
|
||||
typeof WeatherFeedItemType.Alert,
|
||||
WeatherAlertData
|
||||
> {}
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
export { WeatherKey, type Weather } from "./weather-context"
|
||||
export {
|
||||
WeatherSource,
|
||||
Units,
|
||||
type Units as UnitsType,
|
||||
type WeatherSourceOptions,
|
||||
} from "./weather-source"
|
||||
export { WeatherSource, Units, type WeatherSourceOptions } from "./weather-source"
|
||||
|
||||
export {
|
||||
WeatherFeedItemType,
|
||||
type WeatherFeedItemType as WeatherFeedItemTypeType,
|
||||
type WeatherFeedItem,
|
||||
type CurrentWeatherFeedItem,
|
||||
type CurrentWeatherData,
|
||||
@@ -27,11 +21,6 @@ export {
|
||||
Certainty,
|
||||
PrecipitationType,
|
||||
DefaultWeatherKitClient,
|
||||
type ConditionCode as ConditionCodeType,
|
||||
type Severity as SeverityType,
|
||||
type Urgency as UrgencyType,
|
||||
type Certainty as CertaintyType,
|
||||
type PrecipitationType as PrecipitationTypeType,
|
||||
type WeatherKitClient,
|
||||
type WeatherKitCredentials,
|
||||
type WeatherKitQueryOptions,
|
||||
|
||||
@@ -110,9 +110,9 @@ describe("WeatherSource", () => {
|
||||
const items = await source.fetchItems(context)
|
||||
|
||||
expect(items.length).toBeGreaterThan(0)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.current)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.hourly)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.daily)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Current)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Hourly)).toBe(true)
|
||||
expect(items.some((i) => i.type === WeatherFeedItemType.Daily)).toBe(true)
|
||||
})
|
||||
|
||||
test("applies hourly and daily limits", async () => {
|
||||
@@ -125,8 +125,8 @@ describe("WeatherSource", () => {
|
||||
|
||||
const items = await source.fetchItems(context)
|
||||
|
||||
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.hourly)
|
||||
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.daily)
|
||||
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly)
|
||||
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily)
|
||||
|
||||
expect(hourlyItems.length).toBe(3)
|
||||
expect(dailyItems.length).toBe(2)
|
||||
@@ -158,7 +158,7 @@ describe("WeatherSource", () => {
|
||||
expect(item.signals!.timeRelevance).toBeDefined()
|
||||
}
|
||||
|
||||
const currentItem = items.find((i) => i.type === WeatherFeedItemType.current)
|
||||
const currentItem = items.find((i) => i.type === WeatherFeedItemType.Current)
|
||||
expect(currentItem).toBeDefined()
|
||||
expect(currentItem!.signals!.urgency).toBeGreaterThanOrEqual(0.5)
|
||||
})
|
||||
|
||||
@@ -291,7 +291,7 @@ function createCurrentWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-current-${timestamp.getTime()}`,
|
||||
type: WeatherFeedItemType.current,
|
||||
type: WeatherFeedItemType.Current,
|
||||
timestamp,
|
||||
data: {
|
||||
conditionCode: current.conditionCode,
|
||||
@@ -325,7 +325,7 @@ function createHourlyWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-hourly-${timestamp.getTime()}-${index}`,
|
||||
type: WeatherFeedItemType.hourly,
|
||||
type: WeatherFeedItemType.Hourly,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastTime: new Date(hourly.forecastStart),
|
||||
@@ -359,7 +359,7 @@ function createDailyWeatherFeedItem(
|
||||
|
||||
return {
|
||||
id: `weather-daily-${timestamp.getTime()}-${index}`,
|
||||
type: WeatherFeedItemType.daily,
|
||||
type: WeatherFeedItemType.Daily,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastDate: new Date(daily.forecastStart),
|
||||
@@ -386,7 +386,7 @@ function createWeatherAlertFeedItem(alert: WeatherAlert, timestamp: Date): Weath
|
||||
|
||||
return {
|
||||
id: `weather-alert-${alert.id}`,
|
||||
type: WeatherFeedItemType.alert,
|
||||
type: WeatherFeedItemType.Alert,
|
||||
timestamp,
|
||||
data: {
|
||||
alertId: alert.id,
|
||||
|
||||
Reference in New Issue
Block a user