feat: SignalSource abstraction — generalize signal ingestion beyond Todoist (#78)

- Add Signal + SignalSource interfaces to packages/shared-types
- TipCandidate.features widened to Record<string,number|boolean> to match Signal
- TodoistSignalSource: encapsulates fetch, cache, 401 handling, bus events, and act()
- SignalAggregator: parallel fan-out across sources with per-source failure isolation
- Recommender refactored to consume Signal[] via aggregator; source action dispatch via aggregator.act()
- ADR-0009: signal normalization strategy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 01:11:56 +00:00
parent 46dee7377e
commit e3ca3ba733
8 changed files with 289 additions and 122 deletions

View File

@@ -0,0 +1,18 @@
/** A normalized signal from any connected source */
export interface Signal {
id: string;
source: string; // e.g. 'todoist', 'google-calendar', 'manual'
kind: 'task' | 'event' | 'habit' | 'insight';
content: string;
metadata: Record<string, unknown>; // source-specific raw fields
features: Record<string, number | boolean>; // bandit-ready numeric/boolean features
timestamp: string; // ISO 8601
}
/** A pluggable data source that produces normalized signals */
export interface SignalSource {
readonly id: string;
fetchSignals(userId: string): Promise<Signal[]>;
/** Optional: perform an action on the originating system (e.g. mark task done) */
act?(userId: string, signalId: string, action: string): Promise<void>;
}

View File

@@ -18,13 +18,11 @@ export interface Tip {
/**
* A scored tip candidate flowing through the bandit pipeline.
* Extends Tip with features needed for scoring.
* features is a flexible map so new signal sources can contribute without
* schema changes — the bandit serialises them as-is.
*/
export interface TipCandidate extends Tip {
features: {
is_overdue: boolean;
task_age_days: number;
priority: number;
};
features: Record<string, number | boolean>;
}
/** POST /recommend response */

View File

@@ -2,3 +2,4 @@ export * from './http/tip.js';
export * from './http/auth.js';
export * from './http/integrations.js';
export * from './http/user.js';
export * from './http/signal.js';