28 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	Integration Point Refactor Analysis
Author: Matthew Raymer Date: 2025-10-29 Status: 🎯 ANALYSIS - Architectural refactoring proposal
Objective
Refactor the Daily Notification Plugin architecture so that TimeSafari-specific integration logic is implemented by the Capacitor host app rather than hardcoded in the plugin. This makes the plugin generic and reusable for other applications.
Current Architecture
Plugin-Side Integration (Current State)
The plugin currently contains TimeSafari-specific code:
- 
TimeSafariIntegrationManager.java- Manages API server URL and DID lifecycle
 - Coordinates fetching and scheduling
 - Converts TimeSafari data to 
NotificationContent 
 - 
EnhancedDailyNotificationFetcher.java- Makes HTTP calls to TimeSafari API endpoints:
POST /api/v2/offers/personPOST /api/v2/offers/plansPOST /api/v2/report/plansLastUpdatedBetween
 - Handles JWT authentication
 - Parses TimeSafari response formats
 
 - Makes HTTP calls to TimeSafari API endpoints:
 - 
DailyNotificationJWTManager.java- Generates JWT tokens for TimeSafari API authentication
 - Manages JWT expiration and refresh
 
 - 
TimeSafari Data Models
- Plugin imports 
PlanSummary,OffersResponse,TimeSafariNotificationBundle - Converts TimeSafari data structures to 
NotificationContent[] 
 - Plugin imports 
 
Current Data Flow
Host App (Capacitor)
  ↓ configure() with activeDidIntegration config
Plugin (DailyNotificationPlugin)
  ↓ initializes TimeSafariIntegrationManager
TimeSafariIntegrationManager
  ↓ creates EnhancedDailyNotificationFetcher
EnhancedDailyNotificationFetcher
  ↓ makes HTTP calls with JWT
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
EnhancedDailyNotificationFetcher
  ↓ converts to NotificationContent[]
TimeSafariIntegrationManager
  ↓ saves & schedules
Plugin Storage & Scheduler
Proposed Architecture
App-Side Integration (Target State)
The plugin becomes a generic notification scheduler that accepts a content fetcher callback from the host app:
- Plugin exposes interface for content fetching
 - Host app implements the TimeSafari API integration
 - Plugin calls app's implementation when fetching is needed
 - Plugin remains generic - no TimeSafari-specific code
 
Proposed Data Flow
Host App (Capacitor)
  ↓ implements ContentFetcher interface
  ↓ provides fetcher callback to plugin
Plugin (DailyNotificationPlugin)
  ↓ calls app's fetcher callback
Host App's TimeSafari Integration
  ↓ makes HTTP calls, handles JWT, parses responses
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
Host App's Integration
  ↓ converts to NotificationContent[]
  ↓ returns to plugin
Plugin
  ↓ saves & schedules (no TimeSafari knowledge)
Plugin Storage & Scheduler
What Needs to Change
1. Plugin Interface Changes
New TypeScript Interface
/**
 * Content fetcher callback that host app must implement
 */
export interface NotificationContentFetcher {
  /**
   * Fetch notification content from external source
   * 
   * This is called by the plugin when:
   * - Background fetch work is triggered
   * - Prefetch is scheduled before notification time
   * - Manual refresh is requested
   * 
   * @param context Context about why fetch was triggered
   * @returns Promise with notification content array
   */
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}
export interface FetchContext {
  /**
   * Why the fetch was triggered
   */
  trigger: 'background_work' | 'prefetch' | 'manual' | 'scheduled';
  
  /**
   * Scheduled notification time (millis since epoch)
   */
  scheduledTime?: number;
  
  /**
   * When fetch was triggered (millis since epoch)
   */
  fetchTime: number;
  
  /**
   * Any additional context from plugin
   */
  metadata?: Record<string, unknown>;
}
New Plugin Method
export interface DailyNotificationPlugin {
  // ... existing methods ...
  
  /**
   * Register content fetcher callback from host app
   * 
   * @param fetcher Implementation provided by host app
   */
  setContentFetcher(fetcher: NotificationContentFetcher): void;
}
2. Android Plugin Changes
New Java Interface
/**
 * Content fetcher interface for host app to implement
 */
public interface NotificationContentFetcher {
    /**
     * Fetch notification content from external source
     * 
     * Called by plugin when background fetch is needed.
     * Host app implements TimeSafari API calls here.
     * 
     * @param context Fetch context with trigger info
     * @return Future with list of notification content
     */
    CompletableFuture<List<NotificationContent>> fetchContent(
        FetchContext context
    );
}
public class FetchContext {
    public final String trigger; // "background_work", "prefetch", "manual", "scheduled"
    public final Long scheduledTime; // Optional: when notification is scheduled
    public final long fetchTime; // When fetch was triggered
    public final Map<String, Object> metadata; // Additional context
    
    // Constructor, getters...
}
Plugin Implementation
public class DailyNotificationPlugin extends Plugin {
    private @Nullable NotificationContentFetcher contentFetcher;
    
    @PluginMethod
    public void setContentFetcher(PluginCall call) {
        // Receive callback reference from JavaScript
        // Store for use in background workers
        // Implementation depends on how to bridge JS -> Java callback
    }
    
    // In DailyNotificationFetchWorker:
    private NotificationContent fetchContentWithTimeout() {
        if (contentFetcher == null) {
            // Fallback: return null, use cached content
            return null;
        }
        
        FetchContext context = new FetchContext(
            "background_work",
            scheduledTime,
            System.currentTimeMillis(),
            metadata
        );
        
        try {
            List<NotificationContent> results = 
                contentFetcher.fetchContent(context).get(30, TimeUnit.SECONDS);
            return results.isEmpty() ? null : results.get(0);
        } catch (Exception e) {
            // Handle timeout, errors
            return null;
        }
    }
}
3. What Gets Removed from Plugin
Delete These Classes:
TimeSafariIntegrationManager.java- Move logic to host appEnhancedDailyNotificationFetcher.java- Move to host app- TimeSafari-specific data model imports
 
Keep These (Generic):
DailyNotificationStorage- Generic storageDailyNotificationScheduler- Generic schedulingDailyNotificationPlugin- Core plugin (modified)DailyNotificationFetchWorker- Modified to call callbackNotificationContent- Generic data model
4. Host App Implementation
TypeScript Implementation
import { DailyNotification, NotificationContentFetcher, FetchContext } 
  from '@timesafari/daily-notification-plugin';
import { TimeSafariAPI } from './timesafari-api'; // Host app's API client
class TimeSafariContentFetcher implements NotificationContentFetcher {
  constructor(
    private api: TimeSafariAPI,
    private activeDid: string
  ) {}
  
  async fetchContent(context: FetchContext): Promise<NotificationContent[]> {
    // 1. Generate JWT for authentication
    const jwt = await this.api.generateJWT(this.activeDid);
    
    // 2. Fetch TimeSafari data
    const [offersToPerson, offersToProjects, projectUpdates] = 
      await Promise.all([
        this.api.fetchOffersToPerson(jwt),
        this.api.fetchOffersToProjects(jwt),
        this.api.fetchProjectsLastUpdated(this.starredPlanIds, jwt)
      ]);
    
    // 3. Convert TimeSafari data to NotificationContent[]
    const contents: NotificationContent[] = [];
    
    // Convert offers to person
    if (offersToPerson?.data) {
      for (const offer of offersToPerson.data) {
        contents.push({
          id: `offer_${offer.id}`,
          title: `New Offer: ${offer.title}`,
          body: offer.description || '',
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: offer.imageUrl
        });
      }
    }
    
    // Convert project updates
    if (projectUpdates?.data) {
      for (const update of projectUpdates.data) {
        contents.push({
          id: `project_${update.planSummary.jwtId}`,
          title: `${update.planSummary.name} Updated`,
          body: `New updates for ${update.planSummary.name}`,
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: update.planSummary.image
        });
      }
    }
    
    return contents;
  }
}
// In host app initialization:
const plugin = new DailyNotification();
const fetcher = new TimeSafariContentFetcher(apiClient, activeDid);
plugin.setContentFetcher(fetcher);
Android Implementation (if needed)
If you need native Android implementation (for WorkManager background tasks):
// In host app's native code
class TimeSafariContentFetcher : NotificationContentFetcher {
    private val apiClient: TimeSafariAPIClient
    private val activeDid: String
    
    override fun fetchContent(context: FetchContext): CompletableFuture<List<NotificationContent>> {
        return CompletableFuture.supplyAsync {
            // Make TimeSafari API calls
            // Convert to NotificationContent[]
            // Return list
        }
    }
}
Implementation Challenges
Challenge 1: JavaScript → Java Callback Bridge
Problem: Capacitor plugins need to bridge JavaScript callbacks to Java for background workers.
Solutions:
- 
Bridge via Plugin Method (Simplest)
- Host app calls plugin method with callback ID
 - Plugin stores callback reference
 - Plugin calls back via Capacitor bridge when needed
 - Limitations: Callback must be serializable
 
 - 
Bridge via Event System (More Flexible)
- Plugin emits events when fetch needed
 - Host app listens and responds
 - Plugin waits for response
 - More complex but more flexible
 
 - 
Native Implementation (Best Performance)
- Host app provides native Java/Kotlin implementation
 - Direct Java interface (no JS bridge needed)
 - Best for background work
 - Requires host app to include native code
 
 
Recommendation: Start with Option 1 (Plugin Method), add Option 3 (Native) later if needed.
Challenge 2: Background Worker Execution
Problem: Background WorkManager workers need to call host app's fetcher, but WorkManager runs in separate process.
Solutions:
- 
Store callback reference in SharedPreferences (Limited)
- Serialize callback config, not actual callback
 - Host app provides API endpoint URL + auth info
 - Plugin makes HTTP call to host app's endpoint
 - Host app handles TimeSafari logic server-side
 
 - 
Use Capacitor Background Tasks (Platform-specific)
- iOS: BGTaskScheduler
 - Android: WorkManager with Capacitor bridge
 - Bridge JS callback from background task
 - Complex but works
 
 - 
Native Background Implementation (Recommended)
- Host app provides native Java/Kotlin fetcher
 - No JS bridge needed in background
 - Direct Java interface
 - Best performance
 
 
Recommendation: Option 3 (Native) for background work, Option 1 for foreground.
Challenge 3: Data Model Conversion
Problem: Plugin's generic NotificationContent vs TimeSafari's specific data structures.
Solution:
- Host app owns conversion logic
 - Plugin never sees TimeSafari data structures
 - Plugin only knows about 
NotificationContent[] 
This is actually simpler than current architecture.
Challenge 4: Configuration Management
Problem: Plugin currently manages TimeSafari config (apiServer, activeDid, starredPlanIds).
Solution:
- Host app manages all TimeSafari config
 - Plugin receives only fetcher callback
 - Host app provides config to fetcher, not plugin
 - Plugin becomes stateless regarding TimeSafari
 
Migration Path
Phase 1: Add Callback Interface (Non-Breaking)
- Add 
setContentFetcher()method to plugin - Add callback interface definitions
 - Keep existing TimeSafari integration as fallback
 - Allow host app to opt-in to callback approach
 
Phase 2: Move TimeSafari Code to Host App
- Create 
TimeSafariContentFetcherin host app - Migrate 
EnhancedDailyNotificationFetcherlogic - Migrate JWT generation logic
 - Test with callback approach
 
Phase 3: Remove Plugin Integration (Breaking)
- Remove 
TimeSafariIntegrationManagerfrom plugin - Remove 
EnhancedDailyNotificationFetcherfrom plugin - Remove TimeSafari data model imports
 - Make callback approach required
 - Update documentation
 
Effort Estimation
Plugin Changes
- Add callback interface: 4-8 hours
 - Modify fetch worker: 4-8 hours
 - Update documentation: 2-4 hours
 - Testing: 8-16 hours
 - Total: 18-36 hours
 
Host App Changes
- Create TimeSafariContentFetcher: 8-16 hours
 - Migrate API client logic: 8-16 hours
 - Migrate JWT logic: 4-8 hours
 - Testing: 8-16 hours
 - Total: 28-56 hours
 
Total Effort
Estimated: 46-92 hours (6-12 days) for complete migration
Benefits
- Generic Plugin: Plugin can be used by any app, not just TimeSafari
 - Clear Separation: Plugin handles scheduling, app handles fetching
 - Easier Testing: Mock fetcher for unit tests
 - Better Maintainability: TimeSafari changes don't require plugin updates
 - Flexibility: Host app controls all API integration details
 
Risks
- Breaking Changes: Requires migration of host app
 - Complexity: Callback bridge adds complexity
 - Performance: JavaScript bridge may be slower for background work
 - Testing Burden: More complex integration testing
 
Recommendations
Short Term (Keep Current Architecture)
Recommendation: Keep current architecture for now, add callback interface as optional enhancement.
Rationale:
- Current architecture works
 - Migration is significant effort
 - Benefits may not justify costs yet
 - Can add callback interface without removing existing code
 
Long Term (Full Refactor)
Recommendation: Plan for full refactor when:
- Plugin needs to support multiple apps
 - TimeSafari API changes frequently
 - Plugin team wants to decouple from TimeSafari
 
Approach:
- Phase 1: Add callback interface alongside existing code
 - Migrate TimeSafari app to use callbacks
 - Phase 3: Remove old integration code
 
Alternative: Hybrid Approach
Keep TimeSafari integration in plugin but make it optional and pluggable:
interface DailyNotificationPlugin {
  // Current approach (TimeSafari-specific)
  configure(options: ConfigureOptions): void;
  
  // New approach (generic callback)
  setContentFetcher(fetcher: NotificationContentFetcher): void;
  
  // Plugin uses callback if provided, falls back to TimeSafari integration
}
This allows:
- Existing apps continue working (no breaking changes)
 - New apps can use callback approach
 - Plugin gradually becomes generic
 
Questions to Resolve
- 
Background Work: How to call JS callback from background WorkManager?
- Native implementation required?
 - Or HTTP callback to host app endpoint?
 
 - 
Error Handling: How does plugin handle fetcher errors?
- Retry logic?
 - Fallback to cached content?
 - Event notifications?
 
 - 
Configuration: Where does TimeSafari config live?
- Host app only?
 - Or plugin still needs some config?
 
 - 
Testing: How to test plugin without TimeSafari integration?
- Mock fetcher interface?
 - Test fixtures?
 
 
Improvement Directive Integration
This analysis has been superseded by the Improvement Directive — Daily Notification Plugin "Integration Point" Refactor (see below). The directive provides concrete, implementable specifications with clear interfaces, test contracts, and migration paths.
Key Directive Decisions
- Dual-Path SPI: Native Fetcher (required for background) + Optional JS Fetcher (foreground only)
 - Background Reliability: Native SPI only; JS fetcher explicitly disabled in background workers
 - Reversibility: Legacy TimeSafari code behind feature flag for one minor release
 - Test Contract: Single JSON fixture contract used by both native and JS paths
 
See full directive below for complete specifications.
Improvement Directive — Daily Notification Plugin "Integration Point" Refactor
Audience: Plugin & TimeSafari app teams
Goal: Convert the analysis into an implementable, low-risk refactor with crisp interfaces, background-compatibility, testability, and a reversible migration path.
Source reviewed: docs/integration-point-refactor-analysis.md
A) Decision Record (ADR-001): Architecture Shape
Decision: Ship a dual-path SPI where the plugin exposes:
- a Native Fetcher SPI (Android/Kotlin, iOS/Swift) for background reliability, and
 - an Optional JS Fetcher for foreground/manual refresh & prototyping.
 
Why: Your analysis correctly targets decoupling, but the WorkManager/BGTaskScheduler reality means native fetchers are the only dependable background path. The JS callback remains useful for manual refresh, prefetch while the app is foregrounded, or rapid iteration.
Reversibility: Keep the legacy TimeSafari path behind a feature flag for one minor release; remove in the next major.
Exit Criteria: Both fetcher paths produce identical NotificationContent[] and pass the same test contract.
B) Public Interfaces (Final Form)
B1. TypeScript (Host App)
// core types
export interface NotificationContent {
  id: string;
  title: string;
  body?: string;
  scheduledTime?: number;  // epoch ms
  fetchTime: number;       // epoch ms
  mediaUrl?: string;
  ttlSeconds?: number;     // cache validity
  dedupeKey?: string;      // for idempotency
  priority?: 'min'|'low'|'default'|'high'|'max';
  metadata?: Record<string, unknown>;
}
export type FetchTrigger = 'background_work' | 'prefetch' | 'manual' | 'scheduled';
export interface FetchContext {
  trigger: FetchTrigger;
  scheduledTime?: number;
  fetchTime: number;
  metadata?: Record<string, unknown>;
}
export interface JsNotificationContentFetcher {
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}
Plugin API Additions
export interface DailyNotificationPlugin {
  setJsContentFetcher(fetcher: JsNotificationContentFetcher): void;   // foreground-only
  enableNativeFetcher(enable: boolean): Promise<void>;                // default true
  setPolicy(policy: SchedulingPolicy): Promise<void>;                 // see §C2
}
B2. Android SPI (Host App native)
interface NativeNotificationContentFetcher {
    suspend fun fetchContent(context: FetchContext): List<NotificationContent>
}
data class FetchContext(
    val trigger: String,                 // enum mirror
    val scheduledTime: Long?,            // epoch ms
    val fetchTime: Long,                 // epoch ms
    val metadata: Map<String, Any?> = emptyMap()
)
Registration: Host app injects an implementation via a generated ServiceLoader or explicit setter in Application.onCreate():
DailyNotification.setNativeFetcher(TimeSafariNativeFetcher())
Note: JS fetcher remains available but is not used by background workers.
C) Scheduling & Policy Guardrails
C1. Background Reliability
- Android: WorkManager + 
setRequiredNetworkType(CONNECTED), exponential backoff, content URI triggers optional, no JS bridging in background. - iOS: 
BGAppRefreshTask+BGProcessingTaskwith earliestBeginDate windows; keep tasks short (< 30s), aggregate fetches. 
C2. SchedulingPolicy
export interface SchedulingPolicy {
  prefetchWindowMs?: number;        // how early to prefetch before schedule
  retryBackoff: { minMs: number; maxMs: number; factor: number; jitterPct: number };
  maxBatchSize?: number;            // upper bound per run
  dedupeHorizonMs?: number;         // window for dedupeKey suppression
  cacheTtlSeconds?: number;         // default if item.ttlSeconds absent
  exactAlarmsAllowed?: boolean;     // guarded by OS version & battery policy
}
C3. Idempotency & De-duplication
- The plugin maintains a recent keys bloom/filter using 
dedupeKey(fallbackid) fordedupeHorizonMs. - No notification is enqueued if 
(dedupeKey, title, body)unchanged within horizon. 
D) Security & Config Partition
D1. Secrets & Tokens
- JWT generation lives in the host app, not the plugin. For extra hardening consider DPoP or JWE-wrapped access tokens if your API supports it.
 - Rotate keys; keep key refs in host app secure storage; never serialize tokens through JS bridges in background.
 
D2. Least-Knowledge Plugin
- Plugin stores only generic 
NotificationContent. - No TimeSafari DTOs in plugin artifacts. (As your analysis recommends.)
 
E) Failure Taxonomy & Handling
| Class | Examples | Plugin Behavior | 
|---|---|---|
| Retryable | 5xx, network, timeouts | Backoff per policy; stop on maxAttempts | 
| Unretryable | 4xx auth, schema error | Log & surface event; skip batch; keep schedule | 
| Partial | Some items bad | Enqueue valid subset; metric + structured error | 
| Over-quota | 429 | Honor Retry-After if present; otherwise backoff | 
Cache fallback: If fetch fails, reuse unexpired cached contents (by item ttlSeconds or default policy).
F) Metrics, Logs, Events
- Metrics: 
fetch_duration_ms,fetch_success,fetch_fail_class,items_fetched,items_enqueued,deduped_count,cache_hits. - Structured logs: Correlate by 
run_id, includetrigger, backoff stage, and per-itemdedupeKey. - Event bus: Emit 
onFetchStart,onFetchEnd,onFetchError,onEnqueue(debug UI can subscribe). 
G) Test Contract & Fixtures
G1. Golden Contract
A single JSON fixture contract used by both native & JS fetchers:
{
  "context": {"trigger":"scheduled","scheduledTime":1730179200000,"fetchTime":1730175600000},
  "output": [
    {"id":"offer_123","title":"New Offer","body":"...","fetchTime":1730175600000,"dedupeKey":"offer_123","ttlSeconds":86400}
  ]
}
- CI runs the same contract through (1) JS fetcher (foreground test harness) and (2) Native fetcher (instrumented test) and compares normalized outputs.
 
G2. Failure Sims
- Network chaos (drop, latency), 401→refresh→200, 429 with 
Retry-After, malformed item (should be skipped, not crash). 
H) Migration Plan (Minor → Major)
Minor (X.Y):
- Add 
setJsContentFetcher,enableNativeFetcher,setPolicy. - Implement Native Fetcher SPI & plugin side registry.
 - Keep legacy TimeSafari code behind 
useLegacyIntegrationflag (default false). 
Major (X+1.0):
- Remove legacy TimeSafari classes (
TimeSafariIntegrationManager,EnhancedDailyNotificationFetcher, etc.). - Publish migration guide & codemod mapping old config → SPI registration.
 
I) Pull-Request Breakdown (7 PRs)
- PR1 — SPI Shell: Kotlin/Swift interfaces, TS types, 
SchedulingPolicy, no behavior change. - PR2 — Background Workers: Wire Native SPI in WorkManager/BGTasks; metrics skeleton; cache store.
 - PR3 — JS Fetcher Path: Foreground bridge, event bus, dev harness screen.
 - PR4 — Dedupe/Idempotency: Bloom/filter store + tests; 
dedupeKeyenforcement. - PR5 — Failure Policy: Retry taxonomy, backoff, cache fallback; structured errors.
 - PR6 — Docs & Samples: Minimal host app examples (JS + Kotlin), migration notes.
 - PR7 — Feature Flag Legacy: Guard & regression tests; changelog for major removal.
 
J) Acceptance Criteria (Definition of Done)
- Background Reliability: Native fetcher runs with app killed; JS fetcher never required for background.
 - Parity: Same inputs → same 
NotificationContent[]via JS & Native paths (contract tests green). - No TimeSafari in Plugin: No imports, types, or endpoints leak into plugin modules.
 - Safety: No notifications emitted twice within 
dedupeHorizonMsfor identicaldedupeKey. - Observability: Metrics visible; error classes distinguish retryable vs. not; logs correlated by run.
 - Docs: Host app integration guide includes copy-paste Kotlin & TS skeletons; migration guide from legacy.
 - Battery & Quotas: Backoff respected; tasks kept <30s; policy toggles documented; exact alarms gated.
 
K) Open Questions (Resolve Before PR3)
- iOS capabilities: Which BG task identifiers and permitted intervals will Product accept (refresh vs processing)?
 - Token Hardening: Do we want DPoP or short-lived JWE now or later?
 - User Controls: Should we surface a per-plan "snooze/mute" map inside 
metadatawith plugin-side persistence? 
L) Example Skeletons
L1. Android Native Fetcher (Host App)
class TimeSafariNativeFetcher(
    private val api: TimeSafariApi,
    private val tokenProvider: TokenProvider
) : NativeNotificationContentFetcher {
    override suspend fun fetchContent(context: FetchContext): List<NotificationContent> {
        val jwt = tokenProvider.freshToken()
        val updates = api.updates(jwt)     // domain-specific
        val offers  = api.offers(jwt)
        return buildList {
            for (o in offers) add(
                NotificationContent(
                    id = "offer_${o.id}",
                    title = "New Offer: ${o.title}",
                    body = o.description ?: "",
                    scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000),
                    fetchTime = context.fetchTime,
                    mediaUrl = o.imageUrl,
                    ttlSeconds = 86_400,
                    dedupeKey = "offer_${o.id}"
                )
            )
            // … map updates similarly …
        }
    }
}
L2. JS Fetcher Registration (Foreground Only)
const fetcher: JsNotificationContentFetcher = {
  async fetchContent(ctx) {
    const jwt = await api.generateJWT(activeDid);
    const { offers, updates } = await api.fetchEverything(jwt);
    return mapToNotificationContent(offers, updates, ctx);
  }
};
DailyNotification.setJsContentFetcher(fetcher);
await DailyNotification.setPolicy({
  retryBackoff: { minMs: 2000, maxMs: 600000, factor: 2, jitterPct: 20 },
  dedupeHorizonMs: 24*60*60*1000,
  cacheTtlSeconds: 6*60*60
});
M) Docs to Produce
- INTEGRATION_GUIDE.md (host app: Native vs JS, when to use which, copy-paste snippets).
 - SCHEDULING_POLICY.md (all knobs with OS caveats).
 - MIGRATION_GUIDE_X→X+1.md (flag removal plan, old→new mapping).
 - TEST_CONTRACTS.md (fixtures, CI commands).
 
N) Risk Register & Mitigations
- Risk: JS callback used in background → fetch starves. Mitigation: Disable in background by design; lint rule + runtime guard.
 - Risk: Duplicate notifications.
Mitigation: 
dedupeKey+ horizon filter; contract tests to assert no dupes. - Risk: Token leakage through JS. Mitigation: Tokens stay in native path for background; JS path only in foreground; redact logs.
 - Risk: Major removal shocks dependents. Mitigation: Two-release migration window + codemod and samples.
 
Final Note
Your analysis nails the separation of concerns and enumerates the callback bridge challenges. This directive turns it into a concrete, staged plan with native-first background fetch, JS optionality, clear policies, and verifiable acceptance criteria while preserving a rollback path.
Status: Improvement Directive - Active Implementation Plan Next Steps: Begin PR1 (SPI Shell) implementation Dependencies: None Stakeholders: Plugin developers, TimeSafari app developers