@ateam-ai-mobile/expo 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/device-bridge/cookies.ts","../src/device-bridge/nfc.ts","../src/bootstrap.ts","../src/index.ts","../src/haptics.ts","../src/bundleStorage.ts","../src/attachmentCache.ts","../src/device-state/domains/deviceContext.ts","../src/device-state/quality.ts","../src/device-state/events.ts","../src/device-state/domains/connectivity.ts","../src/device-state/domains/capability.ts","../src/device-state/domains/interaction.ts","../src/device-state/headingSource.ts","../src/device-state/turnDetector.ts","../src/geo.ts","../src/device-state/spatialEvents.ts","../src/device-state/domains/spatial.ts","../src/device-state/visualEngine.ts","../src/device-state/domains/visual.ts","../src/device-state/activityEngine.ts","../src/device-state/activityClassifier.ts","../src/device-state/domains/activity.ts","../src/device-state/index.ts","../src/camera/CameraTaskHost.tsx","../src/camera/cameraTasks.ts","../src/device/createDeviceProvider.ts","../src/vision/index.ts","../src/maps/index.ts","../src/args.ts","../src/maps/normalize.ts","../src/device-bridge/actions/messaging.ts","../src/device-bridge/actions/calendar-actions.ts","../src/device-bridge/actions/navigation.ts","../src/device-bridge/collectors/health.ts","../src/device-bridge/background.ts","../src/device-bridge/collectors/calendar.ts","../src/device-bridge/collectors/contacts.ts","../src/device-bridge/collectors/location.ts","../src/device-bridge/collectors/battery.ts","../src/device-bridge/collectors/connectivity.ts","../src/device-bridge/collectors/notifications.ts","../src/device-bridge/collectors/appEngagement.ts","../src/device-bridge/collectors/camera.ts","../src/device-bridge/biometrics.ts","../src/device-bridge/sharing.ts","../src/device-bridge/scanner.ts","../src/oauth/expoOAuth.tsx"],"sourcesContent":["/**\n * Device-bridge: Cookies API\n *\n * Reads the platform's shared cookie jar (NSHTTPCookieStorage on iOS,\n * CookieManager on Android) so upper layers (plugin-sdk → RN plugins) can\n * extract session cookies captured during a native WebView login flow.\n *\n * Wraps `@react-native-cookies/cookies` so upper layers never import the\n * native module directly. If we swap implementations, only this file changes.\n *\n * Why device-bridge owns this: cookies are a device-level capability (tied\n * to the platform's shared HTTP stack). Plugins should consume a clean\n * abstraction; the native wiring lives here.\n */\n\nimport CookieManager from '@react-native-cookies/cookies';\n\nexport interface DeviceCookie {\n name: string;\n value: string;\n domain?: string;\n path?: string;\n /** Unix seconds. Omitted for session cookies. */\n expires?: number;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n}\n\n/**\n * Get every cookie attached to the URL's effective domain.\n * Set `includeHttpOnly` true to include HttpOnly cookies (required for\n * LinkedIn/Google session tokens).\n */\nexport async function get(url: string, includeHttpOnly: boolean = true): Promise<DeviceCookie[]> {\n const raw = (await CookieManager.get(url, includeHttpOnly)) as Record<string, any>;\n const out: DeviceCookie[] = [];\n for (const [name, c] of Object.entries(raw || {})) {\n if (!c || typeof c !== 'object') continue;\n const value = typeof c.value === 'string' ? c.value : String(c.value ?? '');\n const cookie: DeviceCookie = { name, value };\n if (c.domain) cookie.domain = String(c.domain);\n if (c.path) cookie.path = String(c.path);\n if (c.expires) {\n const t = Date.parse(c.expires);\n if (!Number.isNaN(t)) cookie.expires = Math.floor(t / 1000);\n }\n if (typeof c.httpOnly === 'boolean') cookie.httpOnly = c.httpOnly;\n if (typeof c.secure === 'boolean') cookie.secure = c.secure;\n if (c.sameSite === 'Lax' || c.sameSite === 'Strict' || c.sameSite === 'None') {\n cookie.sameSite = c.sameSite;\n }\n out.push(cookie);\n }\n return out;\n}\n\n/**\n * Wipe the shared cookie jar. Cross-platform scoped clear isn't supported\n * uniformly; `clearAll` is the safe operation on both iOS and Android.\n */\nexport async function clearAll(): Promise<{ ok: boolean; error?: string }> {\n try {\n await CookieManager.clearAll(true);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n\n/**\n * Set a cookie programmatically. Useful when replaying a saved session on\n * the device's shared jar before opening a WebView.\n */\nexport async function set(url: string, cookie: DeviceCookie): Promise<{ ok: boolean; error?: string }> {\n try {\n await CookieManager.set(url, {\n name: cookie.name,\n value: cookie.value,\n domain: cookie.domain,\n path: cookie.path || '/',\n httpOnly: cookie.httpOnly,\n secure: cookie.secure,\n } as any, true);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n","/**\n * Device-bridge: NFC\n *\n * Wraps react-native-nfc-manager for:\n * - Reading NFC tags (NDEF, Mifare)\n * - Writing to writable tags\n * - App-triggered tag discovery\n *\n * Use cases: physical-world triggers, inventory lookup, hardware keys,\n * loyalty cards, transit top-up, pairing.\n *\n * iOS: requires \"Near Field Communication Tag Reading\" entitlement in the\n * app's provisioning profile. Android: requires NFC permission in manifest.\n */\n\nimport NfcManager, { NfcTech, Ndef } from 'react-native-nfc-manager';\n\nlet _initialized = false;\n\nasync function ensureInit(): Promise<boolean> {\n if (_initialized) return true;\n try {\n const supported = await NfcManager.isSupported();\n if (!supported) return false;\n await NfcManager.start();\n _initialized = true;\n return true;\n } catch (e) {\n return false;\n }\n}\n\nexport async function isSupported(): Promise<boolean> {\n try { return await NfcManager.isSupported(); } catch { return false; }\n}\n\nexport async function isEnabled(): Promise<boolean> {\n try { return await NfcManager.isEnabled(); } catch { return false; }\n}\n\nexport interface NdefRecord {\n type: string;\n payload: string;\n}\n\nexport interface TagReadResult {\n ok: boolean;\n tagId?: string;\n records?: NdefRecord[];\n error?: string;\n}\n\n/** Wait for the next tag to be scanned, read NDEF records, release, and return. */\nexport async function readOnce(timeoutMs: number = 30_000): Promise<TagReadResult> {\n const ready = await ensureInit();\n if (!ready) return { ok: false, error: 'nfc_unsupported' };\n try {\n const tag = await Promise.race([\n NfcManager.requestTechnology(NfcTech.Ndef).then(() => NfcManager.getTag()),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),\n ]);\n if (!tag) {\n try { await NfcManager.cancelTechnologyRequest(); } catch {}\n return { ok: false, error: 'timeout' };\n }\n const records: NdefRecord[] = [];\n if ((tag as any).ndefMessage) {\n for (const r of (tag as any).ndefMessage) {\n try {\n const type = String.fromCharCode(...(r.type || []));\n const payload = Ndef.text.decodePayload(r.payload || []);\n records.push({ type, payload: String(payload) });\n } catch {\n // skip records we can't decode\n }\n }\n }\n const tagId = (tag as any).id ? Array.from((tag as any).id).map((b: any) => b.toString(16).padStart(2, '0')).join('') : undefined;\n return { ok: true, tagId, records };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n } finally {\n try { await NfcManager.cancelTechnologyRequest(); } catch {}\n }\n}\n","/**\n * bootstrapExpoAdas — the single, self-contained SDK bootstrap (R10).\n *\n * The final high-level entry point of the \"fat SDK / thin shell\" arc: it folds\n * the ~15 hand-wired seams the host used to assemble in its own adas.ts into ONE\n * call, so the shell stops manually assembling platform providers/collectors/\n * plugins. Internally it initializes, in the order the migrated code established:\n * the Expo providers (haptics/bundle cache/clipboard/attachment cache + the\n * device-primitive provider), the host-bundled native plugins, the plugin-WebView\n * allow-list, native OAuth, the ACS reachability reporter, the L3 DeviceState\n * engine (with the SDK's OWN collectors — the shell injects none), the plugin/\n * UI-action navigator, and finally the core config+token bridge (initAdas).\n *\n * The API is DERIVED from the host's migrated adas.ts, not invented. Everything\n * the host still owns is passed in as a small values-and-getters bag: endpoint\n * getters (the host owns the dev/prod switch), the identity/token bridge, its\n * branded Google client id, its host-bundled plugin resolvers, the native\n * WebView module from its build, and its router callback.\n *\n * A SUBPATH (`@ateam-ai-mobile/expo/bootstrap`), like /device and /oauth: it\n * statically pulls the optional expo native modules (via createDeviceProvider /\n * the device-state collectors / the OAuth provider), so it must not sit on the\n * package's main entry, which a host without those modules still has to build.\n *\n * The two physical reasoning paths stay DISTINCT and are wired as SEPARATE\n * statements — never a merged bridge: LOCAL (the local tool registry + device\n * state the plugin/voice runtime reads) and SERVER (the relay allow-list here;\n * the DeviceBridge relay runtime itself is started by the host lifecycle, out of\n * this module-load bootstrap's scope).\n */\nimport { initAdas, setAcsReachabilityReporter, type TokenProvider, type AdasUser, type AdasSession } from '@ateam-ai-mobile/core';\nimport { setDeviceStateProvider, registerNativePlugin, registerPlatformDeviceTools, type RegisteredPlugin } from '@ateam-ai-mobile/plugin-runtime';\nimport { setOAuthProvider, setUIActionNavigator, setPluginWebViewExtraHosts, type UIActionRoute } from '@ateam-ai-mobile/ui';\n\nimport { setupExpoProviders, type ExpoAttachmentCache } from './index';\nimport { createDeviceProvider } from './device/createDeviceProvider';\nimport { createDeviceState } from './device-state';\nimport { createExpoOAuthProvider } from './oauth/expoOAuth';\nimport * as deviceConnectivity from './device-bridge/collectors/connectivity';\nimport * as deviceLocation from './device-bridge/collectors/location';\n\n/** A host-bundled native plugin: one lazy resolver registered under one or more ids. */\nexport interface NativePluginRegistration {\n resolver: () => Promise<RegisteredPlugin>;\n ids: string[];\n}\n\nexport interface BootstrapExpoAdasOptions {\n /**\n * Endpoint GETTERS — the host owns the dev/prod env switch, so these are read\n * live on every SDK request (never snapshotted). `core` is required; the rest\n * are optional (voice/consumer-auth features light up when present).\n */\n urls: {\n core: () => string;\n voice?: () => string;\n consumerAuth?: () => string;\n /** Trusted extra host for plugin WebViews (the relay device-bridge auth flows). */\n relay?: () => string;\n };\n /** The host's token bridge — outcome mapping (transient vs auth_failed) owned by the host. */\n tokenProvider: TokenProvider;\n /** Current user projection for chat routing (host reads its auth store). */\n getUser: () => AdasUser | null;\n /** Active tenant getter (signed-in tenant, falling back to the pre-auth pick). */\n tenant?: () => string;\n /** Adopt a freshly-minted session on tenant switch (host persists tenant-first, then logs in). */\n onSession?: (session: AdasSession) => void | Promise<void>;\n /** The host's bundled Google iOS client id, for the join/open-join OAuth path. */\n googleIosClientId?: string;\n /** Host-bundled native plugins (AuthWebView, ConnectionsPanel, …) — lazy resolvers. */\n nativePlugins?: NativePluginRegistration[];\n /** Native modules remote bundles may require() (e.g. react-native-webview) — from the host build. */\n bundleModules?: Record<string, unknown>;\n /** Pre-SDK AsyncStorage keys, so an existing install keeps its warm caches. */\n storageKeys?: { attachmentIndex?: string; bundleIndex?: string };\n /** Connectivity sink — the SDK reports fetch reachability up/down; the host feeds its banner. */\n onReachabilityChange?: (up: boolean) => void;\n /** Open the host's plugin screen for a UI-action chip (the host owns its router). */\n navigateToPlugin: (route: UIActionRoute) => void;\n /**\n * Reachability probe URL for the device-primitive connectivity API. The host\n * passes this EXPLICITLY (typically `${core}/health`) as a load-time snapshot —\n * deliberately NOT derived from `urls.core()`, so a dev/E2E core override does\n * not silently flow into the health probe (the asymmetry the host relied on).\n */\n reachabilityUrl?: string;\n}\n\n/**\n * Wire the entire Expo-backed ADAS platform in one call. Run for its side effect\n * at module load, before the router mounts. Returns the attachment cache so the\n * host can hand it to anything that needs it (initAdas already received it).\n */\nexport function bootstrapExpoAdas(opts: BootstrapExpoAdasOptions): { attachmentCache: ExpoAttachmentCache } {\n // Expo providers + the device-primitive implementation (props.device). The SDK\n // owns createDeviceProvider; the shell injects only its reachability URL.\n const { attachmentCache } = setupExpoProviders({\n bundleModules: opts.bundleModules,\n attachmentIndexKey: opts.storageKeys?.attachmentIndex,\n bundleIndexKey: opts.storageKeys?.bundleIndex,\n deviceProvider: createDeviceProvider({ reachabilityUrl: opts.reachabilityUrl }),\n });\n\n // Host-bundled native plugins: register each lazy resolver under its id aliases.\n for (const plugin of opts.nativePlugins ?? []) {\n for (const id of plugin.ids) registerNativePlugin(id, plugin.resolver);\n }\n\n // Plugin WebViews may navigate to the relay (device-bridge auth flows) —\n // whitelist it alongside core/voice. Getter → live across env switches.\n const relay = opts.urls.relay;\n setPluginWebViewExtraHosts(() => (relay ? [relay()] : []));\n\n // Native OAuth for the relocated auth screens (the ONLY native OAuth wiring).\n setOAuthProvider(createExpoOAuthProvider({ googleIosClientId: opts.googleIosClientId }));\n\n // ACS fetch outcomes feed the host's connectivity banner (any response = up,\n // thrown fetch = down). The host's sink stays host-side (lazy, no load cycle).\n if (opts.onReachabilityChange) {\n const onReach = opts.onReachabilityChange;\n setAcsReachabilityReporter({ up: () => onReach(true), down: () => onReach(false) });\n }\n\n // L3 DeviceState (props.deviceState) — the SEMANTIC layer, wired with the SDK's\n // OWN device-bridge collectors. The shell injects none (the R10 payoff): an\n // undeclared/unimplemented domain returns explicit `unknown`s, never a silent\n // undefined. Kept a SEPARATE statement from the SERVER relay — the two\n // reasoning paths never merge.\n setDeviceStateProvider(\n createDeviceState({ connectivity: deviceConnectivity, location: deviceLocation }),\n );\n\n // LOCAL device tools — register the static, host-owned platform tools\n // (device.echo / device.ping) at load, so the device path answers even before\n // any plugin bundle or sign-in: the platform tier deliberately does not depend\n // on the thing being proved. A tenant's EXTERNAL device tools load on\n // session-authenticated, which the host wires to its auth-session subscription\n // via syncDeviceToolsForSession() — ambient, not voice-triggered.\n registerPlatformDeviceTools();\n\n // Plugin/UI-action chips delegate navigation to the host's router.\n setUIActionNavigator(opts.navigateToPlugin);\n\n // Core config + token bridge. URL getters (not snapshots) so the env switch\n // flows through; identity/token bridge owned by the host.\n initAdas({\n coreUrl: opts.urls.core,\n ...(opts.urls.voice ? { voiceUrl: opts.urls.voice } : {}),\n ...(opts.urls.consumerAuth ? { consumerAuthUrl: opts.urls.consumerAuth } : {}),\n ...(opts.onSession ? { onSession: opts.onSession } : {}),\n ...(opts.tenant ? { tenant: opts.tenant } : {}),\n getUser: opts.getUser,\n tokenProvider: opts.tokenProvider,\n attachmentCache,\n });\n\n return { attachmentCache };\n}\n","/**\n * @ateam-ai-mobile/expo — Expo-backed providers for the ADAS SDK.\n *\n * Bare-RN hosts skip this package entirely (core + plugin-runtime have safe\n * defaults); Expo hosts call setupExpoProviders() once at startup to light\n * up haptics, persistent bundle caching, attachment previews, and the\n * expo modules remote bundles may require.\n */\nimport * as ExpoWebBrowser from 'expo-web-browser';\nimport * as ExpoLinking from 'expo-linking';\nimport * as ExpoClipboard from 'expo-clipboard';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport { setAcsCacheStorage } from '@ateam-ai-mobile/core';\nimport {\n setHapticsProvider,\n setBundleStorage,\n registerBundleModules,\n setPluginNativeProvider,\n type PluginNative,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { setClipboardProvider } from '@ateam-ai-mobile/ui';\n\nimport { expoHaptics } from './haptics';\nimport { ExpoBundleStorage } from './bundleStorage';\nimport { ExpoAttachmentCache } from './attachmentCache';\n\nexport { expoHaptics } from './haptics';\nexport { ExpoBundleStorage } from './bundleStorage';\nexport { ExpoAttachmentCache } from './attachmentCache';\n// NOTE: createBridge (device capabilities bridge) is intentionally NOT\n// exported here — it statically imports the optional expo modules\n// (calendar/contacts/location/…), which would break Metro builds in hosts\n// that don't install them. Install those modules and import it from the\n// subpath: `import { createBridge } from '@ateam-ai-mobile/expo/bridge'`.\n\n/**\n * Wire all Expo providers in one call. Runs before initAdas() is fine —\n * these registries are independent of the core config.\n *\n * Returns the attachment cache so the host can pass it to initAdas():\n * const { attachmentCache } = setupExpoProviders();\n * initAdas({ ..., attachmentCache });\n */\nexport function setupExpoProviders(opts?: {\n /** Extra modules remote bundles may require(), e.g. react-native-webview.\n * Static imports must live in the HOST (it owns the native build). */\n bundleModules?: Record<string, any>;\n /** AsyncStorage key overrides — pass your pre-SDK keys when migrating an\n * existing app so users keep their attachment previews / bundle cache. */\n attachmentIndexKey?: string;\n bundleIndexKey?: string;\n /**\n * The device-primitive implementation plugins receive as `props.device`.\n * Build it with `createDeviceProvider(...)` from `@ateam-ai-mobile/expo/device`\n * and pass it here so the SDK owns the implementation and this call registers\n * it (setPluginNativeProvider). It is injected — not imported by this entry —\n * because createDeviceProvider statically pulls optional expo modules that a\n * host without them cannot build (same reason createBridge is a subpath).\n */\n deviceProvider?: PluginNative;\n}): { attachmentCache: ExpoAttachmentCache } {\n setHapticsProvider(expoHaptics);\n if (opts?.deviceProvider) setPluginNativeProvider(opts.deviceProvider);\n setBundleStorage(new ExpoBundleStorage({ indexKey: opts?.bundleIndexKey }));\n setClipboardProvider({\n setString: (text: string) => ExpoClipboard.setStringAsync(text).then(() => {}),\n });\n registerBundleModules({\n 'expo-web-browser': ExpoWebBrowser,\n 'expo-linking': ExpoLinking,\n ...(opts?.bundleModules || {}),\n });\n // Persist the ACS home-actions feed across cold starts.\n setAcsCacheStorage({\n getItem: (k: string) => AsyncStorage.getItem(k),\n setItem: (k: string, v: string) => AsyncStorage.setItem(k, v),\n });\n return { attachmentCache: new ExpoAttachmentCache({ indexKey: opts?.attachmentIndexKey }) };\n}\n\n// Device Bridge v2 — the semantic state engines this build provides.\n// The host passes this to setDeviceStateProvider(); per-plugin gating by\n// manifest.stateDomains happens in the SDK.\nexport { createDeviceState, IMPLEMENTED_DOMAINS } from './device-state';\nexport type { DeviceStateCollectors } from './device-state';\n// The mounted half of the Visual engine. The app renders this ONCE; it draws\n// nothing until a visual session is open. Required for deviceState.visual to\n// produce frames at all — expo-camera has no headless capture.\n// The rolling frame timeline. Exposed so the host's native.vision layer can\n// resolve a frame id to its on-device file (frameById) and write a measured\n// quality back onto the frame (setFrameQuality) — see visualEngine.ts.\nexport { visualEngine } from './device-state/visualEngine';\nexport type { VisualFrame } from './device-state/visualEngine';\n// The mounted camera surface behind scanner.scanOnce() and\n// camera.recordVideo(). Mount ONCE near the app root; it renders nothing\n// until a plugin asks for a scan or a recording. Without it both APIs\n// reject immediately with UNAVAILABLE — deliberately, since a plugin must\n// never await a promise nothing can settle.\nexport { CameraTaskHost } from './camera/CameraTaskHost';\nexport { cameraTasks } from './camera/cameraTasks';\n// Replay — run guidance logic against a RECORDED WALK instead of a phone.\n// See docs/SPEC_device_walk_recorder.md for the fixture format.\nexport { createReplayDeviceState, parseWalkJsonl } from './device-state/replay';\nexport type { WalkFixture, WalkSample, WalkRefusal, ReplayClock, ReplaySession } from './device-state/replay';\n","/**\n * expo-haptics implementation of the plugin HapticsAPI.\n */\nimport * as Haptics from 'expo-haptics';\nimport type { HapticsAPI } from '@ateam-ai-mobile/plugin-runtime';\n\nexport const expoHaptics: HapticsAPI = {\n impact(style) {\n const map = {\n light: Haptics.ImpactFeedbackStyle.Light,\n medium: Haptics.ImpactFeedbackStyle.Medium,\n heavy: Haptics.ImpactFeedbackStyle.Heavy,\n } as const;\n Haptics.impactAsync(map[style]).catch(() => {});\n },\n notification(type) {\n const map = {\n success: Haptics.NotificationFeedbackType.Success,\n warning: Haptics.NotificationFeedbackType.Warning,\n error: Haptics.NotificationFeedbackType.Error,\n } as const;\n Haptics.notificationAsync(map[type]).catch(() => {});\n },\n selection() {\n Haptics.selectionAsync().catch(() => {});\n },\n // Shorthand for notification('error') — the form plugins actually call.\n error() {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});\n },\n};\n","/**\n * Disk-backed BundleStorage: bundle files in the Expo cache directory,\n * index in AsyncStorage. Mirrors the reference app's cache semantics\n * (index version key, files under plugin-bundles/).\n */\nimport * as FileSystem from 'expo-file-system/legacy';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { BundleStorage, BundleCacheEntry } from '@ateam-ai-mobile/plugin-runtime';\n\nconst CACHE_DIR = `${FileSystem.cacheDirectory}plugin-bundles/`;\nconst DEFAULT_CACHE_INDEX_KEY = 'adas-plugin-bundles-index-v1';\n\nexport class ExpoBundleStorage implements BundleStorage {\n private dirReady = false;\n private indexKey: string;\n\n /** Pass `indexKey` when migrating from an app that already persisted a\n * bundle index under a different key — keeps the existing cache warm. */\n constructor(opts?: { indexKey?: string }) {\n this.indexKey = opts?.indexKey || DEFAULT_CACHE_INDEX_KEY;\n }\n\n private async ensureDir(): Promise<void> {\n if (this.dirReady) return;\n try {\n const info = await FileSystem.getInfoAsync(CACHE_DIR);\n if (!info.exists) {\n await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true });\n }\n } catch (err) {\n console.warn('[ExpoBundleStorage] cache dir error:', err);\n }\n this.dirReady = true;\n }\n\n async loadIndex(): Promise<Record<string, BundleCacheEntry>> {\n try {\n const raw = await AsyncStorage.getItem(this.indexKey);\n return raw ? JSON.parse(raw) : {};\n } catch {\n return {};\n }\n }\n\n async saveIndex(index: Record<string, BundleCacheEntry>): Promise<void> {\n try {\n await AsyncStorage.setItem(this.indexKey, JSON.stringify(index));\n } catch (err) {\n console.warn('[ExpoBundleStorage] index save error:', err);\n }\n }\n\n async readBundle(localPath: string): Promise<string | null> {\n try {\n const info = await FileSystem.getInfoAsync(localPath);\n if (!info.exists) return null;\n return await FileSystem.readAsStringAsync(localPath);\n } catch {\n return null; // file gone — caller re-downloads\n }\n }\n\n async writeBundle(key: string, content: string): Promise<string> {\n await this.ensureDir();\n const localPath = `${CACHE_DIR}${key}`;\n await FileSystem.writeAsStringAsync(localPath, content);\n return localPath;\n }\n\n async deleteBundle(localPath: string): Promise<void> {\n await FileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => {});\n }\n\n async clearAll(): Promise<void> {\n await FileSystem.deleteAsync(CACHE_DIR, { idempotent: true }).catch(() => {});\n await AsyncStorage.removeItem(this.indexKey).catch(() => {});\n this.dirReady = false;\n }\n}\n","/**\n * Expo implementation of @ateam-ai-mobile/core's AttachmentCache: attachment bytes on\n * disk, index in AsyncStorage. Mirrors the reference app's chat attachment\n * cache (last-100 retention).\n */\nimport * as FileSystem from 'expo-file-system/legacy';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { AttachmentCache, AttachmentPreview, OutgoingAttachment } from '@ateam-ai-mobile/core';\n\nconst DEFAULT_INDEX_KEY = 'adas-chat-attachment-index-v1';\nconst DIR = `${FileSystem.documentDirectory}chat-attachments/`;\nconst MAX_JOBS = 100;\n\ninterface CachedAttachment { mimeType: string; name: string; filePath: string }\n\nexport class ExpoAttachmentCache implements AttachmentCache {\n private index: Record<string, CachedAttachment[]> = {};\n private loaded: Promise<void>;\n private indexKey: string;\n\n /** Pass `indexKey` when migrating from an app that already persisted an\n * index under a different AsyncStorage key — otherwise existing users\n * lose every cached preview on upgrade. */\n constructor(opts?: { indexKey?: string }) {\n const indexKey = opts?.indexKey || DEFAULT_INDEX_KEY;\n this.indexKey = indexKey;\n this.loaded = (async () => {\n try {\n const info = await FileSystem.getInfoAsync(DIR);\n if (!info.exists) await FileSystem.makeDirectoryAsync(DIR, { intermediates: true });\n const raw = await AsyncStorage.getItem(indexKey);\n if (raw) this.index = JSON.parse(raw);\n } catch {}\n })();\n }\n\n async ready(): Promise<void> {\n await this.loaded;\n }\n\n async save(jobId: string, attachments: OutgoingAttachment[]): Promise<void> {\n await this.loaded;\n const entries: CachedAttachment[] = [];\n for (let i = 0; i < attachments.length; i++) {\n const att = attachments[i];\n const ext = att.mimeType.startsWith('image/') ? att.mimeType.split('/')[1] || 'jpg' : 'bin';\n const filePath = `${DIR}${jobId}_${i}.${ext}`;\n try {\n await FileSystem.writeAsStringAsync(filePath, att.data, { encoding: FileSystem.EncodingType.Base64 });\n entries.push({ mimeType: att.mimeType, name: att.name, filePath });\n } catch (err) {\n console.warn('[ExpoAttachmentCache] write failed:', err);\n }\n }\n if (entries.length === 0) return;\n\n this.index[jobId] = entries;\n // Keep only the most recent MAX_JOBS jobs; delete evicted files.\n const keys = Object.keys(this.index);\n if (keys.length > MAX_JOBS) {\n for (const k of keys.slice(0, keys.length - MAX_JOBS)) {\n const old = this.index[k];\n if (old) for (const o of old) FileSystem.deleteAsync(o.filePath, { idempotent: true }).catch(() => {});\n delete this.index[k];\n }\n }\n try {\n await AsyncStorage.setItem(this.indexKey, JSON.stringify(this.index));\n } catch {}\n }\n\n get(jobId: string): AttachmentPreview[] | undefined {\n const entries = this.index[jobId];\n if (!entries || entries.length === 0) return undefined;\n return entries.map(e => ({\n mimeType: e.mimeType,\n name: e.name,\n previewUri: e.filePath,\n }));\n }\n}\n","// ── deviceContext ──────────────────────────────────────────────────────────\n// power ✓ thermal ✗ (needs a native module) memory/storage ✗\nimport * as ExpoBattery from 'expo-battery';\nimport type { DeviceState, DeviceContextSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { qualityFrom, qualityUnknown } from '../quality';\nimport { stateEvent } from '../events';\n\nexport function createDeviceContextDomain(): DeviceState['deviceContext'] {\n return {\n async getSnapshot(): Promise<DeviceContextSnapshot> {\n const observedAt = Date.now();\n try {\n const [level, state, lowPower] = await Promise.all([\n ExpoBattery.getBatteryLevelAsync(),\n ExpoBattery.getBatteryStateAsync(),\n ExpoBattery.isLowPowerModeEnabledAsync(),\n ]);\n return {\n power: {\n // expo-battery returns -1 when it cannot tell. That is not a level.\n batteryLevel: typeof level === 'number' && level >= 0 ? level : null,\n charging:\n state === ExpoBattery.BatteryState.CHARGING ||\n state === ExpoBattery.BatteryState.FULL,\n lowPowerMode: lowPower,\n },\n // No Expo API for either. iOS needs ProcessInfo.thermalState, Android\n // PowerManager.getThermalStatus — a native module that does not exist.\n thermal: { state: 'unknown' },\n resources: { memoryPressure: 'unknown', storagePressure: 'unknown' },\n quality: qualityFrom(observedAt),\n };\n } catch {\n // The battery read failing is rare and uninteresting; reporting it as\n // 0% would not be.\n return {\n power: { batteryLevel: null, charging: null, lowPowerMode: null },\n thermal: { state: 'unknown' },\n resources: { memoryPressure: 'unknown', storagePressure: 'unknown' },\n quality: qualityUnknown(),\n };\n }\n },\n\n // Real events, not a placeholder: expo-battery exposes native listeners,\n // so a plugin that subscribes genuinely hears about a change rather than\n // sitting on a subscription that can never fire. Nudges without a payload:\n // no Device Context payload schema is decided, so the subscriber re-reads.\n subscribe(listener) {\n const subs = [\n ExpoBattery.addBatteryLevelListener(() => listener(stateEvent('deviceContext', 'batteryLevelChanged'))),\n ExpoBattery.addBatteryStateListener(() => listener(stateEvent('deviceContext', 'chargingChanged'))),\n ];\n return { unsubscribe: () => subs.forEach(x => x?.remove?.()) };\n },\n };\n}\n","/**\n * The honesty helpers — one place, shared by every domain module.\n *\n * A snapshot's `quality` says how much to trust it. R5 centralizes the three\n * ways a domain answers honestly here so no domain re-implements them:\n * - qualityFrom(observedAt) a live read, freshness stamped from its age.\n * - qualityUnknown() \"nothing was observed\" — never dressed as fresh.\n * - unsupported(what) an ACTION this build can't honour, refused by name.\n *\n * The `unknown` freshness shape is NOT re-declared here: it is the single\n * canonical `neverObserved()` from the contract package, so the engine, the gate\n * (deviceStateHost) and the types can never drift into three subtly different\n * \"unknown\"s.\n */\nimport { neverObserved } from '@ateam-ai-mobile/plugin-runtime';\nimport type { StateQuality } from '@ateam-ai-mobile/plugin-runtime';\n\n// ── Freshness ────────────────────────────────────────────────────────────\n//\n// Every snapshot carries when it was observed. `observedAt` is stamped at read\n// time by a live-reading domain; a caching domain passes its cache timestamp\n// instead. This is the one place the fresh/aging/stale thresholds live.\n\nexport const FRESH_MS = 10_000;\nexport const AGING_MS = 60_000;\n\nexport function qualityFrom(\n observedAt: number,\n confidence?: StateQuality['confidence'],\n): StateQuality {\n const ageMs = Date.now() - observedAt;\n return {\n freshness: {\n state: ageMs < FRESH_MS ? 'fresh' : ageMs < AGING_MS ? 'aging' : 'stale',\n observedAt,\n ageMs,\n },\n ...(confidence ? { confidence } : {}),\n };\n}\n\n/** Age genuinely unknown — treated as stale, never as fresh. */\nexport function qualityUnknown(): StateQuality {\n return { freshness: neverObserved() };\n}\n\n/**\n * Refuse, by name, with the frozen error code.\n *\n * Used where the contract declares a method this build cannot honour. The\n * alternative — a no-op that resolves — is the fabrication pattern in method\n * form: the caller cannot tell \"did nothing\" from \"worked\".\n */\nexport function unsupported(what: string): Error {\n const err: any = new Error(\n `${what} is not implemented in this build — the native engine behind it does not exist yet.`,\n );\n err.code = 'UNSUPPORTED';\n return err;\n}\n","/**\n * The ONE event envelope every domain emits, and the one way listeners are held.\n *\n * The frozen shape is `StateEvent { domain, name, timestamp, payload }`. Three\n * domains were born emitting `{ type: '…' }` instead, so a plugin written\n * against the contract — `e.name === 'appStateChanged'` — never matched, and\n * `e.name.endsWith(…)` threw on `undefined`. Building every event here is what\n * keeps a second shape from growing back.\n *\n * Type-only imports, so the pure modules that use this (the spatial event\n * detector) stay testable under plain `node --test`.\n */\nimport type { DeviceBridgeDomain, StateEvent, Subscription } from '@ateam-ai-mobile/plugin-runtime';\n\n/**\n * Build an event. `payload` is omitted, not `undefined`, when there is none —\n * an event that is only a nudge (\"re-read the snapshot\") says so by shape.\n * `timestamp` is when the change was recognised; it defaults to now, and replay\n * passes its virtual clock.\n */\nexport function stateEvent<TDomain extends DeviceBridgeDomain, TName extends string, TPayload = never>(\n domain: TDomain,\n name: TName,\n payload?: TPayload,\n timestamp: number = Date.now(),\n): StateEvent<TName, TPayload> & { domain: TDomain } {\n return payload === undefined\n ? { domain, name, timestamp }\n : { domain, name, timestamp, payload };\n}\n\n/**\n * Listeners, each isolated from the others. A listener that throws must not\n * stop the next one hearing the event, nor the engine that emitted it.\n */\nexport function listenerSet<E>() {\n const listeners = new Set<(event: E) => void>();\n return {\n add(fn: (event: E) => void): Subscription {\n listeners.add(fn);\n return { unsubscribe: () => { listeners.delete(fn); } };\n },\n emit(event: E): void {\n for (const fn of listeners) {\n try { fn(event); } catch { /* a bad listener is not the engine's problem */ }\n }\n },\n };\n}\n","// ── connectivity ─────────────────────────────────────────────────────────\n// reachability ✓ transport ✓ quality/stability/metered ✗\nimport type { DeviceState, ConnectivitySnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport type { DeviceStateCollectors } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\n\nexport function createConnectivityDomain(collectors: DeviceStateCollectors): DeviceState['connectivity'] {\n return {\n async getSnapshot(): Promise<ConnectivitySnapshot> {\n const observedAt = Date.now();\n try {\n if (!collectors.connectivity) throw unsupported('connectivity collector');\n const data: any = await collectors.connectivity.collectConnectivity();\n const online = data?.is_connected === true;\n const type = String(data?.type || '').toLowerCase();\n\n return {\n reachability: data?.is_connected == null ? 'unknown' : online ? 'online' : 'offline',\n transport:\n type === 'wifi' ? 'wifi'\n : type === 'cellular' ? 'cellular'\n : type === 'ethernet' ? 'ethernet'\n : online ? 'other'\n : data?.is_connected === false ? 'none'\n : 'unknown',\n // All four below need measurement nobody takes yet. `unknown` is the\n // designed answer — \"return unknown rather than stale precision\".\n qualityClass: 'unknown',\n stability: 'unknown',\n latencyMs: null,\n bandwidthClass: 'unknown',\n metered: null,\n constrained: null,\n quality: qualityFrom(observedAt),\n };\n } catch {\n return {\n reachability: 'unknown',\n transport: 'unknown',\n qualityClass: 'unknown',\n stability: 'unknown',\n latencyMs: null,\n bandwidthClass: 'unknown',\n metered: null,\n constrained: null,\n quality: qualityUnknown(),\n };\n }\n },\n\n // Intent selects a native sampling strategy — latency probes, quality\n // classification. Nothing here samples anything, so accepting an intent\n // would be accepting an instruction we do not carry out.\n async start() { throw unsupported('connectivity.start'); },\n async stop() { throw unsupported('connectivity.stop'); },\n // A subscription that can never fire is worse than none: the caller\n // reasonably reads silence as \"nothing changed\".\n subscribe() { throw unsupported('connectivity.subscribe'); },\n };\n}\n","// ── capability ───────────────────────────────────────────────────────────\n// permission ✓ availability ✗ restriction ✗\nimport type { DeviceState, CapabilitySnapshot, CapabilityStatus } from '@ateam-ai-mobile/plugin-runtime';\nimport type { DeviceStateCollectors } from '../collectors';\nimport { qualityFrom, unsupported } from '../quality';\n\n/**\n * The bridge's permission model is 4-valued (`not_requested | granted | denied\n * | not_needed`); the frozen one is 7-valued. The extra states are real OS\n * distinctions nothing currently reads, so they map to `unknown` rather than\n * being collapsed into `denied` — \"restricted by policy\" and \"the user said no\"\n * are different problems with different remedies, and guessing between them\n * would put a wrong instruction in front of the user.\n */\nfunction statusFrom(granted: boolean | null): CapabilityStatus {\n return {\n support: 'unknown',\n permission: granted === null ? 'unknown' : granted ? 'granted' : 'denied',\n availability: 'unknown',\n };\n}\n\nconst UNKNOWN_STATUS: CapabilityStatus = {\n support: 'unknown',\n permission: 'unknown',\n availability: 'unknown',\n};\n\nexport function createCapabilityDomain(collectors: DeviceStateCollectors): DeviceState['capability'] {\n return {\n async getSnapshot(): Promise<CapabilitySnapshot> {\n const observedAt = Date.now();\n const locationGranted = collectors.location\n ? await collectors.location.hasPermission().catch(() => null)\n : null;\n\n return {\n // Only location has a real reader today. The rest are declared by the\n // contract and answered honestly rather than omitted.\n camera: UNKNOWN_STATUS,\n microphone: UNKNOWN_STATUS,\n location: {\n ...statusFrom(locationGranted),\n // iOS 14+ distinguishes precise from approximate; nothing reads it.\n precision: 'unknown',\n backgroundPermission: 'unknown',\n },\n motion: UNKNOWN_STATUS,\n bluetooth: UNKNOWN_STATUS,\n uwb: UNKNOWN_STATUS,\n nfc: UNKNOWN_STATUS,\n quality: qualityFrom(observedAt),\n } as CapabilitySnapshot;\n },\n\n // No OS permission-change stream is wired. Same reasoning as connectivity:\n // silence would be indistinguishable from stability.\n subscribe() { throw unsupported('capability.subscribe'); },\n };\n}\n","// ── interaction ──────────────────────────────────────────────────────────\n// app lifecycle ✓ screenOn ✗ audio ✗\nimport { AppState } from 'react-native';\nimport type { DeviceState, InteractionSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { qualityFrom } from '../quality';\nimport { stateEvent } from '../events';\n\n/**\n * Lifecycle only, and honest about the rest.\n *\n * `screenOn` is NOT derivable from AppState: RN reports `inactive` for a\n * notification shade, a control centre swipe, an incoming call AND a locked\n * screen. Mapping `inactive` to \"screen off\" would be wrong most of the time\n * it fired — so it stays null until a native module can answer it.\n *\n * The whole `audio` block reports unknown for the same reason. `interruption`\n * carries the distinction that matters: `none` means \"checked, nothing is\n * interrupting\", `unknown` means \"no knowledge\" — and we have the latter.\n */\nexport function createInteractionDomain(): DeviceState['interaction'] {\n const mapState = (s: string): InteractionSnapshot['lifecycle']['appState'] =>\n s === 'active' ? 'foreground'\n : s === 'background' ? 'background'\n : s === 'inactive' ? 'inactive'\n : 'unknown';\n\n return {\n async getSnapshot(): Promise<InteractionSnapshot> {\n const observedAt = Date.now();\n return {\n lifecycle: {\n appState: mapState(AppState.currentState as string),\n screenOn: null,\n userActive: null,\n idleDurationMs: null,\n },\n audio: {\n captureActive: null,\n playbackActive: null,\n route: 'unknown',\n interruption: 'unknown',\n },\n // The voice block is OMITTED rather than filled with nulls: the design\n // says a platform that cannot report voice session state leaves the\n // group out instead of guessing `false` for each field. The app's own\n // voice store knows this, but it is not reachable from the SDK.\n quality: qualityFrom(observedAt),\n };\n },\n\n // Real: AppState genuinely emits on every foreground/background change,\n // which is the one part of this domain that works.\n subscribe(listener) {\n const sub = AppState.addEventListener('change', next => {\n listener(stateEvent('interaction', 'appStateChanged', { appState: mapState(next as string) }));\n });\n return { unsubscribe: () => sub.remove() };\n },\n };\n}\n","/**\n * The compass, kept warm — and the turn detector fed from it.\n *\n * `getHeadingAsync()` is not usable on the capture path: it starts a watch,\n * waits for a reading and stops it, which costs hundreds of milliseconds — per\n * frame. So this holds ONE subscription while a session is open and callers\n * read the last value synchronously.\n *\n * Returns null until a reading arrives, and null means NOT MEASURED. A heading\n * of 0 is due north, so an unstarted compass must never report 0.\n *\n * WHY THE TURN DETECTOR IS FED HERE. A turn happens BETWEEN reads — it is an\n * event, not a state — so a detector driven by sampled snapshots would miss\n * exactly the moment a guide needs. It runs on the compass callback instead.\n * The logic itself lives in `turnDetector.ts`, free of any Expo import, so it\n * can be tested against a scripted walk. A completed turn is also PUSHED to\n * `onTurn` listeners — spatial publishes it as an event the moment it lands.\n *\n * SHARED, SO COUNTED. Spatial and visual both keep the compass warm, and each\n * used to stop it on its own `stop()`: visual's stop killed the compass — and\n * wiped the turn history — under a live spatial session. Each caller now names\n * itself, and the watch stops only when the last one leaves.\n */\nimport * as ExpoLocation from 'expo-location';\n\nimport { TurnDetector, type TurnEvent } from './turnDetector';\nimport { listenerSet } from './events';\n\nexport type { TurnEvent };\nexport { signedDelta } from './turnDetector';\n\n/** Who is holding the compass open. */\nexport type HeadingOwner = 'spatial' | 'visual';\n\ninterface HeadingReading {\n headingDeg: number | null;\n /**\n * iOS calibration rank: 3 high, 2 medium, 1 low, 0 none. NOT degrees of\n * error, which is why it is never surfaced as `headingAccuracyDeg` — that\n * field wants degrees, and converting a rank into one would be a fabricated\n * error bar. It drives CONFIDENCE instead, which is what it can honestly say.\n */\n calibration: number | null;\n timestamp: number;\n}\n\nlet latest: HeadingReading | null = null;\nlet sub: { remove(): void } | null = null;\n/** The watch being opened, so two owners starting at once open ONE. */\nlet opening: Promise<void> | null = null;\nconst owners = new Set<HeadingOwner>();\nconst turns = new TurnDetector();\nconst turnListeners = listenerSet<TurnEvent>();\n\n/** Start watching on behalf of `owner`. Idempotent per owner. */\nexport function startHeading(owner: HeadingOwner): Promise<void> {\n owners.add(owner);\n if (sub) return Promise.resolve();\n opening ??= open().finally(() => { opening = null; });\n return opening;\n}\n\nasync function open(): Promise<void> {\n try {\n const s = await ExpoLocation.watchHeadingAsync(h => {\n // A reading landing after the last owner left is not kept.\n if (!owners.size) return;\n // trueHeading is -1 until the compass calibrates; magHeading is the\n // fallback the OS itself recommends.\n const raw = h.trueHeading >= 0 ? h.trueHeading : h.magHeading;\n const deg = typeof raw === 'number' && raw >= 0 ? raw : null;\n const cal = typeof h.accuracy === 'number' && h.accuracy >= 0 ? h.accuracy : null;\n const now = Date.now();\n latest = { headingDeg: deg, calibration: cal, timestamp: now };\n const turn = turns.push(deg, now);\n if (turn) turnListeners.emit(turn);\n });\n // Every owner may have left while the OS was starting the watch.\n if (!owners.size) { s.remove(); return; }\n sub = s;\n } catch {\n // No compass, or permission refused. Callers get headingDeg: null, which\n // is the honest answer and does not stop anything else working.\n sub = null;\n }\n}\n\n/** Release `owner`'s hold. The watch stops only when no owner is left. */\nexport function stopHeading(owner: HeadingOwner): void {\n owners.delete(owner);\n if (owners.size) return;\n sub?.remove();\n sub = null;\n // Clear everything: a heading from a finished session describes a direction\n // the user may have long since turned away from, and a turn from a previous\n // walk is not something a new one should report.\n latest = null;\n turns.reset();\n}\n\n/** Hear each completed turn as it completes. Signed: positive = right. */\nexport function onTurn(fn: (turn: TurnEvent) => void) {\n return turnListeners.add(fn);\n}\n\n/** The newest reading, or null if the compass has said nothing yet. */\nexport function currentHeading(): { headingDeg: number | null } | null {\n return latest ? { headingDeg: latest.headingDeg } : null;\n}\n\n/** The newest reading with its calibration and age. */\nexport function currentHeadingDetail(): HeadingReading | null {\n return latest;\n}\n\n/** The most recent completed turn. Signed: positive = clockwise = right. */\nexport function lastMeaningfulTurn(): TurnEvent | null {\n return turns.last();\n}\n","/**\n * Turning, and which way.\n *\n * \"Turn right at the corner\" and \"you have just turned left\" are the two\n * sentences a walking guide says most, and neither can be read off a single\n * heading — they need heading OVER TIME. This is that, as a pure state machine\n * with no Expo import, so it can be tested against a scripted walk instead of\n * against a phone in someone's hand.\n *\n * SIGN IS THE WHOLE POINT. Compass degrees increase clockwise, so a positive\n * delta is a turn to the RIGHT and a negative one to the LEFT. A caller that\n * wants only the magnitude can take the absolute value; a caller handed a\n * magnitude cannot recover the side, and \"turn 90°\" without the side is worse\n * than saying nothing.\n *\n * THE HARD PART IS NOT DETECTING TURNS, IT IS NOT INVENTING THEM. A phone held\n * by a walking person swings 15–20° with every stride, and a compass drifts.\n * Compare against a fixed origin and the drift eventually crosses any threshold\n * on its own, so the machine re-baselines whenever the heading holds steady:\n * \"straight ahead\" is wherever you have been pointing for the last couple of\n * seconds, not wherever you were pointing when the session opened.\n */\n\n/** A completed turn. `deg` is signed: **positive = clockwise = right**. */\nexport interface TurnEvent {\n deg: number;\n /** When the turn was recognised as complete. */\n timestamp: number;\n}\n\nexport interface TurnDetectorOptions {\n /**\n * How far the heading must swing before it counts as a turn.\n *\n * 35° is a judgement, not a measurement: a street corner is ~90°, a gentle\n * bend in a corridor 30–45°, and hand-sway while walking routinely 15–20°.\n * Lower and the detector narrates the user's arm.\n */\n minTurnDeg?: number;\n /** Within this of the reference counts as \"still pointing the same way\". */\n stableDeg?: number;\n /** How long it must stay there before the reference moves. */\n stableMs?: number;\n}\n\n/** Signed shortest arc from `a` to `b`, in (-180, 180]. */\nexport function signedDelta(a: number, b: number): number {\n return ((b - a + 540) % 360) - 180;\n}\n\nexport class TurnDetector {\n private readonly minTurnDeg: number;\n private readonly stableDeg: number;\n private readonly stableMs: number;\n\n /** The heading currently treated as \"straight ahead\". */\n private refDeg: number | null = null;\n private stableSince = 0;\n private lastTurn: TurnEvent | null = null;\n\n constructor(opts: TurnDetectorOptions = {}) {\n this.minTurnDeg = opts.minTurnDeg ?? 35;\n this.stableDeg = opts.stableDeg ?? 12;\n this.stableMs = opts.stableMs ?? 2500;\n }\n\n /** Feed one compass reading. `null` degrees means the compass said nothing. */\n push(deg: number | null, now: number): TurnEvent | null {\n if (deg === null) return null;\n\n if (this.refDeg === null) {\n this.refDeg = deg;\n this.stableSince = now;\n return null;\n }\n\n const delta = signedDelta(this.refDeg, deg);\n\n if (Math.abs(delta) >= this.minTurnDeg) {\n // Re-baseline immediately, so a continuous 180° about-face is reported as\n // the turns it is made of. Left as one growing delta it would flip sign\n // at the halfway point and report a right turn for a left one.\n this.lastTurn = { deg: Math.round(delta), timestamp: now };\n this.refDeg = deg;\n this.stableSince = now;\n return this.lastTurn;\n }\n\n if (Math.abs(delta) > this.stableDeg) {\n // Moving, but not far enough yet. Hold the reference and keep watching —\n // this is what lets a slow 90° turn accumulate instead of being\n // re-baselined away one degree at a time.\n this.stableSince = now;\n return null;\n }\n\n // Steady. After a while, treat here as the new straight-ahead.\n if (now - this.stableSince >= this.stableMs) {\n this.refDeg = deg;\n this.stableSince = now;\n }\n return null;\n }\n\n /**\n * The most recent completed turn, or null if none this session.\n *\n * It does NOT expire on its own: the age is derivable from `timestamp`, and\n * \"recent enough to mention\" is a product judgement this layer must not make\n * on the caller's behalf.\n */\n last(): TurnEvent | null {\n return this.lastTurn;\n }\n\n reset(): void {\n this.refDeg = null;\n this.stableSince = 0;\n this.lastTurn = null;\n }\n}\n","// Great-circle distance — the SDK's one answer to \"how far apart are these\".\n//\n// Moved out of device-state/domains/spatial.ts when native.maps needed the same\n// question answered (a search result's distance from `near`). Two haversines\n// would be two answers that could drift; both callers import this one.\n//\n// No imports on purpose: it runs under plain `node --test` with the maps\n// normaliser that uses it.\n\n/** Metres along a path, great-circle per leg, rounded to the metre. */\nexport function distanceOf(path: ReadonlyArray<{ latitude: number; longitude: number }>): number {\n const R = 6_371_000;\n let total = 0;\n for (let i = 1; i < path.length; i++) {\n const a = path[i - 1], b = path[i];\n const dLat = ((b.latitude - a.latitude) * Math.PI) / 180;\n const dLon = ((b.longitude - a.longitude) * Math.PI) / 180;\n const la1 = (a.latitude * Math.PI) / 180, la2 = (b.latitude * Math.PI) / 180;\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2;\n total += 2 * R * Math.asin(Math.sqrt(h));\n }\n return Math.round(total);\n}\n","/**\n * Spatial events, derived from spatial snapshots.\n *\n * ONE MODEL, NOT TWO. An event is what changed between two snapshots — the\n * same snapshots `getSnapshot()` returns — so an event can never say \"moving\"\n * while a read says \"stationary\", and no threshold here has a second copy\n * somewhere else. The thresholds a read uses (the stationary speed, the GPS\n * confidence bands, distance along a path) live in this file and the domain\n * imports them, rather than the other way round, so the detector can be tested\n * without an Expo import: the live engine feeds it on every fix, every turn and\n * a one-second tick; replay feeds it recorded samples on a virtual clock.\n *\n * THE HARD PART IS NOT DETECTING CHANGES, IT IS NOT INVENTING THEM (the turn\n * detector's lesson, again). GPS speed wobbles around any threshold, and a fix\n * band flickers when accuracy sits near a boundary. So every state change must\n * HOLD before it is reported, and position changes must beat the fix's own\n * error bar. Holds delay an event on purpose; `since` in the payload carries\n * the real onset.\n *\n * A HOLD IS HELD BY FIXES, NOT BY THE CLOCK ALONE. The one-second tick re-reads\n * the SAME fix, so a hold measured only in time would let one outlier fix\n * followed by a gap — sparse fixes at Balanced accuracy, a wifi jump read as\n * speed — become \"you started moving\". A state settles only once two distinct\n * fixes have shown it; after that the tick may complete the hold, which is what\n * it is for (\"stationary for five seconds\" completes while nothing arrives).\n *\n * Type-only imports from plugin-runtime, so `node --test` can load this file.\n */\nimport type {\n Confidence,\n SpatialEvent,\n SpatialIntent,\n SpatialPoint,\n SpatialSnapshot,\n SpatialStateEvent,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { stateEvent } from './events';\nimport { distanceOf } from '../geo';\n\n/**\n * Below this the user is standing still.\n *\n * 0.4 m/s is well under a slow walk (~1.2 m/s) and above the jitter a\n * stationary phone's GPS produces. The point of the band is that a guide\n * must not say \"keep going straight\" to someone who has stopped.\n */\nexport const STATIONARY_MPS = 0.4;\n\n/**\n * Only claim stationary/moving when a speed was actually obtained. With no\n * speed the truthful answer is `unknown`, not `stationary` — \"you have\n * stopped\" is a false statement, not a neutral default.\n */\nexport function movementStateOf(speedMps: number | null): 'stationary' | 'moving' | 'unknown' {\n return speedMps === null ? 'unknown' : speedMps < STATIONARY_MPS ? 'stationary' : 'moving';\n}\n\n/**\n * The GPS fix's confidence band, from metres of horizontal error. ~10m is a\n * good open-sky fix; beyond ~50m the position is street-level at best and\n * useless for \"turn at the next corner\".\n */\nexport function fixConfidence(accuracyM: number | null): Confidence['level'] {\n return accuracyM === null ? 'unknown' : accuracyM <= 10 ? 'high' : accuracyM <= 50 ? 'medium' : 'low';\n}\n\n\nexport interface SpatialEventProfile {\n /** How long `moving` must hold before `movementStarted`. */\n movingHoldMs: number;\n /** How long `stationary` must hold, after moving, before `movementStopped`. */\n stoppedHoldMs: number;\n /** How long a new GPS confidence band must hold before it is reported. */\n confidenceHoldMs: number;\n /** Floor for `positionChanged`, metres. The real threshold is\n * max(this, moveThresholdM(anchor, fix)) — both fixes' uncertainty — so a\n * loose fix on EITHER end needs a bigger move. */\n minMoveM: number;\n}\n\n/**\n * Intent → how eager the events are.\n *\n * These are the engine's judgement, not the freeze's — the architecture doc\n * says only \"thresholds are profile-dependent\". Recorded here so the reasoning\n * is visible and arguable rather than buried. The intent changes ONLY these\n * holds, not sampling: there is no native strategy layer for it to reach.\n *\n * - `navigation`: a guide needs \"you've stopped\" soon enough to act on, but a\n * pause at a crossing is not a stop worth announcing — 3 s to start, 5 s to\n * stop. 10 m is about one building frontage.\n * - `fitness`: same movement holds; a slower confidence hold, because a runner\n * under trees dips in and out of a band constantly and nobody needs to hear it.\n * - `preciseLocate`: the caller is deliberately pinning a position, so the\n * holds are shortest and a few metres counts.\n * - `geofence` / `lowPowerTracking`: coarse by nature — tens of metres, and a\n * state must hold for a while before it is worth waking anyone for.\n */\nexport const SPATIAL_EVENT_PROFILES: Record<SpatialIntent, SpatialEventProfile> = {\n navigation: { movingHoldMs: 3000, stoppedHoldMs: 5000, confidenceHoldMs: 3000, minMoveM: 10 },\n fitness: { movingHoldMs: 3000, stoppedHoldMs: 5000, confidenceHoldMs: 5000, minMoveM: 10 },\n preciseLocate: { movingHoldMs: 2000, stoppedHoldMs: 3000, confidenceHoldMs: 2000, minMoveM: 3 },\n geofence: { movingHoldMs: 10000, stoppedHoldMs: 30000, confidenceHoldMs: 10000, minMoveM: 25 },\n lowPowerTracking: { movingHoldMs: 10000, stoppedHoldMs: 30000, confidenceHoldMs: 10000, minMoveM: 50 },\n};\n\n/** The profile for an intent; an unrecognised one gets `navigation`'s. */\n/**\n * The move two fixes can vouch for. A fix is a circle, not a point, and so is\n * the anchor it is compared with: two fixes of a phone lying still differ by up\n * to BOTH their errors. Combined (root-sum-square, each accuracyM ~ one radius)\n * and doubled, like a single fix's 2× before. No estimate = 0 (the floor rules).\n * Anchor ±100 m → fix ±5 m: a 20 m difference is noise, not a walk.\n */\nexport function moveThresholdM(anchor: { accuracyM?: number | null }, fix: { accuracyM?: number | null }): number {\n const a = anchor.accuracyM ?? 0, b = fix.accuracyM ?? 0;\n return 2 * Math.sqrt(a * a + b * b);\n}\n\nexport function spatialEventProfile(intent: string | null | undefined): SpatialEventProfile {\n return SPATIAL_EVENT_PROFILES[intent as SpatialIntent] ?? SPATIAL_EVENT_PROFILES.navigation;\n}\n\ntype Band = 'high' | 'medium' | 'low';\nconst RANK: Record<Band, number> = { high: 3, medium: 2, low: 1 };\ntype PayloadOf<N extends SpatialEvent> = Extract<SpatialStateEvent, { name: N }>['payload'];\n\n/** Distinct fixes a state must be seen on before its hold can complete. */\nconst MIN_FIXES = 2;\n\n/**\n * An unbroken run of one raw state: when it began, and how many distinct fixes\n * (by `position.timestamp`) have shown it. A re-read of the same fix extends\n * the run's time, never its evidence.\n */\ninterface Run<S> { state: S; since: number; fixAt: number | null; fixes: number }\n\nfunction extend<S>(run: Run<S> | null, state: S, fixAt: number | null, now: number): Run<S> {\n if (!run || run.state !== state) return { state, since: now, fixAt, fixes: fixAt === null ? 0 : 1 };\n if (fixAt !== null && fixAt !== run.fixAt) { run.fixAt = fixAt; run.fixes++; }\n return run;\n}\n\nconst settled = (run: Run<unknown>, now: number, holdMs: number): boolean =>\n run.fixes >= MIN_FIXES && now - run.since >= holdMs;\n\n/** Which fix a snapshot was built from — its identity when counting evidence. */\nconst fixAt = (s: SpatialSnapshot): number | null => s.position?.timestamp ?? null;\n\nexport class SpatialEventDetector {\n private profile: SpatialEventProfile;\n\n /** The movement state last settled on. Only MEASURED states settle. */\n private movement: 'stationary' | 'moving' | 'unknown' = 'unknown';\n private run: Run<'stationary' | 'moving' | 'unknown'> | null = null;\n\n /** The completed turn already on record — reported turns, and the one the\n * session opened onto. Only a turn with a new timestamp is news. */\n private lastTurnAt: number | null = null;\n\n /** The GPS band last settled on; null until the session's first band. */\n private band: Band | null = null;\n private bandRun: Run<Band> | null = null;\n\n /** The last position REPORTED — distance is measured from here, not from\n * the previous fix, so a slow walk still adds up to a change. */\n private anchor: SpatialPoint | null = null;\n\n private staleArmed = true;\n\n constructor(profile: SpatialEventProfile = SPATIAL_EVENT_PROFILES.navigation) {\n this.profile = profile;\n }\n\n /**\n * Forget everything — a new session, or replay seeking backwards. Emits\n * nothing. `turnOnRecordAt` is the timestamp of the turn already completed\n * when the session opens (a visual session shares the compass): history, not\n * news. Seeded from the turn itself rather than compared against a clock, so\n * it holds whichever clock the turn was stamped with — epoch live, the\n * recording's epoch in replay, where `now` is virtual.\n */\n reset(profile?: SpatialEventProfile, turnOnRecordAt: number | null = null): void {\n if (profile) this.profile = profile;\n this.movement = 'unknown';\n this.run = null;\n this.lastTurnAt = turnOnRecordAt;\n this.band = null;\n this.bandRun = null;\n this.anchor = null;\n this.staleArmed = true;\n }\n\n /**\n * Feed the newest snapshot. Returns what changed since the last one, in this\n * order: movement, turn, position, confidence, staleness. Fields are read\n * defensively — replay hands over recorded snapshots verbatim, and a recording\n * may lack a block the live engine always fills.\n */\n push(snapshot: SpatialSnapshot, now: number): SpatialStateEvent[] {\n const out: SpatialStateEvent[] = [];\n const emit: Emit = (name, payload) => {\n // The payload is checked against its name by `Emit`; the union cannot\n // follow a generic name through stateEvent's return, hence the assertion.\n out.push(stateEvent('spatial', name, payload as unknown, now) as SpatialStateEvent);\n };\n this.movementFrom(snapshot, now, emit);\n this.turnFrom(snapshot, emit);\n this.positionFrom(snapshot, emit);\n this.confidenceFrom(snapshot, now, emit);\n this.staleFrom(snapshot, emit);\n return out;\n }\n\n private movementFrom(s: SpatialSnapshot, now: number, emit: Emit): void {\n const state = s.movement?.state ?? 'unknown';\n this.run = extend(this.run, state, fixAt(s), now);\n if (state === this.movement) return;\n\n if (state === 'moving' && settled(this.run, now, this.profile.movingHoldMs)) {\n emit('movementStarted', {\n previous: this.movement === 'stationary' ? 'stationary' : 'unknown',\n speedMps: s.movement?.speedMps ?? null,\n bearingDeg: s.movement?.bearingDeg ?? null,\n since: this.run.since,\n });\n this.movement = 'moving';\n } else if (state === 'stationary' && settled(this.run, now, this.profile.stoppedHoldMs)) {\n if (this.movement === 'moving') emit('movementStopped', { previous: 'moving', since: this.run.since });\n this.movement = 'stationary';\n }\n // `unknown` never settles. Losing the speed is not evidence of stopping, so\n // moving → unknown says nothing, and a later measured `stationary` is still\n // reported as the stop it is.\n }\n\n private turnFrom(s: SpatialSnapshot, emit: Emit): void {\n const at = s.changes?.timestamp ?? null;\n const deg = s.changes?.lastMeaningfulTurnDeg ?? null;\n // A compass reset clears the turn to null. That is not a new turn.\n if (at === null || deg === null || at === this.lastTurnAt) return;\n this.lastTurnAt = at;\n emit('lastMeaningfulTurnChanged', { lastMeaningfulTurnDeg: deg, timestamp: at });\n }\n\n private positionFrom(s: SpatialSnapshot, emit: Emit): void {\n const p = s.position ?? null;\n if (!p) return;\n if (!this.anchor) {\n emit('positionChanged', { position: { ...p }, previous: null, distanceM: null });\n this.anchor = p;\n return;\n }\n const d = distanceOf([this.anchor, p]);\n if (d < Math.max(this.profile.minMoveM, moveThresholdM(this.anchor, p))) {\n // Not a move — but if this fix is far more precise than the anchor, keep\n // IT as the reference (silently): measuring later moves from a ±100 m\n // anchor would hide real ones behind the old fix's error.\n const aNew = p.accuracyM ?? null, aOld = this.anchor.accuracyM ?? null;\n if (aNew != null && aOld != null && aNew * 2 < aOld) this.anchor = p;\n return;\n }\n emit('positionChanged', { position: { ...p }, previous: { ...this.anchor }, distanceM: d });\n this.anchor = p;\n }\n\n private confidenceFrom(s: SpatialSnapshot, now: number, emit: Emit): void {\n const p = s.position ?? null;\n if (!p) return;\n const level = fixConfidence(p.accuracyM ?? null);\n // A fix with no accuracy estimate says nothing about the band — the same\n // way the snapshot's confidence ignores an `unknown` part.\n if (level === 'unknown') return;\n // The first band is where the session starts, not a change.\n if (this.band === null) { this.band = level; return; }\n if (level === this.band) { this.bandRun = null; return; }\n this.bandRun = extend(this.bandRun, level, fixAt(s), now);\n if (!settled(this.bandRun, now, this.profile.confidenceHoldMs)) return;\n emit('positionConfidenceChanged', {\n level,\n previous: this.band,\n accuracyM: p.accuracyM ?? null,\n direction: RANK[level] < RANK[this.band] ? 'dropped' : 'recovered',\n });\n this.band = level;\n this.bandRun = null;\n }\n\n private staleFrom(s: SpatialSnapshot, emit: Emit): void {\n const f = s.quality?.freshness;\n if (!f) return;\n // Re-armed only by a FRESH read: `aging` on the way back is not recovery.\n if (f.state === 'fresh') { this.staleArmed = true; return; }\n if (f.state !== 'stale' || !this.staleArmed) return;\n emit('spatialStateStale', { observedAt: f.observedAt, ageMs: f.ageMs });\n this.staleArmed = false;\n }\n}\n\ntype Emit = <N extends SpatialEvent>(name: N, payload: PayloadOf<N>) => void;\n","// ── spatial ──────────────────────────────────────────────────────────────\n// position ✓ path ✓ heading ✓ turns ✓ events ✓ (while a session is open) speed ~derived\nimport type {\n DeviceState,\n SpatialIntent,\n SpatialSnapshot,\n SpatialStateEvent,\n SpatialPoint,\n StateQuality,\n Subscription,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { startHeading, stopHeading, currentHeadingDetail, lastMeaningfulTurn, onTurn } from '../headingSource';\nimport type { DeviceStateCollectors, SpeedRef, WatchAccuracy } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\nimport { distanceOf } from '../../geo';\nimport { listenerSet } from '../events';\nimport {\n SpatialEventDetector,\n fixConfidence,\n movementStateOf,\n spatialEventProfile,\n} from '../spatialEvents';\n\n/**\n * Position, movement, facing and a short recent path.\n *\n * The design's performance rule is that navigation reads come from a WARM\n * native cache, not a fresh GPS+compass call per invocation. There is no such\n * cache today, so `getSnapshot` takes a fix — correct but not cheap. That is\n * why `start()` matters: it opens a `watchPosition` stream AND a compass watch,\n * and every later snapshot serves the newest streamed values instead, which is\n * the warm path this layer can offer without a native module.\n *\n * TWO DIRECTIONS, NOT ONE, and conflating them is the bug this replaced.\n *\n * `orientation.headingDeg` — where the phone is POINTING. The compass.\n * `movement.bearingDeg` — where the user is GOING. Course over ground.\n *\n * They disagree constantly and each answers a different sentence. \"Turn right\n * at the corner\" is decided by the compass; \"you are heading north on Main\n * Street\" by the course. The previous version assigned the same variable to\n * both, which is only right by accident, and that variable was permanently\n * null because the streaming collector dropped the fields.\n *\n * TURNS ARE SIGNED. `changes.lastMeaningfulTurnDeg` is positive for clockwise\n * (right) and negative for anticlockwise (left), so a guide can say which way\n * the user just went without a second field to consult. The freeze names the\n * field but states no convention; this is a decision, recorded here.\n *\n * EVENTS ARE THE SNAPSHOT, DIFFED. `subscribe` publishes what changed between\n * successive warm snapshots — the same ones `getSnapshot` returns — via the\n * pure detector in spatialEvents.ts. It is fed on every fix, the moment a turn\n * completes, and once a second, so a transition that happens between fixes\n * (fixes stopping is itself one: the state goes stale) is still noticed.\n */\n/** The GPS tier each intent's stream runs at. Navigation is the one that needs\n * \"which side of the street\"; everything else trades precision for battery. */\nconst WATCH_ACCURACY: Record<SpatialIntent, WatchAccuracy> = {\n navigation: 'navigation',\n preciseLocate: 'high',\n fitness: 'high',\n geofence: 'balanced',\n lowPowerTracking: 'low',\n};\n\nexport function createSpatialDomain(\n collectors: DeviceStateCollectors,\n speedRef: SpeedRef,\n): DeviceState['spatial'] {\n // The warm cache, when a session is open. Newest last, bounded — an unbounded\n // path on a walking user grows without limit.\n const RECENT_PATH_MAX = 60;\n\n let recentPath: SpatialPoint[] = [];\n let latest: SpatialPoint | null = null;\n /** Speed and course from the newest STREAMED fix — see `latest`. */\n let latestSpeed: number | null = null;\n let latestCourse: number | null = null;\n let stopWatch: (() => void) | null = null;\n /** The running session. A second `start()` joins it, and is told so. */\n let session: { id: string; intent: SpatialIntent; startedAt: number } | null = null;\n\n const detector = new SpatialEventDetector();\n const listeners = listenerSet<SpatialStateEvent>();\n let ticker: ReturnType<typeof setInterval> | null = null;\n let turnSub: Subscription | null = null;\n\n const nonNegative = (v: unknown): number | null =>\n typeof v === 'number' && v >= 0 ? v : null;\n\n /**\n * Speed from the path when the OS declined to estimate one.\n *\n * Only over the last two points and only if they are close in time: two fixes\n * a minute apart give an average that describes neither moment. Returns null\n * rather than a number the caller cannot tell apart from a measured one —\n * the confidence on the snapshot is what marks it as derived.\n */\n const derivedSpeed = (): number | null => {\n if (recentPath.length < 2) return null;\n const b = recentPath[recentPath.length - 1];\n const a = recentPath[recentPath.length - 2];\n const dtS = (b.timestamp - a.timestamp) / 1000;\n if (dtS <= 0 || dtS > 10) return null;\n return distanceOf([a, b]) / dtS;\n };\n\n /** Course from the path, for the same reason and with the same caution. */\n const derivedCourse = (): number | null => {\n if (recentPath.length < 2) return null;\n const b = recentPath[recentPath.length - 1];\n const a = recentPath[recentPath.length - 2];\n if (b.timestamp - a.timestamp > 10_000) return null;\n // Too small a step is noise, not a direction.\n if (distanceOf([a, b]) < 3) return null;\n return bearingBetween(a, b);\n };\n\n /**\n * The snapshot for a fix — the ONE builder, used by the read and by the\n * event detector alike. Synchronous: everything it needs is already in hand.\n */\n const snapshotFrom = (\n point: SpatialPoint,\n measuredSpeed: number | null,\n measuredCourse: number | null,\n ): SpatialSnapshot => {\n // Fall back to the path only where the OS said nothing.\n let speedMps = measuredSpeed;\n let courseDeg = measuredCourse;\n let derived = false;\n if (speedMps === null) { const d = derivedSpeed(); if (d !== null) { speedMps = d; derived = true; } }\n if (courseDeg === null) { const d = derivedCourse(); if (d !== null) { courseDeg = d; derived = true; } }\n\n const heading = currentHeadingDetail();\n const turn = lastMeaningfulTurn();\n\n return {\n position: point,\n movement: {\n speedMps,\n bearingDeg: courseDeg,\n // Needs two points from the SAME session; without a stream there is\n // nothing to measure against, and a distance of 0 would read as\n // \"the user has not moved\".\n distanceRecentM: recentPath.length >= 2 ? distanceOf(recentPath) : null,\n state: movementStateOf(speedMps),\n },\n orientation: {\n headingDeg: heading?.headingDeg ?? null,\n // NOT the calibration rank. iOS reports 0–3 (none…high), which is not\n // a number of degrees, and converting one to the other would be an\n // invented error bar. The rank drives `confidence` below instead.\n headingAccuracyDeg: null,\n },\n changes: {\n lastMeaningfulTurnDeg: turn ? turn.deg : null,\n timestamp: turn ? turn.timestamp : null,\n },\n recentPath: [...recentPath],\n quality: qualityFrom(\n point.timestamp,\n spatialConfidence(point.accuracyM ?? null, heading?.calibration ?? null, derived),\n ),\n };\n };\n\n /** \"No position\" — honest nulls, never a (0,0). The compass may still know\n * which way the phone points, so heading is kept when it has one. */\n const noFix = (): SpatialSnapshot => ({\n position: null,\n movement: { speedMps: null, bearingDeg: null, distanceRecentM: null, state: 'unknown' },\n orientation: { headingDeg: currentHeadingDetail()?.headingDeg ?? null },\n recentPath: [],\n quality: qualityUnknown(),\n });\n\n /** The warm snapshot, or null before the session's first streamed fix. */\n const warmSnapshot = (): SpatialSnapshot | null =>\n latest ? snapshotFrom(latest, latestSpeed, latestCourse) : null;\n\n /**\n * Diff the snapshot a read would get RIGHT NOW against the last one, and\n * publish what changed. Before the first fix there is nothing to diff; a\n * turn completed meanwhile is not lost, it is reported with the first fix.\n */\n const observe = () => {\n const snapshot = warmSnapshot();\n if (!snapshot) return;\n for (const event of detector.push(snapshot, Date.now())) listeners.emit(event);\n };\n\n /**\n * Re-diff once a second while a session is open. Fixes alone are not enough:\n * the OS may pause them while the phone is still, and \"the state has gone\n * stale\" or \"you have been stationary for five seconds\" are exactly the\n * transitions that complete while nothing arrives. A tick re-reads the same\n * fix, so it only completes a state the fixes already showed — it never\n * turns one outlier fix into an event (spatialEvents.ts, MIN_FIXES).\n */\n const startTicking = () => {\n ticker = setInterval(observe, 1000);\n };\n\n const stopTicking = () => {\n if (ticker) clearInterval(ticker);\n ticker = null;\n };\n\n return {\n async getSnapshot(): Promise<SpatialSnapshot> {\n // Serve the warm fix when a session is streaming; otherwise take one.\n const warm = warmSnapshot();\n if (warm) return warm;\n // A session is open but its stream has not delivered yet: say so NOW.\n // A one-shot fix here would make every read wait on a cold GPS (seconds),\n // stalling whatever loop reads us — and the stream answers soon anyway.\n if (session) return noFix();\n\n let point: SpatialPoint;\n let speedMps: number | null;\n let courseDeg: number | null;\n try {\n if (!collectors.location) throw unsupported('location collector');\n const fix: any = await collectors.location.getPosition({ accuracy: 'low' });\n point = {\n latitude: fix.latitude,\n longitude: fix.longitude,\n altitude: fix.altitude ?? null,\n accuracyM: nonNegative(fix.accuracy),\n timestamp: fix.timestamp ?? Date.now(),\n };\n speedMps = nonNegative(fix.speed);\n courseDeg = nonNegative(fix.course);\n } catch {\n // Permission refused, or no fix. Not a position at (0,0).\n return noFix();\n }\n return snapshotFrom(point, speedMps, courseDeg);\n },\n\n /**\n * Open the warm window: position stream + compass + event feed.\n *\n * The intent selects how eager the EVENTS are (spatialEvents.ts profiles)\n * and the GPS TIER the stream runs at (WATCH_ACCURACY): navigation pays the\n * battery for BestForNavigation; low-power tracking does not.\n *\n * A second `start()` while one is open joins the running session and is\n * told ITS intent, not handed back the one it asked for: answering with the\n * caller's intent would claim a profile that is not in force.\n */\n async start(options) {\n if (session) return { ...session };\n if (!collectors.location) throw unsupported('location collector');\n const startedAt = Date.now();\n session = { id: `spatial_${startedAt}`, intent: options.intent, startedAt };\n // A turn the compass completed before this session (a visual session\n // shares it) is already on record, not news.\n detector.reset(spatialEventProfile(options.intent), lastMeaningfulTurn()?.timestamp ?? null);\n // The compass is what makes heading and turn detection possible, and it\n // has to run for the session's whole life: a turn happens between reads,\n // so a compass started on demand would miss exactly the event a guide\n // most needs. Shared with a visual session if one is open.\n void startHeading('spatial');\n // A turn is published the moment it completes, not on the next tick.\n turnSub = onTurn(observe);\n stopWatch = collectors.location.watchPosition((pos: any) => {\n const p: SpatialPoint = {\n latitude: pos.latitude,\n longitude: pos.longitude,\n altitude: pos.altitude ?? null,\n accuracyM: nonNegative(pos.accuracy),\n timestamp: pos.timestamp ?? Date.now(),\n };\n latest = p;\n latestSpeed = nonNegative(pos.speed);\n latestCourse = nonNegative(pos.course);\n speedRef.mps = latestSpeed;\n speedRef.at = p.timestamp;\n recentPath = [...recentPath, p].slice(-RECENT_PATH_MAX);\n observe();\n }, { accuracy: WATCH_ACCURACY[options.intent] ?? 'balanced' });\n startTicking();\n return { ...session };\n },\n\n async stop() {\n stopWatch?.();\n stopWatch = null;\n session = null;\n stopTicking();\n turnSub?.unsubscribe();\n turnSub = null;\n // Listeners are KEPT: a subscription outlives a session, and hears the\n // next one. Only what this session observed is forgotten.\n detector.reset();\n // The path belongs to the session that recorded it. Keeping it would let\n // a later snapshot report movement from a walk that ended hours ago.\n recentPath = [];\n latest = null;\n latestSpeed = null;\n latestCourse = null;\n speedRef.mps = null;\n speedRef.at = 0;\n // Releases only spatial's hold — a visual session keeps the compass.\n stopHeading('spatial');\n },\n\n /**\n * The six spatial events (see `SpatialEvent`). Subscribing opens no sensor:\n * events flow only while a session is open, and a subscription made before\n * `start()` hears that session from its first fix.\n */\n subscribe(listener) {\n return listeners.add(listener);\n },\n };\n}\n\n/**\n * How much to trust this spatial reading.\n *\n * Three things degrade it and they are not interchangeable: a loose GPS fix\n * (the position is vague), an uncalibrated compass (the FACING is vague, which\n * is what breaks \"turn left\"), and values derived from the path rather than\n * measured by the OS. The lowest of the three wins, because a caller acting on\n * the snapshot is exposed to the worst part of it, not the average.\n *\n * This is the signal that decides Ada Guide's modality switch: indoors the fix\n * degrades, confidence drops, and that drop is what makes the guide ask for the\n * camera instead of guessing from a GPS fix it should not trust.\n */\nfunction spatialConfidence(\n accuracyM: number | null,\n calibration: number | null,\n derived: boolean,\n): StateQuality['confidence'] {\n const rank = (v: 'high' | 'medium' | 'low' | 'unknown') =>\n v === 'high' ? 3 : v === 'medium' ? 2 : v === 'low' ? 1 : 0;\n\n // The fix band is shared with positionConfidenceChanged — one set of numbers.\n const fromFix = fixConfidence(accuracyM);\n\n // iOS calibration rank: 3 high, 2 medium, 1 low, 0 none.\n const fromCompass: 'high' | 'medium' | 'low' | 'unknown' =\n calibration === null ? 'unknown'\n : calibration >= 3 ? 'high'\n : calibration >= 2 ? 'medium'\n : 'low';\n\n const parts = [fromFix, fromCompass, ...(derived ? (['low'] as const) : [])];\n const known = parts.filter(p => p !== 'unknown');\n if (!known.length) return { level: 'unknown' };\n const worst = known.reduce((a, b) => (rank(a) <= rank(b) ? a : b));\n return { level: worst };\n}\n\n/** Initial bearing from `a` to `b`, degrees clockwise from north. */\nfunction bearingBetween(a: SpatialPoint, b: SpatialPoint): number {\n const toRad = (d: number) => (d * Math.PI) / 180;\n const la1 = toRad(a.latitude), la2 = toRad(b.latitude);\n const dLon = toRad(b.longitude - a.longitude);\n const y = Math.sin(dLon) * Math.cos(la2);\n const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLon);\n return (((Math.atan2(y, x) * 180) / Math.PI) + 360) % 360;\n}\n","/**\n * The Visual engine — a rolling frame timeline the Visual domain reads.\n *\n * WHY THIS IS SHAPED ODDLY. expo-camera can only produce frames through a\n * MOUNTED `<CameraView>`; there is no headless capture API. So the engine is\n * split in two:\n *\n * this module — the state: the ring, the session, the intent, the cadence.\n * Pure TS, no React, so the domain and tests can use it.\n * VisualEngineHost — a component the app mounts once. When a session is open\n * it mounts the camera and feeds frames in; otherwise it\n * renders nothing and the camera is off.\n *\n * The design's rule is that `getSnapshot()` must be a CACHED read that does not\n * open the camera or take a picture — \"avoid repeatedly starting the camera or\n * capturing a new image for every plugin-tool call during an interactive\n * session\". The ring is what makes that possible: capture runs on the engine's\n * own cadence, and reads serve whatever is already there.\n *\n * THE CADENCE IS THE ENGINE'S CHOICE, NOT THE PLUGIN'S. A plugin declares an\n * INTENT — `navigation`, `document`, `ocr` — and this picks the interval. That\n * is the whole point of the semantic layer: a caller asking for frames every\n * 500ms is specifying an implementation, and cannot know the analysis latency,\n * the battery state or the thermal headroom. Intent survives those changing;\n * a number does not.\n *\n * EACH FRAME CARRIES ITS ORIENTATION — heading, read at capture time. Without\n * it a frame cannot support a guidance instruction: \"turn left at the corner\"\n * needs to know which way the lens faced. Reading it afterwards would pair a\n * frame with a heading from after the user turned, which is how a guide\n * describes the wrong junction with confidence.\n *\n * POSITION IS NOT ON THE FRAME, and that is the freeze's decision, not an\n * omission. VisualFrameRef (API_FREEZE §4) carries `orientation` only; where\n * the user IS belongs to Spatial. A caller that needs both correlates them by\n * `timestamp` — both snapshots carry one. Duplicating position here would\n * create a second copy that can disagree with Spatial's, which is precisely\n * the two-descriptions-of-one-thing problem this whole layer exists to end.\n *\n * NOT IMPLEMENTED, and reported as `unknown`/null rather than guessed:\n * per-frame blur, exposure and obstruction (need image analysis), and\n * pitch/roll (need device-motion, not installed). A frame too dark to use must\n * not be described as `exposure: 'good'` because nothing looked at it.\n */\nimport type { StateQuality } from '@ateam-ai-mobile/plugin-runtime';\n\n/** A frame in the ring. `uri` is a local file the app can read; nothing is uploaded. */\nexport interface VisualFrame {\n handle: { id: string; mimeType: string; expiresAt: number | null };\n timestamp: number;\n /**\n * WHERE THE CAMERA WAS POINTING, stamped at capture time.\n *\n * A frame without this is nearly useless for guidance: \"turn left at the\n * corner\" needs to know which way the lens faced and where the user stood.\n * And it has to be captured WITH the frame, not read afterwards — by the\n * time a snapshot is read the user has walked on, and pairing a frame with a\n * later heading is how a guide describes the wrong junction confidently.\n *\n * Every field is nullable and null means NOT MEASURED. `headingDeg: 0` is due\n * north, so a missing compass must not collapse to 0.\n */\n orientation?: {\n /** True heading if available, else magnetic. Null when the compass said nothing. */\n headingDeg?: number | null;\n /** Not measured — needs device-motion, which is not installed. */\n pitchDeg?: number | null;\n rollDeg?: number | null;\n };\n quality?: { blur?: string; exposure?: string; obstructed?: boolean | null };\n}\n\n/**\n * Supplies the spatial context stamped onto each frame.\n *\n * Injected rather than imported for the same reason the collectors are, and\n * because it must be CHEAP: it is read once per capture, on the engine's\n * cadence. Both getters return null rather than throwing — a frame with no\n * heading is still a usable frame, and losing the picture because the compass\n * was busy would be the wrong trade.\n */\nexport interface VisualContextProvider {\n heading(): { headingDeg: number | null } | null;\n}\n\nexport type VisualIntentName =\n | 'navigation' | 'document' | 'ocr' | 'shopping'\n | 'inspection' | 'accessibility' | 'stream' | 'highQuality';\n\n/**\n * Intent → capture cadence and window.\n *\n * These are the engine's judgement, not the freeze's — the docs say native\n * owns sampling and deliberately do not fix numbers. Recorded here so the\n * reasoning is visible and arguable rather than buried:\n *\n * - `navigation`: a walking user's scene changes continuously, so a steady\n * cadence is right; ~1s is the ceiling at which a guidance instruction is\n * still about the present. Faster mostly queues behind the analysis.\n * - `document` / `ocr`: the subject is held still and the user is aiming. A\n * slow cadence is enough, and a big window is pointless.\n * - `stream`: the fastest we offer, for a caller that really is watching.\n * - `highQuality`: rare, deliberate, single good frame.\n */\nconst PROFILES: Record<VisualIntentName, { intervalMs: number; windowMs: number }> = {\n navigation: { intervalMs: 1000, windowMs: 5000 },\n accessibility: { intervalMs: 1000, windowMs: 5000 },\n stream: { intervalMs: 500, windowMs: 5000 },\n inspection: { intervalMs: 2000, windowMs: 8000 },\n shopping: { intervalMs: 2000, windowMs: 5000 },\n document: { intervalMs: 3000, windowMs: 3000 },\n ocr: { intervalMs: 3000, windowMs: 3000 },\n highQuality: { intervalMs: 5000, windowMs: 5000 },\n};\n\nexport interface VisualSessionState {\n id: string;\n intent: VisualIntentName;\n startedAt: number;\n /** Set while the app is backgrounded. The session SURVIVES; the camera does not. */\n suspended: boolean;\n}\n\ntype Listener = () => void;\n\nclass VisualEngine {\n private frames: VisualFrame[] = [];\n private session: VisualSessionState | null = null;\n private listeners = new Set<Listener>();\n /** Set by the host component while a camera is actually mounted and ready. */\n private cameraReady = false;\n /** Optional: supplies heading/position stamped onto each frame. */\n private context: VisualContextProvider | null = null;\n\n /** Host DI. Without it frames carry no orientation, and say so with nulls. */\n setContextProvider(p: VisualContextProvider | null): void { this.context = p; }\n\n // ── session ──\n\n startSession(intent: VisualIntentName): VisualSessionState {\n if (this.session) return this.session;\n this.session = {\n id: `visual_${Date.now()}`,\n intent: PROFILES[intent] ? intent : 'navigation',\n startedAt: Date.now(),\n suspended: false,\n };\n this.notify();\n return this.session;\n }\n\n stopSession(): void {\n this.session = null;\n // The timeline belongs to the session. Keeping it would let a later read\n // describe a room the user left.\n this.frames = [];\n this.cameraReady = false;\n this.notify();\n }\n\n /**\n * Background: suspend, do not end.\n *\n * iOS tears the camera down when the app leaves the foreground. The session\n * is a STATE that survives that; what does not survive is the claim that the\n * camera is live. While suspended the ring is kept but ages — a read gets\n * old frames clearly marked old, rather than a fresh-looking lie.\n */\n suspend(): void {\n if (!this.session || this.session.suspended) return;\n this.session.suspended = true;\n this.cameraReady = false;\n this.notify();\n }\n\n resume(): void {\n if (!this.session?.suspended) return;\n this.session.suspended = false;\n this.notify();\n }\n\n getSession(): VisualSessionState | null { return this.session; }\n\n /** Is a camera mounted and ready right now? Only the host knows. */\n setCameraReady(ready: boolean): void {\n this.cameraReady = ready;\n this.notify();\n }\n isCameraReady(): boolean { return this.cameraReady && !this.session?.suspended; }\n\n // ── the ring ──\n\n profile() {\n const intent = this.session?.intent || 'navigation';\n return PROFILES[intent];\n }\n\n /**\n * Feed from the native vision session (VisionSession, iOS) — the FAST frame\n * source that replaces the takePictureAsync loop. The frame is IN-MEMORY: the\n * native ring holds the pixels keyed by `id`, and native.vision resolves that\n * id inside the trusted layer. No `uri` crosses here; the quality is a REAL\n * measurement (not `unknown`), and `expiresAt` is the ring's real eviction time.\n * The public VisualFrameRef strip to id-only is owned by the device-state session.\n */\n pushFrameMeta(meta: {\n id: string;\n timestamp: number;\n expiresAt?: number | null;\n quality?: { blur?: string; exposure?: string; obstructed?: boolean | null };\n }): void {\n let heading: { headingDeg: number | null } | null = null;\n try { heading = this.context?.heading() ?? null; } catch { /* a frame without a heading beats no frame */ }\n\n this.frames.push({\n // In-memory: no file. native.vision resolves the id inside the native ring.\n handle: { id: meta.id, mimeType: 'image/none', expiresAt: meta.expiresAt ?? null },\n timestamp: meta.timestamp,\n orientation: { headingDeg: heading?.headingDeg ?? null, pitchDeg: null, rollDeg: null },\n quality: meta.quality ?? { blur: 'unknown', exposure: 'unknown', obstructed: null },\n });\n this.evict();\n this.notify();\n }\n\n // The native ring's actual retention (ms). The engine's effective window is\n // min(intent window, retention) — set by whoever owns the clamp semantics; this\n // just stores the input so that logic has one source of truth to read.\n private _retentionMs: number | null = null;\n setRetentionMs(ms: number | null): void { this._retentionMs = ms && ms > 0 ? ms : null; }\n retentionMs(): number | null { return this._retentionMs; }\n\n /**\n * How far back the timeline ACTUALLY reaches: `min(intent window, native\n * retention)`.\n *\n * The intent asks for a window; native decides what it can afford to keep.\n * Full-res luma at ~24fps is tens of megabytes for a few seconds, so the ring\n * is memory-bounded and shrinks under pressure — `retentionMs` is native\n * publishing what it is really holding.\n *\n * EVERY FRAME IN THE TIMELINE CAN BE ANALYSED. That is the one sentence a\n * plugin author has to hold, and this clamp is what makes it true. The\n * alternative — listing frames past retention marked \"not analysable\" — was\n * considered and rejected: a caller would have to check a flag before every\n * use, and forgetting throws mid-guidance on a frame the model can see\n * listed. Present-but-unusable is the same trap as an empty rowset reading\n * as \"no rows matched\".\n *\n * If per-frame history beyond retention is ever needed, the shape is an\n * AGGREGATE on the snapshot (\"the view has been unusable for N ms\"), never a\n * per-entry flag.\n */\n effectiveWindowMs(): number {\n const intent = this.profile().windowMs;\n const retention = this._retentionMs;\n return retention === null ? intent : Math.min(intent, retention);\n }\n\n /**\n * Time-based, not count-based: the design specifies a rolling WINDOW.\n *\n * Evicts on the EFFECTIVE window, so a frame whose pixels native has already\n * dropped leaves the timeline at the same moment. Without this, `frameById`\n * would start returning null for frames still being listed.\n */\n private evict(): void {\n const cutoff = Date.now() - this.effectiveWindowMs();\n this.frames = this.frames.filter(f => f.timestamp >= cutoff);\n }\n\n timeline(): { windowMs: number; frames: VisualFrame[] } {\n this.evict();\n // The EFFECTIVE window, never the intent's. Reporting the intent would have\n // a guide reason about 8 seconds of history it does not have — precisely\n // the confusion the clamp exists to prevent.\n return { windowMs: this.effectiveWindowMs(), frames: [...this.frames] };\n }\n\n latest(): VisualFrame | null {\n this.evict();\n return this.frames.length ? this.frames[this.frames.length - 1] : null;\n }\n\n /**\n * Resolve a frame handle id (from a VisualFrameRef) back to the live frame so\n * the mechanical layer — native.vision — can read its on-device file. The\n * whole contract of that layer is that it takes an id, never pixels; this is\n * where an id becomes a `handle.uri` it can open. Returns null when the id is\n * unknown OR its frame has aged out of the rolling window (the cache file may\n * also be gone by then) — a real \"I can't look at that frame\" answer, which\n * the caller must treat as distinct from \"I looked and found nothing\".\n */\n frameById(id: string): VisualFrame | null {\n this.evict();\n return this.frames.find(f => f.handle.id === id) ?? null;\n }\n\n /**\n * Record a MEASURED quality back onto a frame (native.vision.assessQuality).\n * Frames are minted with `quality: unknown` because nothing looks at the\n * pixels at capture time; this is how a real measurement replaces that so a\n * later read of VisualFrameRef.quality reflects it. No-op if the frame has\n * aged out — we never resurrect an evicted frame just to label it.\n */\n setFrameQuality(id: string, quality: NonNullable<VisualFrame['quality']>): void {\n const f = this.frames.find(fr => fr.handle.id === id);\n if (!f) return;\n f.quality = quality;\n this.notify();\n }\n\n // ── change notification ──\n\n subscribe(fn: Listener): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n private notify(): void {\n for (const fn of this.listeners) { try { fn(); } catch { /* a bad listener is not the engine's problem */ } }\n }\n}\n\n/** One engine per app. The camera is a singleton resource; so is this. */\nexport const visualEngine = new VisualEngine();\n\n/** Freshness for a visual read — exported so the domain and host agree. */\nexport function frameQuality(frame: VisualFrame | null): StateQuality {\n if (!frame) return { freshness: { state: 'unknown', observedAt: null, ageMs: null } };\n const ageMs = Date.now() - frame.timestamp;\n return {\n freshness: {\n // Tighter than the other domains on purpose: a two-second-old frame of a\n // moving scene is not \"fresh\" in any useful sense.\n state: ageMs < 2000 ? 'fresh' : ageMs < 10_000 ? 'aging' : 'stale',\n observedAt: frame.timestamp,\n ageMs,\n },\n confidence: { level: 'unknown' },\n };\n}\n","// ── visual ───────────────────────────────────────────────────────────────\n// frames ✓ heading ✓ freshness ✓ blur/exposure ✗\n//\n// Visual owns the semantic frame timeline/retention: the rolling ring, opaque\n// frame ids, and the freshness the frames carry (frameQuality) — see\n// visualEngine.ts. Opaque ids only above L1; pixels never cross to JS.\nimport type { DeviceState, VisualSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { visualEngine, frameQuality } from '../visualEngine';\nimport { currentHeading, startHeading, stopHeading } from '../headingSource';\nimport { unsupported } from '../quality';\nimport { stateEvent } from '../events';\n\n/**\n * The camera block of a visual snapshot — one builder, so the read and the\n * change event cannot describe the camera two ways.\n */\nfunction cameraState(): VisualSnapshot['camera'] {\n const session = visualEngine.getSession();\n return {\n // `available` = the app could use a camera at all. `active` = one is\n // running RIGHT NOW. They differ exactly when it matters: a pocketed\n // phone is available but not active, and answering `true` to both\n // would let a guide speak about a scene nothing is watching.\n available: !!session,\n active: visualEngine.isCameraReady(),\n facing: session ? 'back' : 'unknown',\n };\n}\n\n/**\n * Reads the rolling timeline the engine maintains. See visualEngine.ts for why\n * the capture half lives in a mounted component.\n *\n * `getSnapshot` NEVER opens the camera and never takes a picture — that is the\n * design's explicit performance rule, and the reason a guidance loop can ask\n * every turn without paying for it. What it costs is one array read.\n */\nexport function createVisualDomain(): DeviceState['visual'] {\n return {\n async getSnapshot(): Promise<VisualSnapshot> {\n const latest = visualEngine.latest();\n const timeline = visualEngine.timeline();\n\n return {\n camera: cameraState(),\n // null, not a stale frame dressed as current. A caller with no usable\n // frame must see nothing rather than something old.\n latest: latest as any,\n recentTimeline: timeline as any,\n quality: frameQuality(latest),\n };\n },\n\n async start(options) {\n const s = visualEngine.startSession((options?.intent as any) || 'navigation');\n // Warm the compass alongside the camera. Started here rather than at app\n // launch: a magnetometer subscription running all day for a feature\n // nobody opened is exactly the kind of cost this layer is meant to avoid.\n visualEngine.setContextProvider({ heading: currentHeading });\n void startHeading('visual');\n return { id: s.id, intent: s.intent, startedAt: s.startedAt } as any;\n },\n\n async stop() {\n visualEngine.stopSession();\n visualEngine.setContextProvider(null);\n // Releases only visual's hold — a spatial session keeps the compass.\n stopHeading('visual');\n },\n\n /**\n * A deliberate one-shot. Distinct from getSnapshot because it is an\n * OPERATION: it waits for a frame rather than serving the newest one.\n *\n * Throws when it cannot, rather than returning an empty result — the\n * design is explicit that capture is an operation and reads are state.\n */\n async capture() {\n if (!visualEngine.getSession()) {\n throw unsupported('visual.capture without an open session — call start({ intent }) first');\n }\n if (!visualEngine.isCameraReady()) {\n const err: any = new Error(\n 'The camera is not running right now — the app may be in the background.',\n );\n err.code = 'TEMPORARILY_UNAVAILABLE';\n throw err;\n }\n // Wait briefly for the NEXT frame rather than returning the last one:\n // \"capture\" means now, and the newest ring entry may be a second old.\n const before = visualEngine.latest()?.timestamp ?? 0;\n const deadline = Date.now() + 4000;\n while (Date.now() < deadline) {\n const f = visualEngine.latest();\n if (f && f.timestamp > before) return { frame: f } as any;\n await new Promise(r => setTimeout(r, 100));\n }\n const err: any = new Error('No frame arrived within 4s.');\n err.code = 'TIMEOUT';\n throw err;\n },\n\n /**\n * Fires when the CAMERA block changes — a session opening or closing, the\n * camera coming up or going down. The engine notifies on every frame too,\n * and forwarding that made this a raw frame stream, which the freeze's core\n * rule 4 forbids. A frame arriving is what `getSnapshot()` is for.\n */\n subscribe(listener) {\n let last = JSON.stringify(cameraState());\n const off = visualEngine.subscribe(() => {\n const camera = cameraState();\n const key = JSON.stringify(camera);\n if (key === last) return;\n last = key;\n listener(stateEvent('visual', 'visualStateChanged', { camera }));\n });\n return { unsubscribe: off };\n },\n };\n}\n","/**\n * The Activity engine — steps and motion, sampled into short windows.\n *\n * Sensors only. Every judgement made from them lives in `activityClassifier`,\n * which is pure and tested; this file's whole job is to keep two subscriptions\n * alive and turn their streams into the three numbers the classifier wants.\n *\n * IT IS SESSION-SCOPED, and that is a power decision rather than a stylistic\n * one. An accelerometer left running is a background drain and a pedometer\n * subscription is a permission-visible one, so neither starts until a plugin\n * declares it needs Activity. `getSnapshot()` with no session open returns an\n * explicit unknown rather than quietly switching the sensors on — a read must\n * not have that side effect.\n */\nimport type { HumanActivityValue } from './activityClassifier';\n\n/**\n * expo-sensors is loaded LAZILY, on the first `start()`, and never at import.\n *\n * This module is reachable from app startup, and expo-sensors is a NATIVE\n * module: an app binary built before it was added does not contain it. A\n * top-level import that threw during evaluation would not degrade the Activity\n * domain — it would fail the bundle and black-screen the whole app, on exactly\n * the builds that predate this change. So the failure is contained here, where\n * it can be reported as \"no sensors\" instead.\n */\nlet sensors: any = null;\nlet sensorsMissing = false;\n\nfunction loadSensors(): any {\n if (sensors || sensorsMissing) return sensors;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n sensors = require('expo-sensors');\n } catch {\n sensorsMissing = true;\n sensors = null;\n }\n return sensors;\n}\n\n/** Accelerometer cadence. 5 Hz is plenty for pose and for a variance band. */\nconst ACCEL_INTERVAL_MS = 200;\n/** Motion window. Long enough to smooth a stride, short enough to be current. */\nconst MOTION_WINDOW_MS = 3000;\n/**\n * Step window.\n *\n * 20s, because cadence over a shorter window swings wildly at the start and end\n * of walking — someone who takes four steps and stops would otherwise read as\n * a steady 120 spm for a moment.\n */\nconst STEP_WINDOW_MS = 20_000;\n/** Low-pass coefficient isolating gravity from the raw accelerometer. */\nconst GRAVITY_ALPHA = 0.1;\n\ninterface StepSample { steps: number; t: number }\n\nclass ActivityEngine {\n private accelSub: { remove(): void } | null = null;\n private stepSub: { remove(): void } | null = null;\n\n private magnitudes: Array<{ m: number; t: number }> = [];\n private gravity: { x: number; y: number; z: number } | null = null;\n private steps: StepSample[] = [];\n\n private sessionId: string | null = null;\n private startedAt = 0;\n /** When the CURRENT reading last changed — feeds `human.since`. */\n private pedometerAvailable: boolean | null = null;\n\n isRunning(): boolean { return this.sessionId !== null; }\n session(): { id: string; startedAt: number } | null {\n return this.sessionId ? { id: this.sessionId, startedAt: this.startedAt } : null;\n }\n\n /**\n * @param classify how to name the current activity — injected so the engine\n * keeps no opinion of its own and the classifier stays pure and testable.\n */\n async start(classify: () => HumanActivityValue): Promise<{ id: string; startedAt: number }> {\n if (this.sessionId) return this.session()!;\n this.sessionId = `activity_${Date.now()}`;\n this.startedAt = Date.now();\n\n const s = loadSensors();\n if (!s) {\n // An app binary built before expo-sensors was added. The session is open\n // — stopping it would be a lie in the other direction — but nothing\n // samples, so every window stays empty and the classifier says `unknown`.\n this.pedometerAvailable = false;\n this.startTicking(classify);\n return this.session()!;\n }\n const { Accelerometer, Pedometer } = s;\n\n Accelerometer.setUpdateInterval(ACCEL_INTERVAL_MS);\n this.accelSub = Accelerometer.addListener(({ x, y, z }: { x: number; y: number; z: number }) => {\n const now = Date.now();\n // Raw magnitude includes gravity, so a still phone sits at ~1g and the\n // variance of this series is the motion signal.\n this.magnitudes.push({ m: Math.sqrt(x * x + y * y + z * z), t: now });\n const cutoff = now - MOTION_WINDOW_MS;\n if (this.magnitudes.length > 8 && this.magnitudes[0].t < cutoff) {\n this.magnitudes = this.magnitudes.filter(s => s.t >= cutoff);\n }\n // Gravity by low-pass: what survives smoothing is the constant pull, and\n // that is what says which way up the phone is.\n this.gravity = this.gravity\n ? {\n x: this.gravity.x + GRAVITY_ALPHA * (x - this.gravity.x),\n y: this.gravity.y + GRAVITY_ALPHA * (y - this.gravity.y),\n z: this.gravity.z + GRAVITY_ALPHA * (z - this.gravity.z),\n }\n : { x, y, z };\n });\n\n try {\n this.pedometerAvailable = await Pedometer.isAvailableAsync();\n } catch {\n this.pedometerAvailable = false;\n }\n if (this.pedometerAvailable) {\n try {\n this.stepSub = Pedometer.watchStepCount(({ steps }: { steps: number }) => {\n this.steps.push({ steps, t: Date.now() });\n const cutoff = Date.now() - STEP_WINDOW_MS;\n if (this.steps.length > 2 && this.steps[0].t < cutoff) {\n this.steps = this.steps.filter(s => s.t >= cutoff);\n }\n });\n } catch {\n // Permission refused, most likely (iOS Motion & Fitness). Cadence stays\n // null, the classifier says `unknown`, and nothing pretends otherwise.\n this.stepSub = null;\n this.pedometerAvailable = false;\n }\n }\n\n this.startTicking(classify);\n return this.session()!;\n }\n\n stop(): void {\n this.accelSub?.remove();\n this.stepSub?.remove();\n this.accelSub = null;\n this.stepSub = null;\n this.sessionId = null;\n this.stopTicking();\n // The windows belong to the session. Keeping them would let a later read\n // report a walk that finished before the phone went in a drawer.\n this.magnitudes = [];\n this.steps = [];\n this.gravity = null;\n }\n\n /** Variance of accelerometer magnitude over the motion window, in g². */\n accelVariance(): number | null {\n const cutoff = Date.now() - MOTION_WINDOW_MS;\n const window = this.magnitudes.filter(s => s.t >= cutoff);\n // Two samples is not a variance; it is a coincidence.\n if (window.length < 5) return null;\n const mean = window.reduce((a, s) => a + s.m, 0) / window.length;\n return window.reduce((a, s) => a + (s.m - mean) ** 2, 0) / window.length;\n }\n\n currentGravity(): { x: number; y: number; z: number } | null { return this.gravity; }\n\n /**\n * Steps per minute over the step window.\n *\n * Computed from the DIFFERENCE between the first and last cumulative counts,\n * so it does not matter whether the platform counts from subscription start\n * or from midnight.\n *\n * Returns null — not 0 — when the window is too short to divide by. Zero\n * steps per minute is a real claim (\"standing still\") and must only be made\n * once there is a window long enough to support it.\n */\n cadenceSpm(): number | null {\n const cutoff = Date.now() - STEP_WINDOW_MS;\n const window = this.steps.filter(s => s.t >= cutoff);\n if (window.length < 2) {\n // The pedometer is alive and has simply reported nothing recently, which\n // is what standing still looks like: no callbacks at all.\n if (this.pedometerAvailable && Date.now() - this.startedAt > STEP_WINDOW_MS) return 0;\n return null;\n }\n const first = window[0], last = window[window.length - 1];\n const minutes = (last.t - first.t) / 60_000;\n if (minutes <= 0) return null;\n return (last.steps - first.steps) / minutes;\n }\n\n /** Whether a pedometer exists and is permitted. Null before `start()`. */\n hasPedometer(): boolean | null { return this.pedometerAvailable; }\n\n /** When the newest sample arrived — what freshness is actually about. */\n observedAt(): number | null {\n const ts = [\n this.magnitudes[this.magnitudes.length - 1]?.t,\n this.steps[this.steps.length - 1]?.t,\n ].filter((t): t is number => typeof t === 'number');\n return ts.length ? Math.max(...ts) : null;\n }\n\n // ── transitions ──\n //\n // Tracked on a timer inside the engine rather than computed when someone\n // reads, because a transition is an EVENT: \"you started walking\" happens\n // between two reads, and a domain that only classifies on demand reports the\n // state and loses the moment it changed. `since` has the same problem.\n\n private current: HumanActivityValue = 'unknown';\n private since: number | null = null;\n private transitions: Array<{ name: string; timestamp: number }> = [];\n private ticker: ReturnType<typeof setInterval> | null = null;\n\n /** Feed the engine its own classification once a second. */\n private startTicking(classify: () => HumanActivityValue): void {\n this.ticker = setInterval(() => {\n const next = classify();\n if (next === this.current) return;\n this.current = next;\n this.since = Date.now();\n // The transition NAME reuses the frozen HumanActivity vocabulary — it is\n // the state being entered. The freeze leaves transition names as bare\n // strings with no controlled vocabulary, and inventing one (\"walk_start\")\n // would put a word in plugins that the freeze may later contradict.\n this.transitions = [...this.transitions, { name: next, timestamp: this.since }].slice(-10);\n }, 1000);\n }\n\n private stopTicking(): void {\n if (this.ticker) clearInterval(this.ticker);\n this.ticker = null;\n this.current = 'unknown';\n this.since = null;\n this.transitions = [];\n }\n\n /** When the current activity began, or null if it has not been established. */\n currentSince(): number | null { return this.since; }\n /** Newest last, bounded. Empty is a real answer: nothing has changed yet. */\n recentTransitions(): Array<{ name: string; timestamp: number }> {\n return [...this.transitions];\n }\n}\n\n/** One engine per app: the sensors are singleton hardware. */\nexport const activityEngine = new ActivityEngine();\n","/**\n * What the user is doing, and how the phone is being held — as a pure function.\n *\n * Separated from the sensor plumbing so the JUDGEMENTS are testable: every\n * threshold below is a claim about human beings, and a claim that decides\n * whether a guide says \"you have arrived\" to someone sitting in a moving car\n * deserves a test rather than a comment.\n *\n * WHY THIS DOMAIN EXISTS AT ALL WHEN SPATIAL ALREADY REPORTS MOVEMENT: GPS is\n * what fails indoors, and indoors is where a guide most needs to know whether\n * the user is walking. Steps come from the pedometer, which does not care about\n * a roof. Spatial answers \"are you moving through the world\"; Activity answers\n * \"are you walking\" — in a mall those are different questions with different\n * sensors, and only one of them still works.\n */\nimport type { Confidence } from '@ateam-ai-mobile/plugin-runtime';\n\nexport type HumanActivityValue =\n | 'stationary' | 'walking' | 'running' | 'cycling' | 'driving' | 'unknown';\nexport type PlacementValue = 'handheld' | 'pocket' | 'table' | 'mounted' | 'unknown';\nexport type PoseOrientation = 'portrait' | 'landscape' | 'flat' | 'unknown';\n\nexport interface ActivityInput {\n /** Steps per minute over the recent window. `null` = no pedometer reading. */\n cadenceSpm: number | null;\n /**\n * Variance of accelerometer magnitude (g²) over the recent window.\n * `null` = the accelerometer has not reported yet.\n */\n accelVariance: number | null;\n /** Low-pass gravity estimate in g. `null` = not sampled yet. */\n gravity: { x: number; y: number; z: number } | null;\n /**\n * Speed from Spatial, when a spatial session is streaming.\n *\n * Passed IN rather than read here, so this stays pure. It exists for exactly\n * one job: a phone in a moving car looks identical to a phone on a desk —\n * no steps, little vibration — and calling that `stationary` would tell a\n * guide the user has arrived while they are doing 50 km/h.\n */\n speedMps: number | null;\n}\n\nexport interface ActivityVerdict {\n human: { state: HumanActivityValue; confidence: Confidence };\n pose: { placement: PlacementValue; orientation: PoseOrientation; confidence: Confidence };\n}\n\n/**\n * Cadence bands, in steps per minute.\n *\n * Ordinary walking sits around 100–120 spm and running around 160–180. 140 is\n * placed in the gap rather than at either edge: misreading a fast walk as a run\n * is harmless, but the reverse would have a guide give walking directions to\n * someone covering ground twice as fast.\n *\n * 20 spm is the floor for \"walking at all\" — below that it is a few steps of\n * shuffling, which is not travel.\n */\nconst RUN_SPM = 140;\nconst WALK_SPM = 20;\n\n/**\n * Accelerometer magnitude variance, in g².\n *\n * A phone lying on a table reads a near-constant 1g, so its variance is\n * essentially sensor noise. A hand holding a phone never manages that.\n */\nconst STILL_VAR = 0.002;\n/** Above this the phone is being moved about, not merely held. */\nconst ACTIVE_VAR = 0.02;\n\n/**\n * Speed at which the user is certainly not walking, in m/s.\n *\n * 3 m/s is ~11 km/h — above a fast walk (~2 m/s) and below anything a vehicle\n * does in traffic.\n */\nconst VEHICLE_MPS = 3;\n\n/** Gravity component that has to dominate before an axis names the pose. */\nconst AXIS_DOMINANT = 0.75;\n\nexport function classifyActivity(input: ActivityInput): ActivityVerdict {\n return { human: classifyHuman(input), pose: classifyPose(input) };\n}\n\nfunction classifyHuman({ cadenceSpm, accelVariance, speedMps }: ActivityInput): ActivityVerdict['human'] {\n // Steps are the strongest evidence there is, and the only one that survives\n // indoors. Take them first when they exist.\n if (cadenceSpm !== null && cadenceSpm >= RUN_SPM) {\n return { state: 'running', confidence: { level: 'high' } };\n }\n if (cadenceSpm !== null && cadenceSpm >= WALK_SPM) {\n return { state: 'walking', confidence: { level: 'high' } };\n }\n\n // No meaningful stepping. Before calling it stationary, rule out a vehicle:\n // riding is not walking, and it is not standing still either.\n if (speedMps !== null && speedMps >= VEHICLE_MPS) {\n // Moving fast without stepping. Which vehicle it is cannot be told from an\n // accelerometer and a pedometer — separating cycling from driving needs\n // real activity recognition (CMMotionActivity), which is not available\n // here. `unknown` with the speed already published by Spatial is honest;\n // guessing `driving` because it is the commoner case is not.\n return { state: 'unknown', confidence: { level: 'low' } };\n }\n\n if (accelVariance !== null && accelVariance < STILL_VAR && (cadenceSpm === null || cadenceSpm < WALK_SPM)) {\n // Genuinely still. Medium rather than high: a phone resting in a\n // stationary car at a red light produces exactly this, and Spatial's speed\n // cannot separate them either while the car is stopped.\n return { state: 'stationary', confidence: { level: 'medium' } };\n }\n\n // Something is moving but nothing identifies it — a phone being handled\n // while its owner stands still, most often. Naming an activity here would be\n // a guess dressed as a reading.\n return { state: 'unknown', confidence: { level: 'unknown' } };\n}\n\nfunction classifyPose({ gravity, accelVariance }: ActivityInput): ActivityVerdict['pose'] {\n if (!gravity) {\n return { placement: 'unknown', orientation: 'unknown', confidence: { level: 'unknown' } };\n }\n\n const { x, y, z } = gravity;\n const orientation: PoseOrientation =\n Math.abs(z) >= AXIS_DOMINANT ? 'flat'\n : Math.abs(y) >= AXIS_DOMINANT ? 'portrait'\n : Math.abs(x) >= AXIS_DOMINANT ? 'landscape'\n : 'unknown';\n\n // Flat and utterly still is a surface. This is the one placement the sensors\n // can actually establish.\n if (orientation === 'flat' && accelVariance !== null && accelVariance < STILL_VAR) {\n return { placement: 'table', orientation, confidence: { level: 'high' } };\n }\n\n // Upright and moving a little is a hand. Medium, because a phone in a shirt\n // pocket looks much the same and this cannot tell them apart.\n if (orientation !== 'flat' && accelVariance !== null && accelVariance >= ACTIVE_VAR) {\n return { placement: 'handheld', orientation, confidence: { level: 'medium' } };\n }\n\n // `pocket` and `mounted` are NEVER returned. Distinguishing a pocket needs\n // the proximity or light sensor, and a car mount needs Bluetooth or CarPlay\n // context — none of which is wired. The orientation is still reported,\n // because it was actually measured.\n return { placement: 'unknown', orientation, confidence: { level: 'unknown' } };\n}\n","// ── activity ─────────────────────────────────────────────────────────────\n// THE INDOOR MOVEMENT SIGNAL (Spatial stops working indoors).\nimport type { DeviceState, ActivitySnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { activityEngine } from '../activityEngine';\nimport { classifyActivity, type ActivityInput } from '../activityClassifier';\nimport type { SpeedRef } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\n\n/**\n * What the user is doing, and how the phone is being held.\n *\n * THIS IS THE INDOOR MOVEMENT SIGNAL. Spatial answers \"are you moving through\n * the world\" and stops being able to indoors, which is exactly where a guide\n * most needs to know whether its user is walking. The pedometer does not care\n * about a roof.\n *\n * SESSION-SCOPED, unlike the passive domains. An accelerometer left running is\n * a background drain and a pedometer subscription is permission-visible on\n * iOS, so nothing samples until a plugin says it needs this. A `getSnapshot()`\n * with no session open returns an explicit unknown rather than switching the\n * sensors on — a read must not have that side effect, and one that did would\n * also return junk, since the windows would be empty at the moment it started.\n */\nexport function createActivityDomain(speedRef: SpeedRef): DeviceState['activity'] {\n /** A speed older than this says nothing about now. */\n const SPEED_MAX_AGE_MS = 15_000;\n\n const input = (): ActivityInput => ({\n cadenceSpm: activityEngine.cadenceSpm(),\n accelVariance: activityEngine.accelVariance(),\n gravity: activityEngine.currentGravity(),\n speedMps:\n speedRef.at && Date.now() - speedRef.at < SPEED_MAX_AGE_MS ? speedRef.mps : null,\n });\n\n return {\n async getSnapshot(): Promise<ActivitySnapshot> {\n if (!activityEngine.isRunning()) {\n // Nothing is sampling. Every field says so rather than describing a\n // motionless phone, which is what an unstarted engine would look like.\n return {\n human: { state: 'unknown', since: null, confidence: { level: 'unknown' } },\n devicePose: { placement: 'unknown', orientation: 'unknown', confidence: { level: 'unknown' } },\n recentTransitions: [],\n quality: qualityUnknown(),\n };\n }\n\n const verdict = classifyActivity(input());\n const observedAt = activityEngine.observedAt();\n const transitions = activityEngine.recentTransitions();\n\n return {\n human: {\n state: verdict.human.state,\n since: activityEngine.currentSince(),\n confidence: verdict.human.confidence,\n },\n devicePose: {\n placement: verdict.pose.placement,\n orientation: verdict.pose.orientation,\n confidence: verdict.pose.confidence,\n },\n transition: {\n current: verdict.human.state,\n // A hint, and we have no basis for one. The freeze calls `likelyNext`\n // advisory; advisory is not licence to invent it.\n likelyNext: null,\n },\n recentTransitions: transitions,\n quality: observedAt === null\n ? qualityUnknown()\n : qualityFrom(observedAt, verdict.human.confidence),\n };\n },\n\n /**\n * Start sampling. The intent is recorded but does not yet change strategy —\n * there is no native power-tuning layer for it to reach.\n */\n async start(options) {\n const s = await activityEngine.start(() => classifyActivity(input()).human.state);\n return { id: s.id, intent: options.intent, startedAt: s.startedAt } as any;\n },\n\n async stop() { activityEngine.stop(); },\n\n // No activity event vocabulary is frozen — the design notes there is no\n // \"Initial Activity Events\" list at all — so naming events here would put\n // words in plugins that the freeze may contradict.\n subscribe() { throw unsupported('activity.subscribe'); },\n };\n}\n","/**\n * Device Bridge v2 — the runtime behind `props.deviceState`.\n *\n * R5 split the former monolith into one module per domain (./domains/*), a\n * single quality module that owns freshness/confidence/unknown (./quality), and\n * this composition root, which wires the domains and injects their dependencies.\n * The behaviour — and every snapshot shape — is unchanged; only the file layout is.\n *\n * WHAT IS AND IS NOT HERE — stated up front, because a half-built state layer\n * that pretends to be whole is the exact failure this whole effort exists to\n * end:\n *\n * deviceContext power ✓ thermal ✗ memory/storage ✗\n * connectivity reachability ✓ transport ✓ quality/stability/metered ✗\n * capability permission ✓ availability ✗ restriction ✗\n * interaction app lifecycle ✓ screenOn ✗ audio ✗\n * spatial position ✓ heading ✓ turns ✓ path/events ✓ (while a session is open) speed ~derived\n * visual frames ✓ heading ✓ freshness ✓ blur/exposure ✗\n * activity walking/running/stationary ✓ pose ✓ cycling/driving ✗ events ✗\n * environment NOT IMPLEMENTED AT ALL\n *\n * Everything marked ✗ reports `unknown`, which is a real answer in this design\n * and the only honest one. It never reports a plausible default: an invented\n * 'nominal' thermal state is indistinguishable from a measured one, and that is\n * how 56 fabrications reached users from the layer below.\n *\n * A domain with no engine here is not simply missing: `gateDeviceState` (in\n * plugin-runtime/src/deviceStateHost.ts) fills the gap with a domain whose\n * reads answer explicit `unknown`s and whose ACTIONS throw. So a plugin can\n * call any of the eight without an existence check, and still cannot mistake\n * \"not built\" for \"nothing to report\". `environment` is deliberately absent\n * from the composition below for exactly that reason.\n */\nimport type { DeviceState } from '@ateam-ai-mobile/plugin-runtime';\nimport { DeviceStateCollectors, SpeedRef } from './collectors';\nimport { createDeviceContextDomain } from './domains/deviceContext';\nimport { createConnectivityDomain } from './domains/connectivity';\nimport { createCapabilityDomain } from './domains/capability';\nimport { createInteractionDomain } from './domains/interaction';\nimport { createSpatialDomain } from './domains/spatial';\nimport { createVisualDomain } from './domains/visual';\nimport { createActivityDomain } from './domains/activity';\n\nexport type { DeviceStateCollectors, SpeedRef } from './collectors';\n\n/**\n * Build the semantic surface.\n *\n * Only implemented domains appear. `environment` is deliberately absent: a\n * plugin touching it gets a TypeError naming it, which is a far better failure\n * than a snapshot of nulls that reads like a quiet device.\n */\nexport function createDeviceState(collectors: DeviceStateCollectors = {}): Partial<DeviceState> {\n // The ONE explicit cross-domain dependency: Activity needs Spatial's newest\n // speed to tell \"walking\" from \"sitting in a moving car\". Passed as a shared\n // reference, not a cross-domain import — see SpeedRef.\n const speedRef: SpeedRef = { mps: null, at: 0 };\n return {\n deviceContext: createDeviceContextDomain(),\n connectivity: createConnectivityDomain(collectors),\n capability: createCapabilityDomain(collectors),\n interaction: createInteractionDomain(),\n spatial: createSpatialDomain(collectors, speedRef),\n visual: createVisualDomain(),\n activity: createActivityDomain(speedRef),\n };\n}\n\n/** Which domains this build can actually answer. */\nexport const IMPLEMENTED_DOMAINS = [\n 'deviceContext',\n 'connectivity',\n 'capability',\n 'interaction',\n 'spatial',\n 'visual',\n 'activity',\n] as const;\n","/**\n * The mounted half of the camera tasks — scanning and recording.\n *\n * The app renders this ONCE, near the root. It draws nothing until a plugin\n * asks for a scan or a recording; then it takes over the screen with a live\n * preview and settles the plugin's promise when the work is done.\n *\n * WHY IT IS VISIBLE, unlike VisualEngineHost's 1×1 invisible camera. These two\n * tasks need the USER to aim: a barcode has to be framed, a clip has to point\n * at something. An invisible scanner is not a privacy nicety, it is unusable —\n * the user cannot tell what the camera sees or when it has read something.\n *\n * WHY IT CAN ALWAYS BE CANCELLED. The camera is on and pointed at whatever is\n * in front of the person holding it. A capture surface with no way out is a\n * trap; the cancel path settles the promise with a distinct CANCELLED code so\n * the caller can tell \"they chose not to\" from \"it broke\".\n */\nimport React, { useEffect, useRef, useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { CameraView, useCameraPermissions, useMicrophonePermissions } from 'expo-camera';\n\nimport { cameraTasks, cameraError, type CameraTask } from './cameraTasks';\n\n/** How long a scan waits before giving up. */\nconst SCAN_TIMEOUT_MS = 30_000;\n/** Ceiling on a single clip, so a forgotten recording cannot fill the disk. */\nconst MAX_RECORD_MS = 60_000;\n\nexport function CameraTaskHost(): React.ReactElement | null {\n const [task, setTask] = useState<CameraTask | null>(cameraTasks.current());\n const [camPermission, requestCamPermission] = useCameraPermissions();\n const [micPermission, requestMicPermission] = useMicrophonePermissions();\n const cameraRef = useRef<CameraView | null>(null);\n /** Guards against a barcode firing twice before the task is cleared. */\n const settling = useRef(false);\n\n // Announce that a surface exists. Without this the broker rejects every\n // request immediately, which is the correct behaviour when no host is\n // mounted — a plugin must never await a promise nothing can settle.\n useEffect(() => {\n cameraTasks.setHostMounted(true);\n return () => cameraTasks.setHostMounted(false);\n }, []);\n\n useEffect(() => cameraTasks.subscribe(() => {\n settling.current = false;\n setTask(cameraTasks.current());\n }), []);\n\n // Ask for permission when a task arrives, not at app start: a camera prompt\n // with no visible reason attached is one users refuse.\n useEffect(() => {\n if (!task) return;\n let cancelled = false;\n void (async () => {\n const cam = camPermission?.granted ? camPermission : await requestCamPermission();\n if (cancelled) return;\n if (!cam?.granted) {\n cameraTasks.fail(cameraError(\n 'PERMISSION_DENIED',\n 'Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera.',\n ));\n return;\n }\n if (task.kind === 'record' && task.opts.withAudio) {\n const mic = micPermission?.granted ? micPermission : await requestMicPermission();\n if (cancelled) return;\n if (!mic?.granted) {\n // Refuse rather than silently recording a silent video — a caller\n // that asked for audio and got none has no way to find out.\n cameraTasks.fail(cameraError(\n 'PERMISSION_DENIED',\n 'Microphone permission was not granted, so a video with sound cannot be recorded.',\n ));\n }\n }\n })();\n return () => { cancelled = true; };\n }, [task]);\n\n // Scan timeout. A scan that never finds a code must END, and say so: leaving\n // the camera up indefinitely is both a battery drain and a promise that\n // never settles.\n useEffect(() => {\n if (task?.kind !== 'scan') return;\n const ms = task.opts.timeoutMs ?? SCAN_TIMEOUT_MS;\n const t = setTimeout(() => {\n cameraTasks.fail(cameraError('TIMEOUT', `No barcode was found within ${Math.round(ms / 1000)}s.`));\n }, ms);\n return () => clearTimeout(t);\n }, [task]);\n\n // Recording runs for its duration and resolves with the file.\n useEffect(() => {\n if (task?.kind !== 'record' || !camPermission?.granted) return;\n let cancelled = false;\n const maxMs = Math.min(task.opts.maxDurationMs ?? 15_000, MAX_RECORD_MS);\n const startedAt = Date.now();\n\n void (async () => {\n // A frame late is better than a crash: the ref is attached on the render\n // after the task arrives.\n await new Promise(r => setTimeout(r, 50));\n if (cancelled || !cameraRef.current) return;\n try {\n const video = await cameraRef.current.recordAsync({ maxDuration: Math.ceil(maxMs / 1000) });\n if (cancelled) return;\n if (!video?.uri) {\n cameraTasks.fail(cameraError('INTERNAL', 'Recording finished but produced no file.'));\n return;\n }\n cameraTasks.complete({ uri: video.uri, durationMs: Date.now() - startedAt });\n } catch (err: any) {\n if (!cancelled) cameraTasks.fail(cameraError('INTERNAL', err?.message || 'Recording failed.'));\n }\n })();\n\n return () => {\n cancelled = true;\n // Stop the hardware if the surface goes away mid-take.\n try { cameraRef.current?.stopRecording(); } catch { /* already stopped */ }\n };\n }, [task, camPermission?.granted]);\n\n if (!task || !camPermission?.granted) return null;\n\n const isScan = task.kind === 'scan';\n\n return (\n <View style={styles.overlay}>\n <CameraView\n ref={cameraRef}\n style={StyleSheet.absoluteFill}\n facing=\"back\"\n mode={isScan ? 'picture' : 'video'}\n barcodeScannerSettings={\n isScan && task.opts.formats?.length\n ? { barcodeTypes: task.opts.formats as any }\n : undefined\n }\n onBarcodeScanned={\n isScan\n ? ({ data, type }) => {\n // onBarcodeScanned fires continuously while a code is in frame.\n // Without this guard the same scan resolves, then tries to\n // resolve an already-cleared task on the very next frame.\n if (settling.current) return;\n settling.current = true;\n cameraTasks.complete({ data, format: type });\n }\n : undefined\n }\n />\n\n <View style={styles.hud} pointerEvents=\"box-none\">\n <Text style={styles.hint}>\n {isScan ? 'Point the camera at the code' : 'Recording…'}\n </Text>\n <Pressable style={styles.cancel} onPress={() => cameraTasks.cancel()} accessibilityRole=\"button\">\n <Text style={styles.cancelText}>Cancel</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000', zIndex: 9999 },\n hud: { flex: 1, justifyContent: 'space-between', paddingVertical: 64, alignItems: 'center' },\n hint: { color: '#fff', fontSize: 16, textAlign: 'center', paddingHorizontal: 24 },\n cancel: { paddingHorizontal: 28, paddingVertical: 14, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.18)' },\n cancelText: { color: '#fff', fontSize: 16, fontWeight: '600' },\n});\n\nexport default CameraTaskHost;\n","/**\n * The broker between a plugin's `await` and a mounted camera.\n *\n * WHY THIS EXISTS. expo-camera has no headless capture: scanning a barcode and\n * recording a clip both require a MOUNTED `<CameraView>`. But a plugin calls\n * `native.scanner.scanOnce()` and expects a promise — it has no way to mount\n * anything, and it must not have to.\n *\n * So the API enqueues a TASK here and awaits it; `CameraTaskHost`, mounted once\n * by the app, notices, shows the camera, does the work, and settles the\n * promise. This is the same split as the Visual engine, for the same reason —\n * and it is why `native.camera.scanBarcode` refuses and points here rather than\n * pretending a function call can own a React lifecycle.\n *\n * ONE TASK AT A TIME, and it is the phone that says so: there is one camera. A\n * second request while one is running is REJECTED rather than queued, because a\n * queued scan would open the camera again minutes later, long after the user\n * moved on and with no idea why it appeared.\n */\nexport interface ScanTask {\n kind: 'scan';\n opts: { formats?: string[]; timeoutMs?: number };\n resolve: (v: { data: string; format: string }) => void;\n reject: (e: Error) => void;\n}\n\nexport interface RecordTask {\n kind: 'record';\n opts: { maxDurationMs?: number; withAudio?: boolean };\n resolve: (v: { uri: string; durationMs: number | null }) => void;\n reject: (e: Error) => void;\n}\n\nexport type CameraTask = ScanTask | RecordTask;\n\ntype Listener = () => void;\n\n/** Typed failure, so a caller can say something specific and true. */\nexport function cameraError(code: string, message: string): Error {\n const err: any = new Error(message);\n err.code = code;\n return err;\n}\n\nclass CameraTaskBroker {\n private task: CameraTask | null = null;\n private listeners = new Set<Listener>();\n /** Set by the host while a camera is mounted and ready. */\n private hostMounted = false;\n\n current(): CameraTask | null { return this.task; }\n\n subscribe(fn: Listener): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n /**\n * Declare that a `CameraTaskHost` exists.\n *\n * Without it nothing will ever settle the promise, so a request must fail\n * IMMEDIATELY and say why. The alternative is a plugin awaiting forever,\n * which is the worst failure of the set: no error, no timeout, no camera,\n * and a conversation that simply stops.\n */\n setHostMounted(mounted: boolean): void {\n this.hostMounted = mounted;\n if (!mounted) this.fail(cameraError('UNAVAILABLE', 'The camera surface was dismissed before the task completed.'));\n this.notify();\n }\n\n isHostMounted(): boolean { return this.hostMounted; }\n\n /** Enqueue and hand back the promise the plugin awaits. */\n run<T>(kind: CameraTask['kind'], opts: any): Promise<T> {\n if (!this.hostMounted) {\n return Promise.reject(cameraError(\n 'UNAVAILABLE',\n 'No camera surface is mounted in this app, so the camera cannot be shown. ' +\n 'The host must render <CameraTaskHost /> once, near the app root.',\n ));\n }\n if (this.task) {\n return Promise.reject(cameraError(\n 'BUSY',\n `The camera is already in use (${this.task.kind}). Only one camera task can run at a time.`,\n ));\n }\n return new Promise<T>((resolve, reject) => {\n this.task = { kind, opts, resolve, reject } as unknown as CameraTask;\n this.notify();\n });\n }\n\n /** Settle the running task successfully and clear it. */\n complete(value: any): void {\n const t = this.task;\n if (!t) return;\n this.task = null;\n this.notify();\n t.resolve(value);\n }\n\n /** Settle the running task with a failure and clear it. */\n fail(err: Error): void {\n const t = this.task;\n if (!t) return;\n this.task = null;\n this.notify();\n t.reject(err);\n }\n\n /**\n * The user backed out.\n *\n * A distinct code, because \"the user chose not to\" is not a malfunction and\n * a guide should say something different about it than about a broken lens.\n */\n cancel(): void {\n this.fail(cameraError('CANCELLED', 'Cancelled — the camera was dismissed before anything was captured.'));\n }\n\n private notify(): void {\n for (const fn of this.listeners) { try { fn(); } catch { /* a bad listener is not the broker's problem */ } }\n }\n}\n\n/** One broker per app: there is one camera. */\nexport const cameraTasks = new CameraTaskBroker();\n","/**\n * SDK device-primitive provider (Expo-backed).\n *\n * `createDeviceProvider()` builds the FULL, UNGATED `PluginNative` implementation\n * a host registers once via `setPluginNativeProvider`. Per-plugin capability\n * gating and per-plugin filesystem scoping are applied downstream by\n * plugin-runtime's `gatePluginNative` — this module does not gate.\n *\n * Not a React hook: it holds no React state and must be safe to build at module\n * scope, including on a headless background wake before any UI exists.\n */\nimport * as Haptics from 'expo-haptics';\n// Both are already app dependencies and already used by the device-bridge\n// collectors — safe for eager import (unlike cookies/nfc, which need native\n// modules absent from Expo Go).\nimport * as ExpoLocation from 'expo-location';\nimport * as ImagePicker from 'expo-image-picker';\nimport * as Notifications from 'expo-notifications';\nimport * as DocumentPicker from 'expo-document-picker';\nimport * as FS from 'expo-file-system';\nimport * as SQLite from 'expo-sqlite';\n// The broker behind scanner.scanOnce() and camera.recordVideo(): both need a\n// MOUNTED camera, which a function call cannot provide.\nimport { cameraTasks } from '../camera/cameraTasks';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n// native.vision: the on-device analyzer (ada-vision module) — resolve a frame id\n// to its local pixels, measure/read/classify on-device, never off the phone.\nimport * as adaVision from '../vision';\n// native.maps: place search + walking routes (ada-maps module, MapKit) and the\n// SDK's one reverse geocode. NOT on-device — queries go to Apple's servers.\nimport * as adaMaps from '../maps';\nimport { countArg, fractionArg, refuseArgument, shown } from '../args';\nimport {\n coordinateArg, mapsFailure, searchArgs, toMapsPlaces, toMapsRoute, toReverseGeocodedPlace,\n} from '../maps/normalize';\n// Device-bridge owns every native module. plugin-sdk delegates so RN\n// plugins never import native modules directly. When adding a new\n// capability: add it to device-bridge, import here, declare in types.ts,\n// wire in the factory below.\n//\n// IMPORTANT: cookies and nfc use native modules NOT available in Expo Go.\n// We lazy-import them so the app doesn't crash at load time.\n// Only biometrics (expo-local-authentication), sharing (expo-sharing),\n// and scanner (expo-camera) are safe for eager import.\nimport {\n biometrics as deviceBiometrics,\n sharing as deviceSharing,\n scanner as deviceScanner,\n} from '../device-bridge';\n\n// Lazy loaders for native modules not in Expo Go\nfunction getDeviceCookies() {\n try { return require('../device-bridge/cookies'); } catch { return null; }\n}\nfunction getDeviceNfc() {\n try { return require('../device-bridge/nfc'); } catch { return null; }\n}\nimport type {\n NativeCapabilities,\n PluginNative,\n CameraAPI,\n LocationAPI,\n StorageAPI,\n BiometricsAPI,\n BiometricsAPIv2,\n NotificationsAPI,\n HapticsAPI,\n ConnectivityAPI,\n FileSystemAPI,\n CookiesAPI,\n WebViewCookie,\n SharingAPI,\n ScannerAPI,\n NfcAPI,\n BluetoothAPI,\n VisionAPI,\n MapsAPI,\n} from '@ateam-ai-mobile/plugin-runtime';\n\nclass CapabilityUnavailableError extends Error {\n code = 'UNAVAILABLE' as const;\n constructor(capability: string, reason?: string) {\n super(reason || `${capability} is not yet implemented. Coming in Phase 2.`);\n this.name = 'CapabilityUnavailableError';\n }\n}\n\nclass CapabilityNotDeclaredError extends Error {\n code = 'NOT_DECLARED' as const;\n constructor(capability: string) {\n super(`Capability '${capability}' not declared in plugin manifest. Add it to the native.${capability} field.`);\n this.name = 'CapabilityNotDeclaredError';\n }\n}\n\nfunction stubApi<T extends object>(capability: string): T {\n return new Proxy({} as T, {\n get(_target, prop) {\n if (typeof prop === 'string') {\n return () => {\n throw new CapabilityUnavailableError(capability);\n };\n }\n },\n });\n}\n\n// ── Location (expo-location — was stubbed, is real now) ───────────\n//\n// `location` was wired to stubApi, so a plugin that correctly DECLARED\n// `location: true` still got \"location is not yet implemented. Coming in\n// Phase 2.\" on every call. expo-location has been a dependency all along and\n// the device-bridge collectors use it; only the plugin-facing API was missing.\n\nfunction createLocationAPI(): LocationAPI {\n /** Ask once, and say what the user can do about a refusal. */\n const ensurePermission = async (): Promise<void> => {\n const existing = await ExpoLocation.getForegroundPermissionsAsync();\n if (existing.status === 'granted') return;\n const asked = await ExpoLocation.requestForegroundPermissionsAsync();\n if (asked.status !== 'granted') {\n // A voice assistant reads this out, so it has to be actionable rather\n // than a status code.\n throw new Error(\n 'Location permission was not granted. Enable it for this app in Settings > Privacy > Location Services.',\n );\n }\n };\n\n return {\n async getCurrent(opts) {\n await ensurePermission();\n // 'high' costs battery and a second or two of settling; default balanced,\n // which is plenty for \"which street am I on\".\n const fix = await ExpoLocation.getCurrentPositionAsync({\n accuracy:\n opts?.accuracy === 'high'\n ? ExpoLocation.Accuracy.High\n : ExpoLocation.Accuracy.Balanced,\n });\n return {\n latitude: fix.coords.latitude,\n longitude: fix.coords.longitude,\n altitude: fix.coords.altitude ?? undefined,\n };\n },\n\n watchPosition(cb) {\n let subscription: { remove(): void } | null = null;\n let cancelled = false;\n\n void (async () => {\n try {\n await ensurePermission();\n if (cancelled) return;\n subscription = await ExpoLocation.watchPositionAsync(\n { accuracy: ExpoLocation.Accuracy.Balanced },\n fix => cb({ latitude: fix.coords.latitude, longitude: fix.coords.longitude }),\n );\n // Unsubscribed while we were still asking for permission — otherwise\n // the GPS stays on with nobody listening.\n if (cancelled) { subscription?.remove(); subscription = null; }\n } catch (err: any) {\n console.warn('[location] watchPosition failed:', err?.message || String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n subscription?.remove();\n subscription = null;\n };\n },\n };\n}\n\n// ── Camera (expo-image-picker — was stubbed, is real now) ─────────\n//\n// Every method here opens the SYSTEM camera or picker UI, which is why they\n// are honest one-shot actions rather than a frame source: expo-camera can\n// deliver a live feed, but only through a MOUNTED CameraView component, which\n// a headless tool call has no way to mount. A continuous visual feed is the\n// semantic Visual domain's job (Device Bridge v2), not this API's.\n\nfunction createCameraAPI(): CameraAPI {\n const ensurePermission = async (): Promise<void> => {\n const existing = await ImagePicker.getCameraPermissionsAsync();\n if (existing.granted) return;\n const asked = await ImagePicker.requestCameraPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera.',\n );\n }\n };\n\n return {\n async takePhoto(opts) {\n await ensurePermission();\n const res = await ImagePicker.launchCameraAsync({\n quality: opts?.quality ?? 0.7,\n cameraType:\n opts?.facing === 'front'\n ? ImagePicker.CameraType.front\n : ImagePicker.CameraType.back,\n });\n // The user backing out is not an error — but it is also not a photo, so\n // it must not come back as an empty-looking success.\n if (res.canceled || !res.assets?.[0]) {\n throw new Error('No photo taken — the camera was dismissed.');\n }\n const a = res.assets[0];\n return { uri: a.uri, width: a.width, height: a.height };\n },\n\n async scanBarcode(opts) {\n // Kept refusing, and now the refusal has somewhere to send you. Scanning\n // is a mounted camera lifecycle, which is why it lives on `scanner`.\n // Silently forwarding would leave a misleading name in every plugin.\n throw new CapabilityUnavailableError(\n 'camera.scanBarcode',\n 'Use native.scanner.scanOnce() instead — scanning needs a mounted camera, ' +\n 'which this call cannot provide. Same arguments, and it returns { data, format }.' +\n (opts?.formats?.length ? ' Pass your formats through to scanOnce.' : ''),\n );\n },\n\n async recordVideo(opts) {\n // A FILE the user deliberately makes — not the Visual domain's frame\n // ring, which is ephemeral and nobody keeps. Runs on the shared camera\n // surface, so it throws the same coded failures as scanOnce.\n return cameraTasks.run('record', {\n maxDurationMs: opts?.maxDurationMs,\n withAudio: opts?.withAudio ?? false,\n });\n },\n\n async pickImage(opts) {\n const existing = await ImagePicker.getMediaLibraryPermissionsAsync();\n if (!existing.granted) {\n const asked = await ImagePicker.requestMediaLibraryPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Photo library permission was not granted. Enable it for this app in Settings > Privacy > Photos.',\n );\n }\n }\n const res = await ImagePicker.launchImageLibraryAsync({\n allowsMultipleSelection: !!opts?.multiple,\n });\n if (res.canceled) return [];\n return (res.assets || []).map(a => ({ uri: a.uri }));\n },\n };\n}\n\n// ── Storage (AsyncStorage — was stubbed) ──────────────────────────\n//\n// KV is real. `db.execute` is NOT: expo-sqlite is not installed, and a fake\n// SQL layer over AsyncStorage would be a worse lie than an error — it would\n// accept a query and return an empty rowset, which reads as \"no rows matched\".\n\n/**\n * One SQLite file for plugin storage, opened lazily and once.\n *\n * Lazy because opening a database at module load costs startup time for every\n * user who never touches a plugin that needs one; once because two handles to\n * the same file is how you get a locked database under concurrent writes.\n */\n// Typed `any` for the handle: expo-sqlite is an any-shim in this SDK repo's\n// typecheck (the host supplies the real types), so `SQLite.SQLiteDatabase` is\n// not a nameable type here. Runtime is unchanged — SQLite.openDatabaseAsync\n// still returns a real SQLiteDatabase on device.\n// One handle PER plugin database file, opened lazily and once each. A Map (not a\n// single promise) because storage is now per-plugin: each plugin gets its own\n// file, and two handles to the same file is how you get a locked database.\nconst _pluginDbs = new Map<string, Promise<any>>();\nfunction openPluginDb(file: string): Promise<any> {\n const existing = _pluginDbs.get(file);\n if (existing) return existing;\n const opened: Promise<any> = SQLite.openDatabaseAsync(file);\n _pluginDbs.set(file, opened);\n return opened;\n}\n\n// The leading SQL keyword, uppercased, after stripping leading whitespace and\n// leading `-- line` / `/* block */` comments — so a comment cannot smuggle the\n// verb past the cross-file check (e.g. `/* */ ATTACH …`).\nfunction sqlLeadingKeyword(sql: string): string {\n let s = String(sql);\n for (;;) {\n const before = s;\n s = s.replace(/^\\s+/, '');\n if (s.startsWith('--')) { const nl = s.indexOf('\\n'); s = nl === -1 ? '' : s.slice(nl + 1); }\n else if (s.startsWith('/*')) { const end = s.indexOf('*/'); s = end === -1 ? '' : s.slice(end + 2); }\n if (s === before) break;\n }\n const m = s.match(/^[A-Za-z]+/);\n return m ? m[0].toUpperCase() : '';\n}\n\n// R3 SECURITY — per-plugin storage isolation. Both the KV namespace and the\n// SQLite database are scoped to the plugin's OWN id, so Plugin A cannot read,\n// overwrite, or query Plugin B's entries/tables. The id is threaded in from the\n// TRUSTED gate (storageFor(pluginId)) — never a caller-supplied prefix.\n//\n// The id is encoded with encodeURIComponent (not used raw) precisely because KV\n// is a FLAT concatenated keyspace with no structural boundary: a raw id like\n// `mcp:conn:plug` shares the `:` delimiter, so `@plugin:mcp:conn:plug:<k>` from\n// one plugin could collide with another plugin's id+key split. Encoding escapes\n// `:` (→ %3A), making the id segment unambiguous and the mapping injective, so\n// distinct ids can never produce overlapping namespaces regardless of the key.\n// (The filesystem sandbox can use the raw id as a directory name because a\n// directory boundary is structural, not delimiter-based.) The same encoded\n// token names the plugin's database file, giving each plugin a distinct DB.\nfunction createStorageAPI(pluginId: string): StorageAPI {\n const ns = encodeURIComponent(pluginId);\n const key = (k: string) => `@plugin:${ns}:${k}`;\n const dbFile = `plugin-storage-${ns}.db`;\n\n return {\n get: (k) => AsyncStorage.getItem(key(k)),\n set: (k, v) => AsyncStorage.setItem(key(k), v),\n delete: (k) => AsyncStorage.removeItem(key(k)),\n db: {\n async execute(sql: string, params?: any[]) {\n const verb = sqlLeadingKeyword(sql);\n // Per-plugin storage isolation rests on each plugin owning its OWN\n // database FILE. SQLite can cross files — ATTACH mounts another db,\n // VACUUM INTO writes one — so the file boundary only holds if those are\n // refused. Otherwise a plugin could `ATTACH '.../plugin-storage-<other\n // id>.db'` (the id is public and the filename deterministic) and read or\n // overwrite another plugin's tables. Refuse cross-file statements at the\n // trusted gate; a plugin never needs them for its own single database.\n // (runAsync/getAllAsync compile only the FIRST statement, so a trailing\n // `; ATTACH …` never runs — the leading verb is the whole surface.)\n // ATTACH/DETACH/VACUUM are the only cross-FILE statements in the standard\n // expo-sqlite build: loadable extensions are disabled and the file-vtab\n // modules (fileio/csv/zipfile) are not compiled in, so there is no other\n // way for a plugin's own connection to reach another plugin's file. If a\n // future SQLite build enables those, revisit (an allowlist would be the\n // durable form).\n if (verb === 'ATTACH' || verb === 'DETACH' || verb === 'VACUUM') {\n throw new CapabilityUnavailableError(\n 'storage.db',\n `SQL '${verb}' is not permitted: a plugin's storage is confined to its own database and cannot reach another file.`,\n );\n }\n const db = await openPluginDb(dbFile);\n // A statement either RETURNS ROWS or CHANGES THEM, and the two need\n // different calls. Deciding by the leading keyword rather than by\n // trying one and falling back: a failed INSERT retried as a query\n // would run it twice.\n const returnsRows = verb === 'SELECT' || verb === 'PRAGMA' || verb === 'WITH';\n\n if (returnsRows) {\n const rows = await db.getAllAsync(sql, (params ?? []) as any);\n // changes: 0 is the truth for a SELECT, not a placeholder.\n return { rows, changes: 0 };\n }\n const res = await db.runAsync(sql, (params ?? []) as any);\n // rows: [] is likewise true for a write — it did not select anything.\n return { rows: [], changes: res.changes ?? 0 };\n },\n },\n };\n}\n\n/**\n * Wipe ALL plugin storage — KV, SQLite, and the file sandbox — across EVERY\n * plugin. Per-plugin isolation (R3) keeps plugins apart from each other; this is\n * the USER boundary: on sign-out it must run so the NEXT user/tenant cannot read\n * the previous one's plugin data. Best-effort per store — a hiccup clearing one\n * must never block sign-out — so each part is guarded independently.\n */\nexport async function clearAllPluginData(): Promise<void> {\n // 1. KV — every `@plugin:` namespace (all plugins, all keys).\n try {\n const keys = await AsyncStorage.getAllKeys();\n const mine = keys.filter((k: string) => k.startsWith('@plugin:'));\n if (mine.length) await AsyncStorage.multiRemove(mine);\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: KV clear failed:', e?.message || e);\n }\n\n // 2. SQLite — close any open per-plugin handles, then delete the db files.\n try {\n for (const [, p] of _pluginDbs) {\n try { const db = await p; await db.closeAsync?.(); } catch { /* already closed */ }\n }\n _pluginDbs.clear();\n const sqliteDir = new FS.Directory(FS.Paths.document, 'SQLite');\n if (sqliteDir.exists) {\n for (const entry of sqliteDir.list()) {\n if (/^plugin-storage-.*\\.db$/.test(String(entry.name ?? ''))) {\n try { entry.delete(); } catch { /* best effort */ }\n }\n }\n }\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: SQLite clear failed:', e?.message || e);\n }\n\n // 3. Files — the whole plugin-files/ sandbox tree (all plugins).\n try {\n const dir = new FS.Directory(FS.Paths.document, 'plugin-files');\n if (dir.exists) dir.delete();\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: file sandbox clear failed:', e?.message || e);\n }\n}\n\n// ── Notifications (expo-notifications — was stubbed) ──────────────\n\nfunction createNotificationsAPI(): NotificationsAPI {\n return {\n async schedule({ title, body, trigger }) {\n const perm = await Notifications.getPermissionsAsync();\n if (!perm.granted) {\n const asked = await Notifications.requestPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Notification permission was not granted. Enable it for this app in Settings > Notifications.',\n );\n }\n }\n // `seconds` and `date` are the two the contract declares; anything else\n // would be this API inventing a scheduling model of its own.\n const when: any =\n typeof trigger?.seconds === 'number'\n ? { type: 'timeInterval', seconds: trigger.seconds, repeats: false }\n : trigger?.date\n ? { type: 'date', date: new Date(trigger.date) }\n : null;\n if (!when) {\n throw new Error('schedule() needs trigger.seconds or trigger.date.');\n }\n return Notifications.scheduleNotificationAsync({\n content: { title, body },\n trigger: when,\n });\n },\n\n cancel: (id) => Notifications.cancelScheduledNotificationAsync(id),\n\n onReceived(cb) {\n const sub = Notifications.addNotificationReceivedListener(n => {\n cb({\n id: n.request.identifier,\n title: n.request.content.title || '',\n body: n.request.content.body || '',\n data: n.request.content.data,\n });\n });\n return () => sub.remove();\n },\n };\n}\n\n// ── File system (expo-document-picker — partially real) ───────────\n//\n// pickDocument works. read/write do NOT: expo-file-system is not installed.\n// Named honestly rather than returning empty strings, which would read as an\n// empty file rather than a missing capability.\n\nfunction createFileSystemAPI(pluginId: string): FileSystemAPI {\n /**\n * Every path a plugin gives us is resolved INSIDE its own sandbox directory.\n *\n * A plugin is code a solution ships that runs on someone's phone, so\n * `readFile('/etc/passwd')` — or, more realistically, reading the app's own\n * auth store or another solution's data — must not be reachable. This is the\n * same reasoning that took the `sw.*` filesystem tools out of the kernel and\n * anchored them per tenant.\n *\n * The check is on the RESOLVED path, not the input string: `..` segments are\n * what a blocklist of prefixes misses, and `plugin-files/../../secrets` looks\n * innocent until it is normalised.\n */\n /**\n * Resolved on FIRST USE, never at construction.\n *\n * `new FS.Directory(FS.Paths.document, …)` is a SYNCHRONOUS NATIVE CALL, and\n * `createNativeCapabilities({ fileSystem: true, … })` runs at MODULE SCOPE in\n * src/adas.ts. Building it eagerly therefore did native work during startup\n * module evaluation — on every launch, including a headless background wake\n * from a push, before any UI exists. If the native module is missing or\n * `Paths.document` throws there, the whole app dies with no screen to show it\n * on. Nothing in this factory may touch native until a method is called.\n */\n let _root: InstanceType<typeof FS.Directory> | null = null;\n const rootDir = () => (_root ??= new FS.Directory(FS.Paths.document, 'plugin-files', pluginId));\n\n const resolve = (p: string): InstanceType<typeof FS.File> => {\n const root = rootDir();\n const file = new FS.File(root, p);\n if (!file.uri.startsWith(root.uri)) {\n throw new CapabilityUnavailableError(\n 'fileSystem',\n `Path escapes the plugin's storage area: ${p}. Plugin files live under plugin-files/ and paths cannot traverse out of it.`,\n );\n }\n return file;\n };\n\n return {\n async readFile(path) {\n const file = resolve(path);\n if (!file.exists) {\n // A missing file is not an empty file. Returning '' would read as a\n // document that exists and happens to have nothing in it.\n throw new CapabilityUnavailableError(\n 'fileSystem.readFile',\n `No such file: ${path}. Nothing was read.`,\n );\n }\n return file.text();\n },\n\n async writeFile(path, content) {\n const file = resolve(path);\n // Create parents as needed — a plugin writing 'cache/report.json' should\n // not have to know that directories are a separate concept.\n const parent = file.parentDirectory;\n if (!parent.exists) parent.create({ intermediates: true });\n if (!file.exists) file.create();\n file.write(content);\n },\n async pickDocument(opts) {\n const res = await DocumentPicker.getDocumentAsync({\n type: opts?.types?.length ? opts.types : '*/*',\n copyToCacheDirectory: true,\n });\n // Backing out is not a file. Returning zeros here would look like an\n // empty document the user actually chose.\n if (res.canceled || !res.assets?.[0]) {\n throw new Error('No document chosen — the picker was dismissed.');\n }\n const a = res.assets[0];\n return { uri: a.uri, name: a.name, size: a.size ?? 0 };\n },\n };\n}\n\n// ── Haptics (fully implemented — expo-haptics already installed) ───\n\nfunction createHapticsAPI(): HapticsAPI {\n return {\n impact(style: 'light' | 'medium' | 'heavy') {\n const map = {\n light: Haptics.ImpactFeedbackStyle.Light,\n medium: Haptics.ImpactFeedbackStyle.Medium,\n heavy: Haptics.ImpactFeedbackStyle.Heavy,\n };\n Haptics.impactAsync(map[style]);\n },\n notification(type: 'success' | 'warning' | 'error') {\n const map = {\n success: Haptics.NotificationFeedbackType.Success,\n warning: Haptics.NotificationFeedbackType.Warning,\n error: Haptics.NotificationFeedbackType.Error,\n };\n Haptics.notificationAsync(map[type]);\n },\n // Shorthand for notification('error'). Plugins call this; it was never\n // implemented, and optional chaining meant the failure the user was meant\n // to FEEL simply did not happen.\n error() {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n },\n selection() {\n Haptics.selectionAsync();\n },\n };\n}\n\n// ── Connectivity (implemented using basic RN fetch check) ──────\n\nfunction createConnectivityAPI(reachabilityUrl?: string): ConnectivityAPI {\n let online = true;\n const listeners = new Set<(online: boolean) => void>();\n // Lazy + re-armable polling: the instance is now a singleton shared by ALL\n // plugins (injected once via setPluginNativeProvider), so the interval must\n // (a) not run at app boot when nobody is listening, and (b) restart when a\n // later plugin subscribes after an earlier one unsubscribed to zero.\n // Pre-singleton, each Plugin mount got its own instance and clearInterval\n // was harmless; with a shared instance it would kill polling app-wide.\n let interval: ReturnType<typeof setInterval> | null = null;\n\n // Simple connectivity check — upgrade to @react-native-community/netinfo in Phase 2.\n // NO hardcoded URL: the host injects its own health endpoint. With none given\n // we assume reachable (return true) rather than pin an A-Team-specific default.\n const checkOnline = async () => {\n if (!reachabilityUrl) return true;\n try {\n const res = await fetch(reachabilityUrl, {\n method: 'HEAD',\n signal: AbortSignal.timeout(5000),\n });\n return res.ok;\n } catch {\n return false;\n }\n };\n\n const startPolling = () => {\n // Nothing to poll without a reachability URL: `online` stays constant-true,\n // so an interval would only wake the app to learn the same answer.\n if (!reachabilityUrl) return;\n if (interval) return;\n interval = setInterval(async () => {\n const newState = await checkOnline();\n if (newState !== online) {\n online = newState;\n listeners.forEach(cb => cb(online));\n }\n }, 30000);\n };\n\n return {\n isOnline() {\n return online;\n },\n onChange(cb: (online: boolean) => void) {\n listeners.add(cb);\n startPolling();\n return () => {\n listeners.delete(cb);\n if (listeners.size === 0 && interval) {\n clearInterval(interval);\n interval = null;\n }\n };\n },\n };\n}\n\n// ── Cookies (delegates to device-bridge, lazy-loaded) ──\nfunction createCookiesAPI(): CookiesAPI {\n return {\n async get(url: string): Promise<WebViewCookie[]> {\n const mod = getDeviceCookies();\n if (!mod) throw new CapabilityUnavailableError('cookies (native module not available — use a dev build)');\n const list = await mod.get(url, true);\n return list as WebViewCookie[];\n },\n async clear(_url: string): Promise<{ ok: boolean }> {\n const mod = getDeviceCookies();\n if (!mod) throw new CapabilityUnavailableError('cookies (native module not available — use a dev build)');\n const r = await mod.clearAll();\n return { ok: r.ok };\n },\n };\n}\n\n// ── Biometrics v2 (Face ID / Touch ID / fingerprint) ──\nfunction createBiometricsAPIv2(): BiometricsAPIv2 {\n return {\n capabilities: () => deviceBiometrics.capabilities(),\n authenticate: (opts) => deviceBiometrics.authenticate(opts),\n };\n}\n\n// ── Sharing (native share sheet) ──\nfunction createSharingAPI(): SharingAPI {\n return {\n isAvailable: () => deviceSharing.isAvailable(),\n shareFile: (fileUri, opts) => deviceSharing.shareFile(fileUri, opts),\n };\n}\n\n// ── Scanner (QR / barcode permission helper) ──\nfunction createScannerAPI(): ScannerAPI {\n return {\n requestPermissions: () => deviceScanner.requestPermissions(),\n // Raises the shared camera surface and settles on the first code read.\n // The plugin mounts nothing — that lifecycle is owned once, by\n // CameraTaskHost, instead of by every plugin that wants to scan.\n scanOnce: (opts) => cameraTasks.run('scan', {\n formats: opts?.formats,\n timeoutMs: opts?.timeoutMs,\n }),\n };\n}\n\n// ── NFC (tag read, lazy-loaded) ──\nfunction createNfcAPI(): NfcAPI {\n return {\n async isSupported() {\n const mod = getDeviceNfc();\n if (!mod) return false;\n return mod.isSupported();\n },\n async isEnabled() {\n const mod = getDeviceNfc();\n if (!mod) return false;\n return mod.isEnabled();\n },\n readOnce(timeoutMs) {\n const mod = getDeviceNfc();\n if (!mod) return Promise.resolve({ ok: false, error: 'nfc_unavailable' } as any);\n return mod.readOnce(timeoutMs);\n },\n };\n}\n\n// ── Bluetooth / BLE (SHAPE ONLY — every method refuses) ──\n//\n// Nothing implements this yet, and it is wired up anyway. `bluetooth` has been\n// a manifest flag with no API behind it, so a plugin that correctly declared it\n// got `undefined` and crashed with \"cannot read properties of undefined\" — a\n// failure that names nothing and teaches nothing.\n//\n// Refusing BY NAME is the whole difference. The rule this layer runs on is that\n// absence is an answer, but only when it says what is absent.\n//\n// KEPT rather than dropped because BLE beacons are one of the few indoor\n// position signals that survive where GPS does not — which is precisely where\n// Ada Guide ends up the moment the user walks into a building.\nfunction createBluetoothAPI(): BluetoothAPI {\n const refuse = (method: string) =>\n new CapabilityUnavailableError(\n `bluetooth.${method}`,\n 'Bluetooth is not implemented in this build — no BLE native module is installed. ' +\n 'The capability is declared so it fails by name rather than silently; nothing was scanned.',\n );\n\n return {\n // These two answer `false` rather than throwing, because \"is BLE usable?\"\n // is exactly the question a plugin should be allowed to ask before\n // committing to a flow, and `false` is the true answer here.\n async isSupported() { return false; },\n async isEnabled() { return false; },\n // scan() THROWS, because there is no honest empty result: an empty device\n // list is a real finding (\"nothing is advertising nearby\") and returning\n // one would report a scan that never happened.\n async scan() { throw refuse('scan'); },\n };\n}\n\n\n// ── Vision — on-device image understanding (SHAPE ONLY) ──\n//\n// Nothing implements this yet; every method refuses BY NAME. Declared now\n// because it is the last missing piece of Ada Guide and a solution should be\n// able to design against it before the native side exists.\n//\n// WHY IT IS NOT IN THE VISUAL DOMAIN: the freeze reserves Visual for the frame\n// TIMELINE and puts everything interpretive — what is in the scene, what the\n// sign says — ABOVE Device Bridge. This is that layer.\n//\n// FRAMES MUST NEVER LEAVE THE PHONE (Arie, 2026-08-15). Whoever implements\n// this: iOS Vision (VNRecognizeTextRequest / VNClassifyImageRequest) or\n// Android MLKit, both on-device and free per call. An implementation that\n// POSTs the frame anywhere satisfies these types and violates the design.\nfunction createVisionAPI(): VisionAPI {\n // All three methods are implemented on-device via the ada-vision native module:\n // assessQuality (pixel statistics), readText (VNRecognizeText), describe\n // (VNClassifyImage). Frames never leave the phone.\n return {\n // \"Can I see?\" — true once the native module is present in the binary. On a\n // build without it, adaVision.isSupported() answers false, and assessQuality\n // below refuses cleanly (no crash), exactly the pre-module behaviour.\n async isSupported() { return adaVision.isSupported(); },\n // readText: on-device OCR on a published frame (native resolves the id →\n // pixels). [] = looked, found no text (a real finding); throws = could not\n // look (unknown/aged-out id). describe (VNClassifyImage) is the next pass.\n async readText(frameId: string, opts?: { languages?: string[] }) {\n const langs = opts?.languages;\n if (langs != null && (!Array.isArray(langs) || langs.some((l) => typeof l !== 'string'))) {\n throw refuseArgument('vision', 'readText', `languages must be an array of language codes, not ${shown(langs)}`, 'nothing was read');\n }\n return adaVision.readText(frameId, langs);\n },\n // describe: on-device labels (VNClassifyImage), not prose — the sentence is\n // the skill's job. [] = looked, recognised nothing.\n async describe(frameId: string, opts?: { maxLabels?: number }) {\n // Checked here: a NaN/Infinity count would trap Swift's Int conversion (src/args.ts).\n return adaVision.describe(frameId, countArg('vision', 'describe', 'maxLabels', opts?.maxLabels, 5, 50, 'nothing was labelled'));\n },\n // detectObjects: on-device located objects (bundled detector), each with a\n // box — what a guidance loop reads to say WHERE a thing is, not just that the\n // scene contains it. [] = looked, found nothing; throws = could not look.\n async detectObjects(frameId: string, opts?: { maxItems?: number; minConfidence?: number }) {\n const nothing = 'nothing was detected';\n return adaVision.detectObjects(\n frameId,\n countArg('vision', 'detectObjects', 'maxItems', opts?.maxItems, 10, 50, nothing),\n fractionArg('vision', 'detectObjects', 'minConfidence', opts?.minConfidence, 0.25, nothing),\n );\n },\n // assessQuality: resolve the frame id to its LOCAL file, measure on-device,\n // and record the result back onto the frame so a later read of\n // VisualFrameRef.quality reflects it. Throwing here means \"could not look\"\n // (unknown/aged-out id, or no native module) — never a measured verdict.\n async assessQuality(frameId: string) {\n // Quality was measured at publish; the pixels live in the native ring,\n // resolved by id inside native. Null = the frame is unknown or has aged out\n // of the ring — \"could not look\", never \"looked and found nothing\".\n const q = adaVision.qualityById(frameId);\n if (!q) {\n throw new CapabilityUnavailableError(\n 'vision.assessQuality',\n `No frame for id \"${frameId}\" — unknown or aged out of the ring. ` +\n 'This is \"could not look\", not a measurement.',\n );\n }\n return q;\n },\n };\n}\n\n// ── Maps — place search, reverse geocoding, walking routes ──\n//\n// NOT on-device, unlike vision: the query and the coordinates it is given go to\n// Apple's map servers (the platform geocoder on Android). It never reads the\n// user's location — the caller passes every coordinate — so it is not a second\n// door to where the user is.\n//\n// search/route are the ada-maps module (MapKit, iOS only). reverseGeocode is the\n// SDK's one reverse geocode (expo-location), the same call the location\n// collector and the connector bridge make — so it works on Android and on a\n// build without ada-maps.\nfunction createMapsAPI(): MapsAPI {\n // Refuse BY NAME before any native call: what this build or platform is\n // missing, and that nothing left the phone.\n const refuseIfMissing = (verb: string, nothing: string) => {\n const missing = adaMaps.missingReason();\n if (missing) {\n throw new CapabilityUnavailableError(`maps.${verb}`, `maps.${verb} needs ${missing}; ${nothing}.`);\n }\n };\n\n // Every argument is checked (searchArgs / coordinateArg) BEFORE the missing\n // check and before anything crosses the bridge: a remote bundle is plain JS,\n // and a malformed call refuses as INVALID_ARGUMENT instead of crashing Swift\n // (NaN limit) or searching around 0,0 (`{ lat, lng }`).\n return {\n async isSupported() { return adaMaps.isSupported(); },\n // [] = Apple looked and found nothing (a real finding); throws = could not\n // look. The defaults (radiusM 3000, limit 8) live in searchArgs.\n async search(query, opts) {\n const args = searchArgs(query, opts);\n refuseIfMissing('search', 'nothing was searched');\n const raw = await adaMaps\n .search(args.query, args.near, args.radiusM, args.limit)\n .catch(err => { throw mapsFailure('search', err); });\n return toMapsPlaces(raw, args.near);\n },\n // null = nothing at that point; throws = could not look.\n async reverseGeocode(at) {\n const point = coordinateArg('reverseGeocode', 'at', at, 'nothing was looked up');\n const address = await adaMaps\n .reverseGeocode(point)\n .catch(err => { throw mapsFailure('reverseGeocode', err); });\n return address ? toReverseGeocodedPlace(point, address) : null;\n },\n // null = Apple found no route. Never an empty 0 m route, which would read as\n // \"you have arrived\". A remote bundle can pass any string as `mode`, so a\n // mode this build cannot honour refuses rather than silently walking.\n async route(from, to, opts) {\n if (opts?.mode !== undefined && opts.mode !== 'walking') {\n throw new CapabilityUnavailableError(\n 'maps.route',\n `maps.route supports walking only, not \"${String(opts.mode)}\"; no route was requested.`,\n );\n }\n const a = coordinateArg('route', 'from', from, 'no route was requested');\n const b = coordinateArg('route', 'to', to, 'no route was requested');\n refuseIfMissing('route', 'no route was requested');\n const raw = await adaMaps\n .route(a, b)\n .catch(err => { throw mapsFailure('route', err); });\n return raw ? toMapsRoute(raw) : null;\n },\n };\n}\n\n// ── Provider factory ───────────────────────────────────────────\n\nexport interface DeviceProviderConfig {\n /** Endpoint the connectivity check HEAD-fetches (host-injected, e.g. the app's\n * own health URL). Omitted → connectivity assumes reachable (no hardcoded URL). */\n reachabilityUrl?: string;\n}\n\n/**\n * The full device-primitive implementation a host registers once via\n * setPluginNativeProvider. It is UNGATED — per-plugin capability gating and\n * per-plugin filesystem scoping are applied by plugin-runtime's gatePluginNative.\n */\nexport function createDeviceProvider(config?: DeviceProviderConfig): PluginNative {\n const connectivity = createConnectivityAPI(config?.reachabilityUrl);\n return {\n camera: createCameraAPI(),\n location: createLocationAPI(),\n // Only the per-plugin storageFor/fileSystemFor are exposed — NO shared\n // `storage`/`fileSystem` bucket. The gate scopes by pluginId and fails\n // closed without one, so a shared instance would be an unreachable\n // (and dangerous) fallback. Each plugin gets its own namespace + db file /\n // sandbox directory, or nothing.\n storageFor: (pluginId: string) => createStorageAPI(pluginId),\n biometrics: stubApi<BiometricsAPI>('biometrics'),\n biometricsV2: createBiometricsAPIv2(),\n notifications: createNotificationsAPI(),\n haptics: createHapticsAPI(),\n connectivity,\n fileSystemFor: (pluginId: string) => createFileSystemAPI(pluginId),\n cookies: createCookiesAPI(),\n sharing: createSharingAPI(),\n scanner: createScannerAPI(),\n nfc: createNfcAPI(),\n bluetooth: createBluetoothAPI(),\n vision: createVisionAPI(),\n maps: createMapsAPI(),\n };\n}\n","// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Start the vision session — publishes a timeline frame every `intervalMs`. */\nexport async function startVision(intervalMs = 1000): Promise<void> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.startVision(intervalMs);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.readText(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.describe(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.detectObjects(id, maxItems, minConfidence);\n}\n","// ada-maps — place search + walking directions (Apple MapKit), our own native\n// code, plus the SDK's one reverse geocode.\n//\n// NOT on-device — the opposite of ada-vision. Every call sends the query and the\n// coordinates it is given to a map service over the network. None of it reads\n// the device's location: the caller supplies every coordinate.\n//\n// search(...) — MKLocalSearch (AdaMaps module, iOS only)\n// route(...) — MKDirections (AdaMaps module, iOS only)\n// reverseGeocode(at) — expo-location's reverseGeocodeAsync, which IS CLGeocoder\n// on iOS and the platform Geocoder on Android. No Swift for\n// it, and the ONLY reverse geocode in the SDK: the location\n// collector and the connector bridge call this too.\n//\n// requireOptionalNativeModule: on a binary WITHOUT the module the import is null,\n// isSupported() is false, and search/route refuse cleanly (no crash). Android has\n// no AdaMaps at all, so it refuses by name the same way.\n\nimport { Platform } from 'react-native';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress, MapsCoordinate } from '@ateam-ai-mobile/plugin-runtime';\nimport { toMapsAddress } from './normalize';\nimport type { RawMapsPlace, RawMapsRoute } from './normalize';\n\ninterface AdaMapsNative {\n isSupported(): boolean;\n search(query: string, lat: number, lng: number, hasNear: boolean, radiusM: number, limit: number): Promise<RawMapsPlace[]>;\n route(fromLat: number, fromLng: number, toLat: number, toLng: number): Promise<RawMapsRoute | null>;\n}\n\nconst native = requireOptionalNativeModule<AdaMapsNative>('AdaMaps');\n\n/** What search/route are missing in THIS build, as the clause after \"needs\" —\n * or null when they can run. A voice assistant reads it out, so it names the\n * missing thing rather than giving a code. */\nexport function missingReason(): string | null {\n if (Platform.OS !== 'ios') return `iOS (Apple MapKit) — ${Platform.OS} has no implementation yet`;\n if (!native) return 'the app build with the AdaMaps module (this build predates it — a native rebuild is needed)';\n return null;\n}\n\n/** Is the native maps module present in this binary (and on a platform it runs on)? */\nexport function isSupported(): boolean {\n try { return missingReason() === null && (native?.isSupported() ?? false); } catch { return false; }\n}\n\n// search/route hand their arguments to native AS GIVEN: a NaN or Infinity\n// `limit` traps Swift's Int conversion and kills the app. createMapsAPI checks\n// every argument first (searchArgs / coordinateArg in ./normalize); a host\n// calling these directly must pass what those checks would accept.\n\n/** MKLocalSearch around `near` (when given). [] = looked, found nothing; throws =\n * couldn't look, with a string code OFFLINE / THROTTLED / UNAVAILABLE. */\nexport async function search(\n query: string,\n near: MapsCoordinate | null,\n radiusM: number,\n limit: number,\n): Promise<RawMapsPlace[]> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n // Without `near`, hasNear is false and Swift never reads lat/lng — the zeros\n // are placeholders for a primitive argument, not a place.\n return near\n ? native.search(query, near.latitude, near.longitude, true, radiusM, limit)\n : native.search(query, 0, 0, false, radiusM, limit);\n}\n\n/** MKDirections, walking, between two caller-supplied points. null = no route. */\nexport async function route(from: MapsCoordinate, to: MapsCoordinate): Promise<RawMapsRoute | null> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n return native.route(from.latitude, from.longitude, to.latitude, to.longitude);\n}\n\n/** The address at a coordinate, or null when the geocoder has nothing there.\n * Throws when it could not look (network, rate limit, Android without location\n * permission) — callers decide whether that is fatal. */\nexport async function reverseGeocode(at: MapsCoordinate): Promise<MapsAddress | null> {\n const [first] = await ExpoLocation.reverseGeocodeAsync({ latitude: at.latitude, longitude: at.longitude });\n return first ? toMapsAddress(first) : null;\n}\n","// Argument checks for native verbs — one place, shared by every capability that\n// hands caller numbers to Swift.\n//\n// Why this exists: expo-modules-core converts a JS number to a Swift Int with\n// `Int(double.rounded())`, which TRAPS on NaN, ±Infinity or anything past\n// Int.max. A plugin (or a model driving a device tool) passing `limit: Infinity`\n// or `maxItems: NaN` would therefore kill the host app. Every count that crosses\n// the bridge is checked here first and refused BY NAME instead.\n//\n// No imports on purpose: it runs under plain `node --test`.\n\n/** A malformed call, refused before anything is sent. Retrying will not help;\n * fixing the call will. */\nexport type ArgumentError = Error & { code: 'INVALID_ARGUMENT' };\n\n/** What the caller passed, short enough to quote back in a refusal. NaN and\n * Infinity are spelled out, unquoted (JSON would print them as null). */\nexport const shown = (v: unknown): string => {\n let s: string | undefined;\n try {\n s = JSON.stringify(v, (_k, x) => (typeof x === 'number' && !Number.isFinite(x) ? `\\u0001${x}\\u0001` : x))\n ?.replace(/\"\\\\u0001(NaN|-?Infinity)\\\\u0001\"/g, '$1');\n } catch { /* cyclic — fall back to String */ }\n s = s ?? String(v);\n return s.length > 80 ? `${s.slice(0, 77)}...` : s;\n};\n\n/** `<capability>.<verb>: <problem>; <what was not done>.` with code INVALID_ARGUMENT. */\nexport function refuseArgument(capability: string, verb: string, problem: string, nothing: string): ArgumentError {\n const e = new Error(`${capability}.${verb}: ${problem}; ${nothing}.`) as ArgumentError;\n e.code = 'INVALID_ARGUMENT';\n return e;\n}\n\n/** A count bound for a Swift Int: absent → `def`; otherwise a finite number ≥ 1,\n * floored and capped at `max`. Anything else refuses by name. */\nexport function countArg(\n capability: string, verb: string, name: string, value: unknown, def: number, max: number, nothing: string,\n): number {\n const v = value ?? def;\n if (typeof v !== 'number' || !Number.isFinite(v) || v < 1) {\n throw refuseArgument(capability, verb, `${name} must be a number from 1 (at most ${max} are used), not ${shown(value)}`, nothing);\n }\n return Math.min(max, Math.floor(v));\n}\n\n/** A 0..1 fraction (a confidence): absent → `def`; NaN or out of range refuses. */\nexport function fractionArg(\n capability: string, verb: string, name: string, value: unknown, def: number, nothing: string,\n): number {\n const v = value ?? def;\n if (typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > 1) {\n throw refuseArgument(capability, verb, `${name} must be a number from 0 to 1, not ${shown(value)}`, nothing);\n }\n return v;\n}\n","// Pure shaping for native.maps — no Expo, no React Native, so it runs under\n// plain `node --test` (tests/adaMapsNormalize.test.mjs pins it).\n//\n// The native side hands back loose dictionaries — AdaMaps (MapKit) for search\n// and route, expo-location's geocoder for reverse geocoding. This turns them\n// into the typed MapsAPI shapes, and one set of failure codes. Unknown is\n// `null`, never '' or 0: an empty street reads as a street with no name, and a\n// 0 m route reads as \"you have arrived\".\n//\n// It also checks what a caller sends BEFORE it crosses the bridge (searchArgs,\n// coordinateArg). A remote bundle is plain JS at runtime, so the types promise\n// nothing: a NaN or Infinity `limit` traps Swift's Int conversion and kills the\n// app, and a `{ lat, lng }` near would search around 0,0 and answer \"found\n// nothing\" with confidence.\n\nimport type {\n MapsAddress,\n MapsCoordinate,\n MapsPlace,\n MapsRoute,\n MapsRouteStep,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { distanceOf } from '../geo';\nimport { countArg, refuseArgument as refuse, shown, type ArgumentError } from '../args';\n\n/** An address as the native geocoders spell it. AdaMaps sends exactly these\n * keys; expo-location sends these plus `subregion`/`timezone`/\n * `formattedAddress`, which MapsAddress does not carry. */\nexport interface RawMapsAddress {\n name?: string | null;\n street?: string | null;\n streetNumber?: string | null;\n district?: string | null;\n city?: string | null;\n region?: string | null;\n postalCode?: string | null;\n country?: string | null;\n isoCountryCode?: string | null;\n}\n\n/** One AdaMaps search hit (AdaMapsModule.place). */\nexport interface RawMapsPlace {\n name: string | null;\n latitude: number;\n longitude: number;\n /** MKPointOfInterestCategory raw value, e.g. \"MKPOICategoryPharmacy\". */\n category: string | null;\n address: RawMapsAddress;\n}\n\n/** An AdaMaps walking route (AdaMapsModule.route) — distances in metres,\n * expectedTravelTime in SECONDS, as MapKit reports them. */\nexport interface RawMapsRoute {\n distance: number;\n expectedTravelTime: number;\n steps: Array<{ instructions: string | null; distance: number; notice: string | null; polyline: MapsCoordinate[] }>;\n polyline: MapsCoordinate[];\n}\n\n/** The three ways a maps call can fail to look — see MapsAPI. */\nexport type MapsFailureCode = 'UNAVAILABLE' | 'OFFLINE' | 'THROTTLED';\n\n/** A malformed call, refused before anything is sent. Not a fourth way to fail\n * to look: retrying will not help, fixing the call will. */\nexport type MapsArgumentError = ArgumentError;\n\nconst SEARCH_RADIUS_DEFAULT_M = 3000;\n/** Keeps the region MapKit is handed a small, valid one. A \"near\" search wider\n * than this is a different question. */\nconst SEARCH_RADIUS_MAX_M = 100_000;\nconst SEARCH_LIMIT_DEFAULT = 8;\nconst SEARCH_LIMIT_MAX = 25;\n\nconst text = (v: unknown): string | null =>\n typeof v === 'string' && v.trim() !== '' ? v : null;\n\nconst refuseArgument = (verb: string, problem: string, nothing: string): MapsArgumentError =>\n refuse('maps', verb, problem, nothing);\n\n/** A caller's coordinate, checked and copied to exactly `{ latitude, longitude }`.\n * Anything else — `{ lat, lng }`, strings, NaN, out of range — refuses BY NAME\n * rather than being sent as 0,0 or NaN. */\nexport function coordinateArg(verb: string, name: string, value: unknown, nothing: string): MapsCoordinate {\n const c = value as { latitude?: unknown; longitude?: unknown } | null | undefined;\n const latitude = c?.latitude, longitude = c?.longitude;\n if (\n typeof latitude === 'number' && Number.isFinite(latitude) && Math.abs(latitude) <= 90 &&\n typeof longitude === 'number' && Number.isFinite(longitude) && Math.abs(longitude) <= 180\n ) {\n return { latitude, longitude };\n }\n throw refuseArgument(\n verb,\n `${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,\n nothing,\n );\n}\n\n/** search's arguments, checked and defaulted (radiusM 3000, limit 8). `limit`\n * is floored and capped at 25; everything else out of range refuses. */\nexport function searchArgs(\n query: unknown,\n opts: { near?: unknown; radiusM?: unknown; limit?: unknown } | null | undefined,\n): { query: string; near: MapsCoordinate | null; radiusM: number; limit: number } {\n const nothing = 'nothing was searched';\n if (typeof query !== 'string' || query.trim() === '') {\n throw refuseArgument('search', `query must be a non-empty string, not ${shown(query)}`, nothing);\n }\n const near = opts?.near == null ? null : coordinateArg('search', 'near', opts.near, nothing);\n const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;\n if (typeof radiusM !== 'number' || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {\n throw refuseArgument(\n 'search', `radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`, nothing);\n }\n const limit = countArg('maps', 'search', 'limit', opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);\n return { query, near, radiusM, limit };\n}\n\nconst point = (c: MapsCoordinate): MapsCoordinate => ({\n latitude: Number(c.latitude),\n longitude: Number(c.longitude),\n});\n\nexport function toMapsAddress(raw: RawMapsAddress | null | undefined): MapsAddress {\n const r = raw ?? {};\n return {\n name: text(r.name),\n street: text(r.street),\n streetNumber: text(r.streetNumber),\n district: text(r.district),\n city: text(r.city),\n region: text(r.region),\n postalCode: text(r.postalCode),\n country: text(r.country),\n isoCountryCode: text(r.isoCountryCode),\n };\n}\n\n/** \"MKPOICategoryPharmacy\" → \"Pharmacy\": the vocabulary term, without the\n * framework's prefix. */\nexport function toMapsCategory(raw: unknown): string | null {\n const c = text(raw);\n return c ? c.replace(/^MKPOICategory/, '') || null : null;\n}\n\n/** Search hits → places. `distanceM` is straight-line from `near`, and only\n * when the caller gave one — a distance from nowhere would be invented. */\nexport function toMapsPlaces(raw: RawMapsPlace[] | null | undefined, near: MapsCoordinate | null): MapsPlace[] {\n return (raw ?? []).map(r => {\n const coordinate = point(r);\n return {\n name: text(r.name),\n coordinate,\n address: toMapsAddress(r.address),\n category: toMapsCategory(r.category),\n distanceM: near ? distanceOf([near, coordinate]) : null,\n };\n });\n}\n\n/** A reverse-geocoded address → a place AT the point asked about. The platform\n * geocoder returns an address, not a snapped point or a category. */\nexport function toReverseGeocodedPlace(at: MapsCoordinate, address: MapsAddress): MapsPlace {\n return {\n name: address.name,\n coordinate: point(at),\n address,\n category: null,\n distanceM: null,\n };\n}\n\n/** A MapKit route → MapsRoute. Seconds become ms; every step is kept, including\n * Apple's empty-instruction origin step, so indexes match Apple's. */\nexport function toMapsRoute(raw: RawMapsRoute): MapsRoute {\n const steps: MapsRouteStep[] = (raw.steps ?? []).map(s => ({\n instructions: typeof s.instructions === 'string' ? s.instructions : '',\n distanceM: Number(s.distance),\n notice: text(s.notice),\n polyline: (s.polyline ?? []).map(point),\n }));\n return {\n mode: 'walking',\n distanceM: Number(raw.distance),\n expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1000),\n steps,\n polyline: (raw.polyline ?? []).map(point),\n };\n}\n\n/**\n * A native rejection → an Error whose `code` is one of MapsAPI's three.\n *\n * AdaMaps already rejects with OFFLINE / THROTTLED / UNAVAILABLE. expo-location\n * (reverseGeocode) has its own codes: on iOS a CLError.network becomes\n * ERR_GEOCODING_NETWORK, which Apple uses for BOTH a lost connection and a rate\n * limit — so it maps to OFFLINE and the message says it may be either. Anything\n * else is UNAVAILABLE, with the platform's own words kept beside ours.\n */\nexport function mapsFailure(verb: string, err: unknown): Error & { code: MapsFailureCode } {\n const nativeCode = String((err as any)?.code ?? '');\n const detail = String((err as any)?.message ?? err ?? '').trim();\n let code: MapsFailureCode;\n let why: string;\n if (nativeCode === 'THROTTLED') {\n code = 'THROTTLED';\n why = 'Apple is rate-limiting map requests from this app — wait before retrying';\n } else if (nativeCode === 'OFFLINE') {\n code = 'OFFLINE';\n why = 'the map service could not be reached — the phone may be offline';\n } else if (nativeCode === 'ERR_GEOCODING_NETWORK') {\n code = 'OFFLINE';\n why = 'the geocoder could not be reached — the phone may be offline, or Apple is ' +\n 'rate-limiting requests (iOS reports both the same way)';\n } else {\n code = 'UNAVAILABLE';\n why = 'the map service failed';\n }\n const failure = new Error(\n `maps.${verb} could not look: ${why}. This is not \"found nothing\".` +\n (detail ? ` (${nativeCode ? nativeCode + ': ' : ''}${detail})` : ''),\n ) as Error & { code: MapsFailureCode };\n failure.code = code;\n return failure;\n}\n","/**\n * Messaging Action Executor — sends SMS via expo-sms.\n * Executes 'messaging.send' actions from the action queue.\n */\n\nimport * as SMS from 'expo-sms';\nimport type { ActionResult } from '../types';\n\n/**\n * Check if SMS is available on this device.\n */\nexport async function isAvailable(): Promise<boolean> {\n return SMS.isAvailableAsync();\n}\n\n/**\n * Execute a messaging.send action.\n *\n * Params expected:\n * - contact_name: string (for display only)\n * - phone: string (phone number to send to)\n * - message: string (message text)\n */\nexport async function execute(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const phone = params.phone as string;\n const message = params.message as string;\n\n if (!phone || !message) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing phone or message parameter',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const available = await SMS.isAvailableAsync();\n if (!available) {\n return {\n id: actionId,\n ok: false,\n error: 'SMS not available on this device',\n executed_at: new Date().toISOString(),\n };\n }\n\n // Opens the native SMS compose screen with pre-filled content\n const { result } = await SMS.sendSMSAsync([phone], message);\n\n return {\n id: actionId,\n ok: result === 'sent' || result === 'unknown', // 'unknown' on Android means it was sent\n result: {\n to: params.contact_name || phone,\n phone,\n message,\n status: result,\n sent_at: new Date().toISOString(),\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `SMS send failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n","/**\n * Calendar Action Executor — creates/modifies events via expo-calendar.\n * Executes 'calendar.create', 'calendar.reschedule', 'calendar.cancel' actions.\n */\n\nimport * as ExpoCalendar from 'expo-calendar';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoCalendar.Calendar`-style annotations need this.\n// Derived rather than imported — see collectors/calendar.ts.\ntype Event = Awaited<ReturnType<typeof ExpoCalendar.getEventsAsync>>[number];\nimport type { ActionResult } from '../types';\n\n/**\n * Get the default calendar ID for creating events.\n */\nasync function getDefaultCalendarId(): Promise<string | null> {\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n\n // Prefer the default calendar\n const defaultCal = calendars.find(c => c.isPrimary) || calendars[0];\n return defaultCal?.id || null;\n}\n\n/**\n * Execute a calendar.create action.\n *\n * Params expected:\n * - title: string\n * - date: string (YYYY-MM-DD)\n * - start_time: string (HH:MM)\n * - end_time?: string (HH:MM)\n * - location?: string\n * - attendees?: string[] (names — no email lookup here)\n * - notes?: string\n */\nexport async function executeCreate(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const title = params.title as string;\n const date = params.date as string;\n const startTime = params.start_time as string;\n const endTime = params.end_time as string | undefined;\n\n if (!title || !date || !startTime) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing title, date, or start_time',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const calendarId = await getDefaultCalendarId();\n if (!calendarId) {\n return {\n id: actionId,\n ok: false,\n error: 'No calendar found on device',\n executed_at: new Date().toISOString(),\n };\n }\n\n const [year, month, day] = date.split('-').map(Number);\n const [startH, startM] = startTime.split(':').map(Number);\n\n const startDate = new Date(year, month - 1, day, startH, startM);\n let endDate: Date;\n\n if (endTime) {\n const [endH, endM] = endTime.split(':').map(Number);\n endDate = new Date(year, month - 1, day, endH, endM);\n } else {\n // Default 1 hour duration\n endDate = new Date(startDate.getTime() + 60 * 60 * 1000);\n }\n\n const eventId = await ExpoCalendar.createEventAsync(calendarId, {\n title,\n startDate,\n endDate,\n location: (params.location as string) || undefined,\n notes: (params.notes as string) || undefined,\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n });\n\n return {\n id: actionId,\n ok: true,\n result: {\n event_id: eventId,\n title,\n date,\n start: startTime,\n end: endTime || formatTime(endDate),\n message: `Event \"${title}\" created successfully`,\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar create failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Execute a calendar.reschedule action.\n *\n * Params expected:\n * - event_id: string\n * - new_date?: string (YYYY-MM-DD)\n * - new_start_time?: string (HH:MM)\n * - new_end_time?: string (HH:MM)\n */\nexport async function executeReschedule(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const eventId = params.event_id as string;\n\n if (!eventId) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing event_id',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const updates: Partial<Event> = {};\n\n if (params.new_date && params.new_start_time) {\n const [year, month, day] = (params.new_date as string).split('-').map(Number);\n const [h, m] = (params.new_start_time as string).split(':').map(Number);\n updates.startDate = new Date(year, month - 1, day, h, m);\n\n if (params.new_end_time) {\n const [eh, em] = (params.new_end_time as string).split(':').map(Number);\n updates.endDate = new Date(year, month - 1, day, eh, em);\n } else {\n updates.endDate = new Date(updates.startDate.getTime() + 60 * 60 * 1000);\n }\n }\n\n await ExpoCalendar.updateEventAsync(eventId, updates);\n\n return {\n id: actionId,\n ok: true,\n result: { event_id: eventId, message: 'Event rescheduled successfully' },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar reschedule failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Execute a calendar.cancel action.\n *\n * Params expected:\n * - event_id: string\n */\nexport async function executeCancel(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const eventId = params.event_id as string;\n\n if (!eventId) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing event_id',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n await ExpoCalendar.deleteEventAsync(eventId);\n\n return {\n id: actionId,\n ok: true,\n result: { event_id: eventId, message: 'Event cancelled successfully' },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar cancel failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n// ─── Helpers ───\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n });\n}\n","/**\n * Navigation Action Executor — opens maps app via expo-linking.\n * Executes 'navigation.start' actions from the action queue.\n */\n\nimport * as Linking from 'expo-linking';\nimport { Platform } from 'react-native';\nimport type { ActionResult } from '../types';\n\n/**\n * Execute a navigation.start action.\n * Opens the native maps app with turn-by-turn directions.\n *\n * Params expected:\n * - destination: string (address or place name)\n * - mode?: 'driving' | 'transit' | 'walking' (default: 'driving')\n */\nexport async function execute(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const destination = params.destination as string;\n const mode = (params.mode as string) || 'driving';\n\n if (!destination) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing destination parameter',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const encodedDest = encodeURIComponent(destination);\n let url: string;\n\n if (Platform.OS === 'ios') {\n // Apple Maps with directions\n const modeMap: Record<string, string> = {\n driving: 'd',\n transit: 'r',\n walking: 'w',\n };\n url = `maps://maps.apple.com/?daddr=${encodedDest}&dirflg=${modeMap[mode] || 'd'}`;\n } else {\n // Google Maps on Android\n const modeMap: Record<string, string> = {\n driving: 'driving',\n transit: 'transit',\n walking: 'walking',\n };\n url = `google.navigation:q=${encodedDest}&mode=${modeMap[mode] || 'driving'}`;\n }\n\n const canOpen = await Linking.canOpenURL(url);\n if (!canOpen) {\n // Fallback to Google Maps web URL\n const webModeMap: Record<string, string> = {\n driving: 'driving',\n transit: 'transit',\n walking: 'walking',\n };\n url = `https://www.google.com/maps/dir/?api=1&destination=${encodedDest}&travelmode=${webModeMap[mode] || 'driving'}`;\n }\n\n await Linking.openURL(url);\n\n return {\n id: actionId,\n ok: true,\n result: {\n message: `Navigation started to ${destination} via ${mode}`,\n destination,\n mode,\n status: 'navigating',\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Navigation failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n","/**\n * Health Collector — reads HealthKit aggregates via @kingstinct/react-native-healthkit\n * (TurboModule / Nitro-Modules based; works in React Native's New Architecture).\n *\n * iOS only. On Android the functions resolve with zeros so the caller doesn't\n * need to platform-check.\n *\n * Public:\n * - hasPermission() — returns true once authorization has been requested.\n * - requestPermission() — triggers the iOS HealthKit permission sheet (red heart).\n * - collectToday() — one HealthDayMetrics for today.\n * - collectHistory(days) — array of HealthDayMetrics, newest-first.\n */\nimport { Platform } from 'react-native';\nimport type { HealthDayMetrics } from '../types';\n\n// Lazy-loaded so the module is harmless on Android / Expo Go / non-iOS.\n// In Expo Go the JS module resolves but the native bridge is missing —\n// resulting object exists yet methods like requestAuthorization are undefined.\nlet _HK: any = null;\nlet _hkProbed = false;\nfunction getHK(): any | null {\n if (Platform.OS !== 'ios') return null;\n if (_hkProbed) return _HK;\n _hkProbed = true;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod = require('@kingstinct/react-native-healthkit');\n if (mod && typeof mod.requestAuthorization === 'function') {\n _HK = mod;\n } else {\n console.warn('[health] @kingstinct/react-native-healthkit native bridge unavailable — HealthKit disabled (Expo Go?)');\n }\n } catch (e) {\n console.warn('[health] @kingstinct/react-native-healthkit require failed:', e);\n }\n return _HK;\n}\n\n// Last error from requestAuthorization — surfaced to UI so we don't fail silently.\nexport let lastHealthInitError: string | null = null;\n\nconst READ_TYPES = [\n 'HKQuantityTypeIdentifierStepCount',\n 'HKQuantityTypeIdentifierFlightsClimbed',\n 'HKQuantityTypeIdentifierActiveEnergyBurned',\n 'HKQuantityTypeIdentifierDistanceWalkingRunning',\n // Added for mood signals — all read-only, covered by the same auth sheet.\n 'HKQuantityTypeIdentifierRestingHeartRate',\n 'HKQuantityTypeIdentifierAppleExerciseTime',\n 'HKCategoryTypeIdentifierSleepAnalysis',\n] as const;\n\nlet _initPromise: Promise<boolean> | null = null;\n\n/** Request HealthKit authorization once; subsequent calls reuse the result. */\nasync function ensureInit(): Promise<boolean> {\n const HK = getHK();\n if (!HK) {\n lastHealthInitError = '@kingstinct/react-native-healthkit native module not loaded';\n return false;\n }\n if (_initPromise) return _initPromise;\n _initPromise = (async () => {\n try {\n // requestAuthorization triggers the iOS HealthKit permission sheet.\n // Resolves once the user dismisses it (regardless of allow/deny).\n // Returns the authorization status — but iOS deliberately does not\n // disclose per-type read permission state for privacy, so we just\n // treat \"we asked\" as \"ready to read\" and let reads return 0 if denied.\n await HK.requestAuthorization({ toRead: READ_TYPES, toWrite: [] });\n lastHealthInitError = null;\n return true;\n } catch (e: any) {\n lastHealthInitError = `requestAuthorization threw: ${e?.message || String(e)}`;\n console.warn('[health]', lastHealthInitError);\n _initPromise = null;\n return false;\n }\n })();\n return _initPromise;\n}\n\nexport async function hasPermission(): Promise<boolean> {\n return ensureInit();\n}\n\nexport async function requestPermission(): Promise<boolean> {\n return ensureInit();\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setHours(0, 0, 0, 0);\n return out;\n}\n\nfunction dateKey(d: Date): string {\n // Local-date YYYY-MM-DD\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n}\n\n/**\n * Read a single daily-aggregate sum for [start, end) in the given unit.\n * Returns 0 when no data, HealthKit unavailable, or the call throws.\n *\n * `queryStatisticsForQuantity` is the modern statistics API; we ask only for\n * the cumulative sum since that's what we need for these data types.\n */\nasync function readDailySum(identifier: string, unit: string, start: Date, end: Date): Promise<number> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return 0;\n try {\n // The kingstinct API takes the date range nested under filter.date — not\n // top-level from/to. Without filter.date the call returns the cumulative\n // sum across ALL historical data, which is what we hit before this fix\n // (millions of steps, thousands of km, hundreds of thousands of kcal).\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['cumulativeSum'],\n {\n unit,\n filter: { date: { startDate: start, endDate: end } },\n },\n );\n const q = result?.sumQuantity?.quantity;\n return typeof q === 'number' ? q : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Daily discrete-average for a quantity (e.g. resting heart rate). Returns\n * null when no sample exists for the day so the backend can tell \"no data\"\n * apart from a real zero.\n */\nasync function readDailyAverage(identifier: string, unit: string, start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return null;\n try {\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['discreteAverage'],\n { unit, filter: { date: { startDate: start, endDate: end } } },\n );\n const q = result?.averageQuantity?.quantity;\n return typeof q === 'number' ? q : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Total minutes asleep for [start, end) from HKCategoryTypeIdentifierSleepAnalysis.\n * Sums sample durations that represent actual sleep (any \"asleep*\" value),\n * excluding inBed/awake. Heavily guarded — sleep-stage enums differ across iOS\n * versions and the kingstinct API surface, so any failure returns null.\n */\nasync function readSleepMinutes(start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n const query = HK?.queryCategorySamples || HK?.queryCategorySamplesAsync || HK?.querySamples;\n if (!query) return null;\n try {\n const samples: any[] = await query.call(\n HK,\n 'HKCategoryTypeIdentifierSleepAnalysis',\n { filter: { date: { startDate: start, endDate: end } } },\n );\n if (!Array.isArray(samples) || samples.length === 0) return null;\n // value semantics: 0 = inBed, 2 = awake; everything else = a sleep stage.\n // Treat anything that isn't explicitly inBed/awake as asleep.\n let ms = 0;\n for (const s of samples) {\n const v = s?.value;\n const isAsleep = v !== 0 && v !== 2; // exclude inBed + awake\n if (!isAsleep) continue;\n const a = new Date(s?.startDate).getTime();\n const b = new Date(s?.endDate).getTime();\n if (Number.isFinite(a) && Number.isFinite(b) && b > a) ms += b - a;\n }\n return ms > 0 ? Math.round(ms / 60000) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Mood-relevant HealthKit extras for today: sleep minutes, resting heart rate,\n * active energy (kcal), exercise minutes. Each is null when unavailable so the\n * backend weights only present fields.\n */\nexport async function collectMoodExtras(): Promise<{\n sleep_min: number | null;\n resting_hr: number | null;\n active_energy_kcal: number | null;\n exercise_min: number | null;\n}> {\n const ok = await ensureInit();\n if (!ok) return { sleep_min: null, resting_hr: null, active_energy_kcal: null, exercise_min: null };\n const start = startOfDay(new Date());\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n // Sleep window: look back from this morning over the past night (~last 18h)\n // so an evening collection still captures the prior night.\n const sleepStart = new Date(start);\n sleepStart.setHours(sleepStart.getHours() - 6); // include after-midnight + prev evening within today's window\n const [sleep_min, resting_hr, energy, exercise] = await Promise.all([\n readSleepMinutes(sleepStart, end),\n readDailyAverage('HKQuantityTypeIdentifierRestingHeartRate', 'count/min', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n readDailySum('HKQuantityTypeIdentifierAppleExerciseTime', 'min', start, end),\n ]);\n return {\n sleep_min,\n resting_hr: resting_hr != null ? Math.round(resting_hr) : null,\n active_energy_kcal: energy ? Math.round(energy) : null,\n exercise_min: exercise ? Math.round(exercise) : null,\n };\n}\n\nasync function readDay(date: Date): Promise<HealthDayMetrics> {\n const start = startOfDay(date);\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n\n const [steps, floors, energy, distance] = await Promise.all([\n readDailySum('HKQuantityTypeIdentifierStepCount', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierFlightsClimbed', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n // Ask for kilometers directly so we don't have to convert from meters.\n readDailySum('HKQuantityTypeIdentifierDistanceWalkingRunning', 'km', start, end),\n ]);\n\n return {\n date: dateKey(start),\n available: true,\n steps: Math.round(steps),\n floors: Math.round(floors),\n active_energy_kcal: Math.round(energy),\n distance_km: Math.round(distance * 100) / 100,\n };\n}\n\n// ─── Public API ─────────────────────────────────────────────\n\n/**\n * A day we could not read: metrics null, availability stated.\n *\n * The distinction this preserves is the whole point — \"no data\" and \"a day\n * with no activity\" look identical once you write a zero, and only one of\n * them is something to tell the user about.\n */\nfunction unavailableDay(date: Date): HealthDayMetrics {\n return {\n date: dateKey(date),\n available: false,\n steps: null,\n floors: null,\n active_energy_kcal: null,\n distance_km: null,\n };\n}\n\nexport async function collectToday(): Promise<HealthDayMetrics> {\n const ok = await ensureInit();\n if (!ok) {\n // HealthKit unavailable or access denied. NOT a day with no activity —\n // reporting zeros here is how \"you have taken 0 steps today\" reached the\n // user as a fact, with a coaching skill ready to act on it.\n return unavailableDay(new Date());\n }\n return readDay(new Date());\n}\n\n/**\n * Last `days` days, newest-first. Index 0 is today.\n * Caller passes 1..30 — we clamp defensively.\n */\nexport async function collectHistory(days: number): Promise<HealthDayMetrics[]> {\n const n = Math.max(1, Math.min(30, Math.floor(days)));\n const ok = await ensureInit();\n if (!ok) return [];\n\n const today = startOfDay(new Date());\n const out: HealthDayMetrics[] = [];\n // Sequential to avoid hammering HealthKit with 30 parallel queries.\n for (let i = 0; i < n; i++) {\n const d = new Date(today);\n d.setDate(d.getDate() - i);\n out.push(await readDay(d));\n }\n return out;\n}\n","/**\n * Background Sync — registers an expo-task-manager task for periodic\n * data sync when the app is in the background, and exposes a one-shot\n * `syncDomains()` for silent-push triggered syncs.\n *\n * iOS minimum background fetch interval is ~15 minutes.\n * Android allows more frequent background execution via WorkManager.\n *\n * Usage:\n * import { registerBackgroundSync, syncDomains } from '@mobile-pa/device-bridge';\n *\n * // Periodic background fetch — call once at app startup, outside any component\n * registerBackgroundSync({\n * relayUrl: 'https://relay.example.com',\n * deviceId: 'device-123',\n * tenant: 'mobile-pa',\n * actorId: 'user-abc',\n * apiKey: 'key-xxx',\n * });\n *\n * // Silent-push triggered sync — from FCM background message handler\n * await syncDomains(config, ['location', 'calendar']);\n */\n\nimport * as TaskManager from 'expo-task-manager';\nimport * as BackgroundFetch from 'expo-background-fetch';\nimport { RelayClient } from './relay-client';\nimport * as caps from './capabilities';\nimport type { Capability } from './types';\n\nexport const BACKGROUND_TASK_NAME = 'DEVICE_BRIDGE_BACKGROUND_SYNC';\n\nexport interface BackgroundSyncConfig {\n relayUrl: string;\n deviceId: string;\n tenant: string;\n actorId?: string;\n apiKey: string;\n}\n\nconst CAPABILITY_FACTORIES: Record<string, () => Capability> = {\n calendar: caps.calendarCapability,\n contacts: caps.contactsCapability,\n location: caps.locationCapability,\n battery: caps.batteryCapability,\n connectivity: caps.connectivityCapability,\n notifications: caps.notificationsCapability,\n health: caps.healthCapability,\n};\n\n/**\n * Register the periodic background-fetch task.\n * Call once at app startup, outside any React component.\n */\nexport async function registerBackgroundSync(\n config: BackgroundSyncConfig\n): Promise<void> {\n TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {\n try {\n const hasNew = await syncDomains(config, ['location', 'battery', 'calendar']);\n return hasNew\n ? BackgroundFetch.BackgroundFetchResult.NewData\n : BackgroundFetch.BackgroundFetchResult.NoData;\n } catch (error) {\n console.error('[DeviceBridge] Background sync failed:', error);\n return BackgroundFetch.BackgroundFetchResult.Failed;\n }\n });\n\n await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {\n minimumInterval: 300, // 5 minutes (iOS will clamp to ~15 min)\n stopOnTerminate: false,\n startOnBoot: true,\n });\n}\n\n/**\n * Unregister the background sync task.\n */\nexport async function unregisterBackgroundSync(): Promise<void> {\n const isRegistered = await TaskManager.isTaskRegisteredAsync(\n BACKGROUND_TASK_NAME\n );\n if (isRegistered) {\n await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);\n }\n}\n\n/**\n * Collect and push the listed domains once. Used by both the periodic\n * background-fetch task and the silent-push handler.\n *\n * Returns true if at least one domain pushed new data.\n */\nexport async function syncDomains(\n config: BackgroundSyncConfig,\n domains: string[]\n): Promise<boolean> {\n const relay = new RelayClient({\n relayUrl: config.relayUrl,\n deviceId: config.deviceId,\n tenant: config.tenant,\n actorId: config.actorId,\n apiKey: config.apiKey,\n timeout: 15_000, // longer than foreground — background tasks are budget-limited but not latency-sensitive\n });\n\n const results = await Promise.allSettled(\n domains.map(d => collectAndPush(relay, d))\n );\n\n return results.some(r => r.status === 'fulfilled' && r.value);\n}\n\n// ─── Internal ───\n\nasync function collectAndPush(\n relay: RelayClient,\n domain: string\n): Promise<boolean> {\n const factory = CAPABILITY_FACTORIES[domain];\n if (!factory) {\n console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);\n return false;\n }\n\n const cap = factory();\n\n if (cap.requiresPermission) {\n const granted = await cap.checkPermission();\n if (!granted) return false;\n }\n\n const data = await cap.collect();\n const result = await relay.pushDomain(domain, data);\n return result.ok;\n}\n","/**\n * Calendar Collector — reads device calendar via expo-calendar.\n * Output format matches device-mock-mcp's calendar.today / calendar.upcoming schemas.\n */\n\nimport * as ExpoCalendar from 'expo-calendar';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoCalendar.Calendar`-style annotations need this.\n// expo-calendar's types are not visible through the package entry under\n// bundler resolution, so derive them from the APIs that return them —\n// the shape then always matches the installed version.\ntype Calendar = Awaited<ReturnType<typeof ExpoCalendar.getCalendarsAsync>>[number];\ntype Event = Awaited<ReturnType<typeof ExpoCalendar.getEventsAsync>>[number];\nimport type { CalendarEvent, CalendarTodayData, CalendarUpcomingData } from '../types';\n\n/**\n * Request calendar permissions.\n * @returns true if granted\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.requestCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if calendar permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.getCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Get all calendar events for today.\n * Matches the device.calendar.today tool output schema.\n */\nexport async function collectToday(): Promise<CalendarTodayData> {\n const now = new Date();\n const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, startOfDay, endOfDay);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => ({\n id: e.id,\n title: e.title || 'Untitled',\n date: startOfDay.toISOString().slice(0, 10),\n start: formatTime(new Date(e.startDate)),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n }));\n\n // Sort by start time\n mapped.sort((a, b) => a.start.localeCompare(b.start));\n\n return {\n ok: true,\n date: startOfDay.toISOString().slice(0, 10),\n day_of_week: now.toLocaleDateString('en-US', { weekday: 'long' }),\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n/**\n * Get upcoming calendar events for the next N days (tomorrow and beyond).\n * Matches the device.calendar.upcoming tool output schema.\n */\nexport async function collectUpcoming(daysAhead: number = 3): Promise<CalendarUpcomingData> {\n const now = new Date();\n const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\n const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead + 1);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, tomorrow, endDate);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => {\n const startDt = new Date(e.startDate);\n return {\n id: e.id,\n title: e.title || 'Untitled',\n date: startDt.toISOString().slice(0, 10),\n start: formatTime(startDt),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n };\n });\n\n // Sort by date then start time\n mapped.sort((a, b) =>\n a.date === b.date ? a.start.localeCompare(b.start) : a.date.localeCompare(b.date)\n );\n\n return {\n ok: true,\n from: tomorrow.toISOString().slice(0, 10),\n days_ahead: daysAhead,\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n// ─── Helpers ───\n\n/**\n * Filter out holiday/subscription calendars — only keep user-owned calendars\n * (local, CalDAV, Exchange, Google). Subscription calendars are read-only\n * holiday feeds that clutter the real schedule.\n */\nfunction getUserCalendarIds(calendars: Calendar[]): string[] {\n const SKIP_SOURCES = ['birthdays', 'other']; // 'other' = subscribed holiday feeds on iOS\n const SKIP_TYPES = ['birthdays', 'other'];\n\n return calendars\n .filter(c => {\n const sourceType = ((c.source as any)?.type || '').toLowerCase();\n const calType = (c.type || '').toLowerCase();\n // Skip subscription/holiday calendars\n if (SKIP_SOURCES.includes(sourceType)) return false;\n if (SKIP_TYPES.includes(calType)) return false;\n return true;\n })\n .map(c => c.id);\n}\n\n/**\n * Extra safety: filter out all-day events with holiday-like patterns\n * in case they slip through from non-subscription calendars.\n */\nfunction filterHolidays(events: Event[]): Event[] {\n const HOLIDAY_PATTERNS = [\n /holiday/i, /observance/i, /eve\\b.*\\btentative\\b/i, /\\btentative\\b/i,\n ];\n\n return events.filter(e => {\n // All-day events spanning exactly one day with \"tentative\" or \"observance\" in notes → holiday\n const isAllDay = (e as any).allDay === true;\n const notes = (e.notes || '').toLowerCase();\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(notes))) return false;\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(e.title || ''))) return false;\n return true;\n });\n}\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n });\n}\n","/**\n * Contacts Collector — reads device contacts via expo-contacts.\n *\n * Two important properties for users with large address books (6000+ contacts):\n *\n * 1. Hash-based change detection. We persist a map of `contactId → contentHash`\n * across launches and on each collect cycle compare the freshly-read hashes\n * to the stored ones. If nothing changed, `collectAll()` returns null and\n * `syncCapability` skips the relay push entirely — no per-tick 1MB upload.\n *\n * 2. Reduced field set. iOS 16+ restricts which contact fields are accessible\n * without explicit user grants. Requesting `Note`/`Relationships` causes\n * `getContactsAsync` to throw \"Failed to serialize contact data:\n * Unauthorized Keys\", failing the entire collect. We stick to the four\n * fields actually used downstream (Name, PhoneNumbers, Emails, Birthday).\n *\n * Output format on push (when there ARE changes) is unchanged from before —\n * a full `Contact[]` — so the relay/MCP layer doesn't have to know about the\n * incremental scheme. The relay simply gets pushed less often.\n */\nimport * as ExpoContacts from 'expo-contacts';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoContacts.Calendar`-style annotations need this.\n// expo-contacts exports no `Date` type; take the shape from Contact itself.\ntype ExpoContact = Awaited<ReturnType<typeof ExpoContacts.getContactsAsync>>['data'][number];\ntype ContactDate = NonNullable<ExpoContact['birthday']>;\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { Contact } from '../types';\n\nconst HASH_STORE_KEY = 'device-bridge.contacts.hashes.v1';\n\n/** Request contacts permissions. */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/** Check if contacts permission is granted. */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Diagnostic counters from the most recent collect — exposed so the Profile\n * debug view can surface \"what changed since last sync\".\n */\nexport interface ContactsCollectStats {\n total: number;\n added: number;\n updated: number;\n removed: number;\n unchanged: number;\n full_sync: boolean;\n}\nexport let lastContactsStats: ContactsCollectStats | null = null;\n\n/**\n * Read all contacts and detect changes vs the previously-stored hash map.\n *\n * Returns:\n * - the full Contact[] when anything changed (or this is the first sync),\n * - null when every contact's hash matches what we pushed last time, signalling\n * the bridge that no relay push is needed this cycle.\n */\nexport async function collectAll(): Promise<Contact[] | null> {\n // Only the four fields we actually surface downstream. Adding more reawakens\n // the iOS \"Unauthorized Keys\" failure unless the user has granted them via\n // a separate flow.\n const result = await ExpoContacts.getContactsAsync({\n fields: [\n ExpoContacts.Fields.Name,\n ExpoContacts.Fields.PhoneNumbers,\n ExpoContacts.Fields.Emails,\n ExpoContacts.Fields.Birthday,\n ],\n sort: ExpoContacts.SortTypes.LastName,\n });\n\n const raw = Array.isArray(result?.data) ? result.data : [];\n\n // Map raw contacts to our schema, computing a stable per-contact hash that's\n // independent of array ordering and resilient to spurious whitespace/case\n // changes in fields we don't actually compare on.\n const contacts: Contact[] = [];\n const currentHashes: Record<string, string> = {};\n for (let i = 0; i < raw.length; i++) {\n const c = raw[i];\n const id = c.id || `c${i}`;\n const mapped: Contact = {\n id,\n name: c.name || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',\n relationship: '', // dropped — was the iOS Unauthorized-Keys trigger\n phone: c.phoneNumbers?.[0]?.number || null,\n email: c.emails?.[0]?.email || null,\n birthday: formatBirthday(c.birthday),\n notes: '', // dropped — Notes is restricted on iOS 16+\n };\n contacts.push(mapped);\n currentHashes[id] = hashContact(mapped);\n }\n\n // Compare against last-pushed hashes.\n let stored: Record<string, string> = {};\n try {\n const blob = await AsyncStorage.getItem(HASH_STORE_KEY);\n if (blob) stored = JSON.parse(blob) as Record<string, string>;\n } catch {\n /* corrupted store — treat as empty, will full-sync */\n }\n const fullSync = Object.keys(stored).length === 0;\n\n let added = 0;\n let updated = 0;\n let unchanged = 0;\n for (const id of Object.keys(currentHashes)) {\n const prev = stored[id];\n if (prev === undefined) added++;\n else if (prev !== currentHashes[id]) updated++;\n else unchanged++;\n }\n let removed = 0;\n for (const id of Object.keys(stored)) {\n if (currentHashes[id] === undefined) removed++;\n }\n\n lastContactsStats = {\n total: contacts.length,\n added,\n updated,\n removed,\n unchanged,\n full_sync: fullSync,\n };\n\n // No changes → tell the bridge to skip the relay push this cycle. Saves\n // a 6000-row payload going up the wire every 5 minutes when nothing moved.\n if (!fullSync && added === 0 && updated === 0 && removed === 0) {\n return null;\n }\n\n // Persist the new hash map AFTER deciding to push. If the push fails we'll\n // re-detect the same changes on the next cycle (push is idempotent enough).\n try {\n await AsyncStorage.setItem(HASH_STORE_KEY, JSON.stringify(currentHashes));\n } catch {\n /* a failed persist just means we'll redo the diff next time — non-fatal */\n }\n\n return contacts;\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Stable string hash (FNV-1a 32-bit, hex). Pure JS, no native dep, fine for\n * change detection. NOT cryptographic — a determined collision attacker could\n * mask a contact change, but that's not in our threat model.\n */\nfunction fnv1a(str: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\nfunction hashContact(c: Contact): string {\n // Concatenate the canonical-cased, trimmed values of every field we surface.\n // Order matters and is fixed; null becomes a single sentinel character so\n // empty-vs-missing differs from \"actual whitespace\" in the hash space.\n const parts = [\n c.id,\n (c.name || '').trim(),\n c.phone || '\\0',\n (c.email || '\\0').toLowerCase(),\n c.birthday || '\\0',\n ];\n return fnv1a(parts.join('\u001f')); // unit separator — won't appear in any field\n}\n\nfunction formatBirthday(birthday: ContactDate | undefined): string | null {\n if (!birthday) return null;\n const { year, month, day } = birthday;\n if (month === undefined || day === undefined) return null;\n const m = String(month + 1).padStart(2, '0');\n const d = String(day).padStart(2, '0');\n\n // No year is the COMMON case on iOS — the contact editor lets you omit it,\n // and most people do. Substituting 1900 made that indistinguishable from a\n // real birth year, and the server dutifully computed an age from it: \"Dana\n // is turning 126 next Tuesday.\"\n //\n // `--MM-DD` is the ISO 8601 form for a recurring month-day with no year, so\n // the absence is stated rather than filled in.\n if (!year) return `--${m}-${d}`;\n\n return `${year}-${m}-${d}`;\n}\n","/**\n * Location Collector — reads device location via expo-location.\n * Output format matches device-mock-mcp's location.current schema.\n */\n\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress } from '@ateam-ai-mobile/plugin-runtime';\n// The SDK's one reverse geocode — the same call native.maps.reverseGeocode makes.\nimport { reverseGeocode } from '../../maps';\nimport type { LocationData } from '../types';\n\n/**\n * Request location permissions (foreground + background).\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status: fg } = await ExpoLocation.requestForegroundPermissionsAsync();\n if (fg !== 'granted') return false;\n\n // Background is optional — skip in Expo Go (crashes without native module)\n try {\n await ExpoLocation.requestBackgroundPermissionsAsync();\n } catch {\n // Expected in Expo Go — foreground is enough for basic functionality\n }\n return fg === 'granted';\n}\n\n/**\n * Check if location permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoLocation.getForegroundPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect current location.\n * Matches the device.location.current tool output schema.\n */\nexport async function collectCurrent(): Promise<LocationData> {\n const location = await ExpoLocation.getCurrentPositionAsync({\n accuracy: ExpoLocation.Accuracy.Balanced,\n });\n\n // Reverse geocode to get address\n let place = 'Current Location';\n let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;\n let locationType = 'unknown';\n\n try {\n const geocoded = await reverseGeocode({\n latitude: location.coords.latitude,\n longitude: location.coords.longitude,\n });\n\n if (geocoded) {\n place = geocoded.name || geocoded.street || place;\n address = [\n geocoded.streetNumber,\n geocoded.street,\n geocoded.city,\n geocoded.region,\n ]\n .filter(Boolean)\n .join(', ') || address;\n\n // Attempt to classify location type\n locationType = classifyLocation(geocoded);\n }\n } catch {\n // Reverse geocoding can fail — use coordinates only\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n location: {\n place,\n address,\n lat: location.coords.latitude,\n lng: location.coords.longitude,\n type: locationType,\n },\n };\n}\n\n// ─── Plugin-facing position API ───\n// `collectCurrent` above answers the device-context question (\"where am I, in\n// words\") and reverse-geocodes. A plugin tool usually wants the raw fix and\n// nothing else, so these stay separate: no geocoding round trip, no address.\n\nexport interface Position {\n latitude: number;\n longitude: number;\n altitude?: number;\n accuracy?: number;\n timestamp: number;\n /**\n * Metres per second. `undefined` = the OS gave no estimate.\n *\n * iOS reports -1 rather than omitting it, and -1 m/s passed through as a\n * speed would read as \"moving backwards\"; it is normalised away here so no\n * caller has to know the platform's sentinel.\n */\n speed?: number;\n /**\n * COURSE OVER GROUND in degrees — the direction the user is TRAVELLING.\n *\n * Deliberately not called `heading`: this and the compass answer different\n * questions and disagree constantly. Walking backwards down a corridor gives\n * a course of south and a compass heading of north; a phone held sideways\n * while walking forward gives the same course and a heading 90° off.\n * \"Turn right\" needs the compass. \"You are heading north on Main Street\"\n * needs this. The old code assigned one to the other, which is only ever\n * right by accident.\n *\n * `undefined` when not moving or not estimated — iOS reports -1 for both.\n */\n course?: number;\n}\n\n/** Ensure foreground permission, asking once if we do not have it yet. */\nasync function ensurePermission(): Promise<void> {\n if (await hasPermission()) return;\n const granted = await requestPermission();\n if (!granted) {\n // Callers surface this verbatim to the user (a voice assistant reads it\n // out), so it has to say what the user can do about it.\n throw new Error(\n 'Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services.',\n );\n }\n}\n\n/** One position fix. `accuracy: \"high\"` costs battery and time — default low. */\nexport async function getPosition(\n opts: { accuracy?: 'low' | 'high' } = {},\n): Promise<Position> {\n await ensurePermission();\n const fix = await ExpoLocation.getCurrentPositionAsync({\n accuracy:\n opts.accuracy === 'high'\n ? ExpoLocation.Accuracy.High\n : ExpoLocation.Accuracy.Balanced,\n });\n return toPosition(fix);\n}\n\n/**\n * One place that turns an expo fix into a `Position`.\n *\n * It exists because the sentinel handling has to be identical on the one-shot\n * and the streamed path. It was not: `watchPosition` passed lat/lon only, so\n * the semantic Spatial domain could never report speed or course no matter\n * what the OS supplied, and reported `null` as if the phone had no GPS.\n */\nfunction toPosition(fix: any): Position {\n return {\n latitude: fix.coords.latitude,\n longitude: fix.coords.longitude,\n altitude: fix.coords.altitude ?? undefined,\n accuracy: nonNegative(fix.coords.accuracy),\n timestamp: fix.timestamp ?? Date.now(),\n speed: nonNegative(fix.coords.speed),\n course: nonNegative(fix.coords.heading),\n };\n}\n\n/**\n * iOS uses -1 for \"no estimate\" on accuracy, speed and course alike. Dropping\n * the field says \"not measured\"; passing -1 through says \"measured, and the\n * answer is minus one\", which a model will happily repeat.\n */\nfunction nonNegative(v: unknown): number | undefined {\n return typeof v === 'number' && v >= 0 ? v : undefined;\n}\n\nconst WATCH_TIER = {\n navigation: ExpoLocation.Accuracy.BestForNavigation,\n high: ExpoLocation.Accuracy.High,\n balanced: ExpoLocation.Accuracy.Balanced,\n low: ExpoLocation.Accuracy.Low,\n} as const;\n\n/**\n * Stream position updates. Returns an unsubscribe function — call it, or the\n * GPS keeps running and drains the battery.\n *\n * `accuracy` picks the GPS tier. The default stays Balanced (≈100 m on iOS),\n * which is right for \"which city block\" and wrong for \"turn at the next corner\":\n * a navigation session asks for BestForNavigation and pays the battery for it.\n */\nexport function watchPosition(\n cb: (pos: Position) => void,\n opts?: { accuracy?: keyof typeof WATCH_TIER },\n): () => void {\n let subscription: { remove(): void } | null = null;\n let cancelled = false;\n\n (async () => {\n try {\n await ensurePermission();\n if (cancelled) return;\n subscription = await ExpoLocation.watchPositionAsync(\n { accuracy: WATCH_TIER[opts?.accuracy ?? 'balanced'] },\n fix => cb(toPosition(fix)),\n );\n // Unsubscribed while we were still asking for permission.\n if (cancelled) { subscription?.remove(); subscription = null; }\n } catch (err: any) {\n console.warn('[location] watchPosition failed:', err?.message || String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n subscription?.remove();\n subscription = null;\n };\n}\n\n// ─── Helpers ───\n\nfunction classifyLocation(geocoded: MapsAddress): string {\n // Simple heuristic — can be enhanced with user-labeled places from memory\n const name = (geocoded.name || '').toLowerCase();\n const street = (geocoded.street || '').toLowerCase();\n\n if (name.includes('home') || name.includes('residence')) return 'home';\n if (name.includes('office') || name.includes('workplace') || name.includes('tech park')) return 'work';\n if (name.includes('school') || name.includes('elementary') || name.includes('university')) return 'school';\n if (name.includes('gym') || name.includes('fitness')) return 'fitness';\n if (name.includes('hospital') || name.includes('clinic') || name.includes('dental')) return 'medical';\n\n return 'other';\n}\n","/**\n * Battery Collector — reads battery status via expo-battery.\n * Output format matches device-mock-mcp's device.battery schema.\n */\n\nimport * as ExpoBattery from 'expo-battery';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoBattery.Calendar`-style annotations need this.\n// BatteryState is not re-exported from the package entry — derive it from\n// the API that returns it so we never hand-maintain a copy of the enum.\ntype BatteryState = Awaited<ReturnType<typeof ExpoBattery.getBatteryStateAsync>>;\nimport type { BatteryData } from '../types';\n\n// No permission needed for battery\n\n/**\n * Collect current battery status.\n * Matches the device.battery tool output schema.\n */\nexport async function collectBattery(): Promise<BatteryData> {\n const level = await ExpoBattery.getBatteryLevelAsync();\n const state = await ExpoBattery.getBatteryStateAsync();\n const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n battery: {\n level: Math.round(level * 100), // expo-battery returns 0-1\n state: mapBatteryState(state),\n low_power_mode: lowPower,\n },\n };\n}\n\n/**\n * Subscribe to OS-level battery changes (level + charging state).\n *\n * iOS suspends JS timers when the app is backgrounded, so a pure setInterval\n * sync drifts: the relay ends up stuck on whatever level we last sampled\n * before the app went to sleep (commonly 100% from an overnight charge).\n * expo-battery's listeners fire from the native side and survive that gap,\n * so we get a fresh push as soon as the OS notices a change.\n *\n * Level changes are throttled to >=5pp deltas — without that, iOS emits a\n * level event for every 1% tick which would spam the relay.\n */\nexport function subscribeToBattery(callback: () => void): () => void {\n let lastLevel: number | null = null;\n\n const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {\n const pct = Math.round(batteryLevel * 100);\n if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {\n lastLevel = pct;\n callback();\n }\n });\n\n const stateSub = ExpoBattery.addBatteryStateListener(() => {\n callback();\n });\n\n return () => {\n levelSub?.remove?.();\n stateSub?.remove?.();\n };\n}\n\n// ─── Helpers ───\n\nfunction mapBatteryState(\n state: BatteryState\n): 'charging' | 'unplugged' | 'full' {\n switch (state) {\n case ExpoBattery.BatteryState.CHARGING:\n return 'charging';\n case ExpoBattery.BatteryState.FULL:\n return 'full';\n case ExpoBattery.BatteryState.UNPLUGGED:\n default:\n return 'unplugged';\n }\n}\n","/**\n * Connectivity Collector — reads network state via expo-network.\n * Output format matches device-mock-mcp's device.connectivity schema.\n */\n\nimport * as ExpoNetwork from 'expo-network';\nimport type { ConnectivityData } from '../types';\n\n// No special permission needed for network state\n\n/**\n * Collect current connectivity status.\n * Matches the device.connectivity tool output schema.\n */\nexport async function collectConnectivity(): Promise<ConnectivityData> {\n const networkState = await ExpoNetwork.getNetworkStateAsync();\n\n let type: 'wifi' | 'cellular' | 'none' = 'none';\n let wifiName: string | undefined;\n let cellularType: string | undefined;\n\n if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {\n type = 'wifi';\n // Wi-Fi SSID requires additional permissions on newer OS versions\n // and may not always be available\n try {\n wifiName = await getWifiName();\n } catch {\n wifiName = undefined;\n }\n } else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {\n type = 'cellular';\n cellularType = getCellularGeneration();\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n connectivity: {\n type,\n is_connected: networkState.isConnected ?? false,\n ...(wifiName && { wifi_name: wifiName }),\n ...(cellularType && { cellular_type: cellularType }),\n },\n };\n}\n\n// ─── Helpers ───\n\nasync function getWifiName(): Promise<string | undefined> {\n // expo-network doesn't expose SSID directly on all platforms\n // This would need a native module or expo-wifi in production\n return undefined;\n}\n\nfunction getCellularGeneration(): string {\n // expo-network doesn't expose cellular generation\n // In production, use a native module for this\n return 'LTE';\n}\n","/**\n * Notifications Collector — reads notification state via expo-notifications.\n * Output format matches device-mock-mcp's notifications.list schema.\n *\n * Note: On iOS/Android, expo-notifications only gives access to notifications\n * that YOUR app has posted or received. System-wide notification access\n * requires platform-specific APIs (NotificationListenerService on Android,\n * UNUserNotificationCenter on iOS with app extensions).\n *\n * For the PA, the relay approach means we capture notifications delivered\n * TO our app (from the PA system), not FROM other apps.\n * Other-app notifications would need a native module or accessibility service.\n */\n\nimport * as ExpoNotifications from 'expo-notifications';\nimport type { Notification, NotificationsData } from '../types';\n\n/**\n * Request notification permissions.\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if notification permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect delivered notifications from the notification tray.\n * Only captures notifications delivered to THIS app.\n * Matches the device.notifications.list tool output schema.\n */\nexport async function collectNotifications(): Promise<NotificationsData> {\n const presented = await ExpoNotifications.getPresentedNotificationsAsync();\n\n const notifications: Notification[] = presented.map((n, i) => ({\n id: n.request.identifier || `n${i}`,\n app: (n.request.content.data?.app as string) || 'A-Team',\n title: n.request.content.title || '',\n body: n.request.content.body || '',\n timestamp: n.date ? new Date(n.date).toISOString() : new Date().toISOString(),\n sender: (n.request.content.data?.sender as string) || null,\n priority: mapPriority(n.request.content.data?.priority as string),\n read: false, // Presented notifications are unread by definition\n }));\n\n const unread = notifications.filter(n => !n.read).length;\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n total: notifications.length,\n unread,\n notifications,\n };\n}\n\n// ─── Helpers ───\n\nfunction mapPriority(p: string | undefined): 'high' | 'medium' | 'low' {\n if (p === 'high' || p === 'max') return 'high';\n if (p === 'low' || p === 'min') return 'low';\n return 'medium';\n}\n","/**\n * App Engagement Collector — our OWN usage analytics (no OS permission, fully\n * available on every platform). Feeds the mood summary's app_sessions +\n * app_active_min, which iOS otherwise blocks for third-party apps.\n *\n * app_sessions — count of foreground sessions today (a background→active\n * transition, or a cold start, starts a session).\n * app_active_min — total minutes the app was in the foreground today.\n *\n * Counters are kept per local day and persisted to AsyncStorage so they\n * survive relaunches. start() must be called once at app boot to attach the\n * AppState listener; getToday() returns the current day's tallies.\n */\nimport { AppState, type AppStateStatus } from 'react-native';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\nconst KEY = 'ateam.appEngagement.v1';\n\ninterface DayTally {\n date: string; // local YYYY-MM-DD\n sessions: number;\n activeMs: number;\n}\n\nlet _tally: DayTally | null = null;\nlet _foregroundSince: number | null = null;\nlet _started = false;\nlet _sub: { remove: () => void } | null = null;\n\nfunction todayKey(): string {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n\nfunction fresh(): DayTally {\n return { date: todayKey(), sessions: 0, activeMs: 0 };\n}\n\nasync function load(): Promise<void> {\n try {\n const raw = await AsyncStorage.getItem(KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as DayTally;\n _tally = parsed?.date === todayKey() ? parsed : fresh();\n } else {\n _tally = fresh();\n }\n } catch {\n _tally = fresh();\n }\n}\n\nfunction persist(): void {\n if (_tally) AsyncStorage.setItem(KEY, JSON.stringify(_tally)).catch(() => {});\n}\n\nfunction rollIfNewDay(): void {\n if (!_tally || _tally.date !== todayKey()) {\n _tally = fresh();\n persist();\n }\n}\n\n/** Fold the time spent foregrounded since the last checkpoint into activeMs. */\nfunction flushForeground(): void {\n if (_foregroundSince == null || !_tally) return;\n const now = Date.now();\n const delta = now - _foregroundSince;\n if (delta > 0) _tally.activeMs += delta;\n _foregroundSince = now;\n}\n\nfunction onChange(state: AppStateStatus): void {\n rollIfNewDay();\n if (state === 'active') {\n // New foreground session.\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n } else {\n // Leaving foreground — bank the elapsed active time.\n flushForeground();\n _foregroundSince = null;\n persist();\n }\n}\n\n/** Attach the AppState listener. Idempotent; call once at app boot. */\nexport async function start(): Promise<void> {\n if (_started) return;\n _started = true;\n await load();\n // Count the launch itself as the first active session.\n rollIfNewDay();\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n _sub = AppState.addEventListener('change', onChange);\n}\n\nexport function stop(): void {\n flushForeground();\n persist();\n _sub?.remove();\n _sub = null;\n _started = false;\n}\n\n/** Current day's tallies for the mood summary. */\nexport async function getToday(): Promise<{ app_sessions: number | null; app_active_min: number | null }> {\n if (!_tally) await load();\n rollIfNewDay();\n flushForeground(); // include the in-progress foreground stretch\n persist();\n if (!_tally) return { app_sessions: null, app_active_min: null };\n return {\n app_sessions: _tally.sessions,\n app_active_min: Math.round(_tally.activeMs / 60000),\n };\n}\n","/**\n * Camera capability — permission only, NO collector.\n *\n * Unlike calendar/contacts/health, the camera is a LIVE capability: frames are\n * used in the moment (deviceState.visual, a widget's camera preview) and are\n * analysed on-device — never collected to the relay. So this module carries only\n * the OS-permission check/request that the capability toggle drives; there is no\n * `collect`. The user's grant (this toggle) is what gates access, on the same\n * path as location/contacts.\n */\nimport { Camera } from 'expo-camera';\n\nexport async function hasPermission(): Promise<boolean> {\n try {\n return (await Camera.getCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n\nexport async function requestPermission(): Promise<boolean> {\n try {\n return (await Camera.requestCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n","/**\n * Device-bridge: Biometrics (Face ID / Touch ID / fingerprint)\n *\n * Used by solutions that need a biometric gate before sensitive actions\n * (e.g. \"unlock payment\", \"confirm financial transfer\", \"reveal stored\n * credentials\"). Also useful for protecting session cookies at rest.\n *\n * Wraps expo-local-authentication.\n */\n\nimport * as LocalAuthentication from 'expo-local-authentication';\n\nexport type BiometricType = 'fingerprint' | 'face' | 'iris' | 'none';\n\nexport interface BiometricCapabilities {\n hasHardware: boolean;\n isEnrolled: boolean;\n /**\n * Never contains 'none' — the mapper filters it out, since 'none' means\n * \"the OS reported an auth type we do not recognise\" and is not a\n * biometric a caller can offer. Typed as the guarantee the code makes.\n */\n supportedTypes: Exclude<BiometricType, 'none'>[];\n}\n\n/** What can the device do right now? */\nexport async function capabilities(): Promise<BiometricCapabilities> {\n const [hasHardware, isEnrolled, types] = await Promise.all([\n LocalAuthentication.hasHardwareAsync(),\n LocalAuthentication.isEnrolledAsync(),\n LocalAuthentication.supportedAuthenticationTypesAsync(),\n ]);\n const supportedTypes = types.map((t) => {\n if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return 'fingerprint';\n if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return 'face';\n if (t === LocalAuthentication.AuthenticationType.IRIS) return 'iris';\n return 'none';\n }).filter((t) => t !== 'none') as Exclude<BiometricType, 'none'>[];\n return { hasHardware, isEnrolled, supportedTypes };\n}\n\nexport interface AuthenticateOptions {\n /** User-visible reason shown in the system dialog. */\n reason: string;\n /** Label on the \"cancel\" button (iOS only; Android has system default). */\n cancelLabel?: string;\n /** If biometrics aren't enrolled, fall back to device passcode. Default true. */\n fallbackToPasscode?: boolean;\n}\n\n/** Prompt the user for biometric auth. Resolves { success } or { success:false, error }. */\nexport async function authenticate(opts: AuthenticateOptions): Promise<{ success: boolean; error?: string }> {\n try {\n const res = await LocalAuthentication.authenticateAsync({\n promptMessage: opts.reason,\n cancelLabel: opts.cancelLabel,\n disableDeviceFallback: opts.fallbackToPasscode === false,\n });\n if (res.success) return { success: true };\n return { success: false, error: (res as any).error || 'unknown' };\n } catch (e: any) {\n return { success: false, error: String(e?.message || e) };\n }\n}\n","/**\n * Device-bridge: Sharing (native share sheet)\n *\n * Wraps expo-sharing. Lets solutions hand content off to the OS share sheet\n * — any installed app can receive it (Messages, WhatsApp, Mail, Files, etc.).\n *\n * Typical use: \"Share this article / calendar invite / photo with a contact.\"\n */\n\nimport * as Sharing from 'expo-sharing';\n\nexport interface ShareOptions {\n /** MIME type, e.g. 'application/pdf'. Optional. */\n mimeType?: string;\n /** iOS UTI, e.g. 'public.image'. Optional. */\n UTI?: string;\n /** Dialog title (Android only). */\n dialogTitle?: string;\n}\n\n/** Share a local file URI. */\nexport async function shareFile(fileUri: string, opts: ShareOptions = {}): Promise<{ ok: boolean; error?: string }> {\n try {\n const available = await Sharing.isAvailableAsync();\n if (!available) return { ok: false, error: 'sharing_unavailable' };\n await Sharing.shareAsync(fileUri, opts);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n\nexport async function isAvailable(): Promise<boolean> {\n return Sharing.isAvailableAsync();\n}\n","/**\n * Device-bridge: Scanner (QR / barcode)\n *\n * Wraps expo-camera (which replaced the deprecated expo-barcode-scanner\n * in Expo SDK 50+). The CameraView component has built-in barcode scanning.\n *\n * Use cases:\n * - Wi-Fi QR handoff\n * - OAuth / pairing codes\n * - Payment QR codes\n * - Product lookup\n *\n * Plugins mount <CameraView onBarcodeScanned={...} /> directly. This module\n * exposes the permission helper + a result normalizer so plugins don't need\n * to import expo-camera directly.\n */\n\nimport { CameraView, useCameraPermissions, type BarcodeScanningResult } from 'expo-camera';\n\nexport interface ScanResult {\n type: string; // e.g. 'qr', 'ean13', 'code128'\n data: string; // decoded content\n}\n\n/** Request camera permission if not already granted. */\nexport async function requestPermissions(): Promise<{ granted: boolean; status: string }> {\n // useCameraPermissions is a hook; for the non-hook flow plugins should use\n // Camera.requestCameraPermissionsAsync. But it's easier to expose the hook\n // helper — plugins typically use the hook form inside their Component.\n // For service-style callers, fall back to the imperative API:\n const { Camera } = await import('expo-camera');\n const { status } = await Camera.requestCameraPermissionsAsync();\n return { granted: status === 'granted', status };\n}\n\n/** Normalize a CameraView barcode result to a stable shape. */\nexport function normalizeResult(r: BarcodeScanningResult): ScanResult {\n return { type: r.type, data: r.data };\n}\n\n/** Re-export the component + hook so plugins consume them via device-bridge. */\nexport { CameraView, useCameraPermissions };\n","/**\n * @ateam-ai-mobile/expo/oauth — the native OAuth provider for the relocated\n * auth screens (LoginScreen / JoinScreen / OpenJoinScreen in @ateam-ai-mobile/ui).\n *\n * This is the ONLY place @react-native-google-signin + expo-apple-authentication\n * are imported. The `ui` package stays native-free: its screens call\n * getOAuthProvider() for the id_token acquisition + the real Apple HIG button.\n *\n * Deliberately a SUBPATH (`@ateam-ai-mobile/expo/oauth`), not part of the main\n * entry — same rule as `/bridge` and `/device`: a host that doesn't install\n * these native modules must still be able to build @ateam-ai-mobile/expo.\n *\n * Behaviour is ported verbatim from the app's old login.tsx so the iPhone\n * parity gate sees no change:\n * - Google: configure({ webClientId, iosClientId }) → hasPlayServices →\n * signIn → normalize the v15-vs-older id_token shape. A missing iosClientId\n * silently falls back to a web flow (unchanged); the caller gates on\n * webClientId, so we never reach here without one for the login path.\n * - a user-cancel (SIGN_IN_CANCELLED / IN_PROGRESS / ERR_REQUEST_CANCELED)\n * resolves to null — the screen treats null as \"no error\", never a red flash.\n */\nimport React from 'react';\nimport { Platform } from 'react-native';\nimport {\n GoogleSignin,\n statusCodes,\n type ConfigureParams,\n} from '@react-native-google-signin/google-signin';\nimport * as AppleAuthentication from 'expo-apple-authentication';\nimport type { OAuthProvider, OAuthGoogleConfig } from '@ateam-ai-mobile/ui';\n\nconst NOOP = () => {};\n\nexport interface ExpoOAuthConfig {\n /**\n * The host's bundled Google client ids, used by the join / open-join screens\n * that carry no tenant config (they pass no cfg to signInWithGoogle). The\n * login screen passes the tenant's configured ids and overrides these.\n */\n googleWebClientId?: string;\n googleIosClientId?: string;\n}\n\n/**\n * Build the native OAuth provider. Wire it once at startup:\n * setOAuthProvider(createExpoOAuthProvider({ googleWebClientId, googleIosClientId }));\n */\nexport function createExpoOAuthProvider(host: ExpoOAuthConfig = {}): OAuthProvider {\n return {\n async signInWithGoogle(cfg?: OAuthGoogleConfig): Promise<string | null> {\n const webClientId = cfg?.webClientId || host.googleWebClientId || '';\n const iosClientId = cfg?.iosClientId || host.googleIosClientId || '';\n // configure is idempotent + cheap; do it at sign-in time so the tenant's\n // ids are always current (replaces login.tsx's configure effect).\n const params: ConfigureParams = { webClientId };\n if (iosClientId) (params as any).iosClientId = iosClientId;\n GoogleSignin.configure(params);\n try {\n // android-only; no-op on iOS. Called defensively, as before.\n await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });\n const result: any = await GoogleSignin.signIn();\n // v15+ returns { data: { idToken } }; older returns { idToken }.\n return result?.data?.idToken || result?.idToken || null;\n } catch (err: any) {\n const code = err?.code;\n if (code === statusCodes.SIGN_IN_CANCELLED || code === statusCodes.IN_PROGRESS) {\n return null;\n }\n throw err;\n }\n },\n\n async signInWithApple(): Promise<string | null> {\n try {\n const credential = await AppleAuthentication.signInAsync({\n requestedScopes: [\n AppleAuthentication.AppleAuthenticationScope.FULL_NAME,\n AppleAuthentication.AppleAuthenticationScope.EMAIL,\n ],\n });\n return credential.identityToken || null;\n } catch (err: any) {\n // ERR_REQUEST_CANCELED = the user dismissed the sheet.\n if (err?.code === 'ERR_REQUEST_CANCELED') return null;\n throw err;\n }\n },\n\n async isAppleAvailable(): Promise<boolean> {\n if (Platform.OS !== 'ios') return false;\n try {\n return await AppleAuthentication.isAvailableAsync();\n } catch {\n return false;\n }\n },\n\n renderAppleButton({ onPress, disabled, style }): React.ReactNode {\n // Apple's HIG requires their own styled button when Apple Sign-In sits\n // alongside other providers. Same chrome the app shipped: CONTINUE /\n // BLACK / fully-rounded. Layout (width/height) comes from the screen.\n return (\n <AppleAuthentication.AppleAuthenticationButton\n buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}\n buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}\n cornerRadius={999}\n style={style as any}\n onPress={disabled ? NOOP : onPress}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["CookieManager","isSupported","NfcManager","import_core","import_plugin_runtime","import_ui","import_async_storage","import_plugin_runtime","AsyncStorage","FileSystem","import_async_storage","AsyncStorage","sub","fixAt","latest","nonNegative","point","latest","err","s","import_react_native","text","AsyncStorage","Haptics","ExpoLocation","import_async_storage","import_react_native","import_expo_modules_core","ExpoLocation","refuseArgument","native","isSupported","import_react_native","import_react_native","ExpoCalendar","import_async_storage","ExpoLocation","ExpoBattery","import_react_native","import_async_storage","import_expo_camera","import_expo_camera","Camera","ensurePermission","AsyncStorage","sub","isSupported","point","import_react_native","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/device-bridge/cookies.ts","../src/device-bridge/nfc.ts","../src/bootstrap.ts","../src/audio/index.ts","../src/index.ts","../src/haptics.ts","../src/bundleStorage.ts","../src/attachmentCache.ts","../src/device-state/domains/deviceContext.ts","../src/device-state/quality.ts","../src/device-state/events.ts","../src/device-state/domains/connectivity.ts","../src/device-state/domains/capability.ts","../src/device-state/domains/interaction.ts","../src/device-state/headingSource.ts","../src/device-state/turnDetector.ts","../src/geo.ts","../src/device-state/spatialEvents.ts","../src/device-state/domains/spatial.ts","../src/device-state/visualEngine.ts","../src/device-state/domains/visual.ts","../src/device-state/activityEngine.ts","../src/device-state/activityClassifier.ts","../src/device-state/domains/activity.ts","../src/device-state/index.ts","../src/camera/CameraTaskHost.tsx","../src/camera/cameraTasks.ts","../src/device/createDeviceProvider.ts","../src/vision/index.ts","../src/maps/index.ts","../src/args.ts","../src/maps/normalize.ts","../src/device-bridge/actions/messaging.ts","../src/device-bridge/actions/calendar-actions.ts","../src/device-bridge/actions/navigation.ts","../src/device-bridge/collectors/health.ts","../src/device-bridge/background.ts","../src/device-bridge/collectors/calendar.ts","../src/device-bridge/collectors/contacts.ts","../src/device-bridge/collectors/location.ts","../src/device-bridge/collectors/battery.ts","../src/device-bridge/collectors/connectivity.ts","../src/device-bridge/collectors/notifications.ts","../src/device-bridge/collectors/appEngagement.ts","../src/device-bridge/collectors/camera.ts","../src/device-bridge/biometrics.ts","../src/device-bridge/sharing.ts","../src/device-bridge/scanner.ts","../src/oauth/expoOAuth.tsx"],"sourcesContent":["/**\n * Device-bridge: Cookies API\n *\n * Reads the platform's shared cookie jar (NSHTTPCookieStorage on iOS,\n * CookieManager on Android) so upper layers (plugin-sdk → RN plugins) can\n * extract session cookies captured during a native WebView login flow.\n *\n * Wraps `@react-native-cookies/cookies` so upper layers never import the\n * native module directly. If we swap implementations, only this file changes.\n *\n * Why device-bridge owns this: cookies are a device-level capability (tied\n * to the platform's shared HTTP stack). Plugins should consume a clean\n * abstraction; the native wiring lives here.\n */\n\nimport CookieManager from '@react-native-cookies/cookies';\n\nexport interface DeviceCookie {\n name: string;\n value: string;\n domain?: string;\n path?: string;\n /** Unix seconds. Omitted for session cookies. */\n expires?: number;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n}\n\n/**\n * Get every cookie attached to the URL's effective domain.\n * Set `includeHttpOnly` true to include HttpOnly cookies (required for\n * LinkedIn/Google session tokens).\n */\nexport async function get(url: string, includeHttpOnly: boolean = true): Promise<DeviceCookie[]> {\n const raw = (await CookieManager.get(url, includeHttpOnly)) as Record<string, any>;\n const out: DeviceCookie[] = [];\n for (const [name, c] of Object.entries(raw || {})) {\n if (!c || typeof c !== 'object') continue;\n const value = typeof c.value === 'string' ? c.value : String(c.value ?? '');\n const cookie: DeviceCookie = { name, value };\n if (c.domain) cookie.domain = String(c.domain);\n if (c.path) cookie.path = String(c.path);\n if (c.expires) {\n const t = Date.parse(c.expires);\n if (!Number.isNaN(t)) cookie.expires = Math.floor(t / 1000);\n }\n if (typeof c.httpOnly === 'boolean') cookie.httpOnly = c.httpOnly;\n if (typeof c.secure === 'boolean') cookie.secure = c.secure;\n if (c.sameSite === 'Lax' || c.sameSite === 'Strict' || c.sameSite === 'None') {\n cookie.sameSite = c.sameSite;\n }\n out.push(cookie);\n }\n return out;\n}\n\n/**\n * Wipe the shared cookie jar. Cross-platform scoped clear isn't supported\n * uniformly; `clearAll` is the safe operation on both iOS and Android.\n */\nexport async function clearAll(): Promise<{ ok: boolean; error?: string }> {\n try {\n await CookieManager.clearAll(true);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n\n/**\n * Set a cookie programmatically. Useful when replaying a saved session on\n * the device's shared jar before opening a WebView.\n */\nexport async function set(url: string, cookie: DeviceCookie): Promise<{ ok: boolean; error?: string }> {\n try {\n await CookieManager.set(url, {\n name: cookie.name,\n value: cookie.value,\n domain: cookie.domain,\n path: cookie.path || '/',\n httpOnly: cookie.httpOnly,\n secure: cookie.secure,\n } as any, true);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n","/**\n * Device-bridge: NFC\n *\n * Wraps react-native-nfc-manager for:\n * - Reading NFC tags (NDEF, Mifare)\n * - Writing to writable tags\n * - App-triggered tag discovery\n *\n * Use cases: physical-world triggers, inventory lookup, hardware keys,\n * loyalty cards, transit top-up, pairing.\n *\n * iOS: requires \"Near Field Communication Tag Reading\" entitlement in the\n * app's provisioning profile. Android: requires NFC permission in manifest.\n */\n\nimport NfcManager, { NfcTech, Ndef } from 'react-native-nfc-manager';\n\nlet _initialized = false;\n\nasync function ensureInit(): Promise<boolean> {\n if (_initialized) return true;\n try {\n const supported = await NfcManager.isSupported();\n if (!supported) return false;\n await NfcManager.start();\n _initialized = true;\n return true;\n } catch (e) {\n return false;\n }\n}\n\nexport async function isSupported(): Promise<boolean> {\n try { return await NfcManager.isSupported(); } catch { return false; }\n}\n\nexport async function isEnabled(): Promise<boolean> {\n try { return await NfcManager.isEnabled(); } catch { return false; }\n}\n\nexport interface NdefRecord {\n type: string;\n payload: string;\n}\n\nexport interface TagReadResult {\n ok: boolean;\n tagId?: string;\n records?: NdefRecord[];\n error?: string;\n}\n\n/** Wait for the next tag to be scanned, read NDEF records, release, and return. */\nexport async function readOnce(timeoutMs: number = 30_000): Promise<TagReadResult> {\n const ready = await ensureInit();\n if (!ready) return { ok: false, error: 'nfc_unsupported' };\n try {\n const tag = await Promise.race([\n NfcManager.requestTechnology(NfcTech.Ndef).then(() => NfcManager.getTag()),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),\n ]);\n if (!tag) {\n try { await NfcManager.cancelTechnologyRequest(); } catch {}\n return { ok: false, error: 'timeout' };\n }\n const records: NdefRecord[] = [];\n if ((tag as any).ndefMessage) {\n for (const r of (tag as any).ndefMessage) {\n try {\n const type = String.fromCharCode(...(r.type || []));\n const payload = Ndef.text.decodePayload(r.payload || []);\n records.push({ type, payload: String(payload) });\n } catch {\n // skip records we can't decode\n }\n }\n }\n const tagId = (tag as any).id ? Array.from((tag as any).id).map((b: any) => b.toString(16).padStart(2, '0')).join('') : undefined;\n return { ok: true, tagId, records };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n } finally {\n try { await NfcManager.cancelTechnologyRequest(); } catch {}\n }\n}\n","/**\n * bootstrapExpoAdas — the single, self-contained SDK bootstrap (R10).\n *\n * The final high-level entry point of the \"fat SDK / thin shell\" arc: it folds\n * the ~15 hand-wired seams the host used to assemble in its own adas.ts into ONE\n * call, so the shell stops manually assembling platform providers/collectors/\n * plugins. Internally it initializes, in the order the migrated code established:\n * the Expo providers (haptics/bundle cache/clipboard/attachment cache + the\n * device-primitive provider), the host-bundled native plugins, the plugin-WebView\n * allow-list, native OAuth, the ACS reachability reporter, the L3 DeviceState\n * engine (with the SDK's OWN collectors — the shell injects none), the plugin/\n * UI-action navigator, and finally the core config+token bridge (initAdas).\n *\n * The API is DERIVED from the host's migrated adas.ts, not invented. Everything\n * the host still owns is passed in as a small values-and-getters bag: endpoint\n * getters (the host owns the dev/prod switch), the identity/token bridge, its\n * branded Google client id, its host-bundled plugin resolvers, the native\n * WebView module from its build, and its router callback.\n *\n * A SUBPATH (`@ateam-ai-mobile/expo/bootstrap`), like /device and /oauth: it\n * statically pulls the optional expo native modules (via createDeviceProvider /\n * the device-state collectors / the OAuth provider), so it must not sit on the\n * package's main entry, which a host without those modules still has to build.\n *\n * The two physical reasoning paths stay DISTINCT and are wired as SEPARATE\n * statements — never a merged bridge: LOCAL (the local tool registry + device\n * state the plugin/voice runtime reads) and SERVER (the relay allow-list here;\n * the DeviceBridge relay runtime itself is started by the host lifecycle, out of\n * this module-load bootstrap's scope).\n */\nimport { initAdas, setAcsReachabilityReporter, type TokenProvider, type AdasUser, type AdasSession } from '@ateam-ai-mobile/core';\nimport { setDeviceStateProvider, registerNativePlugin, registerPlatformDeviceTools, type RegisteredPlugin } from '@ateam-ai-mobile/plugin-runtime';\nimport { setOAuthProvider, setUIActionNavigator, setPluginWebViewExtraHosts, setCallAudioProvider, type UIActionRoute } from '@ateam-ai-mobile/ui';\nimport { createCallAudioProvider } from './audio';\n\nimport { setupExpoProviders, type ExpoAttachmentCache } from './index';\nimport { createDeviceProvider } from './device/createDeviceProvider';\nimport { createDeviceState } from './device-state';\nimport { createExpoOAuthProvider } from './oauth/expoOAuth';\nimport * as deviceConnectivity from './device-bridge/collectors/connectivity';\nimport * as deviceLocation from './device-bridge/collectors/location';\n\n/** A host-bundled native plugin: one lazy resolver registered under one or more ids. */\nexport interface NativePluginRegistration {\n resolver: () => Promise<RegisteredPlugin>;\n ids: string[];\n}\n\nexport interface BootstrapExpoAdasOptions {\n /**\n * Endpoint GETTERS — the host owns the dev/prod env switch, so these are read\n * live on every SDK request (never snapshotted). `core` is required; the rest\n * are optional (voice/consumer-auth features light up when present).\n */\n urls: {\n core: () => string;\n voice?: () => string;\n consumerAuth?: () => string;\n /** Trusted extra host for plugin WebViews (the relay device-bridge auth flows). */\n relay?: () => string;\n };\n /** The host's token bridge — outcome mapping (transient vs auth_failed) owned by the host. */\n tokenProvider: TokenProvider;\n /** Current user projection for chat routing (host reads its auth store). */\n getUser: () => AdasUser | null;\n /** Active tenant getter (signed-in tenant, falling back to the pre-auth pick). */\n tenant?: () => string;\n /** Adopt a freshly-minted session on tenant switch (host persists tenant-first, then logs in). */\n onSession?: (session: AdasSession) => void | Promise<void>;\n /** The host's bundled Google iOS client id, for the join/open-join OAuth path. */\n googleIosClientId?: string;\n /** Host-bundled native plugins (AuthWebView, ConnectionsPanel, …) — lazy resolvers. */\n nativePlugins?: NativePluginRegistration[];\n /** Native modules remote bundles may require() (e.g. react-native-webview) — from the host build. */\n bundleModules?: Record<string, unknown>;\n /** Pre-SDK AsyncStorage keys, so an existing install keeps its warm caches. */\n storageKeys?: { attachmentIndex?: string; bundleIndex?: string };\n /** Connectivity sink — the SDK reports fetch reachability up/down; the host feeds its banner. */\n onReachabilityChange?: (up: boolean) => void;\n /** Open the host's plugin screen for a UI-action chip (the host owns its router). */\n navigateToPlugin: (route: UIActionRoute) => void;\n /**\n * Reachability probe URL for the device-primitive connectivity API. The host\n * passes this EXPLICITLY (typically `${core}/health`) as a load-time snapshot —\n * deliberately NOT derived from `urls.core()`, so a dev/E2E core override does\n * not silently flow into the health probe (the asymmetry the host relied on).\n */\n reachabilityUrl?: string;\n}\n\n/**\n * Wire the entire Expo-backed ADAS platform in one call. Run for its side effect\n * at module load, before the router mounts. Returns the attachment cache so the\n * host can hand it to anything that needs it (initAdas already received it).\n */\nexport function bootstrapExpoAdas(opts: BootstrapExpoAdasOptions): { attachmentCache: ExpoAttachmentCache } {\n // Expo providers + the device-primitive implementation (props.device). The SDK\n // owns createDeviceProvider; the shell injects only its reachability URL.\n const { attachmentCache } = setupExpoProviders({\n bundleModules: opts.bundleModules,\n attachmentIndexKey: opts.storageKeys?.attachmentIndex,\n bundleIndexKey: opts.storageKeys?.bundleIndex,\n deviceProvider: createDeviceProvider({ reachabilityUrl: opts.reachabilityUrl }),\n });\n\n // Host-bundled native plugins: register each lazy resolver under its id aliases.\n for (const plugin of opts.nativePlugins ?? []) {\n for (const id of plugin.ids) registerNativePlugin(id, plugin.resolver);\n }\n\n // Plugin WebViews may navigate to the relay (device-bridge auth flows) —\n // whitelist it alongside core/voice. Getter → live across env switches.\n const relay = opts.urls.relay;\n setPluginWebViewExtraHosts(() => (relay ? [relay()] : []));\n\n // Native OAuth for the relocated auth screens (the ONLY native OAuth wiring).\n setOAuthProvider(createExpoOAuthProvider({ googleIosClientId: opts.googleIosClientId }));\n\n // The voice call's device audio mode (echo cancellation on the loudspeaker).\n setCallAudioProvider(createCallAudioProvider());\n\n // ACS fetch outcomes feed the host's connectivity banner (any response = up,\n // thrown fetch = down). The host's sink stays host-side (lazy, no load cycle).\n if (opts.onReachabilityChange) {\n const onReach = opts.onReachabilityChange;\n setAcsReachabilityReporter({ up: () => onReach(true), down: () => onReach(false) });\n }\n\n // L3 DeviceState (props.deviceState) — the SEMANTIC layer, wired with the SDK's\n // OWN device-bridge collectors. The shell injects none (the R10 payoff): an\n // undeclared/unimplemented domain returns explicit `unknown`s, never a silent\n // undefined. Kept a SEPARATE statement from the SERVER relay — the two\n // reasoning paths never merge.\n setDeviceStateProvider(\n createDeviceState({ connectivity: deviceConnectivity, location: deviceLocation }),\n );\n\n // LOCAL device tools — register the static, host-owned platform tools\n // (device.echo / device.ping) at load, so the device path answers even before\n // any plugin bundle or sign-in: the platform tier deliberately does not depend\n // on the thing being proved. A tenant's EXTERNAL device tools load on\n // session-authenticated, which the host wires to its auth-session subscription\n // via syncDeviceToolsForSession() — ambient, not voice-triggered.\n registerPlatformDeviceTools();\n\n // Plugin/UI-action chips delegate navigation to the host's router.\n setUIActionNavigator(opts.navigateToPlugin);\n\n // Core config + token bridge. URL getters (not snapshots) so the env switch\n // flows through; identity/token bridge owned by the host.\n initAdas({\n coreUrl: opts.urls.core,\n ...(opts.urls.voice ? { voiceUrl: opts.urls.voice } : {}),\n ...(opts.urls.consumerAuth ? { consumerAuthUrl: opts.urls.consumerAuth } : {}),\n ...(opts.onSession ? { onSession: opts.onSession } : {}),\n ...(opts.tenant ? { tenant: opts.tenant } : {}),\n getUser: opts.getUser,\n tokenProvider: opts.tokenProvider,\n attachmentCache,\n });\n\n return { attachmentCache };\n}\n","// Call audio mode for the voice call — see ios/AdaAudioModule.swift for why.\n//\n// requireOptionalNativeModule: on a binary WITHOUT the module (any build before\n// it shipped) this is null and the provider is a no-op, so an OTA carrying this\n// JS can never crash an older binary. Android: no module yet (its WebView echo\n// path is untouched).\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport type { CallAudioProvider } from '@ateam-ai-mobile/ui';\n\ninterface AdaAudioNative {\n beginVoiceCall(): boolean;\n endVoiceCall(): boolean;\n}\n\nconst native = requireOptionalNativeModule<AdaAudioNative>('AdaAudio');\n\nexport function createCallAudioProvider(): CallAudioProvider | null {\n if (!native) return null;\n return {\n beginCall: () => { native.beginVoiceCall(); },\n endCall: () => { native.endVoiceCall(); },\n };\n}\n","/**\n * @ateam-ai-mobile/expo — Expo-backed providers for the ADAS SDK.\n *\n * Bare-RN hosts skip this package entirely (core + plugin-runtime have safe\n * defaults); Expo hosts call setupExpoProviders() once at startup to light\n * up haptics, persistent bundle caching, attachment previews, and the\n * expo modules remote bundles may require.\n */\nimport * as ExpoWebBrowser from 'expo-web-browser';\nimport * as ExpoLinking from 'expo-linking';\nimport * as ExpoClipboard from 'expo-clipboard';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport { setAcsCacheStorage } from '@ateam-ai-mobile/core';\nimport {\n setHapticsProvider,\n setBundleStorage,\n registerBundleModules,\n setPluginNativeProvider,\n type PluginNative,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { setClipboardProvider } from '@ateam-ai-mobile/ui';\n\nimport { expoHaptics } from './haptics';\nimport { ExpoBundleStorage } from './bundleStorage';\nimport { ExpoAttachmentCache } from './attachmentCache';\n\nexport { expoHaptics } from './haptics';\nexport { ExpoBundleStorage } from './bundleStorage';\nexport { ExpoAttachmentCache } from './attachmentCache';\n// NOTE: createBridge (device capabilities bridge) is intentionally NOT\n// exported here — it statically imports the optional expo modules\n// (calendar/contacts/location/…), which would break Metro builds in hosts\n// that don't install them. Install those modules and import it from the\n// subpath: `import { createBridge } from '@ateam-ai-mobile/expo/bridge'`.\n\n/**\n * Wire all Expo providers in one call. Runs before initAdas() is fine —\n * these registries are independent of the core config.\n *\n * Returns the attachment cache so the host can pass it to initAdas():\n * const { attachmentCache } = setupExpoProviders();\n * initAdas({ ..., attachmentCache });\n */\nexport function setupExpoProviders(opts?: {\n /** Extra modules remote bundles may require(), e.g. react-native-webview.\n * Static imports must live in the HOST (it owns the native build). */\n bundleModules?: Record<string, any>;\n /** AsyncStorage key overrides — pass your pre-SDK keys when migrating an\n * existing app so users keep their attachment previews / bundle cache. */\n attachmentIndexKey?: string;\n bundleIndexKey?: string;\n /**\n * The device-primitive implementation plugins receive as `props.device`.\n * Build it with `createDeviceProvider(...)` from `@ateam-ai-mobile/expo/device`\n * and pass it here so the SDK owns the implementation and this call registers\n * it (setPluginNativeProvider). It is injected — not imported by this entry —\n * because createDeviceProvider statically pulls optional expo modules that a\n * host without them cannot build (same reason createBridge is a subpath).\n */\n deviceProvider?: PluginNative;\n}): { attachmentCache: ExpoAttachmentCache } {\n setHapticsProvider(expoHaptics);\n if (opts?.deviceProvider) setPluginNativeProvider(opts.deviceProvider);\n setBundleStorage(new ExpoBundleStorage({ indexKey: opts?.bundleIndexKey }));\n setClipboardProvider({\n setString: (text: string) => ExpoClipboard.setStringAsync(text).then(() => {}),\n });\n registerBundleModules({\n 'expo-web-browser': ExpoWebBrowser,\n 'expo-linking': ExpoLinking,\n ...(opts?.bundleModules || {}),\n });\n // Persist the ACS home-actions feed across cold starts.\n setAcsCacheStorage({\n getItem: (k: string) => AsyncStorage.getItem(k),\n setItem: (k: string, v: string) => AsyncStorage.setItem(k, v),\n });\n return { attachmentCache: new ExpoAttachmentCache({ indexKey: opts?.attachmentIndexKey }) };\n}\n\n// Device Bridge v2 — the semantic state engines this build provides.\n// The host passes this to setDeviceStateProvider(); per-plugin gating by\n// manifest.stateDomains happens in the SDK.\nexport { createDeviceState, IMPLEMENTED_DOMAINS } from './device-state';\nexport type { DeviceStateCollectors } from './device-state';\n// The mounted half of the Visual engine. The app renders this ONCE; it draws\n// nothing until a visual session is open. Required for deviceState.visual to\n// produce frames at all — expo-camera has no headless capture.\n// The rolling frame timeline. Exposed so the host's native.vision layer can\n// resolve a frame id to its on-device file (frameById) and write a measured\n// quality back onto the frame (setFrameQuality) — see visualEngine.ts.\nexport { visualEngine } from './device-state/visualEngine';\nexport type { VisualFrame } from './device-state/visualEngine';\n// The mounted camera surface behind scanner.scanOnce() and\n// camera.recordVideo(). Mount ONCE near the app root; it renders nothing\n// until a plugin asks for a scan or a recording. Without it both APIs\n// reject immediately with UNAVAILABLE — deliberately, since a plugin must\n// never await a promise nothing can settle.\nexport { CameraTaskHost } from './camera/CameraTaskHost';\nexport { cameraTasks } from './camera/cameraTasks';\n// Replay — run guidance logic against a RECORDED WALK instead of a phone.\n// See docs/SPEC_device_walk_recorder.md for the fixture format.\nexport { createReplayDeviceState, parseWalkJsonl } from './device-state/replay';\nexport type { WalkFixture, WalkSample, WalkRefusal, ReplayClock, ReplaySession } from './device-state/replay';\n","/**\n * expo-haptics implementation of the plugin HapticsAPI.\n */\nimport * as Haptics from 'expo-haptics';\nimport type { HapticsAPI } from '@ateam-ai-mobile/plugin-runtime';\n\nexport const expoHaptics: HapticsAPI = {\n impact(style) {\n const map = {\n light: Haptics.ImpactFeedbackStyle.Light,\n medium: Haptics.ImpactFeedbackStyle.Medium,\n heavy: Haptics.ImpactFeedbackStyle.Heavy,\n } as const;\n Haptics.impactAsync(map[style]).catch(() => {});\n },\n notification(type) {\n const map = {\n success: Haptics.NotificationFeedbackType.Success,\n warning: Haptics.NotificationFeedbackType.Warning,\n error: Haptics.NotificationFeedbackType.Error,\n } as const;\n Haptics.notificationAsync(map[type]).catch(() => {});\n },\n selection() {\n Haptics.selectionAsync().catch(() => {});\n },\n // Shorthand for notification('error') — the form plugins actually call.\n error() {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});\n },\n};\n","/**\n * Disk-backed BundleStorage: bundle files in the Expo cache directory,\n * index in AsyncStorage. Mirrors the reference app's cache semantics\n * (index version key, files under plugin-bundles/).\n */\nimport * as FileSystem from 'expo-file-system/legacy';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { BundleStorage, BundleCacheEntry } from '@ateam-ai-mobile/plugin-runtime';\n\nconst CACHE_DIR = `${FileSystem.cacheDirectory}plugin-bundles/`;\nconst DEFAULT_CACHE_INDEX_KEY = 'adas-plugin-bundles-index-v1';\n\nexport class ExpoBundleStorage implements BundleStorage {\n private dirReady = false;\n private indexKey: string;\n\n /** Pass `indexKey` when migrating from an app that already persisted a\n * bundle index under a different key — keeps the existing cache warm. */\n constructor(opts?: { indexKey?: string }) {\n this.indexKey = opts?.indexKey || DEFAULT_CACHE_INDEX_KEY;\n }\n\n private async ensureDir(): Promise<void> {\n if (this.dirReady) return;\n try {\n const info = await FileSystem.getInfoAsync(CACHE_DIR);\n if (!info.exists) {\n await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true });\n }\n } catch (err) {\n console.warn('[ExpoBundleStorage] cache dir error:', err);\n }\n this.dirReady = true;\n }\n\n async loadIndex(): Promise<Record<string, BundleCacheEntry>> {\n try {\n const raw = await AsyncStorage.getItem(this.indexKey);\n return raw ? JSON.parse(raw) : {};\n } catch {\n return {};\n }\n }\n\n async saveIndex(index: Record<string, BundleCacheEntry>): Promise<void> {\n try {\n await AsyncStorage.setItem(this.indexKey, JSON.stringify(index));\n } catch (err) {\n console.warn('[ExpoBundleStorage] index save error:', err);\n }\n }\n\n async readBundle(localPath: string): Promise<string | null> {\n try {\n const info = await FileSystem.getInfoAsync(localPath);\n if (!info.exists) return null;\n return await FileSystem.readAsStringAsync(localPath);\n } catch {\n return null; // file gone — caller re-downloads\n }\n }\n\n async writeBundle(key: string, content: string): Promise<string> {\n await this.ensureDir();\n const localPath = `${CACHE_DIR}${key}`;\n await FileSystem.writeAsStringAsync(localPath, content);\n return localPath;\n }\n\n async deleteBundle(localPath: string): Promise<void> {\n await FileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => {});\n }\n\n async clearAll(): Promise<void> {\n await FileSystem.deleteAsync(CACHE_DIR, { idempotent: true }).catch(() => {});\n await AsyncStorage.removeItem(this.indexKey).catch(() => {});\n this.dirReady = false;\n }\n}\n","/**\n * Expo implementation of @ateam-ai-mobile/core's AttachmentCache: attachment bytes on\n * disk, index in AsyncStorage. Mirrors the reference app's chat attachment\n * cache (last-100 retention).\n */\nimport * as FileSystem from 'expo-file-system/legacy';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { AttachmentCache, AttachmentPreview, OutgoingAttachment } from '@ateam-ai-mobile/core';\n\nconst DEFAULT_INDEX_KEY = 'adas-chat-attachment-index-v1';\nconst DIR = `${FileSystem.documentDirectory}chat-attachments/`;\nconst MAX_JOBS = 100;\n\ninterface CachedAttachment { mimeType: string; name: string; filePath: string }\n\nexport class ExpoAttachmentCache implements AttachmentCache {\n private index: Record<string, CachedAttachment[]> = {};\n private loaded: Promise<void>;\n private indexKey: string;\n\n /** Pass `indexKey` when migrating from an app that already persisted an\n * index under a different AsyncStorage key — otherwise existing users\n * lose every cached preview on upgrade. */\n constructor(opts?: { indexKey?: string }) {\n const indexKey = opts?.indexKey || DEFAULT_INDEX_KEY;\n this.indexKey = indexKey;\n this.loaded = (async () => {\n try {\n const info = await FileSystem.getInfoAsync(DIR);\n if (!info.exists) await FileSystem.makeDirectoryAsync(DIR, { intermediates: true });\n const raw = await AsyncStorage.getItem(indexKey);\n if (raw) this.index = JSON.parse(raw);\n } catch {}\n })();\n }\n\n async ready(): Promise<void> {\n await this.loaded;\n }\n\n async save(jobId: string, attachments: OutgoingAttachment[]): Promise<void> {\n await this.loaded;\n const entries: CachedAttachment[] = [];\n for (let i = 0; i < attachments.length; i++) {\n const att = attachments[i];\n const ext = att.mimeType.startsWith('image/') ? att.mimeType.split('/')[1] || 'jpg' : 'bin';\n const filePath = `${DIR}${jobId}_${i}.${ext}`;\n try {\n await FileSystem.writeAsStringAsync(filePath, att.data, { encoding: FileSystem.EncodingType.Base64 });\n entries.push({ mimeType: att.mimeType, name: att.name, filePath });\n } catch (err) {\n console.warn('[ExpoAttachmentCache] write failed:', err);\n }\n }\n if (entries.length === 0) return;\n\n this.index[jobId] = entries;\n // Keep only the most recent MAX_JOBS jobs; delete evicted files.\n const keys = Object.keys(this.index);\n if (keys.length > MAX_JOBS) {\n for (const k of keys.slice(0, keys.length - MAX_JOBS)) {\n const old = this.index[k];\n if (old) for (const o of old) FileSystem.deleteAsync(o.filePath, { idempotent: true }).catch(() => {});\n delete this.index[k];\n }\n }\n try {\n await AsyncStorage.setItem(this.indexKey, JSON.stringify(this.index));\n } catch {}\n }\n\n get(jobId: string): AttachmentPreview[] | undefined {\n const entries = this.index[jobId];\n if (!entries || entries.length === 0) return undefined;\n return entries.map(e => ({\n mimeType: e.mimeType,\n name: e.name,\n previewUri: e.filePath,\n }));\n }\n}\n","// ── deviceContext ──────────────────────────────────────────────────────────\n// power ✓ thermal ✗ (needs a native module) memory/storage ✗\nimport * as ExpoBattery from 'expo-battery';\nimport type { DeviceState, DeviceContextSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { qualityFrom, qualityUnknown } from '../quality';\nimport { stateEvent } from '../events';\n\nexport function createDeviceContextDomain(): DeviceState['deviceContext'] {\n return {\n async getSnapshot(): Promise<DeviceContextSnapshot> {\n const observedAt = Date.now();\n try {\n const [level, state, lowPower] = await Promise.all([\n ExpoBattery.getBatteryLevelAsync(),\n ExpoBattery.getBatteryStateAsync(),\n ExpoBattery.isLowPowerModeEnabledAsync(),\n ]);\n return {\n power: {\n // expo-battery returns -1 when it cannot tell. That is not a level.\n batteryLevel: typeof level === 'number' && level >= 0 ? level : null,\n charging:\n state === ExpoBattery.BatteryState.CHARGING ||\n state === ExpoBattery.BatteryState.FULL,\n lowPowerMode: lowPower,\n },\n // No Expo API for either. iOS needs ProcessInfo.thermalState, Android\n // PowerManager.getThermalStatus — a native module that does not exist.\n thermal: { state: 'unknown' },\n resources: { memoryPressure: 'unknown', storagePressure: 'unknown' },\n quality: qualityFrom(observedAt),\n };\n } catch {\n // The battery read failing is rare and uninteresting; reporting it as\n // 0% would not be.\n return {\n power: { batteryLevel: null, charging: null, lowPowerMode: null },\n thermal: { state: 'unknown' },\n resources: { memoryPressure: 'unknown', storagePressure: 'unknown' },\n quality: qualityUnknown(),\n };\n }\n },\n\n // Real events, not a placeholder: expo-battery exposes native listeners,\n // so a plugin that subscribes genuinely hears about a change rather than\n // sitting on a subscription that can never fire. Nudges without a payload:\n // no Device Context payload schema is decided, so the subscriber re-reads.\n subscribe(listener) {\n const subs = [\n ExpoBattery.addBatteryLevelListener(() => listener(stateEvent('deviceContext', 'batteryLevelChanged'))),\n ExpoBattery.addBatteryStateListener(() => listener(stateEvent('deviceContext', 'chargingChanged'))),\n ];\n return { unsubscribe: () => subs.forEach(x => x?.remove?.()) };\n },\n };\n}\n","/**\n * The honesty helpers — one place, shared by every domain module.\n *\n * A snapshot's `quality` says how much to trust it. R5 centralizes the three\n * ways a domain answers honestly here so no domain re-implements them:\n * - qualityFrom(observedAt) a live read, freshness stamped from its age.\n * - qualityUnknown() \"nothing was observed\" — never dressed as fresh.\n * - unsupported(what) an ACTION this build can't honour, refused by name.\n *\n * The `unknown` freshness shape is NOT re-declared here: it is the single\n * canonical `neverObserved()` from the contract package, so the engine, the gate\n * (deviceStateHost) and the types can never drift into three subtly different\n * \"unknown\"s.\n */\nimport { neverObserved } from '@ateam-ai-mobile/plugin-runtime';\nimport type { StateQuality } from '@ateam-ai-mobile/plugin-runtime';\n\n// ── Freshness ────────────────────────────────────────────────────────────\n//\n// Every snapshot carries when it was observed. `observedAt` is stamped at read\n// time by a live-reading domain; a caching domain passes its cache timestamp\n// instead. This is the one place the fresh/aging/stale thresholds live.\n\nexport const FRESH_MS = 10_000;\nexport const AGING_MS = 60_000;\n\nexport function qualityFrom(\n observedAt: number,\n confidence?: StateQuality['confidence'],\n): StateQuality {\n const ageMs = Date.now() - observedAt;\n return {\n freshness: {\n state: ageMs < FRESH_MS ? 'fresh' : ageMs < AGING_MS ? 'aging' : 'stale',\n observedAt,\n ageMs,\n },\n ...(confidence ? { confidence } : {}),\n };\n}\n\n/** Age genuinely unknown — treated as stale, never as fresh. */\nexport function qualityUnknown(): StateQuality {\n return { freshness: neverObserved() };\n}\n\n/**\n * Refuse, by name, with the frozen error code.\n *\n * Used where the contract declares a method this build cannot honour. The\n * alternative — a no-op that resolves — is the fabrication pattern in method\n * form: the caller cannot tell \"did nothing\" from \"worked\".\n */\nexport function unsupported(what: string): Error {\n const err: any = new Error(\n `${what} is not implemented in this build — the native engine behind it does not exist yet.`,\n );\n err.code = 'UNSUPPORTED';\n return err;\n}\n","/**\n * The ONE event envelope every domain emits, and the one way listeners are held.\n *\n * The frozen shape is `StateEvent { domain, name, timestamp, payload }`. Three\n * domains were born emitting `{ type: '…' }` instead, so a plugin written\n * against the contract — `e.name === 'appStateChanged'` — never matched, and\n * `e.name.endsWith(…)` threw on `undefined`. Building every event here is what\n * keeps a second shape from growing back.\n *\n * Type-only imports, so the pure modules that use this (the spatial event\n * detector) stay testable under plain `node --test`.\n */\nimport type { DeviceBridgeDomain, StateEvent, Subscription } from '@ateam-ai-mobile/plugin-runtime';\n\n/**\n * Build an event. `payload` is omitted, not `undefined`, when there is none —\n * an event that is only a nudge (\"re-read the snapshot\") says so by shape.\n * `timestamp` is when the change was recognised; it defaults to now, and replay\n * passes its virtual clock.\n */\nexport function stateEvent<TDomain extends DeviceBridgeDomain, TName extends string, TPayload = never>(\n domain: TDomain,\n name: TName,\n payload?: TPayload,\n timestamp: number = Date.now(),\n): StateEvent<TName, TPayload> & { domain: TDomain } {\n return payload === undefined\n ? { domain, name, timestamp }\n : { domain, name, timestamp, payload };\n}\n\n/**\n * Listeners, each isolated from the others. A listener that throws must not\n * stop the next one hearing the event, nor the engine that emitted it.\n */\nexport function listenerSet<E>() {\n const listeners = new Set<(event: E) => void>();\n return {\n add(fn: (event: E) => void): Subscription {\n listeners.add(fn);\n return { unsubscribe: () => { listeners.delete(fn); } };\n },\n emit(event: E): void {\n for (const fn of listeners) {\n try { fn(event); } catch { /* a bad listener is not the engine's problem */ }\n }\n },\n };\n}\n","// ── connectivity ─────────────────────────────────────────────────────────\n// reachability ✓ transport ✓ quality/stability/metered ✗\nimport type { DeviceState, ConnectivitySnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport type { DeviceStateCollectors } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\n\nexport function createConnectivityDomain(collectors: DeviceStateCollectors): DeviceState['connectivity'] {\n return {\n async getSnapshot(): Promise<ConnectivitySnapshot> {\n const observedAt = Date.now();\n try {\n if (!collectors.connectivity) throw unsupported('connectivity collector');\n const data: any = await collectors.connectivity.collectConnectivity();\n const online = data?.is_connected === true;\n const type = String(data?.type || '').toLowerCase();\n\n return {\n reachability: data?.is_connected == null ? 'unknown' : online ? 'online' : 'offline',\n transport:\n type === 'wifi' ? 'wifi'\n : type === 'cellular' ? 'cellular'\n : type === 'ethernet' ? 'ethernet'\n : online ? 'other'\n : data?.is_connected === false ? 'none'\n : 'unknown',\n // All four below need measurement nobody takes yet. `unknown` is the\n // designed answer — \"return unknown rather than stale precision\".\n qualityClass: 'unknown',\n stability: 'unknown',\n latencyMs: null,\n bandwidthClass: 'unknown',\n metered: null,\n constrained: null,\n quality: qualityFrom(observedAt),\n };\n } catch {\n return {\n reachability: 'unknown',\n transport: 'unknown',\n qualityClass: 'unknown',\n stability: 'unknown',\n latencyMs: null,\n bandwidthClass: 'unknown',\n metered: null,\n constrained: null,\n quality: qualityUnknown(),\n };\n }\n },\n\n // Intent selects a native sampling strategy — latency probes, quality\n // classification. Nothing here samples anything, so accepting an intent\n // would be accepting an instruction we do not carry out.\n async start() { throw unsupported('connectivity.start'); },\n async stop() { throw unsupported('connectivity.stop'); },\n // A subscription that can never fire is worse than none: the caller\n // reasonably reads silence as \"nothing changed\".\n subscribe() { throw unsupported('connectivity.subscribe'); },\n };\n}\n","// ── capability ───────────────────────────────────────────────────────────\n// permission ✓ availability ✗ restriction ✗\nimport type { DeviceState, CapabilitySnapshot, CapabilityStatus } from '@ateam-ai-mobile/plugin-runtime';\nimport type { DeviceStateCollectors } from '../collectors';\nimport { qualityFrom, unsupported } from '../quality';\n\n/**\n * The bridge's permission model is 4-valued (`not_requested | granted | denied\n * | not_needed`); the frozen one is 7-valued. The extra states are real OS\n * distinctions nothing currently reads, so they map to `unknown` rather than\n * being collapsed into `denied` — \"restricted by policy\" and \"the user said no\"\n * are different problems with different remedies, and guessing between them\n * would put a wrong instruction in front of the user.\n */\nfunction statusFrom(granted: boolean | null): CapabilityStatus {\n return {\n support: 'unknown',\n permission: granted === null ? 'unknown' : granted ? 'granted' : 'denied',\n availability: 'unknown',\n };\n}\n\nconst UNKNOWN_STATUS: CapabilityStatus = {\n support: 'unknown',\n permission: 'unknown',\n availability: 'unknown',\n};\n\nexport function createCapabilityDomain(collectors: DeviceStateCollectors): DeviceState['capability'] {\n return {\n async getSnapshot(): Promise<CapabilitySnapshot> {\n const observedAt = Date.now();\n const locationGranted = collectors.location\n ? await collectors.location.hasPermission().catch(() => null)\n : null;\n\n return {\n // Only location has a real reader today. The rest are declared by the\n // contract and answered honestly rather than omitted.\n camera: UNKNOWN_STATUS,\n microphone: UNKNOWN_STATUS,\n location: {\n ...statusFrom(locationGranted),\n // iOS 14+ distinguishes precise from approximate; nothing reads it.\n precision: 'unknown',\n backgroundPermission: 'unknown',\n },\n motion: UNKNOWN_STATUS,\n bluetooth: UNKNOWN_STATUS,\n uwb: UNKNOWN_STATUS,\n nfc: UNKNOWN_STATUS,\n quality: qualityFrom(observedAt),\n } as CapabilitySnapshot;\n },\n\n // No OS permission-change stream is wired. Same reasoning as connectivity:\n // silence would be indistinguishable from stability.\n subscribe() { throw unsupported('capability.subscribe'); },\n };\n}\n","// ── interaction ──────────────────────────────────────────────────────────\n// app lifecycle ✓ screenOn ✗ audio ✗\nimport { AppState } from 'react-native';\nimport type { DeviceState, InteractionSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { qualityFrom } from '../quality';\nimport { stateEvent } from '../events';\n\n/**\n * Lifecycle only, and honest about the rest.\n *\n * `screenOn` is NOT derivable from AppState: RN reports `inactive` for a\n * notification shade, a control centre swipe, an incoming call AND a locked\n * screen. Mapping `inactive` to \"screen off\" would be wrong most of the time\n * it fired — so it stays null until a native module can answer it.\n *\n * The whole `audio` block reports unknown for the same reason. `interruption`\n * carries the distinction that matters: `none` means \"checked, nothing is\n * interrupting\", `unknown` means \"no knowledge\" — and we have the latter.\n */\nexport function createInteractionDomain(): DeviceState['interaction'] {\n const mapState = (s: string): InteractionSnapshot['lifecycle']['appState'] =>\n s === 'active' ? 'foreground'\n : s === 'background' ? 'background'\n : s === 'inactive' ? 'inactive'\n : 'unknown';\n\n return {\n async getSnapshot(): Promise<InteractionSnapshot> {\n const observedAt = Date.now();\n return {\n lifecycle: {\n appState: mapState(AppState.currentState as string),\n screenOn: null,\n userActive: null,\n idleDurationMs: null,\n },\n audio: {\n captureActive: null,\n playbackActive: null,\n route: 'unknown',\n interruption: 'unknown',\n },\n // The voice block is OMITTED rather than filled with nulls: the design\n // says a platform that cannot report voice session state leaves the\n // group out instead of guessing `false` for each field. The app's own\n // voice store knows this, but it is not reachable from the SDK.\n quality: qualityFrom(observedAt),\n };\n },\n\n // Real: AppState genuinely emits on every foreground/background change,\n // which is the one part of this domain that works.\n subscribe(listener) {\n const sub = AppState.addEventListener('change', next => {\n listener(stateEvent('interaction', 'appStateChanged', { appState: mapState(next as string) }));\n });\n return { unsubscribe: () => sub.remove() };\n },\n };\n}\n","/**\n * The compass, kept warm — and the turn detector fed from it.\n *\n * `getHeadingAsync()` is not usable on the capture path: it starts a watch,\n * waits for a reading and stops it, which costs hundreds of milliseconds — per\n * frame. So this holds ONE subscription while a session is open and callers\n * read the last value synchronously.\n *\n * Returns null until a reading arrives, and null means NOT MEASURED. A heading\n * of 0 is due north, so an unstarted compass must never report 0.\n *\n * WHY THE TURN DETECTOR IS FED HERE. A turn happens BETWEEN reads — it is an\n * event, not a state — so a detector driven by sampled snapshots would miss\n * exactly the moment a guide needs. It runs on the compass callback instead.\n * The logic itself lives in `turnDetector.ts`, free of any Expo import, so it\n * can be tested against a scripted walk. A completed turn is also PUSHED to\n * `onTurn` listeners — spatial publishes it as an event the moment it lands.\n *\n * SHARED, SO COUNTED. Spatial and visual both keep the compass warm, and each\n * used to stop it on its own `stop()`: visual's stop killed the compass — and\n * wiped the turn history — under a live spatial session. Each caller now names\n * itself, and the watch stops only when the last one leaves.\n */\nimport * as ExpoLocation from 'expo-location';\n\nimport { TurnDetector, type TurnEvent } from './turnDetector';\nimport { listenerSet } from './events';\n\nexport type { TurnEvent };\nexport { signedDelta } from './turnDetector';\n\n/** Who is holding the compass open. */\nexport type HeadingOwner = 'spatial' | 'visual';\n\ninterface HeadingReading {\n headingDeg: number | null;\n /**\n * iOS calibration rank: 3 high, 2 medium, 1 low, 0 none. NOT degrees of\n * error, which is why it is never surfaced as `headingAccuracyDeg` — that\n * field wants degrees, and converting a rank into one would be a fabricated\n * error bar. It drives CONFIDENCE instead, which is what it can honestly say.\n */\n calibration: number | null;\n timestamp: number;\n}\n\nlet latest: HeadingReading | null = null;\nlet sub: { remove(): void } | null = null;\n/** The watch being opened, so two owners starting at once open ONE. */\nlet opening: Promise<void> | null = null;\nconst owners = new Set<HeadingOwner>();\nconst turns = new TurnDetector();\nconst turnListeners = listenerSet<TurnEvent>();\n\n/** Start watching on behalf of `owner`. Idempotent per owner. */\nexport function startHeading(owner: HeadingOwner): Promise<void> {\n owners.add(owner);\n if (sub) return Promise.resolve();\n opening ??= open().finally(() => { opening = null; });\n return opening;\n}\n\nasync function open(): Promise<void> {\n try {\n const s = await ExpoLocation.watchHeadingAsync(h => {\n // A reading landing after the last owner left is not kept.\n if (!owners.size) return;\n // trueHeading is -1 until the compass calibrates; magHeading is the\n // fallback the OS itself recommends.\n const raw = h.trueHeading >= 0 ? h.trueHeading : h.magHeading;\n const deg = typeof raw === 'number' && raw >= 0 ? raw : null;\n const cal = typeof h.accuracy === 'number' && h.accuracy >= 0 ? h.accuracy : null;\n const now = Date.now();\n latest = { headingDeg: deg, calibration: cal, timestamp: now };\n const turn = turns.push(deg, now);\n if (turn) turnListeners.emit(turn);\n });\n // Every owner may have left while the OS was starting the watch.\n if (!owners.size) { s.remove(); return; }\n sub = s;\n } catch {\n // No compass, or permission refused. Callers get headingDeg: null, which\n // is the honest answer and does not stop anything else working.\n sub = null;\n }\n}\n\n/** Release `owner`'s hold. The watch stops only when no owner is left. */\nexport function stopHeading(owner: HeadingOwner): void {\n owners.delete(owner);\n if (owners.size) return;\n sub?.remove();\n sub = null;\n // Clear everything: a heading from a finished session describes a direction\n // the user may have long since turned away from, and a turn from a previous\n // walk is not something a new one should report.\n latest = null;\n turns.reset();\n}\n\n/** Hear each completed turn as it completes. Signed: positive = right. */\nexport function onTurn(fn: (turn: TurnEvent) => void) {\n return turnListeners.add(fn);\n}\n\n/** The newest reading, or null if the compass has said nothing yet. */\nexport function currentHeading(): { headingDeg: number | null } | null {\n return latest ? { headingDeg: latest.headingDeg } : null;\n}\n\n/** The newest reading with its calibration and age. */\nexport function currentHeadingDetail(): HeadingReading | null {\n return latest;\n}\n\n/** The most recent completed turn. Signed: positive = clockwise = right. */\nexport function lastMeaningfulTurn(): TurnEvent | null {\n return turns.last();\n}\n","/**\n * Turning, and which way.\n *\n * \"Turn right at the corner\" and \"you have just turned left\" are the two\n * sentences a walking guide says most, and neither can be read off a single\n * heading — they need heading OVER TIME. This is that, as a pure state machine\n * with no Expo import, so it can be tested against a scripted walk instead of\n * against a phone in someone's hand.\n *\n * SIGN IS THE WHOLE POINT. Compass degrees increase clockwise, so a positive\n * delta is a turn to the RIGHT and a negative one to the LEFT. A caller that\n * wants only the magnitude can take the absolute value; a caller handed a\n * magnitude cannot recover the side, and \"turn 90°\" without the side is worse\n * than saying nothing.\n *\n * THE HARD PART IS NOT DETECTING TURNS, IT IS NOT INVENTING THEM. A phone held\n * by a walking person swings 15–20° with every stride, and a compass drifts.\n * Compare against a fixed origin and the drift eventually crosses any threshold\n * on its own, so the machine re-baselines whenever the heading holds steady:\n * \"straight ahead\" is wherever you have been pointing for the last couple of\n * seconds, not wherever you were pointing when the session opened.\n */\n\n/** A completed turn. `deg` is signed: **positive = clockwise = right**. */\nexport interface TurnEvent {\n deg: number;\n /** When the turn was recognised as complete. */\n timestamp: number;\n}\n\nexport interface TurnDetectorOptions {\n /**\n * How far the heading must swing before it counts as a turn.\n *\n * 35° is a judgement, not a measurement: a street corner is ~90°, a gentle\n * bend in a corridor 30–45°, and hand-sway while walking routinely 15–20°.\n * Lower and the detector narrates the user's arm.\n */\n minTurnDeg?: number;\n /** Within this of the reference counts as \"still pointing the same way\". */\n stableDeg?: number;\n /** How long it must stay there before the reference moves. */\n stableMs?: number;\n}\n\n/** Signed shortest arc from `a` to `b`, in (-180, 180]. */\nexport function signedDelta(a: number, b: number): number {\n return ((b - a + 540) % 360) - 180;\n}\n\nexport class TurnDetector {\n private readonly minTurnDeg: number;\n private readonly stableDeg: number;\n private readonly stableMs: number;\n\n /** The heading currently treated as \"straight ahead\". */\n private refDeg: number | null = null;\n private stableSince = 0;\n private lastTurn: TurnEvent | null = null;\n\n constructor(opts: TurnDetectorOptions = {}) {\n this.minTurnDeg = opts.minTurnDeg ?? 35;\n this.stableDeg = opts.stableDeg ?? 12;\n this.stableMs = opts.stableMs ?? 2500;\n }\n\n /** Feed one compass reading. `null` degrees means the compass said nothing. */\n push(deg: number | null, now: number): TurnEvent | null {\n if (deg === null) return null;\n\n if (this.refDeg === null) {\n this.refDeg = deg;\n this.stableSince = now;\n return null;\n }\n\n const delta = signedDelta(this.refDeg, deg);\n\n if (Math.abs(delta) >= this.minTurnDeg) {\n // Re-baseline immediately, so a continuous 180° about-face is reported as\n // the turns it is made of. Left as one growing delta it would flip sign\n // at the halfway point and report a right turn for a left one.\n this.lastTurn = { deg: Math.round(delta), timestamp: now };\n this.refDeg = deg;\n this.stableSince = now;\n return this.lastTurn;\n }\n\n if (Math.abs(delta) > this.stableDeg) {\n // Moving, but not far enough yet. Hold the reference and keep watching —\n // this is what lets a slow 90° turn accumulate instead of being\n // re-baselined away one degree at a time.\n this.stableSince = now;\n return null;\n }\n\n // Steady. After a while, treat here as the new straight-ahead.\n if (now - this.stableSince >= this.stableMs) {\n this.refDeg = deg;\n this.stableSince = now;\n }\n return null;\n }\n\n /**\n * The most recent completed turn, or null if none this session.\n *\n * It does NOT expire on its own: the age is derivable from `timestamp`, and\n * \"recent enough to mention\" is a product judgement this layer must not make\n * on the caller's behalf.\n */\n last(): TurnEvent | null {\n return this.lastTurn;\n }\n\n reset(): void {\n this.refDeg = null;\n this.stableSince = 0;\n this.lastTurn = null;\n }\n}\n","// Great-circle distance — the SDK's one answer to \"how far apart are these\".\n//\n// Moved out of device-state/domains/spatial.ts when native.maps needed the same\n// question answered (a search result's distance from `near`). Two haversines\n// would be two answers that could drift; both callers import this one.\n//\n// No imports on purpose: it runs under plain `node --test` with the maps\n// normaliser that uses it.\n\n/** Metres along a path, great-circle per leg, rounded to the metre. */\nexport function distanceOf(path: ReadonlyArray<{ latitude: number; longitude: number }>): number {\n const R = 6_371_000;\n let total = 0;\n for (let i = 1; i < path.length; i++) {\n const a = path[i - 1], b = path[i];\n const dLat = ((b.latitude - a.latitude) * Math.PI) / 180;\n const dLon = ((b.longitude - a.longitude) * Math.PI) / 180;\n const la1 = (a.latitude * Math.PI) / 180, la2 = (b.latitude * Math.PI) / 180;\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2;\n total += 2 * R * Math.asin(Math.sqrt(h));\n }\n return Math.round(total);\n}\n","/**\n * Spatial events, derived from spatial snapshots.\n *\n * ONE MODEL, NOT TWO. An event is what changed between two snapshots — the\n * same snapshots `getSnapshot()` returns — so an event can never say \"moving\"\n * while a read says \"stationary\", and no threshold here has a second copy\n * somewhere else. The thresholds a read uses (the stationary speed, the GPS\n * confidence bands, distance along a path) live in this file and the domain\n * imports them, rather than the other way round, so the detector can be tested\n * without an Expo import: the live engine feeds it on every fix, every turn and\n * a one-second tick; replay feeds it recorded samples on a virtual clock.\n *\n * THE HARD PART IS NOT DETECTING CHANGES, IT IS NOT INVENTING THEM (the turn\n * detector's lesson, again). GPS speed wobbles around any threshold, and a fix\n * band flickers when accuracy sits near a boundary. So every state change must\n * HOLD before it is reported, and position changes must beat the fix's own\n * error bar. Holds delay an event on purpose; `since` in the payload carries\n * the real onset.\n *\n * A HOLD IS HELD BY FIXES, NOT BY THE CLOCK ALONE. The one-second tick re-reads\n * the SAME fix, so a hold measured only in time would let one outlier fix\n * followed by a gap — sparse fixes at Balanced accuracy, a wifi jump read as\n * speed — become \"you started moving\". A state settles only once two distinct\n * fixes have shown it; after that the tick may complete the hold, which is what\n * it is for (\"stationary for five seconds\" completes while nothing arrives).\n *\n * Type-only imports from plugin-runtime, so `node --test` can load this file.\n */\nimport type {\n Confidence,\n SpatialEvent,\n SpatialIntent,\n SpatialPoint,\n SpatialSnapshot,\n SpatialStateEvent,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { stateEvent } from './events';\nimport { distanceOf } from '../geo';\n\n/**\n * Below this the user is standing still.\n *\n * 0.4 m/s is well under a slow walk (~1.2 m/s) and above the jitter a\n * stationary phone's GPS produces. The point of the band is that a guide\n * must not say \"keep going straight\" to someone who has stopped.\n */\nexport const STATIONARY_MPS = 0.4;\n\n/**\n * Only claim stationary/moving when a speed was actually obtained. With no\n * speed the truthful answer is `unknown`, not `stationary` — \"you have\n * stopped\" is a false statement, not a neutral default.\n */\nexport function movementStateOf(speedMps: number | null): 'stationary' | 'moving' | 'unknown' {\n return speedMps === null ? 'unknown' : speedMps < STATIONARY_MPS ? 'stationary' : 'moving';\n}\n\n/**\n * The GPS fix's confidence band, from metres of horizontal error. ~10m is a\n * good open-sky fix; beyond ~50m the position is street-level at best and\n * useless for \"turn at the next corner\".\n */\nexport function fixConfidence(accuracyM: number | null): Confidence['level'] {\n return accuracyM === null ? 'unknown' : accuracyM <= 10 ? 'high' : accuracyM <= 50 ? 'medium' : 'low';\n}\n\n\nexport interface SpatialEventProfile {\n /** How long `moving` must hold before `movementStarted`. */\n movingHoldMs: number;\n /** How long `stationary` must hold, after moving, before `movementStopped`. */\n stoppedHoldMs: number;\n /** How long a new GPS confidence band must hold before it is reported. */\n confidenceHoldMs: number;\n /** Floor for `positionChanged`, metres. The real threshold is\n * max(this, moveThresholdM(anchor, fix)) — both fixes' uncertainty — so a\n * loose fix on EITHER end needs a bigger move. */\n minMoveM: number;\n}\n\n/**\n * Intent → how eager the events are.\n *\n * These are the engine's judgement, not the freeze's — the architecture doc\n * says only \"thresholds are profile-dependent\". Recorded here so the reasoning\n * is visible and arguable rather than buried. The intent changes ONLY these\n * holds, not sampling: there is no native strategy layer for it to reach.\n *\n * - `navigation`: a guide needs \"you've stopped\" soon enough to act on, but a\n * pause at a crossing is not a stop worth announcing — 3 s to start, 5 s to\n * stop. 10 m is about one building frontage.\n * - `fitness`: same movement holds; a slower confidence hold, because a runner\n * under trees dips in and out of a band constantly and nobody needs to hear it.\n * - `preciseLocate`: the caller is deliberately pinning a position, so the\n * holds are shortest and a few metres counts.\n * - `geofence` / `lowPowerTracking`: coarse by nature — tens of metres, and a\n * state must hold for a while before it is worth waking anyone for.\n */\nexport const SPATIAL_EVENT_PROFILES: Record<SpatialIntent, SpatialEventProfile> = {\n navigation: { movingHoldMs: 3000, stoppedHoldMs: 5000, confidenceHoldMs: 3000, minMoveM: 10 },\n fitness: { movingHoldMs: 3000, stoppedHoldMs: 5000, confidenceHoldMs: 5000, minMoveM: 10 },\n preciseLocate: { movingHoldMs: 2000, stoppedHoldMs: 3000, confidenceHoldMs: 2000, minMoveM: 3 },\n geofence: { movingHoldMs: 10000, stoppedHoldMs: 30000, confidenceHoldMs: 10000, minMoveM: 25 },\n lowPowerTracking: { movingHoldMs: 10000, stoppedHoldMs: 30000, confidenceHoldMs: 10000, minMoveM: 50 },\n};\n\n/** The profile for an intent; an unrecognised one gets `navigation`'s. */\n/**\n * The move two fixes can vouch for. A fix is a circle, not a point, and so is\n * the anchor it is compared with: two fixes of a phone lying still differ by up\n * to BOTH their errors. Combined (root-sum-square, each accuracyM ~ one radius)\n * and doubled, like a single fix's 2× before. No estimate = 0 (the floor rules).\n * Anchor ±100 m → fix ±5 m: a 20 m difference is noise, not a walk.\n */\nexport function moveThresholdM(anchor: { accuracyM?: number | null }, fix: { accuracyM?: number | null }): number {\n const a = anchor.accuracyM ?? 0, b = fix.accuracyM ?? 0;\n return 2 * Math.sqrt(a * a + b * b);\n}\n\nexport function spatialEventProfile(intent: string | null | undefined): SpatialEventProfile {\n return SPATIAL_EVENT_PROFILES[intent as SpatialIntent] ?? SPATIAL_EVENT_PROFILES.navigation;\n}\n\ntype Band = 'high' | 'medium' | 'low';\nconst RANK: Record<Band, number> = { high: 3, medium: 2, low: 1 };\ntype PayloadOf<N extends SpatialEvent> = Extract<SpatialStateEvent, { name: N }>['payload'];\n\n/** Distinct fixes a state must be seen on before its hold can complete. */\nconst MIN_FIXES = 2;\n\n/**\n * An unbroken run of one raw state: when it began, and how many distinct fixes\n * (by `position.timestamp`) have shown it. A re-read of the same fix extends\n * the run's time, never its evidence.\n */\ninterface Run<S> { state: S; since: number; fixAt: number | null; fixes: number }\n\nfunction extend<S>(run: Run<S> | null, state: S, fixAt: number | null, now: number): Run<S> {\n if (!run || run.state !== state) return { state, since: now, fixAt, fixes: fixAt === null ? 0 : 1 };\n if (fixAt !== null && fixAt !== run.fixAt) { run.fixAt = fixAt; run.fixes++; }\n return run;\n}\n\nconst settled = (run: Run<unknown>, now: number, holdMs: number): boolean =>\n run.fixes >= MIN_FIXES && now - run.since >= holdMs;\n\n/** Which fix a snapshot was built from — its identity when counting evidence. */\nconst fixAt = (s: SpatialSnapshot): number | null => s.position?.timestamp ?? null;\n\nexport class SpatialEventDetector {\n private profile: SpatialEventProfile;\n\n /** The movement state last settled on. Only MEASURED states settle. */\n private movement: 'stationary' | 'moving' | 'unknown' = 'unknown';\n private run: Run<'stationary' | 'moving' | 'unknown'> | null = null;\n\n /** The completed turn already on record — reported turns, and the one the\n * session opened onto. Only a turn with a new timestamp is news. */\n private lastTurnAt: number | null = null;\n\n /** The GPS band last settled on; null until the session's first band. */\n private band: Band | null = null;\n private bandRun: Run<Band> | null = null;\n\n /** The last position REPORTED — distance is measured from here, not from\n * the previous fix, so a slow walk still adds up to a change. */\n private anchor: SpatialPoint | null = null;\n\n private staleArmed = true;\n\n constructor(profile: SpatialEventProfile = SPATIAL_EVENT_PROFILES.navigation) {\n this.profile = profile;\n }\n\n /**\n * Forget everything — a new session, or replay seeking backwards. Emits\n * nothing. `turnOnRecordAt` is the timestamp of the turn already completed\n * when the session opens (a visual session shares the compass): history, not\n * news. Seeded from the turn itself rather than compared against a clock, so\n * it holds whichever clock the turn was stamped with — epoch live, the\n * recording's epoch in replay, where `now` is virtual.\n */\n reset(profile?: SpatialEventProfile, turnOnRecordAt: number | null = null): void {\n if (profile) this.profile = profile;\n this.movement = 'unknown';\n this.run = null;\n this.lastTurnAt = turnOnRecordAt;\n this.band = null;\n this.bandRun = null;\n this.anchor = null;\n this.staleArmed = true;\n }\n\n /**\n * Feed the newest snapshot. Returns what changed since the last one, in this\n * order: movement, turn, position, confidence, staleness. Fields are read\n * defensively — replay hands over recorded snapshots verbatim, and a recording\n * may lack a block the live engine always fills.\n */\n push(snapshot: SpatialSnapshot, now: number): SpatialStateEvent[] {\n const out: SpatialStateEvent[] = [];\n const emit: Emit = (name, payload) => {\n // The payload is checked against its name by `Emit`; the union cannot\n // follow a generic name through stateEvent's return, hence the assertion.\n out.push(stateEvent('spatial', name, payload as unknown, now) as SpatialStateEvent);\n };\n this.movementFrom(snapshot, now, emit);\n this.turnFrom(snapshot, emit);\n this.positionFrom(snapshot, emit);\n this.confidenceFrom(snapshot, now, emit);\n this.staleFrom(snapshot, emit);\n return out;\n }\n\n private movementFrom(s: SpatialSnapshot, now: number, emit: Emit): void {\n const state = s.movement?.state ?? 'unknown';\n this.run = extend(this.run, state, fixAt(s), now);\n if (state === this.movement) return;\n\n if (state === 'moving' && settled(this.run, now, this.profile.movingHoldMs)) {\n emit('movementStarted', {\n previous: this.movement === 'stationary' ? 'stationary' : 'unknown',\n speedMps: s.movement?.speedMps ?? null,\n bearingDeg: s.movement?.bearingDeg ?? null,\n since: this.run.since,\n });\n this.movement = 'moving';\n } else if (state === 'stationary' && settled(this.run, now, this.profile.stoppedHoldMs)) {\n if (this.movement === 'moving') emit('movementStopped', { previous: 'moving', since: this.run.since });\n this.movement = 'stationary';\n }\n // `unknown` never settles. Losing the speed is not evidence of stopping, so\n // moving → unknown says nothing, and a later measured `stationary` is still\n // reported as the stop it is.\n }\n\n private turnFrom(s: SpatialSnapshot, emit: Emit): void {\n const at = s.changes?.timestamp ?? null;\n const deg = s.changes?.lastMeaningfulTurnDeg ?? null;\n // A compass reset clears the turn to null. That is not a new turn.\n if (at === null || deg === null || at === this.lastTurnAt) return;\n this.lastTurnAt = at;\n emit('lastMeaningfulTurnChanged', { lastMeaningfulTurnDeg: deg, timestamp: at });\n }\n\n private positionFrom(s: SpatialSnapshot, emit: Emit): void {\n const p = s.position ?? null;\n if (!p) return;\n if (!this.anchor) {\n emit('positionChanged', { position: { ...p }, previous: null, distanceM: null });\n this.anchor = p;\n return;\n }\n const d = distanceOf([this.anchor, p]);\n if (d < Math.max(this.profile.minMoveM, moveThresholdM(this.anchor, p))) {\n // Not a move — but if this fix is far more precise than the anchor, keep\n // IT as the reference (silently): measuring later moves from a ±100 m\n // anchor would hide real ones behind the old fix's error.\n const aNew = p.accuracyM ?? null, aOld = this.anchor.accuracyM ?? null;\n if (aNew != null && aOld != null && aNew * 2 < aOld) this.anchor = p;\n return;\n }\n emit('positionChanged', { position: { ...p }, previous: { ...this.anchor }, distanceM: d });\n this.anchor = p;\n }\n\n private confidenceFrom(s: SpatialSnapshot, now: number, emit: Emit): void {\n const p = s.position ?? null;\n if (!p) return;\n const level = fixConfidence(p.accuracyM ?? null);\n // A fix with no accuracy estimate says nothing about the band — the same\n // way the snapshot's confidence ignores an `unknown` part.\n if (level === 'unknown') return;\n // The first band is where the session starts, not a change.\n if (this.band === null) { this.band = level; return; }\n if (level === this.band) { this.bandRun = null; return; }\n this.bandRun = extend(this.bandRun, level, fixAt(s), now);\n if (!settled(this.bandRun, now, this.profile.confidenceHoldMs)) return;\n emit('positionConfidenceChanged', {\n level,\n previous: this.band,\n accuracyM: p.accuracyM ?? null,\n direction: RANK[level] < RANK[this.band] ? 'dropped' : 'recovered',\n });\n this.band = level;\n this.bandRun = null;\n }\n\n private staleFrom(s: SpatialSnapshot, emit: Emit): void {\n const f = s.quality?.freshness;\n if (!f) return;\n // Re-armed only by a FRESH read: `aging` on the way back is not recovery.\n if (f.state === 'fresh') { this.staleArmed = true; return; }\n if (f.state !== 'stale' || !this.staleArmed) return;\n emit('spatialStateStale', { observedAt: f.observedAt, ageMs: f.ageMs });\n this.staleArmed = false;\n }\n}\n\ntype Emit = <N extends SpatialEvent>(name: N, payload: PayloadOf<N>) => void;\n","// ── spatial ──────────────────────────────────────────────────────────────\n// position ✓ path ✓ heading ✓ turns ✓ events ✓ (while a session is open) speed ~derived\nimport type {\n DeviceState,\n SpatialIntent,\n SpatialSnapshot,\n SpatialStateEvent,\n SpatialPoint,\n StateQuality,\n Subscription,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { startHeading, stopHeading, currentHeadingDetail, lastMeaningfulTurn, onTurn } from '../headingSource';\nimport type { DeviceStateCollectors, SpeedRef, WatchAccuracy } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\nimport { distanceOf } from '../../geo';\nimport { listenerSet } from '../events';\nimport {\n SpatialEventDetector,\n fixConfidence,\n movementStateOf,\n spatialEventProfile,\n} from '../spatialEvents';\n\n/**\n * Position, movement, facing and a short recent path.\n *\n * The design's performance rule is that navigation reads come from a WARM\n * native cache, not a fresh GPS+compass call per invocation. There is no such\n * cache today, so `getSnapshot` takes a fix — correct but not cheap. That is\n * why `start()` matters: it opens a `watchPosition` stream AND a compass watch,\n * and every later snapshot serves the newest streamed values instead, which is\n * the warm path this layer can offer without a native module.\n *\n * TWO DIRECTIONS, NOT ONE, and conflating them is the bug this replaced.\n *\n * `orientation.headingDeg` — where the phone is POINTING. The compass.\n * `movement.bearingDeg` — where the user is GOING. Course over ground.\n *\n * They disagree constantly and each answers a different sentence. \"Turn right\n * at the corner\" is decided by the compass; \"you are heading north on Main\n * Street\" by the course. The previous version assigned the same variable to\n * both, which is only right by accident, and that variable was permanently\n * null because the streaming collector dropped the fields.\n *\n * TURNS ARE SIGNED. `changes.lastMeaningfulTurnDeg` is positive for clockwise\n * (right) and negative for anticlockwise (left), so a guide can say which way\n * the user just went without a second field to consult. The freeze names the\n * field but states no convention; this is a decision, recorded here.\n *\n * EVENTS ARE THE SNAPSHOT, DIFFED. `subscribe` publishes what changed between\n * successive warm snapshots — the same ones `getSnapshot` returns — via the\n * pure detector in spatialEvents.ts. It is fed on every fix, the moment a turn\n * completes, and once a second, so a transition that happens between fixes\n * (fixes stopping is itself one: the state goes stale) is still noticed.\n */\n/** The GPS tier each intent's stream runs at. Navigation is the one that needs\n * \"which side of the street\"; everything else trades precision for battery. */\nconst WATCH_ACCURACY: Record<SpatialIntent, WatchAccuracy> = {\n navigation: 'navigation',\n preciseLocate: 'high',\n fitness: 'high',\n geofence: 'balanced',\n lowPowerTracking: 'low',\n};\n\nexport function createSpatialDomain(\n collectors: DeviceStateCollectors,\n speedRef: SpeedRef,\n): DeviceState['spatial'] {\n // The warm cache, when a session is open. Newest last, bounded — an unbounded\n // path on a walking user grows without limit.\n const RECENT_PATH_MAX = 60;\n\n let recentPath: SpatialPoint[] = [];\n let latest: SpatialPoint | null = null;\n /** Speed and course from the newest STREAMED fix — see `latest`. */\n let latestSpeed: number | null = null;\n let latestCourse: number | null = null;\n let stopWatch: (() => void) | null = null;\n /** The running session. A second `start()` joins it, and is told so. */\n let session: { id: string; intent: SpatialIntent; startedAt: number } | null = null;\n\n const detector = new SpatialEventDetector();\n const listeners = listenerSet<SpatialStateEvent>();\n let ticker: ReturnType<typeof setInterval> | null = null;\n let turnSub: Subscription | null = null;\n\n const nonNegative = (v: unknown): number | null =>\n typeof v === 'number' && v >= 0 ? v : null;\n\n /**\n * Speed from the path when the OS declined to estimate one.\n *\n * Only over the last two points and only if they are close in time: two fixes\n * a minute apart give an average that describes neither moment. Returns null\n * rather than a number the caller cannot tell apart from a measured one —\n * the confidence on the snapshot is what marks it as derived.\n */\n const derivedSpeed = (): number | null => {\n if (recentPath.length < 2) return null;\n const b = recentPath[recentPath.length - 1];\n const a = recentPath[recentPath.length - 2];\n const dtS = (b.timestamp - a.timestamp) / 1000;\n if (dtS <= 0 || dtS > 10) return null;\n return distanceOf([a, b]) / dtS;\n };\n\n /** Course from the path, for the same reason and with the same caution. */\n const derivedCourse = (): number | null => {\n if (recentPath.length < 2) return null;\n const b = recentPath[recentPath.length - 1];\n const a = recentPath[recentPath.length - 2];\n if (b.timestamp - a.timestamp > 10_000) return null;\n // Too small a step is noise, not a direction.\n if (distanceOf([a, b]) < 3) return null;\n return bearingBetween(a, b);\n };\n\n /**\n * The snapshot for a fix — the ONE builder, used by the read and by the\n * event detector alike. Synchronous: everything it needs is already in hand.\n */\n const snapshotFrom = (\n point: SpatialPoint,\n measuredSpeed: number | null,\n measuredCourse: number | null,\n ): SpatialSnapshot => {\n // Fall back to the path only where the OS said nothing.\n let speedMps = measuredSpeed;\n let courseDeg = measuredCourse;\n let derived = false;\n if (speedMps === null) { const d = derivedSpeed(); if (d !== null) { speedMps = d; derived = true; } }\n if (courseDeg === null) { const d = derivedCourse(); if (d !== null) { courseDeg = d; derived = true; } }\n\n const heading = currentHeadingDetail();\n const turn = lastMeaningfulTurn();\n\n return {\n position: point,\n movement: {\n speedMps,\n bearingDeg: courseDeg,\n // Needs two points from the SAME session; without a stream there is\n // nothing to measure against, and a distance of 0 would read as\n // \"the user has not moved\".\n distanceRecentM: recentPath.length >= 2 ? distanceOf(recentPath) : null,\n state: movementStateOf(speedMps),\n },\n orientation: {\n headingDeg: heading?.headingDeg ?? null,\n // NOT the calibration rank. iOS reports 0–3 (none…high), which is not\n // a number of degrees, and converting one to the other would be an\n // invented error bar. The rank drives `confidence` below instead.\n headingAccuracyDeg: null,\n },\n changes: {\n lastMeaningfulTurnDeg: turn ? turn.deg : null,\n timestamp: turn ? turn.timestamp : null,\n },\n recentPath: [...recentPath],\n quality: qualityFrom(\n point.timestamp,\n spatialConfidence(point.accuracyM ?? null, heading?.calibration ?? null, derived),\n ),\n };\n };\n\n /** \"No position\" — honest nulls, never a (0,0). The compass may still know\n * which way the phone points, so heading is kept when it has one. */\n const noFix = (): SpatialSnapshot => ({\n position: null,\n movement: { speedMps: null, bearingDeg: null, distanceRecentM: null, state: 'unknown' },\n orientation: { headingDeg: currentHeadingDetail()?.headingDeg ?? null },\n recentPath: [],\n quality: qualityUnknown(),\n });\n\n /** The warm snapshot, or null before the session's first streamed fix. */\n const warmSnapshot = (): SpatialSnapshot | null =>\n latest ? snapshotFrom(latest, latestSpeed, latestCourse) : null;\n\n /**\n * Diff the snapshot a read would get RIGHT NOW against the last one, and\n * publish what changed. Before the first fix there is nothing to diff; a\n * turn completed meanwhile is not lost, it is reported with the first fix.\n */\n const observe = () => {\n const snapshot = warmSnapshot();\n if (!snapshot) return;\n for (const event of detector.push(snapshot, Date.now())) listeners.emit(event);\n };\n\n /**\n * Re-diff once a second while a session is open. Fixes alone are not enough:\n * the OS may pause them while the phone is still, and \"the state has gone\n * stale\" or \"you have been stationary for five seconds\" are exactly the\n * transitions that complete while nothing arrives. A tick re-reads the same\n * fix, so it only completes a state the fixes already showed — it never\n * turns one outlier fix into an event (spatialEvents.ts, MIN_FIXES).\n */\n const startTicking = () => {\n ticker = setInterval(observe, 1000);\n };\n\n const stopTicking = () => {\n if (ticker) clearInterval(ticker);\n ticker = null;\n };\n\n return {\n async getSnapshot(): Promise<SpatialSnapshot> {\n // Serve the warm fix when a session is streaming; otherwise take one.\n const warm = warmSnapshot();\n if (warm) return warm;\n // A session is open but its stream has not delivered yet: say so NOW.\n // A one-shot fix here would make every read wait on a cold GPS (seconds),\n // stalling whatever loop reads us — and the stream answers soon anyway.\n if (session) return noFix();\n\n let point: SpatialPoint;\n let speedMps: number | null;\n let courseDeg: number | null;\n try {\n if (!collectors.location) throw unsupported('location collector');\n const fix: any = await collectors.location.getPosition({ accuracy: 'low' });\n point = {\n latitude: fix.latitude,\n longitude: fix.longitude,\n altitude: fix.altitude ?? null,\n accuracyM: nonNegative(fix.accuracy),\n timestamp: fix.timestamp ?? Date.now(),\n };\n speedMps = nonNegative(fix.speed);\n courseDeg = nonNegative(fix.course);\n } catch {\n // Permission refused, or no fix. Not a position at (0,0).\n return noFix();\n }\n return snapshotFrom(point, speedMps, courseDeg);\n },\n\n /**\n * Open the warm window: position stream + compass + event feed.\n *\n * The intent selects how eager the EVENTS are (spatialEvents.ts profiles)\n * and the GPS TIER the stream runs at (WATCH_ACCURACY): navigation pays the\n * battery for BestForNavigation; low-power tracking does not.\n *\n * A second `start()` while one is open joins the running session and is\n * told ITS intent, not handed back the one it asked for: answering with the\n * caller's intent would claim a profile that is not in force.\n */\n async start(options) {\n if (session) return { ...session };\n if (!collectors.location) throw unsupported('location collector');\n const startedAt = Date.now();\n session = { id: `spatial_${startedAt}`, intent: options.intent, startedAt };\n // A turn the compass completed before this session (a visual session\n // shares it) is already on record, not news.\n detector.reset(spatialEventProfile(options.intent), lastMeaningfulTurn()?.timestamp ?? null);\n // The compass is what makes heading and turn detection possible, and it\n // has to run for the session's whole life: a turn happens between reads,\n // so a compass started on demand would miss exactly the event a guide\n // most needs. Shared with a visual session if one is open.\n void startHeading('spatial');\n // A turn is published the moment it completes, not on the next tick.\n turnSub = onTurn(observe);\n stopWatch = collectors.location.watchPosition((pos: any) => {\n const p: SpatialPoint = {\n latitude: pos.latitude,\n longitude: pos.longitude,\n altitude: pos.altitude ?? null,\n accuracyM: nonNegative(pos.accuracy),\n timestamp: pos.timestamp ?? Date.now(),\n };\n latest = p;\n latestSpeed = nonNegative(pos.speed);\n latestCourse = nonNegative(pos.course);\n speedRef.mps = latestSpeed;\n speedRef.at = p.timestamp;\n recentPath = [...recentPath, p].slice(-RECENT_PATH_MAX);\n observe();\n }, { accuracy: WATCH_ACCURACY[options.intent] ?? 'balanced' });\n startTicking();\n return { ...session };\n },\n\n async stop() {\n stopWatch?.();\n stopWatch = null;\n session = null;\n stopTicking();\n turnSub?.unsubscribe();\n turnSub = null;\n // Listeners are KEPT: a subscription outlives a session, and hears the\n // next one. Only what this session observed is forgotten.\n detector.reset();\n // The path belongs to the session that recorded it. Keeping it would let\n // a later snapshot report movement from a walk that ended hours ago.\n recentPath = [];\n latest = null;\n latestSpeed = null;\n latestCourse = null;\n speedRef.mps = null;\n speedRef.at = 0;\n // Releases only spatial's hold — a visual session keeps the compass.\n stopHeading('spatial');\n },\n\n /**\n * The six spatial events (see `SpatialEvent`). Subscribing opens no sensor:\n * events flow only while a session is open, and a subscription made before\n * `start()` hears that session from its first fix.\n */\n subscribe(listener) {\n return listeners.add(listener);\n },\n };\n}\n\n/**\n * How much to trust this spatial reading.\n *\n * Three things degrade it and they are not interchangeable: a loose GPS fix\n * (the position is vague), an uncalibrated compass (the FACING is vague, which\n * is what breaks \"turn left\"), and values derived from the path rather than\n * measured by the OS. The lowest of the three wins, because a caller acting on\n * the snapshot is exposed to the worst part of it, not the average.\n *\n * This is the signal that decides Ada Guide's modality switch: indoors the fix\n * degrades, confidence drops, and that drop is what makes the guide ask for the\n * camera instead of guessing from a GPS fix it should not trust.\n */\nfunction spatialConfidence(\n accuracyM: number | null,\n calibration: number | null,\n derived: boolean,\n): StateQuality['confidence'] {\n const rank = (v: 'high' | 'medium' | 'low' | 'unknown') =>\n v === 'high' ? 3 : v === 'medium' ? 2 : v === 'low' ? 1 : 0;\n\n // The fix band is shared with positionConfidenceChanged — one set of numbers.\n const fromFix = fixConfidence(accuracyM);\n\n // iOS calibration rank: 3 high, 2 medium, 1 low, 0 none.\n const fromCompass: 'high' | 'medium' | 'low' | 'unknown' =\n calibration === null ? 'unknown'\n : calibration >= 3 ? 'high'\n : calibration >= 2 ? 'medium'\n : 'low';\n\n const parts = [fromFix, fromCompass, ...(derived ? (['low'] as const) : [])];\n const known = parts.filter(p => p !== 'unknown');\n if (!known.length) return { level: 'unknown' };\n const worst = known.reduce((a, b) => (rank(a) <= rank(b) ? a : b));\n return { level: worst };\n}\n\n/** Initial bearing from `a` to `b`, degrees clockwise from north. */\nfunction bearingBetween(a: SpatialPoint, b: SpatialPoint): number {\n const toRad = (d: number) => (d * Math.PI) / 180;\n const la1 = toRad(a.latitude), la2 = toRad(b.latitude);\n const dLon = toRad(b.longitude - a.longitude);\n const y = Math.sin(dLon) * Math.cos(la2);\n const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLon);\n return (((Math.atan2(y, x) * 180) / Math.PI) + 360) % 360;\n}\n","/**\n * The Visual engine — a rolling frame timeline the Visual domain reads.\n *\n * WHY THIS IS SHAPED ODDLY. expo-camera can only produce frames through a\n * MOUNTED `<CameraView>`; there is no headless capture API. So the engine is\n * split in two:\n *\n * this module — the state: the ring, the session, the intent, the cadence.\n * Pure TS, no React, so the domain and tests can use it.\n * VisualEngineHost — a component the app mounts once. When a session is open\n * it mounts the camera and feeds frames in; otherwise it\n * renders nothing and the camera is off.\n *\n * The design's rule is that `getSnapshot()` must be a CACHED read that does not\n * open the camera or take a picture — \"avoid repeatedly starting the camera or\n * capturing a new image for every plugin-tool call during an interactive\n * session\". The ring is what makes that possible: capture runs on the engine's\n * own cadence, and reads serve whatever is already there.\n *\n * THE CADENCE IS THE ENGINE'S CHOICE, NOT THE PLUGIN'S. A plugin declares an\n * INTENT — `navigation`, `document`, `ocr` — and this picks the interval. That\n * is the whole point of the semantic layer: a caller asking for frames every\n * 500ms is specifying an implementation, and cannot know the analysis latency,\n * the battery state or the thermal headroom. Intent survives those changing;\n * a number does not.\n *\n * EACH FRAME CARRIES ITS ORIENTATION — heading, read at capture time. Without\n * it a frame cannot support a guidance instruction: \"turn left at the corner\"\n * needs to know which way the lens faced. Reading it afterwards would pair a\n * frame with a heading from after the user turned, which is how a guide\n * describes the wrong junction with confidence.\n *\n * POSITION IS NOT ON THE FRAME, and that is the freeze's decision, not an\n * omission. VisualFrameRef (API_FREEZE §4) carries `orientation` only; where\n * the user IS belongs to Spatial. A caller that needs both correlates them by\n * `timestamp` — both snapshots carry one. Duplicating position here would\n * create a second copy that can disagree with Spatial's, which is precisely\n * the two-descriptions-of-one-thing problem this whole layer exists to end.\n *\n * NOT IMPLEMENTED, and reported as `unknown`/null rather than guessed:\n * per-frame blur, exposure and obstruction (need image analysis), and\n * pitch/roll (need device-motion, not installed). A frame too dark to use must\n * not be described as `exposure: 'good'` because nothing looked at it.\n */\nimport type { StateQuality } from '@ateam-ai-mobile/plugin-runtime';\n\n/** A frame in the ring. `uri` is a local file the app can read; nothing is uploaded. */\nexport interface VisualFrame {\n handle: { id: string; mimeType: string; expiresAt: number | null };\n timestamp: number;\n /**\n * WHERE THE CAMERA WAS POINTING, stamped at capture time.\n *\n * A frame without this is nearly useless for guidance: \"turn left at the\n * corner\" needs to know which way the lens faced and where the user stood.\n * And it has to be captured WITH the frame, not read afterwards — by the\n * time a snapshot is read the user has walked on, and pairing a frame with a\n * later heading is how a guide describes the wrong junction confidently.\n *\n * Every field is nullable and null means NOT MEASURED. `headingDeg: 0` is due\n * north, so a missing compass must not collapse to 0.\n */\n orientation?: {\n /** True heading if available, else magnetic. Null when the compass said nothing. */\n headingDeg?: number | null;\n /** Not measured — needs device-motion, which is not installed. */\n pitchDeg?: number | null;\n rollDeg?: number | null;\n };\n quality?: { blur?: string; exposure?: string; obstructed?: boolean | null };\n}\n\n/**\n * Supplies the spatial context stamped onto each frame.\n *\n * Injected rather than imported for the same reason the collectors are, and\n * because it must be CHEAP: it is read once per capture, on the engine's\n * cadence. Both getters return null rather than throwing — a frame with no\n * heading is still a usable frame, and losing the picture because the compass\n * was busy would be the wrong trade.\n */\nexport interface VisualContextProvider {\n heading(): { headingDeg: number | null } | null;\n}\n\nexport type VisualIntentName =\n | 'navigation' | 'document' | 'ocr' | 'shopping'\n | 'inspection' | 'accessibility' | 'stream' | 'highQuality';\n\n/**\n * Intent → capture cadence and window.\n *\n * These are the engine's judgement, not the freeze's — the docs say native\n * owns sampling and deliberately do not fix numbers. Recorded here so the\n * reasoning is visible and arguable rather than buried:\n *\n * - `navigation`: a walking user's scene changes continuously, so a steady\n * cadence is right; ~1s is the ceiling at which a guidance instruction is\n * still about the present. Faster mostly queues behind the analysis.\n * - `document` / `ocr`: the subject is held still and the user is aiming. A\n * slow cadence is enough, and a big window is pointless.\n * - `stream`: the fastest we offer, for a caller that really is watching.\n * - `highQuality`: rare, deliberate, single good frame.\n */\nconst PROFILES: Record<VisualIntentName, { intervalMs: number; windowMs: number }> = {\n navigation: { intervalMs: 1000, windowMs: 5000 },\n accessibility: { intervalMs: 1000, windowMs: 5000 },\n stream: { intervalMs: 500, windowMs: 5000 },\n inspection: { intervalMs: 2000, windowMs: 8000 },\n shopping: { intervalMs: 2000, windowMs: 5000 },\n document: { intervalMs: 3000, windowMs: 3000 },\n ocr: { intervalMs: 3000, windowMs: 3000 },\n highQuality: { intervalMs: 5000, windowMs: 5000 },\n};\n\nexport interface VisualSessionState {\n id: string;\n intent: VisualIntentName;\n startedAt: number;\n /** The camera the session reads. The host opens THIS one. */\n facing: 'front' | 'back';\n /** Set while the app is backgrounded. The session SURVIVES; the camera does not. */\n suspended: boolean;\n}\n\ntype Listener = () => void;\n\nclass VisualEngine {\n private frames: VisualFrame[] = [];\n private session: VisualSessionState | null = null;\n private listeners = new Set<Listener>();\n /** Set by the host component while a camera is actually mounted and ready. */\n private cameraReady = false;\n /** Optional: supplies heading/position stamped onto each frame. */\n private context: VisualContextProvider | null = null;\n\n /** Host DI. Without it frames carry no orientation, and say so with nulls. */\n setContextProvider(p: VisualContextProvider | null): void { this.context = p; }\n\n // ── session ──\n\n startSession(intent: VisualIntentName, facing: 'front' | 'back' = 'back'): VisualSessionState {\n if (this.session) {\n if (this.session.facing === facing) return this.session;\n // Same session, other camera. A NEW object, so a host holding the old one\n // sees the change; the frames are dropped because they came from the\n // other camera.\n this.session = { ...this.session, facing };\n this.frames = [];\n this.cameraReady = false;\n this.notify();\n return this.session;\n }\n this.session = {\n id: `visual_${Date.now()}`,\n intent: PROFILES[intent] ? intent : 'navigation',\n startedAt: Date.now(),\n facing,\n suspended: false,\n };\n this.notify();\n return this.session;\n }\n\n stopSession(): void {\n this.session = null;\n // The timeline belongs to the session. Keeping it would let a later read\n // describe a room the user left.\n this.frames = [];\n this.cameraReady = false;\n this.notify();\n }\n\n /**\n * Background: suspend, do not end.\n *\n * iOS tears the camera down when the app leaves the foreground. The session\n * is a STATE that survives that; what does not survive is the claim that the\n * camera is live. While suspended the ring is kept but ages — a read gets\n * old frames clearly marked old, rather than a fresh-looking lie.\n */\n suspend(): void {\n if (!this.session || this.session.suspended) return;\n this.session.suspended = true;\n this.cameraReady = false;\n this.notify();\n }\n\n resume(): void {\n if (!this.session?.suspended) return;\n this.session.suspended = false;\n this.notify();\n }\n\n getSession(): VisualSessionState | null { return this.session; }\n\n /** Is a camera mounted and ready right now? Only the host knows. */\n setCameraReady(ready: boolean): void {\n this.cameraReady = ready;\n this.notify();\n }\n isCameraReady(): boolean { return this.cameraReady && !this.session?.suspended; }\n\n // ── the ring ──\n\n profile() {\n const intent = this.session?.intent || 'navigation';\n return PROFILES[intent];\n }\n\n /**\n * Feed from the native vision session (VisionSession, iOS) — the FAST frame\n * source that replaces the takePictureAsync loop. The frame is IN-MEMORY: the\n * native ring holds the pixels keyed by `id`, and native.vision resolves that\n * id inside the trusted layer. No `uri` crosses here; the quality is a REAL\n * measurement (not `unknown`), and `expiresAt` is the ring's real eviction time.\n * The public VisualFrameRef strip to id-only is owned by the device-state session.\n */\n pushFrameMeta(meta: {\n id: string;\n timestamp: number;\n expiresAt?: number | null;\n quality?: { blur?: string; exposure?: string; obstructed?: boolean | null };\n }): void {\n let heading: { headingDeg: number | null } | null = null;\n try { heading = this.context?.heading() ?? null; } catch { /* a frame without a heading beats no frame */ }\n\n this.frames.push({\n // In-memory: no file. native.vision resolves the id inside the native ring.\n handle: { id: meta.id, mimeType: 'image/none', expiresAt: meta.expiresAt ?? null },\n timestamp: meta.timestamp,\n orientation: { headingDeg: heading?.headingDeg ?? null, pitchDeg: null, rollDeg: null },\n quality: meta.quality ?? { blur: 'unknown', exposure: 'unknown', obstructed: null },\n });\n this.evict();\n this.notify();\n }\n\n // The native ring's actual retention (ms). The engine's effective window is\n // min(intent window, retention) — set by whoever owns the clamp semantics; this\n // just stores the input so that logic has one source of truth to read.\n private _retentionMs: number | null = null;\n setRetentionMs(ms: number | null): void { this._retentionMs = ms && ms > 0 ? ms : null; }\n retentionMs(): number | null { return this._retentionMs; }\n\n /**\n * How far back the timeline ACTUALLY reaches: `min(intent window, native\n * retention)`.\n *\n * The intent asks for a window; native decides what it can afford to keep.\n * Full-res luma at ~24fps is tens of megabytes for a few seconds, so the ring\n * is memory-bounded and shrinks under pressure — `retentionMs` is native\n * publishing what it is really holding.\n *\n * EVERY FRAME IN THE TIMELINE CAN BE ANALYSED. That is the one sentence a\n * plugin author has to hold, and this clamp is what makes it true. The\n * alternative — listing frames past retention marked \"not analysable\" — was\n * considered and rejected: a caller would have to check a flag before every\n * use, and forgetting throws mid-guidance on a frame the model can see\n * listed. Present-but-unusable is the same trap as an empty rowset reading\n * as \"no rows matched\".\n *\n * If per-frame history beyond retention is ever needed, the shape is an\n * AGGREGATE on the snapshot (\"the view has been unusable for N ms\"), never a\n * per-entry flag.\n */\n effectiveWindowMs(): number {\n const intent = this.profile().windowMs;\n const retention = this._retentionMs;\n return retention === null ? intent : Math.min(intent, retention);\n }\n\n /**\n * Time-based, not count-based: the design specifies a rolling WINDOW.\n *\n * Evicts on the EFFECTIVE window, so a frame whose pixels native has already\n * dropped leaves the timeline at the same moment. Without this, `frameById`\n * would start returning null for frames still being listed.\n */\n private evict(): void {\n const cutoff = Date.now() - this.effectiveWindowMs();\n this.frames = this.frames.filter(f => f.timestamp >= cutoff);\n }\n\n timeline(): { windowMs: number; frames: VisualFrame[] } {\n this.evict();\n // The EFFECTIVE window, never the intent's. Reporting the intent would have\n // a guide reason about 8 seconds of history it does not have — precisely\n // the confusion the clamp exists to prevent.\n return { windowMs: this.effectiveWindowMs(), frames: [...this.frames] };\n }\n\n latest(): VisualFrame | null {\n this.evict();\n return this.frames.length ? this.frames[this.frames.length - 1] : null;\n }\n\n /**\n * Resolve a frame handle id (from a VisualFrameRef) back to the live frame so\n * the mechanical layer — native.vision — can read its on-device file. The\n * whole contract of that layer is that it takes an id, never pixels; this is\n * where an id becomes a `handle.uri` it can open. Returns null when the id is\n * unknown OR its frame has aged out of the rolling window (the cache file may\n * also be gone by then) — a real \"I can't look at that frame\" answer, which\n * the caller must treat as distinct from \"I looked and found nothing\".\n */\n frameById(id: string): VisualFrame | null {\n this.evict();\n return this.frames.find(f => f.handle.id === id) ?? null;\n }\n\n /**\n * Record a MEASURED quality back onto a frame (native.vision.assessQuality).\n * Frames are minted with `quality: unknown` because nothing looks at the\n * pixels at capture time; this is how a real measurement replaces that so a\n * later read of VisualFrameRef.quality reflects it. No-op if the frame has\n * aged out — we never resurrect an evicted frame just to label it.\n */\n setFrameQuality(id: string, quality: NonNullable<VisualFrame['quality']>): void {\n const f = this.frames.find(fr => fr.handle.id === id);\n if (!f) return;\n f.quality = quality;\n this.notify();\n }\n\n // ── change notification ──\n\n subscribe(fn: Listener): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n private notify(): void {\n for (const fn of this.listeners) { try { fn(); } catch { /* a bad listener is not the engine's problem */ } }\n }\n}\n\n/** One engine per app. The camera is a singleton resource; so is this. */\nexport const visualEngine = new VisualEngine();\n\n/** Freshness for a visual read — exported so the domain and host agree. */\nexport function frameQuality(frame: VisualFrame | null): StateQuality {\n if (!frame) return { freshness: { state: 'unknown', observedAt: null, ageMs: null } };\n const ageMs = Date.now() - frame.timestamp;\n return {\n freshness: {\n // Tighter than the other domains on purpose: a two-second-old frame of a\n // moving scene is not \"fresh\" in any useful sense.\n state: ageMs < 2000 ? 'fresh' : ageMs < 10_000 ? 'aging' : 'stale',\n observedAt: frame.timestamp,\n ageMs,\n },\n confidence: { level: 'unknown' },\n };\n}\n","// ── visual ───────────────────────────────────────────────────────────────\n// frames ✓ heading ✓ freshness ✓ blur/exposure ✗\n//\n// Visual owns the semantic frame timeline/retention: the rolling ring, opaque\n// frame ids, and the freshness the frames carry (frameQuality) — see\n// visualEngine.ts. Opaque ids only above L1; pixels never cross to JS.\nimport type { DeviceState, VisualSnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { visualEngine, frameQuality } from '../visualEngine';\nimport { currentHeading, startHeading, stopHeading } from '../headingSource';\nimport { unsupported } from '../quality';\nimport { stateEvent } from '../events';\n\n/**\n * The camera block of a visual snapshot — one builder, so the read and the\n * change event cannot describe the camera two ways.\n */\nfunction cameraState(): VisualSnapshot['camera'] {\n const session = visualEngine.getSession();\n return {\n // `available` = the app could use a camera at all. `active` = one is\n // running RIGHT NOW. They differ exactly when it matters: a pocketed\n // phone is available but not active, and answering `true` to both\n // would let a guide speak about a scene nothing is watching.\n available: !!session,\n active: visualEngine.isCameraReady(),\n facing: session ? session.facing : 'unknown',\n };\n}\n\n/**\n * Reads the rolling timeline the engine maintains. See visualEngine.ts for why\n * the capture half lives in a mounted component.\n *\n * `getSnapshot` NEVER opens the camera and never takes a picture — that is the\n * design's explicit performance rule, and the reason a guidance loop can ask\n * every turn without paying for it. What it costs is one array read.\n */\nexport function createVisualDomain(): DeviceState['visual'] {\n return {\n async getSnapshot(): Promise<VisualSnapshot> {\n const latest = visualEngine.latest();\n const timeline = visualEngine.timeline();\n\n return {\n camera: cameraState(),\n // null, not a stale frame dressed as current. A caller with no usable\n // frame must see nothing rather than something old.\n latest: latest as any,\n recentTimeline: timeline as any,\n quality: frameQuality(latest),\n };\n },\n\n async start(options) {\n const facing = options?.facing ?? 'back';\n if (facing !== 'front' && facing !== 'back') {\n // Refused, never quietly swapped for the other camera.\n throw unsupported(`visual.start facing \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n const s = visualEngine.startSession((options?.intent as any) || 'navigation', facing);\n // Warm the compass alongside the camera. Started here rather than at app\n // launch: a magnetometer subscription running all day for a feature\n // nobody opened is exactly the kind of cost this layer is meant to avoid.\n visualEngine.setContextProvider({ heading: currentHeading });\n void startHeading('visual');\n return { id: s.id, intent: s.intent, startedAt: s.startedAt } as any;\n },\n\n async stop() {\n visualEngine.stopSession();\n visualEngine.setContextProvider(null);\n // Releases only visual's hold — a spatial session keeps the compass.\n stopHeading('visual');\n },\n\n /**\n * A deliberate one-shot. Distinct from getSnapshot because it is an\n * OPERATION: it waits for a frame rather than serving the newest one.\n *\n * Throws when it cannot, rather than returning an empty result — the\n * design is explicit that capture is an operation and reads are state.\n */\n async capture() {\n if (!visualEngine.getSession()) {\n throw unsupported('visual.capture without an open session — call start({ intent }) first');\n }\n if (!visualEngine.isCameraReady()) {\n const err: any = new Error(\n 'The camera is not running right now — the app may be in the background.',\n );\n err.code = 'TEMPORARILY_UNAVAILABLE';\n throw err;\n }\n // Wait briefly for the NEXT frame rather than returning the last one:\n // \"capture\" means now, and the newest ring entry may be a second old.\n const before = visualEngine.latest()?.timestamp ?? 0;\n const deadline = Date.now() + 4000;\n while (Date.now() < deadline) {\n const f = visualEngine.latest();\n if (f && f.timestamp > before) return { frame: f } as any;\n await new Promise(r => setTimeout(r, 100));\n }\n const err: any = new Error('No frame arrived within 4s.');\n err.code = 'TIMEOUT';\n throw err;\n },\n\n /**\n * Fires when the CAMERA block changes — a session opening or closing, the\n * camera coming up or going down. The engine notifies on every frame too,\n * and forwarding that made this a raw frame stream, which the freeze's core\n * rule 4 forbids. A frame arriving is what `getSnapshot()` is for.\n */\n subscribe(listener) {\n let last = JSON.stringify(cameraState());\n const off = visualEngine.subscribe(() => {\n const camera = cameraState();\n const key = JSON.stringify(camera);\n if (key === last) return;\n last = key;\n listener(stateEvent('visual', 'visualStateChanged', { camera }));\n });\n return { unsubscribe: off };\n },\n };\n}\n","/**\n * The Activity engine — steps and motion, sampled into short windows.\n *\n * Sensors only. Every judgement made from them lives in `activityClassifier`,\n * which is pure and tested; this file's whole job is to keep two subscriptions\n * alive and turn their streams into the three numbers the classifier wants.\n *\n * IT IS SESSION-SCOPED, and that is a power decision rather than a stylistic\n * one. An accelerometer left running is a background drain and a pedometer\n * subscription is a permission-visible one, so neither starts until a plugin\n * declares it needs Activity. `getSnapshot()` with no session open returns an\n * explicit unknown rather than quietly switching the sensors on — a read must\n * not have that side effect.\n */\nimport type { HumanActivityValue } from './activityClassifier';\n\n/**\n * expo-sensors is loaded LAZILY, on the first `start()`, and never at import.\n *\n * This module is reachable from app startup, and expo-sensors is a NATIVE\n * module: an app binary built before it was added does not contain it. A\n * top-level import that threw during evaluation would not degrade the Activity\n * domain — it would fail the bundle and black-screen the whole app, on exactly\n * the builds that predate this change. So the failure is contained here, where\n * it can be reported as \"no sensors\" instead.\n */\nlet sensors: any = null;\nlet sensorsMissing = false;\n\nfunction loadSensors(): any {\n if (sensors || sensorsMissing) return sensors;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n sensors = require('expo-sensors');\n } catch {\n sensorsMissing = true;\n sensors = null;\n }\n return sensors;\n}\n\n/** Accelerometer cadence. 5 Hz is plenty for pose and for a variance band. */\nconst ACCEL_INTERVAL_MS = 200;\n/** Motion window. Long enough to smooth a stride, short enough to be current. */\nconst MOTION_WINDOW_MS = 3000;\n/**\n * Step window.\n *\n * 20s, because cadence over a shorter window swings wildly at the start and end\n * of walking — someone who takes four steps and stops would otherwise read as\n * a steady 120 spm for a moment.\n */\nconst STEP_WINDOW_MS = 20_000;\n/** Low-pass coefficient isolating gravity from the raw accelerometer. */\nconst GRAVITY_ALPHA = 0.1;\n\ninterface StepSample { steps: number; t: number }\n\nclass ActivityEngine {\n private accelSub: { remove(): void } | null = null;\n private stepSub: { remove(): void } | null = null;\n\n private magnitudes: Array<{ m: number; t: number }> = [];\n private gravity: { x: number; y: number; z: number } | null = null;\n private steps: StepSample[] = [];\n\n private sessionId: string | null = null;\n private startedAt = 0;\n /** When the CURRENT reading last changed — feeds `human.since`. */\n private pedometerAvailable: boolean | null = null;\n\n isRunning(): boolean { return this.sessionId !== null; }\n session(): { id: string; startedAt: number } | null {\n return this.sessionId ? { id: this.sessionId, startedAt: this.startedAt } : null;\n }\n\n /**\n * @param classify how to name the current activity — injected so the engine\n * keeps no opinion of its own and the classifier stays pure and testable.\n */\n async start(classify: () => HumanActivityValue): Promise<{ id: string; startedAt: number }> {\n if (this.sessionId) return this.session()!;\n this.sessionId = `activity_${Date.now()}`;\n this.startedAt = Date.now();\n\n const s = loadSensors();\n if (!s) {\n // An app binary built before expo-sensors was added. The session is open\n // — stopping it would be a lie in the other direction — but nothing\n // samples, so every window stays empty and the classifier says `unknown`.\n this.pedometerAvailable = false;\n this.startTicking(classify);\n return this.session()!;\n }\n const { Accelerometer, Pedometer } = s;\n\n Accelerometer.setUpdateInterval(ACCEL_INTERVAL_MS);\n this.accelSub = Accelerometer.addListener(({ x, y, z }: { x: number; y: number; z: number }) => {\n const now = Date.now();\n // Raw magnitude includes gravity, so a still phone sits at ~1g and the\n // variance of this series is the motion signal.\n this.magnitudes.push({ m: Math.sqrt(x * x + y * y + z * z), t: now });\n const cutoff = now - MOTION_WINDOW_MS;\n if (this.magnitudes.length > 8 && this.magnitudes[0].t < cutoff) {\n this.magnitudes = this.magnitudes.filter(s => s.t >= cutoff);\n }\n // Gravity by low-pass: what survives smoothing is the constant pull, and\n // that is what says which way up the phone is.\n this.gravity = this.gravity\n ? {\n x: this.gravity.x + GRAVITY_ALPHA * (x - this.gravity.x),\n y: this.gravity.y + GRAVITY_ALPHA * (y - this.gravity.y),\n z: this.gravity.z + GRAVITY_ALPHA * (z - this.gravity.z),\n }\n : { x, y, z };\n });\n\n try {\n this.pedometerAvailable = await Pedometer.isAvailableAsync();\n } catch {\n this.pedometerAvailable = false;\n }\n if (this.pedometerAvailable) {\n try {\n this.stepSub = Pedometer.watchStepCount(({ steps }: { steps: number }) => {\n this.steps.push({ steps, t: Date.now() });\n const cutoff = Date.now() - STEP_WINDOW_MS;\n if (this.steps.length > 2 && this.steps[0].t < cutoff) {\n this.steps = this.steps.filter(s => s.t >= cutoff);\n }\n });\n } catch {\n // Permission refused, most likely (iOS Motion & Fitness). Cadence stays\n // null, the classifier says `unknown`, and nothing pretends otherwise.\n this.stepSub = null;\n this.pedometerAvailable = false;\n }\n }\n\n this.startTicking(classify);\n return this.session()!;\n }\n\n stop(): void {\n this.accelSub?.remove();\n this.stepSub?.remove();\n this.accelSub = null;\n this.stepSub = null;\n this.sessionId = null;\n this.stopTicking();\n // The windows belong to the session. Keeping them would let a later read\n // report a walk that finished before the phone went in a drawer.\n this.magnitudes = [];\n this.steps = [];\n this.gravity = null;\n }\n\n /** Variance of accelerometer magnitude over the motion window, in g². */\n accelVariance(): number | null {\n const cutoff = Date.now() - MOTION_WINDOW_MS;\n const window = this.magnitudes.filter(s => s.t >= cutoff);\n // Two samples is not a variance; it is a coincidence.\n if (window.length < 5) return null;\n const mean = window.reduce((a, s) => a + s.m, 0) / window.length;\n return window.reduce((a, s) => a + (s.m - mean) ** 2, 0) / window.length;\n }\n\n currentGravity(): { x: number; y: number; z: number } | null { return this.gravity; }\n\n /**\n * Steps per minute over the step window.\n *\n * Computed from the DIFFERENCE between the first and last cumulative counts,\n * so it does not matter whether the platform counts from subscription start\n * or from midnight.\n *\n * Returns null — not 0 — when the window is too short to divide by. Zero\n * steps per minute is a real claim (\"standing still\") and must only be made\n * once there is a window long enough to support it.\n */\n cadenceSpm(): number | null {\n const cutoff = Date.now() - STEP_WINDOW_MS;\n const window = this.steps.filter(s => s.t >= cutoff);\n if (window.length < 2) {\n // The pedometer is alive and has simply reported nothing recently, which\n // is what standing still looks like: no callbacks at all.\n if (this.pedometerAvailable && Date.now() - this.startedAt > STEP_WINDOW_MS) return 0;\n return null;\n }\n const first = window[0], last = window[window.length - 1];\n const minutes = (last.t - first.t) / 60_000;\n if (minutes <= 0) return null;\n return (last.steps - first.steps) / minutes;\n }\n\n /** Whether a pedometer exists and is permitted. Null before `start()`. */\n hasPedometer(): boolean | null { return this.pedometerAvailable; }\n\n /** When the newest sample arrived — what freshness is actually about. */\n observedAt(): number | null {\n const ts = [\n this.magnitudes[this.magnitudes.length - 1]?.t,\n this.steps[this.steps.length - 1]?.t,\n ].filter((t): t is number => typeof t === 'number');\n return ts.length ? Math.max(...ts) : null;\n }\n\n // ── transitions ──\n //\n // Tracked on a timer inside the engine rather than computed when someone\n // reads, because a transition is an EVENT: \"you started walking\" happens\n // between two reads, and a domain that only classifies on demand reports the\n // state and loses the moment it changed. `since` has the same problem.\n\n private current: HumanActivityValue = 'unknown';\n private since: number | null = null;\n private transitions: Array<{ name: string; timestamp: number }> = [];\n private ticker: ReturnType<typeof setInterval> | null = null;\n\n /** Feed the engine its own classification once a second. */\n private startTicking(classify: () => HumanActivityValue): void {\n this.ticker = setInterval(() => {\n const next = classify();\n if (next === this.current) return;\n this.current = next;\n this.since = Date.now();\n // The transition NAME reuses the frozen HumanActivity vocabulary — it is\n // the state being entered. The freeze leaves transition names as bare\n // strings with no controlled vocabulary, and inventing one (\"walk_start\")\n // would put a word in plugins that the freeze may later contradict.\n this.transitions = [...this.transitions, { name: next, timestamp: this.since }].slice(-10);\n }, 1000);\n }\n\n private stopTicking(): void {\n if (this.ticker) clearInterval(this.ticker);\n this.ticker = null;\n this.current = 'unknown';\n this.since = null;\n this.transitions = [];\n }\n\n /** When the current activity began, or null if it has not been established. */\n currentSince(): number | null { return this.since; }\n /** Newest last, bounded. Empty is a real answer: nothing has changed yet. */\n recentTransitions(): Array<{ name: string; timestamp: number }> {\n return [...this.transitions];\n }\n}\n\n/** One engine per app: the sensors are singleton hardware. */\nexport const activityEngine = new ActivityEngine();\n","/**\n * What the user is doing, and how the phone is being held — as a pure function.\n *\n * Separated from the sensor plumbing so the JUDGEMENTS are testable: every\n * threshold below is a claim about human beings, and a claim that decides\n * whether a guide says \"you have arrived\" to someone sitting in a moving car\n * deserves a test rather than a comment.\n *\n * WHY THIS DOMAIN EXISTS AT ALL WHEN SPATIAL ALREADY REPORTS MOVEMENT: GPS is\n * what fails indoors, and indoors is where a guide most needs to know whether\n * the user is walking. Steps come from the pedometer, which does not care about\n * a roof. Spatial answers \"are you moving through the world\"; Activity answers\n * \"are you walking\" — in a mall those are different questions with different\n * sensors, and only one of them still works.\n */\nimport type { Confidence } from '@ateam-ai-mobile/plugin-runtime';\n\nexport type HumanActivityValue =\n | 'stationary' | 'walking' | 'running' | 'cycling' | 'driving' | 'unknown';\nexport type PlacementValue = 'handheld' | 'pocket' | 'table' | 'mounted' | 'unknown';\nexport type PoseOrientation = 'portrait' | 'landscape' | 'flat' | 'unknown';\n\nexport interface ActivityInput {\n /** Steps per minute over the recent window. `null` = no pedometer reading. */\n cadenceSpm: number | null;\n /**\n * Variance of accelerometer magnitude (g²) over the recent window.\n * `null` = the accelerometer has not reported yet.\n */\n accelVariance: number | null;\n /** Low-pass gravity estimate in g. `null` = not sampled yet. */\n gravity: { x: number; y: number; z: number } | null;\n /**\n * Speed from Spatial, when a spatial session is streaming.\n *\n * Passed IN rather than read here, so this stays pure. It exists for exactly\n * one job: a phone in a moving car looks identical to a phone on a desk —\n * no steps, little vibration — and calling that `stationary` would tell a\n * guide the user has arrived while they are doing 50 km/h.\n */\n speedMps: number | null;\n}\n\nexport interface ActivityVerdict {\n human: { state: HumanActivityValue; confidence: Confidence };\n pose: { placement: PlacementValue; orientation: PoseOrientation; confidence: Confidence };\n}\n\n/**\n * Cadence bands, in steps per minute.\n *\n * Ordinary walking sits around 100–120 spm and running around 160–180. 140 is\n * placed in the gap rather than at either edge: misreading a fast walk as a run\n * is harmless, but the reverse would have a guide give walking directions to\n * someone covering ground twice as fast.\n *\n * 20 spm is the floor for \"walking at all\" — below that it is a few steps of\n * shuffling, which is not travel.\n */\nconst RUN_SPM = 140;\nconst WALK_SPM = 20;\n\n/**\n * Accelerometer magnitude variance, in g².\n *\n * A phone lying on a table reads a near-constant 1g, so its variance is\n * essentially sensor noise. A hand holding a phone never manages that.\n */\nconst STILL_VAR = 0.002;\n/** Above this the phone is being moved about, not merely held. */\nconst ACTIVE_VAR = 0.02;\n\n/**\n * Speed at which the user is certainly not walking, in m/s.\n *\n * 3 m/s is ~11 km/h — above a fast walk (~2 m/s) and below anything a vehicle\n * does in traffic.\n */\nconst VEHICLE_MPS = 3;\n\n/** Gravity component that has to dominate before an axis names the pose. */\nconst AXIS_DOMINANT = 0.75;\n\nexport function classifyActivity(input: ActivityInput): ActivityVerdict {\n return { human: classifyHuman(input), pose: classifyPose(input) };\n}\n\nfunction classifyHuman({ cadenceSpm, accelVariance, speedMps }: ActivityInput): ActivityVerdict['human'] {\n // Steps are the strongest evidence there is, and the only one that survives\n // indoors. Take them first when they exist.\n if (cadenceSpm !== null && cadenceSpm >= RUN_SPM) {\n return { state: 'running', confidence: { level: 'high' } };\n }\n if (cadenceSpm !== null && cadenceSpm >= WALK_SPM) {\n return { state: 'walking', confidence: { level: 'high' } };\n }\n\n // No meaningful stepping. Before calling it stationary, rule out a vehicle:\n // riding is not walking, and it is not standing still either.\n if (speedMps !== null && speedMps >= VEHICLE_MPS) {\n // Moving fast without stepping. Which vehicle it is cannot be told from an\n // accelerometer and a pedometer — separating cycling from driving needs\n // real activity recognition (CMMotionActivity), which is not available\n // here. `unknown` with the speed already published by Spatial is honest;\n // guessing `driving` because it is the commoner case is not.\n return { state: 'unknown', confidence: { level: 'low' } };\n }\n\n if (accelVariance !== null && accelVariance < STILL_VAR && (cadenceSpm === null || cadenceSpm < WALK_SPM)) {\n // Genuinely still. Medium rather than high: a phone resting in a\n // stationary car at a red light produces exactly this, and Spatial's speed\n // cannot separate them either while the car is stopped.\n return { state: 'stationary', confidence: { level: 'medium' } };\n }\n\n // Something is moving but nothing identifies it — a phone being handled\n // while its owner stands still, most often. Naming an activity here would be\n // a guess dressed as a reading.\n return { state: 'unknown', confidence: { level: 'unknown' } };\n}\n\nfunction classifyPose({ gravity, accelVariance }: ActivityInput): ActivityVerdict['pose'] {\n if (!gravity) {\n return { placement: 'unknown', orientation: 'unknown', confidence: { level: 'unknown' } };\n }\n\n const { x, y, z } = gravity;\n const orientation: PoseOrientation =\n Math.abs(z) >= AXIS_DOMINANT ? 'flat'\n : Math.abs(y) >= AXIS_DOMINANT ? 'portrait'\n : Math.abs(x) >= AXIS_DOMINANT ? 'landscape'\n : 'unknown';\n\n // Flat and utterly still is a surface. This is the one placement the sensors\n // can actually establish.\n if (orientation === 'flat' && accelVariance !== null && accelVariance < STILL_VAR) {\n return { placement: 'table', orientation, confidence: { level: 'high' } };\n }\n\n // Upright and moving a little is a hand. Medium, because a phone in a shirt\n // pocket looks much the same and this cannot tell them apart.\n if (orientation !== 'flat' && accelVariance !== null && accelVariance >= ACTIVE_VAR) {\n return { placement: 'handheld', orientation, confidence: { level: 'medium' } };\n }\n\n // `pocket` and `mounted` are NEVER returned. Distinguishing a pocket needs\n // the proximity or light sensor, and a car mount needs Bluetooth or CarPlay\n // context — none of which is wired. The orientation is still reported,\n // because it was actually measured.\n return { placement: 'unknown', orientation, confidence: { level: 'unknown' } };\n}\n","// ── activity ─────────────────────────────────────────────────────────────\n// THE INDOOR MOVEMENT SIGNAL (Spatial stops working indoors).\nimport type { DeviceState, ActivitySnapshot } from '@ateam-ai-mobile/plugin-runtime';\nimport { activityEngine } from '../activityEngine';\nimport { classifyActivity, type ActivityInput } from '../activityClassifier';\nimport type { SpeedRef } from '../collectors';\nimport { qualityFrom, qualityUnknown, unsupported } from '../quality';\n\n/**\n * What the user is doing, and how the phone is being held.\n *\n * THIS IS THE INDOOR MOVEMENT SIGNAL. Spatial answers \"are you moving through\n * the world\" and stops being able to indoors, which is exactly where a guide\n * most needs to know whether its user is walking. The pedometer does not care\n * about a roof.\n *\n * SESSION-SCOPED, unlike the passive domains. An accelerometer left running is\n * a background drain and a pedometer subscription is permission-visible on\n * iOS, so nothing samples until a plugin says it needs this. A `getSnapshot()`\n * with no session open returns an explicit unknown rather than switching the\n * sensors on — a read must not have that side effect, and one that did would\n * also return junk, since the windows would be empty at the moment it started.\n */\nexport function createActivityDomain(speedRef: SpeedRef): DeviceState['activity'] {\n /** A speed older than this says nothing about now. */\n const SPEED_MAX_AGE_MS = 15_000;\n\n const input = (): ActivityInput => ({\n cadenceSpm: activityEngine.cadenceSpm(),\n accelVariance: activityEngine.accelVariance(),\n gravity: activityEngine.currentGravity(),\n speedMps:\n speedRef.at && Date.now() - speedRef.at < SPEED_MAX_AGE_MS ? speedRef.mps : null,\n });\n\n return {\n async getSnapshot(): Promise<ActivitySnapshot> {\n if (!activityEngine.isRunning()) {\n // Nothing is sampling. Every field says so rather than describing a\n // motionless phone, which is what an unstarted engine would look like.\n return {\n human: { state: 'unknown', since: null, confidence: { level: 'unknown' } },\n devicePose: { placement: 'unknown', orientation: 'unknown', confidence: { level: 'unknown' } },\n recentTransitions: [],\n quality: qualityUnknown(),\n };\n }\n\n const verdict = classifyActivity(input());\n const observedAt = activityEngine.observedAt();\n const transitions = activityEngine.recentTransitions();\n\n return {\n human: {\n state: verdict.human.state,\n since: activityEngine.currentSince(),\n confidence: verdict.human.confidence,\n },\n devicePose: {\n placement: verdict.pose.placement,\n orientation: verdict.pose.orientation,\n confidence: verdict.pose.confidence,\n },\n transition: {\n current: verdict.human.state,\n // A hint, and we have no basis for one. The freeze calls `likelyNext`\n // advisory; advisory is not licence to invent it.\n likelyNext: null,\n },\n recentTransitions: transitions,\n quality: observedAt === null\n ? qualityUnknown()\n : qualityFrom(observedAt, verdict.human.confidence),\n };\n },\n\n /**\n * Start sampling. The intent is recorded but does not yet change strategy —\n * there is no native power-tuning layer for it to reach.\n */\n async start(options) {\n const s = await activityEngine.start(() => classifyActivity(input()).human.state);\n return { id: s.id, intent: options.intent, startedAt: s.startedAt } as any;\n },\n\n async stop() { activityEngine.stop(); },\n\n // No activity event vocabulary is frozen — the design notes there is no\n // \"Initial Activity Events\" list at all — so naming events here would put\n // words in plugins that the freeze may contradict.\n subscribe() { throw unsupported('activity.subscribe'); },\n };\n}\n","/**\n * Device Bridge v2 — the runtime behind `props.deviceState`.\n *\n * R5 split the former monolith into one module per domain (./domains/*), a\n * single quality module that owns freshness/confidence/unknown (./quality), and\n * this composition root, which wires the domains and injects their dependencies.\n * The behaviour — and every snapshot shape — is unchanged; only the file layout is.\n *\n * WHAT IS AND IS NOT HERE — stated up front, because a half-built state layer\n * that pretends to be whole is the exact failure this whole effort exists to\n * end:\n *\n * deviceContext power ✓ thermal ✗ memory/storage ✗\n * connectivity reachability ✓ transport ✓ quality/stability/metered ✗\n * capability permission ✓ availability ✗ restriction ✗\n * interaction app lifecycle ✓ screenOn ✗ audio ✗\n * spatial position ✓ heading ✓ turns ✓ path/events ✓ (while a session is open) speed ~derived\n * visual frames ✓ heading ✓ freshness ✓ blur/exposure ✗\n * activity walking/running/stationary ✓ pose ✓ cycling/driving ✗ events ✗\n * environment NOT IMPLEMENTED AT ALL\n *\n * Everything marked ✗ reports `unknown`, which is a real answer in this design\n * and the only honest one. It never reports a plausible default: an invented\n * 'nominal' thermal state is indistinguishable from a measured one, and that is\n * how 56 fabrications reached users from the layer below.\n *\n * A domain with no engine here is not simply missing: `gateDeviceState` (in\n * plugin-runtime/src/deviceStateHost.ts) fills the gap with a domain whose\n * reads answer explicit `unknown`s and whose ACTIONS throw. So a plugin can\n * call any of the eight without an existence check, and still cannot mistake\n * \"not built\" for \"nothing to report\". `environment` is deliberately absent\n * from the composition below for exactly that reason.\n */\nimport type { DeviceState } from '@ateam-ai-mobile/plugin-runtime';\nimport { DeviceStateCollectors, SpeedRef } from './collectors';\nimport { createDeviceContextDomain } from './domains/deviceContext';\nimport { createConnectivityDomain } from './domains/connectivity';\nimport { createCapabilityDomain } from './domains/capability';\nimport { createInteractionDomain } from './domains/interaction';\nimport { createSpatialDomain } from './domains/spatial';\nimport { createVisualDomain } from './domains/visual';\nimport { createActivityDomain } from './domains/activity';\n\nexport type { DeviceStateCollectors, SpeedRef } from './collectors';\n\n/**\n * Build the semantic surface.\n *\n * Only implemented domains appear. `environment` is deliberately absent: a\n * plugin touching it gets a TypeError naming it, which is a far better failure\n * than a snapshot of nulls that reads like a quiet device.\n */\nexport function createDeviceState(collectors: DeviceStateCollectors = {}): Partial<DeviceState> {\n // The ONE explicit cross-domain dependency: Activity needs Spatial's newest\n // speed to tell \"walking\" from \"sitting in a moving car\". Passed as a shared\n // reference, not a cross-domain import — see SpeedRef.\n const speedRef: SpeedRef = { mps: null, at: 0 };\n return {\n deviceContext: createDeviceContextDomain(),\n connectivity: createConnectivityDomain(collectors),\n capability: createCapabilityDomain(collectors),\n interaction: createInteractionDomain(),\n spatial: createSpatialDomain(collectors, speedRef),\n visual: createVisualDomain(),\n activity: createActivityDomain(speedRef),\n };\n}\n\n/** Which domains this build can actually answer. */\nexport const IMPLEMENTED_DOMAINS = [\n 'deviceContext',\n 'connectivity',\n 'capability',\n 'interaction',\n 'spatial',\n 'visual',\n 'activity',\n] as const;\n","/**\n * The mounted half of the camera tasks — scanning and recording.\n *\n * The app renders this ONCE, near the root. It draws nothing until a plugin\n * asks for a scan or a recording; then it takes over the screen with a live\n * preview and settles the plugin's promise when the work is done.\n *\n * WHY IT IS VISIBLE, unlike VisualEngineHost's 1×1 invisible camera. These two\n * tasks need the USER to aim: a barcode has to be framed, a clip has to point\n * at something. An invisible scanner is not a privacy nicety, it is unusable —\n * the user cannot tell what the camera sees or when it has read something.\n *\n * WHY IT CAN ALWAYS BE CANCELLED. The camera is on and pointed at whatever is\n * in front of the person holding it. A capture surface with no way out is a\n * trap; the cancel path settles the promise with a distinct CANCELLED code so\n * the caller can tell \"they chose not to\" from \"it broke\".\n */\nimport React, { useEffect, useRef, useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { CameraView, useCameraPermissions, useMicrophonePermissions } from 'expo-camera';\n\nimport { cameraTasks, cameraError, type CameraTask } from './cameraTasks';\n\n/** How long a scan waits before giving up. */\nconst SCAN_TIMEOUT_MS = 30_000;\n/** Ceiling on a single clip, so a forgotten recording cannot fill the disk. */\nconst MAX_RECORD_MS = 60_000;\n\nexport function CameraTaskHost(): React.ReactElement | null {\n const [task, setTask] = useState<CameraTask | null>(cameraTasks.current());\n const [camPermission, requestCamPermission] = useCameraPermissions();\n const [micPermission, requestMicPermission] = useMicrophonePermissions();\n const cameraRef = useRef<CameraView | null>(null);\n /** Guards against a barcode firing twice before the task is cleared. */\n const settling = useRef(false);\n\n // Announce that a surface exists. Without this the broker rejects every\n // request immediately, which is the correct behaviour when no host is\n // mounted — a plugin must never await a promise nothing can settle.\n useEffect(() => {\n cameraTasks.setHostMounted(true);\n return () => cameraTasks.setHostMounted(false);\n }, []);\n\n useEffect(() => cameraTasks.subscribe(() => {\n settling.current = false;\n setTask(cameraTasks.current());\n }), []);\n\n // Ask for permission when a task arrives, not at app start: a camera prompt\n // with no visible reason attached is one users refuse.\n useEffect(() => {\n if (!task) return;\n let cancelled = false;\n void (async () => {\n const cam = camPermission?.granted ? camPermission : await requestCamPermission();\n if (cancelled) return;\n if (!cam?.granted) {\n cameraTasks.fail(cameraError(\n 'PERMISSION_DENIED',\n 'Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera.',\n ));\n return;\n }\n if (task.kind === 'record' && task.opts.withAudio) {\n const mic = micPermission?.granted ? micPermission : await requestMicPermission();\n if (cancelled) return;\n if (!mic?.granted) {\n // Refuse rather than silently recording a silent video — a caller\n // that asked for audio and got none has no way to find out.\n cameraTasks.fail(cameraError(\n 'PERMISSION_DENIED',\n 'Microphone permission was not granted, so a video with sound cannot be recorded.',\n ));\n }\n }\n })();\n return () => { cancelled = true; };\n }, [task]);\n\n // Scan timeout. A scan that never finds a code must END, and say so: leaving\n // the camera up indefinitely is both a battery drain and a promise that\n // never settles.\n useEffect(() => {\n if (task?.kind !== 'scan') return;\n const ms = task.opts.timeoutMs ?? SCAN_TIMEOUT_MS;\n const t = setTimeout(() => {\n cameraTasks.fail(cameraError('TIMEOUT', `No barcode was found within ${Math.round(ms / 1000)}s.`));\n }, ms);\n return () => clearTimeout(t);\n }, [task]);\n\n // Recording runs for its duration and resolves with the file.\n useEffect(() => {\n if (task?.kind !== 'record' || !camPermission?.granted) return;\n let cancelled = false;\n const maxMs = Math.min(task.opts.maxDurationMs ?? 15_000, MAX_RECORD_MS);\n const startedAt = Date.now();\n\n void (async () => {\n // A frame late is better than a crash: the ref is attached on the render\n // after the task arrives.\n await new Promise(r => setTimeout(r, 50));\n if (cancelled || !cameraRef.current) return;\n try {\n const video = await cameraRef.current.recordAsync({ maxDuration: Math.ceil(maxMs / 1000) });\n if (cancelled) return;\n if (!video?.uri) {\n cameraTasks.fail(cameraError('INTERNAL', 'Recording finished but produced no file.'));\n return;\n }\n cameraTasks.complete({ uri: video.uri, durationMs: Date.now() - startedAt });\n } catch (err: any) {\n if (!cancelled) cameraTasks.fail(cameraError('INTERNAL', err?.message || 'Recording failed.'));\n }\n })();\n\n return () => {\n cancelled = true;\n // Stop the hardware if the surface goes away mid-take.\n try { cameraRef.current?.stopRecording(); } catch { /* already stopped */ }\n };\n }, [task, camPermission?.granted]);\n\n if (!task || !camPermission?.granted) return null;\n\n const isScan = task.kind === 'scan';\n\n return (\n <View style={styles.overlay}>\n <CameraView\n ref={cameraRef}\n style={StyleSheet.absoluteFill}\n facing=\"back\"\n mode={isScan ? 'picture' : 'video'}\n barcodeScannerSettings={\n isScan && task.opts.formats?.length\n ? { barcodeTypes: task.opts.formats as any }\n : undefined\n }\n onBarcodeScanned={\n isScan\n ? ({ data, type }) => {\n // onBarcodeScanned fires continuously while a code is in frame.\n // Without this guard the same scan resolves, then tries to\n // resolve an already-cleared task on the very next frame.\n if (settling.current) return;\n settling.current = true;\n cameraTasks.complete({ data, format: type });\n }\n : undefined\n }\n />\n\n <View style={styles.hud} pointerEvents=\"box-none\">\n <Text style={styles.hint}>\n {isScan ? 'Point the camera at the code' : 'Recording…'}\n </Text>\n <Pressable style={styles.cancel} onPress={() => cameraTasks.cancel()} accessibilityRole=\"button\">\n <Text style={styles.cancelText}>Cancel</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000', zIndex: 9999 },\n hud: { flex: 1, justifyContent: 'space-between', paddingVertical: 64, alignItems: 'center' },\n hint: { color: '#fff', fontSize: 16, textAlign: 'center', paddingHorizontal: 24 },\n cancel: { paddingHorizontal: 28, paddingVertical: 14, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.18)' },\n cancelText: { color: '#fff', fontSize: 16, fontWeight: '600' },\n});\n\nexport default CameraTaskHost;\n","/**\n * The broker between a plugin's `await` and a mounted camera.\n *\n * WHY THIS EXISTS. expo-camera has no headless capture: scanning a barcode and\n * recording a clip both require a MOUNTED `<CameraView>`. But a plugin calls\n * `native.scanner.scanOnce()` and expects a promise — it has no way to mount\n * anything, and it must not have to.\n *\n * So the API enqueues a TASK here and awaits it; `CameraTaskHost`, mounted once\n * by the app, notices, shows the camera, does the work, and settles the\n * promise. This is the same split as the Visual engine, for the same reason —\n * and it is why `native.camera.scanBarcode` refuses and points here rather than\n * pretending a function call can own a React lifecycle.\n *\n * ONE TASK AT A TIME, and it is the phone that says so: there is one camera. A\n * second request while one is running is REJECTED rather than queued, because a\n * queued scan would open the camera again minutes later, long after the user\n * moved on and with no idea why it appeared.\n */\nexport interface ScanTask {\n kind: 'scan';\n opts: { formats?: string[]; timeoutMs?: number };\n resolve: (v: { data: string; format: string }) => void;\n reject: (e: Error) => void;\n}\n\nexport interface RecordTask {\n kind: 'record';\n opts: { maxDurationMs?: number; withAudio?: boolean };\n resolve: (v: { uri: string; durationMs: number | null }) => void;\n reject: (e: Error) => void;\n}\n\nexport type CameraTask = ScanTask | RecordTask;\n\ntype Listener = () => void;\n\n/** Typed failure, so a caller can say something specific and true. */\nexport function cameraError(code: string, message: string): Error {\n const err: any = new Error(message);\n err.code = code;\n return err;\n}\n\nclass CameraTaskBroker {\n private task: CameraTask | null = null;\n private listeners = new Set<Listener>();\n /** Set by the host while a camera is mounted and ready. */\n private hostMounted = false;\n\n current(): CameraTask | null { return this.task; }\n\n subscribe(fn: Listener): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n /**\n * Declare that a `CameraTaskHost` exists.\n *\n * Without it nothing will ever settle the promise, so a request must fail\n * IMMEDIATELY and say why. The alternative is a plugin awaiting forever,\n * which is the worst failure of the set: no error, no timeout, no camera,\n * and a conversation that simply stops.\n */\n setHostMounted(mounted: boolean): void {\n this.hostMounted = mounted;\n if (!mounted) this.fail(cameraError('UNAVAILABLE', 'The camera surface was dismissed before the task completed.'));\n this.notify();\n }\n\n isHostMounted(): boolean { return this.hostMounted; }\n\n /** Enqueue and hand back the promise the plugin awaits. */\n run<T>(kind: CameraTask['kind'], opts: any): Promise<T> {\n if (!this.hostMounted) {\n return Promise.reject(cameraError(\n 'UNAVAILABLE',\n 'No camera surface is mounted in this app, so the camera cannot be shown. ' +\n 'The host must render <CameraTaskHost /> once, near the app root.',\n ));\n }\n if (this.task) {\n return Promise.reject(cameraError(\n 'BUSY',\n `The camera is already in use (${this.task.kind}). Only one camera task can run at a time.`,\n ));\n }\n return new Promise<T>((resolve, reject) => {\n this.task = { kind, opts, resolve, reject } as unknown as CameraTask;\n this.notify();\n });\n }\n\n /** Settle the running task successfully and clear it. */\n complete(value: any): void {\n const t = this.task;\n if (!t) return;\n this.task = null;\n this.notify();\n t.resolve(value);\n }\n\n /** Settle the running task with a failure and clear it. */\n fail(err: Error): void {\n const t = this.task;\n if (!t) return;\n this.task = null;\n this.notify();\n t.reject(err);\n }\n\n /**\n * The user backed out.\n *\n * A distinct code, because \"the user chose not to\" is not a malfunction and\n * a guide should say something different about it than about a broken lens.\n */\n cancel(): void {\n this.fail(cameraError('CANCELLED', 'Cancelled — the camera was dismissed before anything was captured.'));\n }\n\n private notify(): void {\n for (const fn of this.listeners) { try { fn(); } catch { /* a bad listener is not the broker's problem */ } }\n }\n}\n\n/** One broker per app: there is one camera. */\nexport const cameraTasks = new CameraTaskBroker();\n","/**\n * SDK device-primitive provider (Expo-backed).\n *\n * `createDeviceProvider()` builds the FULL, UNGATED `PluginNative` implementation\n * a host registers once via `setPluginNativeProvider`. Per-plugin capability\n * gating and per-plugin filesystem scoping are applied downstream by\n * plugin-runtime's `gatePluginNative` — this module does not gate.\n *\n * Not a React hook: it holds no React state and must be safe to build at module\n * scope, including on a headless background wake before any UI exists.\n */\nimport * as Haptics from 'expo-haptics';\n// Both are already app dependencies and already used by the device-bridge\n// collectors — safe for eager import (unlike cookies/nfc, which need native\n// modules absent from Expo Go).\nimport * as ExpoLocation from 'expo-location';\nimport * as ImagePicker from 'expo-image-picker';\nimport * as Notifications from 'expo-notifications';\nimport * as DocumentPicker from 'expo-document-picker';\nimport * as FS from 'expo-file-system';\nimport * as SQLite from 'expo-sqlite';\n// The broker behind scanner.scanOnce() and camera.recordVideo(): both need a\n// MOUNTED camera, which a function call cannot provide.\nimport { cameraTasks } from '../camera/cameraTasks';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n// native.vision: the on-device analyzer (ada-vision module) — resolve a frame id\n// to its local pixels, measure/read/classify on-device, never off the phone.\nimport * as adaVision from '../vision';\n// native.maps: place search + walking routes (ada-maps module, MapKit) and the\n// SDK's one reverse geocode. NOT on-device — queries go to Apple's servers.\nimport * as adaMaps from '../maps';\nimport { countArg, fractionArg, refuseArgument, shown } from '../args';\nimport {\n coordinateArg, mapsFailure, searchArgs, toMapsPlaces, toMapsRoute, toReverseGeocodedPlace,\n} from '../maps/normalize';\n// Device-bridge owns every native module. plugin-sdk delegates so RN\n// plugins never import native modules directly. When adding a new\n// capability: add it to device-bridge, import here, declare in types.ts,\n// wire in the factory below.\n//\n// IMPORTANT: cookies and nfc use native modules NOT available in Expo Go.\n// We lazy-import them so the app doesn't crash at load time.\n// Only biometrics (expo-local-authentication), sharing (expo-sharing),\n// and scanner (expo-camera) are safe for eager import.\nimport {\n biometrics as deviceBiometrics,\n sharing as deviceSharing,\n scanner as deviceScanner,\n} from '../device-bridge';\n\n// Lazy loaders for native modules not in Expo Go\nfunction getDeviceCookies() {\n try { return require('../device-bridge/cookies'); } catch { return null; }\n}\nfunction getDeviceNfc() {\n try { return require('../device-bridge/nfc'); } catch { return null; }\n}\nimport type {\n NativeCapabilities,\n PluginNative,\n CameraAPI,\n LocationAPI,\n StorageAPI,\n BiometricsAPI,\n BiometricsAPIv2,\n NotificationsAPI,\n HapticsAPI,\n ConnectivityAPI,\n FileSystemAPI,\n CookiesAPI,\n WebViewCookie,\n SharingAPI,\n ScannerAPI,\n NfcAPI,\n BluetoothAPI,\n VisionAPI,\n MapsAPI,\n} from '@ateam-ai-mobile/plugin-runtime';\n\nclass CapabilityUnavailableError extends Error {\n code = 'UNAVAILABLE' as const;\n constructor(capability: string, reason?: string) {\n super(reason || `${capability} is not yet implemented. Coming in Phase 2.`);\n this.name = 'CapabilityUnavailableError';\n }\n}\n\nclass CapabilityNotDeclaredError extends Error {\n code = 'NOT_DECLARED' as const;\n constructor(capability: string) {\n super(`Capability '${capability}' not declared in plugin manifest. Add it to the native.${capability} field.`);\n this.name = 'CapabilityNotDeclaredError';\n }\n}\n\nfunction stubApi<T extends object>(capability: string): T {\n return new Proxy({} as T, {\n get(_target, prop) {\n if (typeof prop === 'string') {\n return () => {\n throw new CapabilityUnavailableError(capability);\n };\n }\n },\n });\n}\n\n// ── Location (expo-location — was stubbed, is real now) ───────────\n//\n// `location` was wired to stubApi, so a plugin that correctly DECLARED\n// `location: true` still got \"location is not yet implemented. Coming in\n// Phase 2.\" on every call. expo-location has been a dependency all along and\n// the device-bridge collectors use it; only the plugin-facing API was missing.\n\nfunction createLocationAPI(): LocationAPI {\n /** Ask once, and say what the user can do about a refusal. */\n const ensurePermission = async (): Promise<void> => {\n const existing = await ExpoLocation.getForegroundPermissionsAsync();\n if (existing.status === 'granted') return;\n const asked = await ExpoLocation.requestForegroundPermissionsAsync();\n if (asked.status !== 'granted') {\n // A voice assistant reads this out, so it has to be actionable rather\n // than a status code.\n throw new Error(\n 'Location permission was not granted. Enable it for this app in Settings > Privacy > Location Services.',\n );\n }\n };\n\n return {\n async getCurrent(opts) {\n await ensurePermission();\n // 'high' costs battery and a second or two of settling; default balanced,\n // which is plenty for \"which street am I on\".\n const fix = await ExpoLocation.getCurrentPositionAsync({\n accuracy:\n opts?.accuracy === 'high'\n ? ExpoLocation.Accuracy.High\n : ExpoLocation.Accuracy.Balanced,\n });\n return {\n latitude: fix.coords.latitude,\n longitude: fix.coords.longitude,\n altitude: fix.coords.altitude ?? undefined,\n };\n },\n\n watchPosition(cb) {\n let subscription: { remove(): void } | null = null;\n let cancelled = false;\n\n void (async () => {\n try {\n await ensurePermission();\n if (cancelled) return;\n subscription = await ExpoLocation.watchPositionAsync(\n { accuracy: ExpoLocation.Accuracy.Balanced },\n fix => cb({ latitude: fix.coords.latitude, longitude: fix.coords.longitude }),\n );\n // Unsubscribed while we were still asking for permission — otherwise\n // the GPS stays on with nobody listening.\n if (cancelled) { subscription?.remove(); subscription = null; }\n } catch (err: any) {\n console.warn('[location] watchPosition failed:', err?.message || String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n subscription?.remove();\n subscription = null;\n };\n },\n };\n}\n\n// ── Camera (expo-image-picker — was stubbed, is real now) ─────────\n//\n// Every method here opens the SYSTEM camera or picker UI, which is why they\n// are honest one-shot actions rather than a frame source: expo-camera can\n// deliver a live feed, but only through a MOUNTED CameraView component, which\n// a headless tool call has no way to mount. A continuous visual feed is the\n// semantic Visual domain's job (Device Bridge v2), not this API's.\n\nfunction createCameraAPI(): CameraAPI {\n const ensurePermission = async (): Promise<void> => {\n const existing = await ImagePicker.getCameraPermissionsAsync();\n if (existing.granted) return;\n const asked = await ImagePicker.requestCameraPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera.',\n );\n }\n };\n\n return {\n async takePhoto(opts) {\n await ensurePermission();\n const res = await ImagePicker.launchCameraAsync({\n quality: opts?.quality ?? 0.7,\n cameraType:\n opts?.facing === 'front'\n ? ImagePicker.CameraType.front\n : ImagePicker.CameraType.back,\n });\n // The user backing out is not an error — but it is also not a photo, so\n // it must not come back as an empty-looking success.\n if (res.canceled || !res.assets?.[0]) {\n throw new Error('No photo taken — the camera was dismissed.');\n }\n const a = res.assets[0];\n return { uri: a.uri, width: a.width, height: a.height };\n },\n\n async scanBarcode(opts) {\n // Kept refusing, and now the refusal has somewhere to send you. Scanning\n // is a mounted camera lifecycle, which is why it lives on `scanner`.\n // Silently forwarding would leave a misleading name in every plugin.\n throw new CapabilityUnavailableError(\n 'camera.scanBarcode',\n 'Use native.scanner.scanOnce() instead — scanning needs a mounted camera, ' +\n 'which this call cannot provide. Same arguments, and it returns { data, format }.' +\n (opts?.formats?.length ? ' Pass your formats through to scanOnce.' : ''),\n );\n },\n\n async recordVideo(opts) {\n // A FILE the user deliberately makes — not the Visual domain's frame\n // ring, which is ephemeral and nobody keeps. Runs on the shared camera\n // surface, so it throws the same coded failures as scanOnce.\n return cameraTasks.run('record', {\n maxDurationMs: opts?.maxDurationMs,\n withAudio: opts?.withAudio ?? false,\n });\n },\n\n async pickImage(opts) {\n const existing = await ImagePicker.getMediaLibraryPermissionsAsync();\n if (!existing.granted) {\n const asked = await ImagePicker.requestMediaLibraryPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Photo library permission was not granted. Enable it for this app in Settings > Privacy > Photos.',\n );\n }\n }\n const res = await ImagePicker.launchImageLibraryAsync({\n allowsMultipleSelection: !!opts?.multiple,\n });\n if (res.canceled) return [];\n return (res.assets || []).map(a => ({ uri: a.uri }));\n },\n };\n}\n\n// ── Storage (AsyncStorage — was stubbed) ──────────────────────────\n//\n// KV is real. `db.execute` is NOT: expo-sqlite is not installed, and a fake\n// SQL layer over AsyncStorage would be a worse lie than an error — it would\n// accept a query and return an empty rowset, which reads as \"no rows matched\".\n\n/**\n * One SQLite file for plugin storage, opened lazily and once.\n *\n * Lazy because opening a database at module load costs startup time for every\n * user who never touches a plugin that needs one; once because two handles to\n * the same file is how you get a locked database under concurrent writes.\n */\n// Typed `any` for the handle: expo-sqlite is an any-shim in this SDK repo's\n// typecheck (the host supplies the real types), so `SQLite.SQLiteDatabase` is\n// not a nameable type here. Runtime is unchanged — SQLite.openDatabaseAsync\n// still returns a real SQLiteDatabase on device.\n// One handle PER plugin database file, opened lazily and once each. A Map (not a\n// single promise) because storage is now per-plugin: each plugin gets its own\n// file, and two handles to the same file is how you get a locked database.\nconst _pluginDbs = new Map<string, Promise<any>>();\nfunction openPluginDb(file: string): Promise<any> {\n const existing = _pluginDbs.get(file);\n if (existing) return existing;\n const opened: Promise<any> = SQLite.openDatabaseAsync(file);\n _pluginDbs.set(file, opened);\n return opened;\n}\n\n// The leading SQL keyword, uppercased, after stripping leading whitespace and\n// leading `-- line` / `/* block */` comments — so a comment cannot smuggle the\n// verb past the cross-file check (e.g. `/* */ ATTACH …`).\nfunction sqlLeadingKeyword(sql: string): string {\n let s = String(sql);\n for (;;) {\n const before = s;\n s = s.replace(/^\\s+/, '');\n if (s.startsWith('--')) { const nl = s.indexOf('\\n'); s = nl === -1 ? '' : s.slice(nl + 1); }\n else if (s.startsWith('/*')) { const end = s.indexOf('*/'); s = end === -1 ? '' : s.slice(end + 2); }\n if (s === before) break;\n }\n const m = s.match(/^[A-Za-z]+/);\n return m ? m[0].toUpperCase() : '';\n}\n\n// R3 SECURITY — per-plugin storage isolation. Both the KV namespace and the\n// SQLite database are scoped to the plugin's OWN id, so Plugin A cannot read,\n// overwrite, or query Plugin B's entries/tables. The id is threaded in from the\n// TRUSTED gate (storageFor(pluginId)) — never a caller-supplied prefix.\n//\n// The id is encoded with encodeURIComponent (not used raw) precisely because KV\n// is a FLAT concatenated keyspace with no structural boundary: a raw id like\n// `mcp:conn:plug` shares the `:` delimiter, so `@plugin:mcp:conn:plug:<k>` from\n// one plugin could collide with another plugin's id+key split. Encoding escapes\n// `:` (→ %3A), making the id segment unambiguous and the mapping injective, so\n// distinct ids can never produce overlapping namespaces regardless of the key.\n// (The filesystem sandbox can use the raw id as a directory name because a\n// directory boundary is structural, not delimiter-based.) The same encoded\n// token names the plugin's database file, giving each plugin a distinct DB.\nfunction createStorageAPI(pluginId: string): StorageAPI {\n const ns = encodeURIComponent(pluginId);\n const key = (k: string) => `@plugin:${ns}:${k}`;\n const dbFile = `plugin-storage-${ns}.db`;\n\n return {\n get: (k) => AsyncStorage.getItem(key(k)),\n set: (k, v) => AsyncStorage.setItem(key(k), v),\n delete: (k) => AsyncStorage.removeItem(key(k)),\n db: {\n async execute(sql: string, params?: any[]) {\n const verb = sqlLeadingKeyword(sql);\n // Per-plugin storage isolation rests on each plugin owning its OWN\n // database FILE. SQLite can cross files — ATTACH mounts another db,\n // VACUUM INTO writes one — so the file boundary only holds if those are\n // refused. Otherwise a plugin could `ATTACH '.../plugin-storage-<other\n // id>.db'` (the id is public and the filename deterministic) and read or\n // overwrite another plugin's tables. Refuse cross-file statements at the\n // trusted gate; a plugin never needs them for its own single database.\n // (runAsync/getAllAsync compile only the FIRST statement, so a trailing\n // `; ATTACH …` never runs — the leading verb is the whole surface.)\n // ATTACH/DETACH/VACUUM are the only cross-FILE statements in the standard\n // expo-sqlite build: loadable extensions are disabled and the file-vtab\n // modules (fileio/csv/zipfile) are not compiled in, so there is no other\n // way for a plugin's own connection to reach another plugin's file. If a\n // future SQLite build enables those, revisit (an allowlist would be the\n // durable form).\n if (verb === 'ATTACH' || verb === 'DETACH' || verb === 'VACUUM') {\n throw new CapabilityUnavailableError(\n 'storage.db',\n `SQL '${verb}' is not permitted: a plugin's storage is confined to its own database and cannot reach another file.`,\n );\n }\n const db = await openPluginDb(dbFile);\n // A statement either RETURNS ROWS or CHANGES THEM, and the two need\n // different calls. Deciding by the leading keyword rather than by\n // trying one and falling back: a failed INSERT retried as a query\n // would run it twice.\n const returnsRows = verb === 'SELECT' || verb === 'PRAGMA' || verb === 'WITH';\n\n if (returnsRows) {\n const rows = await db.getAllAsync(sql, (params ?? []) as any);\n // changes: 0 is the truth for a SELECT, not a placeholder.\n return { rows, changes: 0 };\n }\n const res = await db.runAsync(sql, (params ?? []) as any);\n // rows: [] is likewise true for a write — it did not select anything.\n return { rows: [], changes: res.changes ?? 0 };\n },\n },\n };\n}\n\n/**\n * Wipe ALL plugin storage — KV, SQLite, and the file sandbox — across EVERY\n * plugin. Per-plugin isolation (R3) keeps plugins apart from each other; this is\n * the USER boundary: on sign-out it must run so the NEXT user/tenant cannot read\n * the previous one's plugin data. Best-effort per store — a hiccup clearing one\n * must never block sign-out — so each part is guarded independently.\n */\nexport async function clearAllPluginData(): Promise<void> {\n // 1. KV — every `@plugin:` namespace (all plugins, all keys).\n try {\n const keys = await AsyncStorage.getAllKeys();\n const mine = keys.filter((k: string) => k.startsWith('@plugin:'));\n if (mine.length) await AsyncStorage.multiRemove(mine);\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: KV clear failed:', e?.message || e);\n }\n\n // 2. SQLite — close any open per-plugin handles, then delete the db files.\n try {\n for (const [, p] of _pluginDbs) {\n try { const db = await p; await db.closeAsync?.(); } catch { /* already closed */ }\n }\n _pluginDbs.clear();\n const sqliteDir = new FS.Directory(FS.Paths.document, 'SQLite');\n if (sqliteDir.exists) {\n for (const entry of sqliteDir.list()) {\n if (/^plugin-storage-.*\\.db$/.test(String(entry.name ?? ''))) {\n try { entry.delete(); } catch { /* best effort */ }\n }\n }\n }\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: SQLite clear failed:', e?.message || e);\n }\n\n // 3. Files — the whole plugin-files/ sandbox tree (all plugins).\n try {\n const dir = new FS.Directory(FS.Paths.document, 'plugin-files');\n if (dir.exists) dir.delete();\n } catch (e: any) {\n console.warn('[adas] clearAllPluginData: file sandbox clear failed:', e?.message || e);\n }\n}\n\n// ── Notifications (expo-notifications — was stubbed) ──────────────\n\nfunction createNotificationsAPI(): NotificationsAPI {\n return {\n async schedule({ title, body, trigger }) {\n const perm = await Notifications.getPermissionsAsync();\n if (!perm.granted) {\n const asked = await Notifications.requestPermissionsAsync();\n if (!asked.granted) {\n throw new Error(\n 'Notification permission was not granted. Enable it for this app in Settings > Notifications.',\n );\n }\n }\n // `seconds` and `date` are the two the contract declares; anything else\n // would be this API inventing a scheduling model of its own.\n const when: any =\n typeof trigger?.seconds === 'number'\n ? { type: 'timeInterval', seconds: trigger.seconds, repeats: false }\n : trigger?.date\n ? { type: 'date', date: new Date(trigger.date) }\n : null;\n if (!when) {\n throw new Error('schedule() needs trigger.seconds or trigger.date.');\n }\n return Notifications.scheduleNotificationAsync({\n content: { title, body },\n trigger: when,\n });\n },\n\n cancel: (id) => Notifications.cancelScheduledNotificationAsync(id),\n\n onReceived(cb) {\n const sub = Notifications.addNotificationReceivedListener(n => {\n cb({\n id: n.request.identifier,\n title: n.request.content.title || '',\n body: n.request.content.body || '',\n data: n.request.content.data,\n });\n });\n return () => sub.remove();\n },\n };\n}\n\n// ── File system (expo-document-picker — partially real) ───────────\n//\n// pickDocument works. read/write do NOT: expo-file-system is not installed.\n// Named honestly rather than returning empty strings, which would read as an\n// empty file rather than a missing capability.\n\nfunction createFileSystemAPI(pluginId: string): FileSystemAPI {\n /**\n * Every path a plugin gives us is resolved INSIDE its own sandbox directory.\n *\n * A plugin is code a solution ships that runs on someone's phone, so\n * `readFile('/etc/passwd')` — or, more realistically, reading the app's own\n * auth store or another solution's data — must not be reachable. This is the\n * same reasoning that took the `sw.*` filesystem tools out of the kernel and\n * anchored them per tenant.\n *\n * The check is on the RESOLVED path, not the input string: `..` segments are\n * what a blocklist of prefixes misses, and `plugin-files/../../secrets` looks\n * innocent until it is normalised.\n */\n /**\n * Resolved on FIRST USE, never at construction.\n *\n * `new FS.Directory(FS.Paths.document, …)` is a SYNCHRONOUS NATIVE CALL, and\n * `createNativeCapabilities({ fileSystem: true, … })` runs at MODULE SCOPE in\n * src/adas.ts. Building it eagerly therefore did native work during startup\n * module evaluation — on every launch, including a headless background wake\n * from a push, before any UI exists. If the native module is missing or\n * `Paths.document` throws there, the whole app dies with no screen to show it\n * on. Nothing in this factory may touch native until a method is called.\n */\n let _root: InstanceType<typeof FS.Directory> | null = null;\n const rootDir = () => (_root ??= new FS.Directory(FS.Paths.document, 'plugin-files', pluginId));\n\n const resolve = (p: string): InstanceType<typeof FS.File> => {\n const root = rootDir();\n const file = new FS.File(root, p);\n if (!file.uri.startsWith(root.uri)) {\n throw new CapabilityUnavailableError(\n 'fileSystem',\n `Path escapes the plugin's storage area: ${p}. Plugin files live under plugin-files/ and paths cannot traverse out of it.`,\n );\n }\n return file;\n };\n\n return {\n async readFile(path) {\n const file = resolve(path);\n if (!file.exists) {\n // A missing file is not an empty file. Returning '' would read as a\n // document that exists and happens to have nothing in it.\n throw new CapabilityUnavailableError(\n 'fileSystem.readFile',\n `No such file: ${path}. Nothing was read.`,\n );\n }\n return file.text();\n },\n\n async writeFile(path, content) {\n const file = resolve(path);\n // Create parents as needed — a plugin writing 'cache/report.json' should\n // not have to know that directories are a separate concept.\n const parent = file.parentDirectory;\n if (!parent.exists) parent.create({ intermediates: true });\n if (!file.exists) file.create();\n file.write(content);\n },\n async pickDocument(opts) {\n const res = await DocumentPicker.getDocumentAsync({\n type: opts?.types?.length ? opts.types : '*/*',\n copyToCacheDirectory: true,\n });\n // Backing out is not a file. Returning zeros here would look like an\n // empty document the user actually chose.\n if (res.canceled || !res.assets?.[0]) {\n throw new Error('No document chosen — the picker was dismissed.');\n }\n const a = res.assets[0];\n return { uri: a.uri, name: a.name, size: a.size ?? 0 };\n },\n };\n}\n\n// ── Haptics (fully implemented — expo-haptics already installed) ───\n\nfunction createHapticsAPI(): HapticsAPI {\n return {\n impact(style: 'light' | 'medium' | 'heavy') {\n const map = {\n light: Haptics.ImpactFeedbackStyle.Light,\n medium: Haptics.ImpactFeedbackStyle.Medium,\n heavy: Haptics.ImpactFeedbackStyle.Heavy,\n };\n Haptics.impactAsync(map[style]);\n },\n notification(type: 'success' | 'warning' | 'error') {\n const map = {\n success: Haptics.NotificationFeedbackType.Success,\n warning: Haptics.NotificationFeedbackType.Warning,\n error: Haptics.NotificationFeedbackType.Error,\n };\n Haptics.notificationAsync(map[type]);\n },\n // Shorthand for notification('error'). Plugins call this; it was never\n // implemented, and optional chaining meant the failure the user was meant\n // to FEEL simply did not happen.\n error() {\n Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n },\n selection() {\n Haptics.selectionAsync();\n },\n };\n}\n\n// ── Connectivity (implemented using basic RN fetch check) ──────\n\nfunction createConnectivityAPI(reachabilityUrl?: string): ConnectivityAPI {\n let online = true;\n const listeners = new Set<(online: boolean) => void>();\n // Lazy + re-armable polling: the instance is now a singleton shared by ALL\n // plugins (injected once via setPluginNativeProvider), so the interval must\n // (a) not run at app boot when nobody is listening, and (b) restart when a\n // later plugin subscribes after an earlier one unsubscribed to zero.\n // Pre-singleton, each Plugin mount got its own instance and clearInterval\n // was harmless; with a shared instance it would kill polling app-wide.\n let interval: ReturnType<typeof setInterval> | null = null;\n\n // Simple connectivity check — upgrade to @react-native-community/netinfo in Phase 2.\n // NO hardcoded URL: the host injects its own health endpoint. With none given\n // we assume reachable (return true) rather than pin an A-Team-specific default.\n const checkOnline = async () => {\n if (!reachabilityUrl) return true;\n try {\n const res = await fetch(reachabilityUrl, {\n method: 'HEAD',\n signal: AbortSignal.timeout(5000),\n });\n return res.ok;\n } catch {\n return false;\n }\n };\n\n const startPolling = () => {\n // Nothing to poll without a reachability URL: `online` stays constant-true,\n // so an interval would only wake the app to learn the same answer.\n if (!reachabilityUrl) return;\n if (interval) return;\n interval = setInterval(async () => {\n const newState = await checkOnline();\n if (newState !== online) {\n online = newState;\n listeners.forEach(cb => cb(online));\n }\n }, 30000);\n };\n\n return {\n isOnline() {\n return online;\n },\n onChange(cb: (online: boolean) => void) {\n listeners.add(cb);\n startPolling();\n return () => {\n listeners.delete(cb);\n if (listeners.size === 0 && interval) {\n clearInterval(interval);\n interval = null;\n }\n };\n },\n };\n}\n\n// ── Cookies (delegates to device-bridge, lazy-loaded) ──\nfunction createCookiesAPI(): CookiesAPI {\n return {\n async get(url: string): Promise<WebViewCookie[]> {\n const mod = getDeviceCookies();\n if (!mod) throw new CapabilityUnavailableError('cookies (native module not available — use a dev build)');\n const list = await mod.get(url, true);\n return list as WebViewCookie[];\n },\n async clear(_url: string): Promise<{ ok: boolean }> {\n const mod = getDeviceCookies();\n if (!mod) throw new CapabilityUnavailableError('cookies (native module not available — use a dev build)');\n const r = await mod.clearAll();\n return { ok: r.ok };\n },\n };\n}\n\n// ── Biometrics v2 (Face ID / Touch ID / fingerprint) ──\nfunction createBiometricsAPIv2(): BiometricsAPIv2 {\n return {\n capabilities: () => deviceBiometrics.capabilities(),\n authenticate: (opts) => deviceBiometrics.authenticate(opts),\n };\n}\n\n// ── Sharing (native share sheet) ──\nfunction createSharingAPI(): SharingAPI {\n return {\n isAvailable: () => deviceSharing.isAvailable(),\n shareFile: (fileUri, opts) => deviceSharing.shareFile(fileUri, opts),\n };\n}\n\n// ── Scanner (QR / barcode permission helper) ──\nfunction createScannerAPI(): ScannerAPI {\n return {\n requestPermissions: () => deviceScanner.requestPermissions(),\n // Raises the shared camera surface and settles on the first code read.\n // The plugin mounts nothing — that lifecycle is owned once, by\n // CameraTaskHost, instead of by every plugin that wants to scan.\n scanOnce: (opts) => cameraTasks.run('scan', {\n formats: opts?.formats,\n timeoutMs: opts?.timeoutMs,\n }),\n };\n}\n\n// ── NFC (tag read, lazy-loaded) ──\nfunction createNfcAPI(): NfcAPI {\n return {\n async isSupported() {\n const mod = getDeviceNfc();\n if (!mod) return false;\n return mod.isSupported();\n },\n async isEnabled() {\n const mod = getDeviceNfc();\n if (!mod) return false;\n return mod.isEnabled();\n },\n readOnce(timeoutMs) {\n const mod = getDeviceNfc();\n if (!mod) return Promise.resolve({ ok: false, error: 'nfc_unavailable' } as any);\n return mod.readOnce(timeoutMs);\n },\n };\n}\n\n// ── Bluetooth / BLE (SHAPE ONLY — every method refuses) ──\n//\n// Nothing implements this yet, and it is wired up anyway. `bluetooth` has been\n// a manifest flag with no API behind it, so a plugin that correctly declared it\n// got `undefined` and crashed with \"cannot read properties of undefined\" — a\n// failure that names nothing and teaches nothing.\n//\n// Refusing BY NAME is the whole difference. The rule this layer runs on is that\n// absence is an answer, but only when it says what is absent.\n//\n// KEPT rather than dropped because BLE beacons are one of the few indoor\n// position signals that survive where GPS does not — which is precisely where\n// Ada Guide ends up the moment the user walks into a building.\nfunction createBluetoothAPI(): BluetoothAPI {\n const refuse = (method: string) =>\n new CapabilityUnavailableError(\n `bluetooth.${method}`,\n 'Bluetooth is not implemented in this build — no BLE native module is installed. ' +\n 'The capability is declared so it fails by name rather than silently; nothing was scanned.',\n );\n\n return {\n // These two answer `false` rather than throwing, because \"is BLE usable?\"\n // is exactly the question a plugin should be allowed to ask before\n // committing to a flow, and `false` is the true answer here.\n async isSupported() { return false; },\n async isEnabled() { return false; },\n // scan() THROWS, because there is no honest empty result: an empty device\n // list is a real finding (\"nothing is advertising nearby\") and returning\n // one would report a scan that never happened.\n async scan() { throw refuse('scan'); },\n };\n}\n\n\n// ── Vision — on-device image understanding (SHAPE ONLY) ──\n//\n// Nothing implements this yet; every method refuses BY NAME. Declared now\n// because it is the last missing piece of Ada Guide and a solution should be\n// able to design against it before the native side exists.\n//\n// WHY IT IS NOT IN THE VISUAL DOMAIN: the freeze reserves Visual for the frame\n// TIMELINE and puts everything interpretive — what is in the scene, what the\n// sign says — ABOVE Device Bridge. This is that layer.\n//\n// FRAMES MUST NEVER LEAVE THE PHONE (Arie, 2026-08-15). Whoever implements\n// this: iOS Vision (VNRecognizeTextRequest / VNClassifyImageRequest) or\n// Android MLKit, both on-device and free per call. An implementation that\n// POSTs the frame anywhere satisfies these types and violates the design.\nfunction createVisionAPI(): VisionAPI {\n // All three methods are implemented on-device via the ada-vision native module:\n // assessQuality (pixel statistics), readText (VNRecognizeText), describe\n // (VNClassifyImage). Frames never leave the phone.\n return {\n // \"Can I see?\" — true once the native module is present in the binary. On a\n // build without it, adaVision.isSupported() answers false, and assessQuality\n // below refuses cleanly (no crash), exactly the pre-module behaviour.\n async isSupported() { return adaVision.isSupported(); },\n // readText: on-device OCR on a published frame (native resolves the id →\n // pixels). [] = looked, found no text (a real finding); throws = could not\n // look (unknown/aged-out id). describe (VNClassifyImage) is the next pass.\n async readText(frameId: string, opts?: { languages?: string[] }) {\n const langs = opts?.languages;\n if (langs != null && (!Array.isArray(langs) || langs.some((l) => typeof l !== 'string'))) {\n throw refuseArgument('vision', 'readText', `languages must be an array of language codes, not ${shown(langs)}`, 'nothing was read');\n }\n return adaVision.readText(frameId, langs);\n },\n // describe: on-device labels (VNClassifyImage), not prose — the sentence is\n // the skill's job. [] = looked, recognised nothing.\n async describe(frameId: string, opts?: { maxLabels?: number }) {\n // Checked here: a NaN/Infinity count would trap Swift's Int conversion (src/args.ts).\n return adaVision.describe(frameId, countArg('vision', 'describe', 'maxLabels', opts?.maxLabels, 5, 50, 'nothing was labelled'));\n },\n // detectObjects: on-device located objects (bundled detector), each with a\n // box — what a guidance loop reads to say WHERE a thing is, not just that the\n // scene contains it. [] = looked, found nothing; throws = could not look.\n async detectObjects(frameId: string, opts?: { maxItems?: number; minConfidence?: number }) {\n const nothing = 'nothing was detected';\n return adaVision.detectObjects(\n frameId,\n countArg('vision', 'detectObjects', 'maxItems', opts?.maxItems, 10, 50, nothing),\n fractionArg('vision', 'detectObjects', 'minConfidence', opts?.minConfidence, 0.25, nothing),\n );\n },\n // assessQuality: resolve the frame id to its LOCAL file, measure on-device,\n // and record the result back onto the frame so a later read of\n // VisualFrameRef.quality reflects it. Throwing here means \"could not look\"\n // (unknown/aged-out id, or no native module) — never a measured verdict.\n async assessQuality(frameId: string) {\n // Quality was measured at publish; the pixels live in the native ring,\n // resolved by id inside native. Null = the frame is unknown or has aged out\n // of the ring — \"could not look\", never \"looked and found nothing\".\n const q = adaVision.qualityById(frameId);\n if (!q) {\n throw new CapabilityUnavailableError(\n 'vision.assessQuality',\n `No frame for id \"${frameId}\" — unknown or aged out of the ring. ` +\n 'This is \"could not look\", not a measurement.',\n );\n }\n return q;\n },\n };\n}\n\n// ── Maps — place search, reverse geocoding, walking routes ──\n//\n// NOT on-device, unlike vision: the query and the coordinates it is given go to\n// Apple's map servers (the platform geocoder on Android). It never reads the\n// user's location — the caller passes every coordinate — so it is not a second\n// door to where the user is.\n//\n// search/route are the ada-maps module (MapKit, iOS only). reverseGeocode is the\n// SDK's one reverse geocode (expo-location), the same call the location\n// collector and the connector bridge make — so it works on Android and on a\n// build without ada-maps.\nfunction createMapsAPI(): MapsAPI {\n // Refuse BY NAME before any native call: what this build or platform is\n // missing, and that nothing left the phone.\n const refuseIfMissing = (verb: string, nothing: string) => {\n const missing = adaMaps.missingReason();\n if (missing) {\n throw new CapabilityUnavailableError(`maps.${verb}`, `maps.${verb} needs ${missing}; ${nothing}.`);\n }\n };\n\n // Every argument is checked (searchArgs / coordinateArg) BEFORE the missing\n // check and before anything crosses the bridge: a remote bundle is plain JS,\n // and a malformed call refuses as INVALID_ARGUMENT instead of crashing Swift\n // (NaN limit) or searching around 0,0 (`{ lat, lng }`).\n return {\n async isSupported() { return adaMaps.isSupported(); },\n // [] = Apple looked and found nothing (a real finding); throws = could not\n // look. The defaults (radiusM 3000, limit 8) live in searchArgs.\n async search(query, opts) {\n const args = searchArgs(query, opts);\n refuseIfMissing('search', 'nothing was searched');\n const raw = await adaMaps\n .search(args.query, args.near, args.radiusM, args.limit)\n .catch(err => { throw mapsFailure('search', err); });\n return toMapsPlaces(raw, args.near);\n },\n // null = nothing at that point; throws = could not look.\n async reverseGeocode(at) {\n const point = coordinateArg('reverseGeocode', 'at', at, 'nothing was looked up');\n const address = await adaMaps\n .reverseGeocode(point)\n .catch(err => { throw mapsFailure('reverseGeocode', err); });\n return address ? toReverseGeocodedPlace(point, address) : null;\n },\n // null = Apple found no route. Never an empty 0 m route, which would read as\n // \"you have arrived\". A remote bundle can pass any string as `mode`, so a\n // mode this build cannot honour refuses rather than silently walking.\n async route(from, to, opts) {\n if (opts?.mode !== undefined && opts.mode !== 'walking') {\n throw new CapabilityUnavailableError(\n 'maps.route',\n `maps.route supports walking only, not \"${String(opts.mode)}\"; no route was requested.`,\n );\n }\n const a = coordinateArg('route', 'from', from, 'no route was requested');\n const b = coordinateArg('route', 'to', to, 'no route was requested');\n refuseIfMissing('route', 'no route was requested');\n const raw = await adaMaps\n .route(a, b)\n .catch(err => { throw mapsFailure('route', err); });\n return raw ? toMapsRoute(raw) : null;\n },\n };\n}\n\n// ── Provider factory ───────────────────────────────────────────\n\nexport interface DeviceProviderConfig {\n /** Endpoint the connectivity check HEAD-fetches (host-injected, e.g. the app's\n * own health URL). Omitted → connectivity assumes reachable (no hardcoded URL). */\n reachabilityUrl?: string;\n}\n\n/**\n * The full device-primitive implementation a host registers once via\n * setPluginNativeProvider. It is UNGATED — per-plugin capability gating and\n * per-plugin filesystem scoping are applied by plugin-runtime's gatePluginNative.\n */\nexport function createDeviceProvider(config?: DeviceProviderConfig): PluginNative {\n const connectivity = createConnectivityAPI(config?.reachabilityUrl);\n return {\n camera: createCameraAPI(),\n location: createLocationAPI(),\n // Only the per-plugin storageFor/fileSystemFor are exposed — NO shared\n // `storage`/`fileSystem` bucket. The gate scopes by pluginId and fails\n // closed without one, so a shared instance would be an unreachable\n // (and dangerous) fallback. Each plugin gets its own namespace + db file /\n // sandbox directory, or nothing.\n storageFor: (pluginId: string) => createStorageAPI(pluginId),\n biometrics: stubApi<BiometricsAPI>('biometrics'),\n biometricsV2: createBiometricsAPIv2(),\n notifications: createNotificationsAPI(),\n haptics: createHapticsAPI(),\n connectivity,\n fileSystemFor: (pluginId: string) => createFileSystemAPI(pluginId),\n cookies: createCookiesAPI(),\n sharing: createSharingAPI(),\n scanner: createScannerAPI(),\n nfc: createNfcAPI(),\n bluetooth: createBluetoothAPI(),\n vision: createVisionAPI(),\n maps: createMapsAPI(),\n };\n}\n","// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n /** Absent from binaries built before the front camera existed. */\n startVisionCamera?(intervalMs: number, facing: VisionFacing): Promise<void>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** The native function, or a refusal by name: Android's AdaVision has assessQuality only\n * (AI Models F4), so a verb it lacks says so instead of failing as \"not a function\". */\nfunction verb<K extends keyof AdaVisionNative>(name: K): AdaVisionNative[K] {\n if (!native) throw new Error('ada-vision native module not present in this build');\n const fn = native[name];\n if (typeof fn !== 'function') {\n throw Object.assign(new Error(`UNAVAILABLE: ada-vision ${String(name)} is not implemented on this platform (iOS only)`), { code: 'UNAVAILABLE' });\n }\n return (fn as Function).bind(native) as AdaVisionNative[K];\n}\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Which camera the vision session reads. */\nexport type VisionFacing = 'front' | 'back';\n\n/**\n * Start the vision session on `facing` — publishes a timeline frame every\n * `intervalMs`. The back camera works on every binary; the front camera needs a\n * binary built with it, and an older one REFUSES it rather than quietly reading\n * the back camera instead.\n */\nexport async function startVision(intervalMs = 1000, facing: VisionFacing = 'back'): Promise<void> {\n if (facing !== 'front' && facing !== 'back') {\n throw new Error(`Unknown camera \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n if (facing === 'back') return verb('startVision')(intervalMs);\n if (native && typeof native.startVisionCamera !== 'function') {\n throw new Error('the front camera needs a newer app build — this one can read the back camera only');\n }\n return verb('startVisionCamera')!(intervalMs, facing);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n return verb('readText')(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n return verb('describe')(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n return verb('detectObjects')(id, maxItems, minConfidence);\n}\n","// ada-maps — place search + walking directions (Apple MapKit), our own native\n// code, plus the SDK's one reverse geocode.\n//\n// NOT on-device — the opposite of ada-vision. Every call sends the query and the\n// coordinates it is given to a map service over the network. None of it reads\n// the device's location: the caller supplies every coordinate.\n//\n// search(...) — MKLocalSearch (AdaMaps module, iOS only)\n// route(...) — MKDirections (AdaMaps module, iOS only)\n// reverseGeocode(at) — expo-location's reverseGeocodeAsync, which IS CLGeocoder\n// on iOS and the platform Geocoder on Android. No Swift for\n// it, and the ONLY reverse geocode in the SDK: the location\n// collector and the connector bridge call this too.\n//\n// requireOptionalNativeModule: on a binary WITHOUT the module the import is null,\n// isSupported() is false, and search/route refuse cleanly (no crash). Android has\n// no AdaMaps at all, so it refuses by name the same way.\n\nimport { Platform } from 'react-native';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress, MapsCoordinate } from '@ateam-ai-mobile/plugin-runtime';\nimport { toMapsAddress } from './normalize';\nimport type { RawMapsPlace, RawMapsRoute } from './normalize';\n\ninterface AdaMapsNative {\n isSupported(): boolean;\n search(query: string, lat: number, lng: number, hasNear: boolean, radiusM: number, limit: number): Promise<RawMapsPlace[]>;\n route(fromLat: number, fromLng: number, toLat: number, toLng: number): Promise<RawMapsRoute | null>;\n}\n\nconst native = requireOptionalNativeModule<AdaMapsNative>('AdaMaps');\n\n/** What search/route are missing in THIS build, as the clause after \"needs\" —\n * or null when they can run. A voice assistant reads it out, so it names the\n * missing thing rather than giving a code. */\nexport function missingReason(): string | null {\n if (Platform.OS !== 'ios') return `iOS (Apple MapKit) — ${Platform.OS} has no implementation yet`;\n if (!native) return 'the app build with the AdaMaps module (this build predates it — a native rebuild is needed)';\n return null;\n}\n\n/** Is the native maps module present in this binary (and on a platform it runs on)? */\nexport function isSupported(): boolean {\n try { return missingReason() === null && (native?.isSupported() ?? false); } catch { return false; }\n}\n\n// search/route hand their arguments to native AS GIVEN: a NaN or Infinity\n// `limit` traps Swift's Int conversion and kills the app. createMapsAPI checks\n// every argument first (searchArgs / coordinateArg in ./normalize); a host\n// calling these directly must pass what those checks would accept.\n\n/** MKLocalSearch around `near` (when given). [] = looked, found nothing; throws =\n * couldn't look, with a string code OFFLINE / THROTTLED / UNAVAILABLE. */\nexport async function search(\n query: string,\n near: MapsCoordinate | null,\n radiusM: number,\n limit: number,\n): Promise<RawMapsPlace[]> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n // Without `near`, hasNear is false and Swift never reads lat/lng — the zeros\n // are placeholders for a primitive argument, not a place.\n return near\n ? native.search(query, near.latitude, near.longitude, true, radiusM, limit)\n : native.search(query, 0, 0, false, radiusM, limit);\n}\n\n/** MKDirections, walking, between two caller-supplied points. null = no route. */\nexport async function route(from: MapsCoordinate, to: MapsCoordinate): Promise<RawMapsRoute | null> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n return native.route(from.latitude, from.longitude, to.latitude, to.longitude);\n}\n\n/** The address at a coordinate, or null when the geocoder has nothing there.\n * Throws when it could not look (network, rate limit, Android without location\n * permission) — callers decide whether that is fatal. */\nexport async function reverseGeocode(at: MapsCoordinate): Promise<MapsAddress | null> {\n const [first] = await ExpoLocation.reverseGeocodeAsync({ latitude: at.latitude, longitude: at.longitude });\n return first ? toMapsAddress(first) : null;\n}\n","// Argument checks for native verbs — one place, shared by every capability that\n// hands caller numbers to Swift.\n//\n// Why this exists: expo-modules-core converts a JS number to a Swift Int with\n// `Int(double.rounded())`, which TRAPS on NaN, ±Infinity or anything past\n// Int.max. A plugin (or a model driving a device tool) passing `limit: Infinity`\n// or `maxItems: NaN` would therefore kill the host app. Every count that crosses\n// the bridge is checked here first and refused BY NAME instead.\n//\n// No imports on purpose: it runs under plain `node --test`.\n\n/** A malformed call, refused before anything is sent. Retrying will not help;\n * fixing the call will. */\nexport type ArgumentError = Error & { code: 'INVALID_ARGUMENT' };\n\n/** What the caller passed, short enough to quote back in a refusal. NaN and\n * Infinity are spelled out, unquoted (JSON would print them as null). */\nexport const shown = (v: unknown): string => {\n let s: string | undefined;\n try {\n s = JSON.stringify(v, (_k, x) => (typeof x === 'number' && !Number.isFinite(x) ? `\\u0001${x}\\u0001` : x))\n ?.replace(/\"\\\\u0001(NaN|-?Infinity)\\\\u0001\"/g, '$1');\n } catch { /* cyclic — fall back to String */ }\n s = s ?? String(v);\n return s.length > 80 ? `${s.slice(0, 77)}...` : s;\n};\n\n/** `<capability>.<verb>: <problem>; <what was not done>.` with code INVALID_ARGUMENT. */\nexport function refuseArgument(capability: string, verb: string, problem: string, nothing: string): ArgumentError {\n const e = new Error(`${capability}.${verb}: ${problem}; ${nothing}.`) as ArgumentError;\n e.code = 'INVALID_ARGUMENT';\n return e;\n}\n\n/** A count bound for a Swift Int: absent → `def`; otherwise a finite number ≥ 1,\n * floored and capped at `max`. Anything else refuses by name. */\nexport function countArg(\n capability: string, verb: string, name: string, value: unknown, def: number, max: number, nothing: string,\n): number {\n const v = value ?? def;\n if (typeof v !== 'number' || !Number.isFinite(v) || v < 1) {\n throw refuseArgument(capability, verb, `${name} must be a number from 1 (at most ${max} are used), not ${shown(value)}`, nothing);\n }\n return Math.min(max, Math.floor(v));\n}\n\n/** A 0..1 fraction (a confidence): absent → `def`; NaN or out of range refuses. */\nexport function fractionArg(\n capability: string, verb: string, name: string, value: unknown, def: number, nothing: string,\n): number {\n const v = value ?? def;\n if (typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > 1) {\n throw refuseArgument(capability, verb, `${name} must be a number from 0 to 1, not ${shown(value)}`, nothing);\n }\n return v;\n}\n","// Pure shaping for native.maps — no Expo, no React Native, so it runs under\n// plain `node --test` (tests/adaMapsNormalize.test.mjs pins it).\n//\n// The native side hands back loose dictionaries — AdaMaps (MapKit) for search\n// and route, expo-location's geocoder for reverse geocoding. This turns them\n// into the typed MapsAPI shapes, and one set of failure codes. Unknown is\n// `null`, never '' or 0: an empty street reads as a street with no name, and a\n// 0 m route reads as \"you have arrived\".\n//\n// It also checks what a caller sends BEFORE it crosses the bridge (searchArgs,\n// coordinateArg). A remote bundle is plain JS at runtime, so the types promise\n// nothing: a NaN or Infinity `limit` traps Swift's Int conversion and kills the\n// app, and a `{ lat, lng }` near would search around 0,0 and answer \"found\n// nothing\" with confidence.\n\nimport type {\n MapsAddress,\n MapsCoordinate,\n MapsPlace,\n MapsRoute,\n MapsRouteStep,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { distanceOf } from '../geo';\nimport { countArg, refuseArgument as refuse, shown, type ArgumentError } from '../args';\n\n/** An address as the native geocoders spell it. AdaMaps sends exactly these\n * keys; expo-location sends these plus `subregion`/`timezone`/\n * `formattedAddress`, which MapsAddress does not carry. */\nexport interface RawMapsAddress {\n name?: string | null;\n street?: string | null;\n streetNumber?: string | null;\n district?: string | null;\n city?: string | null;\n region?: string | null;\n postalCode?: string | null;\n country?: string | null;\n isoCountryCode?: string | null;\n}\n\n/** One AdaMaps search hit (AdaMapsModule.place). */\nexport interface RawMapsPlace {\n name: string | null;\n latitude: number;\n longitude: number;\n /** MKPointOfInterestCategory raw value, e.g. \"MKPOICategoryPharmacy\". */\n category: string | null;\n address: RawMapsAddress;\n}\n\n/** An AdaMaps walking route (AdaMapsModule.route) — distances in metres,\n * expectedTravelTime in SECONDS, as MapKit reports them. */\nexport interface RawMapsRoute {\n distance: number;\n expectedTravelTime: number;\n steps: Array<{ instructions: string | null; distance: number; notice: string | null; polyline: MapsCoordinate[] }>;\n polyline: MapsCoordinate[];\n}\n\n/** The three ways a maps call can fail to look — see MapsAPI. */\nexport type MapsFailureCode = 'UNAVAILABLE' | 'OFFLINE' | 'THROTTLED';\n\n/** A malformed call, refused before anything is sent. Not a fourth way to fail\n * to look: retrying will not help, fixing the call will. */\nexport type MapsArgumentError = ArgumentError;\n\nconst SEARCH_RADIUS_DEFAULT_M = 3000;\n/** Keeps the region MapKit is handed a small, valid one. A \"near\" search wider\n * than this is a different question. */\nconst SEARCH_RADIUS_MAX_M = 100_000;\nconst SEARCH_LIMIT_DEFAULT = 8;\nconst SEARCH_LIMIT_MAX = 25;\n\nconst text = (v: unknown): string | null =>\n typeof v === 'string' && v.trim() !== '' ? v : null;\n\nconst refuseArgument = (verb: string, problem: string, nothing: string): MapsArgumentError =>\n refuse('maps', verb, problem, nothing);\n\n/** A caller's coordinate, checked and copied to exactly `{ latitude, longitude }`.\n * Anything else — `{ lat, lng }`, strings, NaN, out of range — refuses BY NAME\n * rather than being sent as 0,0 or NaN. */\nexport function coordinateArg(verb: string, name: string, value: unknown, nothing: string): MapsCoordinate {\n const c = value as { latitude?: unknown; longitude?: unknown } | null | undefined;\n const latitude = c?.latitude, longitude = c?.longitude;\n if (\n typeof latitude === 'number' && Number.isFinite(latitude) && Math.abs(latitude) <= 90 &&\n typeof longitude === 'number' && Number.isFinite(longitude) && Math.abs(longitude) <= 180\n ) {\n return { latitude, longitude };\n }\n throw refuseArgument(\n verb,\n `${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,\n nothing,\n );\n}\n\n/** search's arguments, checked and defaulted (radiusM 3000, limit 8). `limit`\n * is floored and capped at 25; everything else out of range refuses. */\nexport function searchArgs(\n query: unknown,\n opts: { near?: unknown; radiusM?: unknown; limit?: unknown } | null | undefined,\n): { query: string; near: MapsCoordinate | null; radiusM: number; limit: number } {\n const nothing = 'nothing was searched';\n if (typeof query !== 'string' || query.trim() === '') {\n throw refuseArgument('search', `query must be a non-empty string, not ${shown(query)}`, nothing);\n }\n const near = opts?.near == null ? null : coordinateArg('search', 'near', opts.near, nothing);\n const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;\n if (typeof radiusM !== 'number' || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {\n throw refuseArgument(\n 'search', `radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`, nothing);\n }\n const limit = countArg('maps', 'search', 'limit', opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);\n return { query, near, radiusM, limit };\n}\n\nconst point = (c: MapsCoordinate): MapsCoordinate => ({\n latitude: Number(c.latitude),\n longitude: Number(c.longitude),\n});\n\nexport function toMapsAddress(raw: RawMapsAddress | null | undefined): MapsAddress {\n const r = raw ?? {};\n return {\n name: text(r.name),\n street: text(r.street),\n streetNumber: text(r.streetNumber),\n district: text(r.district),\n city: text(r.city),\n region: text(r.region),\n postalCode: text(r.postalCode),\n country: text(r.country),\n isoCountryCode: text(r.isoCountryCode),\n };\n}\n\n/** \"MKPOICategoryPharmacy\" → \"Pharmacy\": the vocabulary term, without the\n * framework's prefix. */\nexport function toMapsCategory(raw: unknown): string | null {\n const c = text(raw);\n return c ? c.replace(/^MKPOICategory/, '') || null : null;\n}\n\n/** Search hits → places. `distanceM` is straight-line from `near`, and only\n * when the caller gave one — a distance from nowhere would be invented. */\nexport function toMapsPlaces(raw: RawMapsPlace[] | null | undefined, near: MapsCoordinate | null): MapsPlace[] {\n return (raw ?? []).map(r => {\n const coordinate = point(r);\n return {\n name: text(r.name),\n coordinate,\n address: toMapsAddress(r.address),\n category: toMapsCategory(r.category),\n distanceM: near ? distanceOf([near, coordinate]) : null,\n };\n });\n}\n\n/** A reverse-geocoded address → a place AT the point asked about. The platform\n * geocoder returns an address, not a snapped point or a category. */\nexport function toReverseGeocodedPlace(at: MapsCoordinate, address: MapsAddress): MapsPlace {\n return {\n name: address.name,\n coordinate: point(at),\n address,\n category: null,\n distanceM: null,\n };\n}\n\n/** A MapKit route → MapsRoute. Seconds become ms; every step is kept, including\n * Apple's empty-instruction origin step, so indexes match Apple's. */\nexport function toMapsRoute(raw: RawMapsRoute): MapsRoute {\n const steps: MapsRouteStep[] = (raw.steps ?? []).map(s => ({\n instructions: typeof s.instructions === 'string' ? s.instructions : '',\n distanceM: Number(s.distance),\n notice: text(s.notice),\n polyline: (s.polyline ?? []).map(point),\n }));\n return {\n mode: 'walking',\n distanceM: Number(raw.distance),\n expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1000),\n steps,\n polyline: (raw.polyline ?? []).map(point),\n };\n}\n\n/**\n * A native rejection → an Error whose `code` is one of MapsAPI's three.\n *\n * AdaMaps already rejects with OFFLINE / THROTTLED / UNAVAILABLE. expo-location\n * (reverseGeocode) has its own codes: on iOS a CLError.network becomes\n * ERR_GEOCODING_NETWORK, which Apple uses for BOTH a lost connection and a rate\n * limit — so it maps to OFFLINE and the message says it may be either. Anything\n * else is UNAVAILABLE, with the platform's own words kept beside ours.\n */\nexport function mapsFailure(verb: string, err: unknown): Error & { code: MapsFailureCode } {\n const nativeCode = String((err as any)?.code ?? '');\n const detail = String((err as any)?.message ?? err ?? '').trim();\n let code: MapsFailureCode;\n let why: string;\n if (nativeCode === 'THROTTLED') {\n code = 'THROTTLED';\n why = 'Apple is rate-limiting map requests from this app — wait before retrying';\n } else if (nativeCode === 'OFFLINE') {\n code = 'OFFLINE';\n why = 'the map service could not be reached — the phone may be offline';\n } else if (nativeCode === 'ERR_GEOCODING_NETWORK') {\n code = 'OFFLINE';\n why = 'the geocoder could not be reached — the phone may be offline, or Apple is ' +\n 'rate-limiting requests (iOS reports both the same way)';\n } else {\n code = 'UNAVAILABLE';\n why = 'the map service failed';\n }\n const failure = new Error(\n `maps.${verb} could not look: ${why}. This is not \"found nothing\".` +\n (detail ? ` (${nativeCode ? nativeCode + ': ' : ''}${detail})` : ''),\n ) as Error & { code: MapsFailureCode };\n failure.code = code;\n return failure;\n}\n","/**\n * Messaging Action Executor — sends SMS via expo-sms.\n * Executes 'messaging.send' actions from the action queue.\n */\n\nimport * as SMS from 'expo-sms';\nimport type { ActionResult } from '../types';\n\n/**\n * Check if SMS is available on this device.\n */\nexport async function isAvailable(): Promise<boolean> {\n return SMS.isAvailableAsync();\n}\n\n/**\n * Execute a messaging.send action.\n *\n * Params expected:\n * - contact_name: string (for display only)\n * - phone: string (phone number to send to)\n * - message: string (message text)\n */\nexport async function execute(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const phone = params.phone as string;\n const message = params.message as string;\n\n if (!phone || !message) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing phone or message parameter',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const available = await SMS.isAvailableAsync();\n if (!available) {\n return {\n id: actionId,\n ok: false,\n error: 'SMS not available on this device',\n executed_at: new Date().toISOString(),\n };\n }\n\n // Opens the native SMS compose screen with pre-filled content\n const { result } = await SMS.sendSMSAsync([phone], message);\n\n return {\n id: actionId,\n ok: result === 'sent' || result === 'unknown', // 'unknown' on Android means it was sent\n result: {\n to: params.contact_name || phone,\n phone,\n message,\n status: result,\n sent_at: new Date().toISOString(),\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `SMS send failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n","/**\n * Calendar Action Executor — creates/modifies events via expo-calendar.\n * Executes 'calendar.create', 'calendar.reschedule', 'calendar.cancel' actions.\n */\n\nimport * as ExpoCalendar from 'expo-calendar';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoCalendar.Calendar`-style annotations need this.\n// Derived rather than imported — see collectors/calendar.ts.\ntype Event = Awaited<ReturnType<typeof ExpoCalendar.getEventsAsync>>[number];\nimport type { ActionResult } from '../types';\n\n/**\n * Get the default calendar ID for creating events.\n */\nasync function getDefaultCalendarId(): Promise<string | null> {\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n\n // Prefer the default calendar\n const defaultCal = calendars.find(c => c.isPrimary) || calendars[0];\n return defaultCal?.id || null;\n}\n\n/**\n * Execute a calendar.create action.\n *\n * Params expected:\n * - title: string\n * - date: string (YYYY-MM-DD)\n * - start_time: string (HH:MM)\n * - end_time?: string (HH:MM)\n * - location?: string\n * - attendees?: string[] (names — no email lookup here)\n * - notes?: string\n */\nexport async function executeCreate(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const title = params.title as string;\n const date = params.date as string;\n const startTime = params.start_time as string;\n const endTime = params.end_time as string | undefined;\n\n if (!title || !date || !startTime) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing title, date, or start_time',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const calendarId = await getDefaultCalendarId();\n if (!calendarId) {\n return {\n id: actionId,\n ok: false,\n error: 'No calendar found on device',\n executed_at: new Date().toISOString(),\n };\n }\n\n const [year, month, day] = date.split('-').map(Number);\n const [startH, startM] = startTime.split(':').map(Number);\n\n const startDate = new Date(year, month - 1, day, startH, startM);\n let endDate: Date;\n\n if (endTime) {\n const [endH, endM] = endTime.split(':').map(Number);\n endDate = new Date(year, month - 1, day, endH, endM);\n } else {\n // Default 1 hour duration\n endDate = new Date(startDate.getTime() + 60 * 60 * 1000);\n }\n\n const eventId = await ExpoCalendar.createEventAsync(calendarId, {\n title,\n startDate,\n endDate,\n location: (params.location as string) || undefined,\n notes: (params.notes as string) || undefined,\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n });\n\n return {\n id: actionId,\n ok: true,\n result: {\n event_id: eventId,\n title,\n date,\n start: startTime,\n end: endTime || formatTime(endDate),\n message: `Event \"${title}\" created successfully`,\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar create failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Execute a calendar.reschedule action.\n *\n * Params expected:\n * - event_id: string\n * - new_date?: string (YYYY-MM-DD)\n * - new_start_time?: string (HH:MM)\n * - new_end_time?: string (HH:MM)\n */\nexport async function executeReschedule(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const eventId = params.event_id as string;\n\n if (!eventId) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing event_id',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const updates: Partial<Event> = {};\n\n if (params.new_date && params.new_start_time) {\n const [year, month, day] = (params.new_date as string).split('-').map(Number);\n const [h, m] = (params.new_start_time as string).split(':').map(Number);\n updates.startDate = new Date(year, month - 1, day, h, m);\n\n if (params.new_end_time) {\n const [eh, em] = (params.new_end_time as string).split(':').map(Number);\n updates.endDate = new Date(year, month - 1, day, eh, em);\n } else {\n updates.endDate = new Date(updates.startDate.getTime() + 60 * 60 * 1000);\n }\n }\n\n await ExpoCalendar.updateEventAsync(eventId, updates);\n\n return {\n id: actionId,\n ok: true,\n result: { event_id: eventId, message: 'Event rescheduled successfully' },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar reschedule failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Execute a calendar.cancel action.\n *\n * Params expected:\n * - event_id: string\n */\nexport async function executeCancel(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const eventId = params.event_id as string;\n\n if (!eventId) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing event_id',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n await ExpoCalendar.deleteEventAsync(eventId);\n\n return {\n id: actionId,\n ok: true,\n result: { event_id: eventId, message: 'Event cancelled successfully' },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Calendar cancel failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n\n// ─── Helpers ───\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n });\n}\n","/**\n * Navigation Action Executor — opens maps app via expo-linking.\n * Executes 'navigation.start' actions from the action queue.\n */\n\nimport * as Linking from 'expo-linking';\nimport { Platform } from 'react-native';\nimport type { ActionResult } from '../types';\n\n/**\n * Execute a navigation.start action.\n * Opens the native maps app with turn-by-turn directions.\n *\n * Params expected:\n * - destination: string (address or place name)\n * - mode?: 'driving' | 'transit' | 'walking' (default: 'driving')\n */\nexport async function execute(\n actionId: string,\n params: Record<string, unknown>\n): Promise<ActionResult> {\n const destination = params.destination as string;\n const mode = (params.mode as string) || 'driving';\n\n if (!destination) {\n return {\n id: actionId,\n ok: false,\n error: 'Missing destination parameter',\n executed_at: new Date().toISOString(),\n };\n }\n\n try {\n const encodedDest = encodeURIComponent(destination);\n let url: string;\n\n if (Platform.OS === 'ios') {\n // Apple Maps with directions\n const modeMap: Record<string, string> = {\n driving: 'd',\n transit: 'r',\n walking: 'w',\n };\n url = `maps://maps.apple.com/?daddr=${encodedDest}&dirflg=${modeMap[mode] || 'd'}`;\n } else {\n // Google Maps on Android\n const modeMap: Record<string, string> = {\n driving: 'driving',\n transit: 'transit',\n walking: 'walking',\n };\n url = `google.navigation:q=${encodedDest}&mode=${modeMap[mode] || 'driving'}`;\n }\n\n const canOpen = await Linking.canOpenURL(url);\n if (!canOpen) {\n // Fallback to Google Maps web URL\n const webModeMap: Record<string, string> = {\n driving: 'driving',\n transit: 'transit',\n walking: 'walking',\n };\n url = `https://www.google.com/maps/dir/?api=1&destination=${encodedDest}&travelmode=${webModeMap[mode] || 'driving'}`;\n }\n\n await Linking.openURL(url);\n\n return {\n id: actionId,\n ok: true,\n result: {\n message: `Navigation started to ${destination} via ${mode}`,\n destination,\n mode,\n status: 'navigating',\n },\n executed_at: new Date().toISOString(),\n };\n } catch (error) {\n return {\n id: actionId,\n ok: false,\n error: `Navigation failed: ${error instanceof Error ? error.message : String(error)}`,\n executed_at: new Date().toISOString(),\n };\n }\n}\n","/**\n * Health Collector — reads HealthKit aggregates via @kingstinct/react-native-healthkit\n * (TurboModule / Nitro-Modules based; works in React Native's New Architecture).\n *\n * iOS only. On Android the functions resolve with zeros so the caller doesn't\n * need to platform-check.\n *\n * Public:\n * - hasPermission() — returns true once authorization has been requested.\n * - requestPermission() — triggers the iOS HealthKit permission sheet (red heart).\n * - collectToday() — one HealthDayMetrics for today.\n * - collectHistory(days) — array of HealthDayMetrics, newest-first.\n */\nimport { Platform } from 'react-native';\nimport type { HealthDayMetrics } from '../types';\n\n// Lazy-loaded so the module is harmless on Android / Expo Go / non-iOS.\n// In Expo Go the JS module resolves but the native bridge is missing —\n// resulting object exists yet methods like requestAuthorization are undefined.\nlet _HK: any = null;\nlet _hkProbed = false;\nfunction getHK(): any | null {\n if (Platform.OS !== 'ios') return null;\n if (_hkProbed) return _HK;\n _hkProbed = true;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod = require('@kingstinct/react-native-healthkit');\n if (mod && typeof mod.requestAuthorization === 'function') {\n _HK = mod;\n } else {\n console.warn('[health] @kingstinct/react-native-healthkit native bridge unavailable — HealthKit disabled (Expo Go?)');\n }\n } catch (e) {\n console.warn('[health] @kingstinct/react-native-healthkit require failed:', e);\n }\n return _HK;\n}\n\n// Last error from requestAuthorization — surfaced to UI so we don't fail silently.\nexport let lastHealthInitError: string | null = null;\n\nconst READ_TYPES = [\n 'HKQuantityTypeIdentifierStepCount',\n 'HKQuantityTypeIdentifierFlightsClimbed',\n 'HKQuantityTypeIdentifierActiveEnergyBurned',\n 'HKQuantityTypeIdentifierDistanceWalkingRunning',\n // Added for mood signals — all read-only, covered by the same auth sheet.\n 'HKQuantityTypeIdentifierRestingHeartRate',\n 'HKQuantityTypeIdentifierAppleExerciseTime',\n 'HKCategoryTypeIdentifierSleepAnalysis',\n] as const;\n\nlet _initPromise: Promise<boolean> | null = null;\n\n/** Request HealthKit authorization once; subsequent calls reuse the result. */\nasync function ensureInit(): Promise<boolean> {\n const HK = getHK();\n if (!HK) {\n lastHealthInitError = '@kingstinct/react-native-healthkit native module not loaded';\n return false;\n }\n if (_initPromise) return _initPromise;\n _initPromise = (async () => {\n try {\n // requestAuthorization triggers the iOS HealthKit permission sheet.\n // Resolves once the user dismisses it (regardless of allow/deny).\n // Returns the authorization status — but iOS deliberately does not\n // disclose per-type read permission state for privacy, so we just\n // treat \"we asked\" as \"ready to read\" and let reads return 0 if denied.\n await HK.requestAuthorization({ toRead: READ_TYPES, toWrite: [] });\n lastHealthInitError = null;\n return true;\n } catch (e: any) {\n lastHealthInitError = `requestAuthorization threw: ${e?.message || String(e)}`;\n console.warn('[health]', lastHealthInitError);\n _initPromise = null;\n return false;\n }\n })();\n return _initPromise;\n}\n\nexport async function hasPermission(): Promise<boolean> {\n return ensureInit();\n}\n\nexport async function requestPermission(): Promise<boolean> {\n return ensureInit();\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setHours(0, 0, 0, 0);\n return out;\n}\n\nfunction dateKey(d: Date): string {\n // Local-date YYYY-MM-DD\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n}\n\n/**\n * Read a single daily-aggregate sum for [start, end) in the given unit.\n * Returns 0 when no data, HealthKit unavailable, or the call throws.\n *\n * `queryStatisticsForQuantity` is the modern statistics API; we ask only for\n * the cumulative sum since that's what we need for these data types.\n */\nasync function readDailySum(identifier: string, unit: string, start: Date, end: Date): Promise<number> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return 0;\n try {\n // The kingstinct API takes the date range nested under filter.date — not\n // top-level from/to. Without filter.date the call returns the cumulative\n // sum across ALL historical data, which is what we hit before this fix\n // (millions of steps, thousands of km, hundreds of thousands of kcal).\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['cumulativeSum'],\n {\n unit,\n filter: { date: { startDate: start, endDate: end } },\n },\n );\n const q = result?.sumQuantity?.quantity;\n return typeof q === 'number' ? q : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Daily discrete-average for a quantity (e.g. resting heart rate). Returns\n * null when no sample exists for the day so the backend can tell \"no data\"\n * apart from a real zero.\n */\nasync function readDailyAverage(identifier: string, unit: string, start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return null;\n try {\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['discreteAverage'],\n { unit, filter: { date: { startDate: start, endDate: end } } },\n );\n const q = result?.averageQuantity?.quantity;\n return typeof q === 'number' ? q : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Total minutes asleep for [start, end) from HKCategoryTypeIdentifierSleepAnalysis.\n * Sums sample durations that represent actual sleep (any \"asleep*\" value),\n * excluding inBed/awake. Heavily guarded — sleep-stage enums differ across iOS\n * versions and the kingstinct API surface, so any failure returns null.\n */\nasync function readSleepMinutes(start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n const query = HK?.queryCategorySamples || HK?.queryCategorySamplesAsync || HK?.querySamples;\n if (!query) return null;\n try {\n const samples: any[] = await query.call(\n HK,\n 'HKCategoryTypeIdentifierSleepAnalysis',\n { filter: { date: { startDate: start, endDate: end } } },\n );\n if (!Array.isArray(samples) || samples.length === 0) return null;\n // value semantics: 0 = inBed, 2 = awake; everything else = a sleep stage.\n // Treat anything that isn't explicitly inBed/awake as asleep.\n let ms = 0;\n for (const s of samples) {\n const v = s?.value;\n const isAsleep = v !== 0 && v !== 2; // exclude inBed + awake\n if (!isAsleep) continue;\n const a = new Date(s?.startDate).getTime();\n const b = new Date(s?.endDate).getTime();\n if (Number.isFinite(a) && Number.isFinite(b) && b > a) ms += b - a;\n }\n return ms > 0 ? Math.round(ms / 60000) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Mood-relevant HealthKit extras for today: sleep minutes, resting heart rate,\n * active energy (kcal), exercise minutes. Each is null when unavailable so the\n * backend weights only present fields.\n */\nexport async function collectMoodExtras(): Promise<{\n sleep_min: number | null;\n resting_hr: number | null;\n active_energy_kcal: number | null;\n exercise_min: number | null;\n}> {\n const ok = await ensureInit();\n if (!ok) return { sleep_min: null, resting_hr: null, active_energy_kcal: null, exercise_min: null };\n const start = startOfDay(new Date());\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n // Sleep window: look back from this morning over the past night (~last 18h)\n // so an evening collection still captures the prior night.\n const sleepStart = new Date(start);\n sleepStart.setHours(sleepStart.getHours() - 6); // include after-midnight + prev evening within today's window\n const [sleep_min, resting_hr, energy, exercise] = await Promise.all([\n readSleepMinutes(sleepStart, end),\n readDailyAverage('HKQuantityTypeIdentifierRestingHeartRate', 'count/min', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n readDailySum('HKQuantityTypeIdentifierAppleExerciseTime', 'min', start, end),\n ]);\n return {\n sleep_min,\n resting_hr: resting_hr != null ? Math.round(resting_hr) : null,\n active_energy_kcal: energy ? Math.round(energy) : null,\n exercise_min: exercise ? Math.round(exercise) : null,\n };\n}\n\nasync function readDay(date: Date): Promise<HealthDayMetrics> {\n const start = startOfDay(date);\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n\n const [steps, floors, energy, distance] = await Promise.all([\n readDailySum('HKQuantityTypeIdentifierStepCount', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierFlightsClimbed', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n // Ask for kilometers directly so we don't have to convert from meters.\n readDailySum('HKQuantityTypeIdentifierDistanceWalkingRunning', 'km', start, end),\n ]);\n\n return {\n date: dateKey(start),\n available: true,\n steps: Math.round(steps),\n floors: Math.round(floors),\n active_energy_kcal: Math.round(energy),\n distance_km: Math.round(distance * 100) / 100,\n };\n}\n\n// ─── Public API ─────────────────────────────────────────────\n\n/**\n * A day we could not read: metrics null, availability stated.\n *\n * The distinction this preserves is the whole point — \"no data\" and \"a day\n * with no activity\" look identical once you write a zero, and only one of\n * them is something to tell the user about.\n */\nfunction unavailableDay(date: Date): HealthDayMetrics {\n return {\n date: dateKey(date),\n available: false,\n steps: null,\n floors: null,\n active_energy_kcal: null,\n distance_km: null,\n };\n}\n\nexport async function collectToday(): Promise<HealthDayMetrics> {\n const ok = await ensureInit();\n if (!ok) {\n // HealthKit unavailable or access denied. NOT a day with no activity —\n // reporting zeros here is how \"you have taken 0 steps today\" reached the\n // user as a fact, with a coaching skill ready to act on it.\n return unavailableDay(new Date());\n }\n return readDay(new Date());\n}\n\n/**\n * Last `days` days, newest-first. Index 0 is today.\n * Caller passes 1..30 — we clamp defensively.\n */\nexport async function collectHistory(days: number): Promise<HealthDayMetrics[]> {\n const n = Math.max(1, Math.min(30, Math.floor(days)));\n const ok = await ensureInit();\n if (!ok) return [];\n\n const today = startOfDay(new Date());\n const out: HealthDayMetrics[] = [];\n // Sequential to avoid hammering HealthKit with 30 parallel queries.\n for (let i = 0; i < n; i++) {\n const d = new Date(today);\n d.setDate(d.getDate() - i);\n out.push(await readDay(d));\n }\n return out;\n}\n","/**\n * Background Sync — registers an expo-task-manager task for periodic\n * data sync when the app is in the background, and exposes a one-shot\n * `syncDomains()` for silent-push triggered syncs.\n *\n * iOS minimum background fetch interval is ~15 minutes.\n * Android allows more frequent background execution via WorkManager.\n *\n * Usage:\n * import { registerBackgroundSync, syncDomains } from '@mobile-pa/device-bridge';\n *\n * // Periodic background fetch — call once at app startup, outside any component\n * registerBackgroundSync({\n * relayUrl: 'https://relay.example.com',\n * deviceId: 'device-123',\n * tenant: 'mobile-pa',\n * actorId: 'user-abc',\n * apiKey: 'key-xxx',\n * });\n *\n * // Silent-push triggered sync — from FCM background message handler\n * await syncDomains(config, ['location', 'calendar']);\n */\n\nimport * as TaskManager from 'expo-task-manager';\nimport * as BackgroundFetch from 'expo-background-fetch';\nimport { RelayClient } from './relay-client';\nimport * as caps from './capabilities';\nimport type { Capability } from './types';\n\nexport const BACKGROUND_TASK_NAME = 'DEVICE_BRIDGE_BACKGROUND_SYNC';\n\nexport interface BackgroundSyncConfig {\n relayUrl: string;\n deviceId: string;\n tenant: string;\n actorId?: string;\n apiKey: string;\n}\n\nconst CAPABILITY_FACTORIES: Record<string, () => Capability> = {\n calendar: caps.calendarCapability,\n contacts: caps.contactsCapability,\n location: caps.locationCapability,\n battery: caps.batteryCapability,\n connectivity: caps.connectivityCapability,\n notifications: caps.notificationsCapability,\n health: caps.healthCapability,\n};\n\n/**\n * Register the periodic background-fetch task.\n * Call once at app startup, outside any React component.\n */\nexport async function registerBackgroundSync(\n config: BackgroundSyncConfig\n): Promise<void> {\n TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {\n try {\n const hasNew = await syncDomains(config, ['location', 'battery', 'calendar']);\n return hasNew\n ? BackgroundFetch.BackgroundFetchResult.NewData\n : BackgroundFetch.BackgroundFetchResult.NoData;\n } catch (error) {\n console.error('[DeviceBridge] Background sync failed:', error);\n return BackgroundFetch.BackgroundFetchResult.Failed;\n }\n });\n\n await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {\n minimumInterval: 300, // 5 minutes (iOS will clamp to ~15 min)\n stopOnTerminate: false,\n startOnBoot: true,\n });\n}\n\n/**\n * Unregister the background sync task.\n */\nexport async function unregisterBackgroundSync(): Promise<void> {\n const isRegistered = await TaskManager.isTaskRegisteredAsync(\n BACKGROUND_TASK_NAME\n );\n if (isRegistered) {\n await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);\n }\n}\n\n/**\n * Collect and push the listed domains once. Used by both the periodic\n * background-fetch task and the silent-push handler.\n *\n * Returns true if at least one domain pushed new data.\n */\nexport async function syncDomains(\n config: BackgroundSyncConfig,\n domains: string[]\n): Promise<boolean> {\n const relay = new RelayClient({\n relayUrl: config.relayUrl,\n deviceId: config.deviceId,\n tenant: config.tenant,\n actorId: config.actorId,\n apiKey: config.apiKey,\n timeout: 15_000, // longer than foreground — background tasks are budget-limited but not latency-sensitive\n });\n\n const results = await Promise.allSettled(\n domains.map(d => collectAndPush(relay, d))\n );\n\n return results.some(r => r.status === 'fulfilled' && r.value);\n}\n\n// ─── Internal ───\n\nasync function collectAndPush(\n relay: RelayClient,\n domain: string\n): Promise<boolean> {\n const factory = CAPABILITY_FACTORIES[domain];\n if (!factory) {\n console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);\n return false;\n }\n\n const cap = factory();\n\n if (cap.requiresPermission) {\n const granted = await cap.checkPermission();\n if (!granted) return false;\n }\n\n const data = await cap.collect();\n const result = await relay.pushDomain(domain, data);\n return result.ok;\n}\n","/**\n * Calendar Collector — reads device calendar via expo-calendar.\n * Output format matches device-mock-mcp's calendar.today / calendar.upcoming schemas.\n */\n\nimport * as ExpoCalendar from 'expo-calendar';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoCalendar.Calendar`-style annotations need this.\n// expo-calendar's types are not visible through the package entry under\n// bundler resolution, so derive them from the APIs that return them —\n// the shape then always matches the installed version.\ntype Calendar = Awaited<ReturnType<typeof ExpoCalendar.getCalendarsAsync>>[number];\ntype Event = Awaited<ReturnType<typeof ExpoCalendar.getEventsAsync>>[number];\nimport type { CalendarEvent, CalendarTodayData, CalendarUpcomingData } from '../types';\n\n/**\n * Request calendar permissions.\n * @returns true if granted\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.requestCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if calendar permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.getCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Get all calendar events for today.\n * Matches the device.calendar.today tool output schema.\n */\nexport async function collectToday(): Promise<CalendarTodayData> {\n const now = new Date();\n const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, startOfDay, endOfDay);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => ({\n id: e.id,\n title: e.title || 'Untitled',\n date: startOfDay.toISOString().slice(0, 10),\n start: formatTime(new Date(e.startDate)),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n }));\n\n // Sort by start time\n mapped.sort((a, b) => a.start.localeCompare(b.start));\n\n return {\n ok: true,\n date: startOfDay.toISOString().slice(0, 10),\n day_of_week: now.toLocaleDateString('en-US', { weekday: 'long' }),\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n/**\n * Get upcoming calendar events for the next N days (tomorrow and beyond).\n * Matches the device.calendar.upcoming tool output schema.\n */\nexport async function collectUpcoming(daysAhead: number = 3): Promise<CalendarUpcomingData> {\n const now = new Date();\n const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\n const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead + 1);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, tomorrow, endDate);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => {\n const startDt = new Date(e.startDate);\n return {\n id: e.id,\n title: e.title || 'Untitled',\n date: startDt.toISOString().slice(0, 10),\n start: formatTime(startDt),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n };\n });\n\n // Sort by date then start time\n mapped.sort((a, b) =>\n a.date === b.date ? a.start.localeCompare(b.start) : a.date.localeCompare(b.date)\n );\n\n return {\n ok: true,\n from: tomorrow.toISOString().slice(0, 10),\n days_ahead: daysAhead,\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n// ─── Helpers ───\n\n/**\n * Filter out holiday/subscription calendars — only keep user-owned calendars\n * (local, CalDAV, Exchange, Google). Subscription calendars are read-only\n * holiday feeds that clutter the real schedule.\n */\nfunction getUserCalendarIds(calendars: Calendar[]): string[] {\n const SKIP_SOURCES = ['birthdays', 'other']; // 'other' = subscribed holiday feeds on iOS\n const SKIP_TYPES = ['birthdays', 'other'];\n\n return calendars\n .filter(c => {\n const sourceType = ((c.source as any)?.type || '').toLowerCase();\n const calType = (c.type || '').toLowerCase();\n // Skip subscription/holiday calendars\n if (SKIP_SOURCES.includes(sourceType)) return false;\n if (SKIP_TYPES.includes(calType)) return false;\n return true;\n })\n .map(c => c.id);\n}\n\n/**\n * Extra safety: filter out all-day events with holiday-like patterns\n * in case they slip through from non-subscription calendars.\n */\nfunction filterHolidays(events: Event[]): Event[] {\n const HOLIDAY_PATTERNS = [\n /holiday/i, /observance/i, /eve\\b.*\\btentative\\b/i, /\\btentative\\b/i,\n ];\n\n return events.filter(e => {\n // All-day events spanning exactly one day with \"tentative\" or \"observance\" in notes → holiday\n const isAllDay = (e as any).allDay === true;\n const notes = (e.notes || '').toLowerCase();\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(notes))) return false;\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(e.title || ''))) return false;\n return true;\n });\n}\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n });\n}\n","/**\n * Contacts Collector — reads device contacts via expo-contacts.\n *\n * Two important properties for users with large address books (6000+ contacts):\n *\n * 1. Hash-based change detection. We persist a map of `contactId → contentHash`\n * across launches and on each collect cycle compare the freshly-read hashes\n * to the stored ones. If nothing changed, `collectAll()` returns null and\n * `syncCapability` skips the relay push entirely — no per-tick 1MB upload.\n *\n * 2. Reduced field set. iOS 16+ restricts which contact fields are accessible\n * without explicit user grants. Requesting `Note`/`Relationships` causes\n * `getContactsAsync` to throw \"Failed to serialize contact data:\n * Unauthorized Keys\", failing the entire collect. We stick to the four\n * fields actually used downstream (Name, PhoneNumbers, Emails, Birthday).\n *\n * Output format on push (when there ARE changes) is unchanged from before —\n * a full `Contact[]` — so the relay/MCP layer doesn't have to know about the\n * incremental scheme. The relay simply gets pushed less often.\n */\nimport * as ExpoContacts from 'expo-contacts';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoContacts.Calendar`-style annotations need this.\n// expo-contacts exports no `Date` type; take the shape from Contact itself.\ntype ExpoContact = Awaited<ReturnType<typeof ExpoContacts.getContactsAsync>>['data'][number];\ntype ContactDate = NonNullable<ExpoContact['birthday']>;\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { Contact } from '../types';\n\nconst HASH_STORE_KEY = 'device-bridge.contacts.hashes.v1';\n\n/** Request contacts permissions. */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/** Check if contacts permission is granted. */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Diagnostic counters from the most recent collect — exposed so the Profile\n * debug view can surface \"what changed since last sync\".\n */\nexport interface ContactsCollectStats {\n total: number;\n added: number;\n updated: number;\n removed: number;\n unchanged: number;\n full_sync: boolean;\n}\nexport let lastContactsStats: ContactsCollectStats | null = null;\n\n/**\n * Read all contacts and detect changes vs the previously-stored hash map.\n *\n * Returns:\n * - the full Contact[] when anything changed (or this is the first sync),\n * - null when every contact's hash matches what we pushed last time, signalling\n * the bridge that no relay push is needed this cycle.\n */\nexport async function collectAll(): Promise<Contact[] | null> {\n // Only the four fields we actually surface downstream. Adding more reawakens\n // the iOS \"Unauthorized Keys\" failure unless the user has granted them via\n // a separate flow.\n const result = await ExpoContacts.getContactsAsync({\n fields: [\n ExpoContacts.Fields.Name,\n ExpoContacts.Fields.PhoneNumbers,\n ExpoContacts.Fields.Emails,\n ExpoContacts.Fields.Birthday,\n ],\n sort: ExpoContacts.SortTypes.LastName,\n });\n\n const raw = Array.isArray(result?.data) ? result.data : [];\n\n // Map raw contacts to our schema, computing a stable per-contact hash that's\n // independent of array ordering and resilient to spurious whitespace/case\n // changes in fields we don't actually compare on.\n const contacts: Contact[] = [];\n const currentHashes: Record<string, string> = {};\n for (let i = 0; i < raw.length; i++) {\n const c = raw[i];\n const id = c.id || `c${i}`;\n const mapped: Contact = {\n id,\n name: c.name || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',\n relationship: '', // dropped — was the iOS Unauthorized-Keys trigger\n phone: c.phoneNumbers?.[0]?.number || null,\n email: c.emails?.[0]?.email || null,\n birthday: formatBirthday(c.birthday),\n notes: '', // dropped — Notes is restricted on iOS 16+\n };\n contacts.push(mapped);\n currentHashes[id] = hashContact(mapped);\n }\n\n // Compare against last-pushed hashes.\n let stored: Record<string, string> = {};\n try {\n const blob = await AsyncStorage.getItem(HASH_STORE_KEY);\n if (blob) stored = JSON.parse(blob) as Record<string, string>;\n } catch {\n /* corrupted store — treat as empty, will full-sync */\n }\n const fullSync = Object.keys(stored).length === 0;\n\n let added = 0;\n let updated = 0;\n let unchanged = 0;\n for (const id of Object.keys(currentHashes)) {\n const prev = stored[id];\n if (prev === undefined) added++;\n else if (prev !== currentHashes[id]) updated++;\n else unchanged++;\n }\n let removed = 0;\n for (const id of Object.keys(stored)) {\n if (currentHashes[id] === undefined) removed++;\n }\n\n lastContactsStats = {\n total: contacts.length,\n added,\n updated,\n removed,\n unchanged,\n full_sync: fullSync,\n };\n\n // No changes → tell the bridge to skip the relay push this cycle. Saves\n // a 6000-row payload going up the wire every 5 minutes when nothing moved.\n if (!fullSync && added === 0 && updated === 0 && removed === 0) {\n return null;\n }\n\n // Persist the new hash map AFTER deciding to push. If the push fails we'll\n // re-detect the same changes on the next cycle (push is idempotent enough).\n try {\n await AsyncStorage.setItem(HASH_STORE_KEY, JSON.stringify(currentHashes));\n } catch {\n /* a failed persist just means we'll redo the diff next time — non-fatal */\n }\n\n return contacts;\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Stable string hash (FNV-1a 32-bit, hex). Pure JS, no native dep, fine for\n * change detection. NOT cryptographic — a determined collision attacker could\n * mask a contact change, but that's not in our threat model.\n */\nfunction fnv1a(str: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\nfunction hashContact(c: Contact): string {\n // Concatenate the canonical-cased, trimmed values of every field we surface.\n // Order matters and is fixed; null becomes a single sentinel character so\n // empty-vs-missing differs from \"actual whitespace\" in the hash space.\n const parts = [\n c.id,\n (c.name || '').trim(),\n c.phone || '\\0',\n (c.email || '\\0').toLowerCase(),\n c.birthday || '\\0',\n ];\n return fnv1a(parts.join('\u001f')); // unit separator — won't appear in any field\n}\n\nfunction formatBirthday(birthday: ContactDate | undefined): string | null {\n if (!birthday) return null;\n const { year, month, day } = birthday;\n if (month === undefined || day === undefined) return null;\n const m = String(month + 1).padStart(2, '0');\n const d = String(day).padStart(2, '0');\n\n // No year is the COMMON case on iOS — the contact editor lets you omit it,\n // and most people do. Substituting 1900 made that indistinguishable from a\n // real birth year, and the server dutifully computed an age from it: \"Dana\n // is turning 126 next Tuesday.\"\n //\n // `--MM-DD` is the ISO 8601 form for a recurring month-day with no year, so\n // the absence is stated rather than filled in.\n if (!year) return `--${m}-${d}`;\n\n return `${year}-${m}-${d}`;\n}\n","/**\n * Location Collector — reads device location via expo-location.\n * Output format matches device-mock-mcp's location.current schema.\n */\n\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress } from '@ateam-ai-mobile/plugin-runtime';\n// The SDK's one reverse geocode — the same call native.maps.reverseGeocode makes.\nimport { reverseGeocode } from '../../maps';\nimport type { LocationData } from '../types';\n\n/**\n * Request location permissions (foreground + background).\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status: fg } = await ExpoLocation.requestForegroundPermissionsAsync();\n if (fg !== 'granted') return false;\n\n // Background is optional — skip in Expo Go (crashes without native module)\n try {\n await ExpoLocation.requestBackgroundPermissionsAsync();\n } catch {\n // Expected in Expo Go — foreground is enough for basic functionality\n }\n return fg === 'granted';\n}\n\n/**\n * Check if location permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoLocation.getForegroundPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect current location.\n * Matches the device.location.current tool output schema.\n */\nexport async function collectCurrent(): Promise<LocationData> {\n const location = await ExpoLocation.getCurrentPositionAsync({\n accuracy: ExpoLocation.Accuracy.Balanced,\n });\n\n // Reverse geocode to get address\n let place = 'Current Location';\n let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;\n let locationType = 'unknown';\n\n try {\n const geocoded = await reverseGeocode({\n latitude: location.coords.latitude,\n longitude: location.coords.longitude,\n });\n\n if (geocoded) {\n place = geocoded.name || geocoded.street || place;\n address = [\n geocoded.streetNumber,\n geocoded.street,\n geocoded.city,\n geocoded.region,\n ]\n .filter(Boolean)\n .join(', ') || address;\n\n // Attempt to classify location type\n locationType = classifyLocation(geocoded);\n }\n } catch {\n // Reverse geocoding can fail — use coordinates only\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n location: {\n place,\n address,\n lat: location.coords.latitude,\n lng: location.coords.longitude,\n type: locationType,\n },\n };\n}\n\n// ─── Plugin-facing position API ───\n// `collectCurrent` above answers the device-context question (\"where am I, in\n// words\") and reverse-geocodes. A plugin tool usually wants the raw fix and\n// nothing else, so these stay separate: no geocoding round trip, no address.\n\nexport interface Position {\n latitude: number;\n longitude: number;\n altitude?: number;\n accuracy?: number;\n timestamp: number;\n /**\n * Metres per second. `undefined` = the OS gave no estimate.\n *\n * iOS reports -1 rather than omitting it, and -1 m/s passed through as a\n * speed would read as \"moving backwards\"; it is normalised away here so no\n * caller has to know the platform's sentinel.\n */\n speed?: number;\n /**\n * COURSE OVER GROUND in degrees — the direction the user is TRAVELLING.\n *\n * Deliberately not called `heading`: this and the compass answer different\n * questions and disagree constantly. Walking backwards down a corridor gives\n * a course of south and a compass heading of north; a phone held sideways\n * while walking forward gives the same course and a heading 90° off.\n * \"Turn right\" needs the compass. \"You are heading north on Main Street\"\n * needs this. The old code assigned one to the other, which is only ever\n * right by accident.\n *\n * `undefined` when not moving or not estimated — iOS reports -1 for both.\n */\n course?: number;\n}\n\n/** Ensure foreground permission, asking once if we do not have it yet. */\nasync function ensurePermission(): Promise<void> {\n if (await hasPermission()) return;\n const granted = await requestPermission();\n if (!granted) {\n // Callers surface this verbatim to the user (a voice assistant reads it\n // out), so it has to say what the user can do about it.\n throw new Error(\n 'Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services.',\n );\n }\n}\n\n/** One position fix. `accuracy: \"high\"` costs battery and time — default low. */\nexport async function getPosition(\n opts: { accuracy?: 'low' | 'high' } = {},\n): Promise<Position> {\n await ensurePermission();\n const fix = await ExpoLocation.getCurrentPositionAsync({\n accuracy:\n opts.accuracy === 'high'\n ? ExpoLocation.Accuracy.High\n : ExpoLocation.Accuracy.Balanced,\n });\n return toPosition(fix);\n}\n\n/**\n * One place that turns an expo fix into a `Position`.\n *\n * It exists because the sentinel handling has to be identical on the one-shot\n * and the streamed path. It was not: `watchPosition` passed lat/lon only, so\n * the semantic Spatial domain could never report speed or course no matter\n * what the OS supplied, and reported `null` as if the phone had no GPS.\n */\nfunction toPosition(fix: any): Position {\n return {\n latitude: fix.coords.latitude,\n longitude: fix.coords.longitude,\n altitude: fix.coords.altitude ?? undefined,\n accuracy: nonNegative(fix.coords.accuracy),\n timestamp: fix.timestamp ?? Date.now(),\n speed: nonNegative(fix.coords.speed),\n course: nonNegative(fix.coords.heading),\n };\n}\n\n/**\n * iOS uses -1 for \"no estimate\" on accuracy, speed and course alike. Dropping\n * the field says \"not measured\"; passing -1 through says \"measured, and the\n * answer is minus one\", which a model will happily repeat.\n */\nfunction nonNegative(v: unknown): number | undefined {\n return typeof v === 'number' && v >= 0 ? v : undefined;\n}\n\nconst WATCH_TIER = {\n navigation: ExpoLocation.Accuracy.BestForNavigation,\n high: ExpoLocation.Accuracy.High,\n balanced: ExpoLocation.Accuracy.Balanced,\n low: ExpoLocation.Accuracy.Low,\n} as const;\n\n/**\n * Stream position updates. Returns an unsubscribe function — call it, or the\n * GPS keeps running and drains the battery.\n *\n * `accuracy` picks the GPS tier. The default stays Balanced (≈100 m on iOS),\n * which is right for \"which city block\" and wrong for \"turn at the next corner\":\n * a navigation session asks for BestForNavigation and pays the battery for it.\n */\nexport function watchPosition(\n cb: (pos: Position) => void,\n opts?: { accuracy?: keyof typeof WATCH_TIER },\n): () => void {\n let subscription: { remove(): void } | null = null;\n let cancelled = false;\n\n (async () => {\n try {\n await ensurePermission();\n if (cancelled) return;\n subscription = await ExpoLocation.watchPositionAsync(\n { accuracy: WATCH_TIER[opts?.accuracy ?? 'balanced'] },\n fix => cb(toPosition(fix)),\n );\n // Unsubscribed while we were still asking for permission.\n if (cancelled) { subscription?.remove(); subscription = null; }\n } catch (err: any) {\n console.warn('[location] watchPosition failed:', err?.message || String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n subscription?.remove();\n subscription = null;\n };\n}\n\n// ─── Helpers ───\n\nfunction classifyLocation(geocoded: MapsAddress): string {\n // Simple heuristic — can be enhanced with user-labeled places from memory\n const name = (geocoded.name || '').toLowerCase();\n const street = (geocoded.street || '').toLowerCase();\n\n if (name.includes('home') || name.includes('residence')) return 'home';\n if (name.includes('office') || name.includes('workplace') || name.includes('tech park')) return 'work';\n if (name.includes('school') || name.includes('elementary') || name.includes('university')) return 'school';\n if (name.includes('gym') || name.includes('fitness')) return 'fitness';\n if (name.includes('hospital') || name.includes('clinic') || name.includes('dental')) return 'medical';\n\n return 'other';\n}\n","/**\n * Battery Collector — reads battery status via expo-battery.\n * Output format matches device-mock-mcp's device.battery schema.\n */\n\nimport * as ExpoBattery from 'expo-battery';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoBattery.Calendar`-style annotations need this.\n// BatteryState is not re-exported from the package entry — derive it from\n// the API that returns it so we never hand-maintain a copy of the enum.\ntype BatteryState = Awaited<ReturnType<typeof ExpoBattery.getBatteryStateAsync>>;\nimport type { BatteryData } from '../types';\n\n// No permission needed for battery\n\n/**\n * Collect current battery status.\n * Matches the device.battery tool output schema.\n */\nexport async function collectBattery(): Promise<BatteryData> {\n const level = await ExpoBattery.getBatteryLevelAsync();\n const state = await ExpoBattery.getBatteryStateAsync();\n const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n battery: {\n level: Math.round(level * 100), // expo-battery returns 0-1\n state: mapBatteryState(state),\n low_power_mode: lowPower,\n },\n };\n}\n\n/**\n * Subscribe to OS-level battery changes (level + charging state).\n *\n * iOS suspends JS timers when the app is backgrounded, so a pure setInterval\n * sync drifts: the relay ends up stuck on whatever level we last sampled\n * before the app went to sleep (commonly 100% from an overnight charge).\n * expo-battery's listeners fire from the native side and survive that gap,\n * so we get a fresh push as soon as the OS notices a change.\n *\n * Level changes are throttled to >=5pp deltas — without that, iOS emits a\n * level event for every 1% tick which would spam the relay.\n */\nexport function subscribeToBattery(callback: () => void): () => void {\n let lastLevel: number | null = null;\n\n const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {\n const pct = Math.round(batteryLevel * 100);\n if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {\n lastLevel = pct;\n callback();\n }\n });\n\n const stateSub = ExpoBattery.addBatteryStateListener(() => {\n callback();\n });\n\n return () => {\n levelSub?.remove?.();\n stateSub?.remove?.();\n };\n}\n\n// ─── Helpers ───\n\nfunction mapBatteryState(\n state: BatteryState\n): 'charging' | 'unplugged' | 'full' {\n switch (state) {\n case ExpoBattery.BatteryState.CHARGING:\n return 'charging';\n case ExpoBattery.BatteryState.FULL:\n return 'full';\n case ExpoBattery.BatteryState.UNPLUGGED:\n default:\n return 'unplugged';\n }\n}\n","/**\n * Connectivity Collector — reads network state via expo-network.\n * Output format matches device-mock-mcp's device.connectivity schema.\n */\n\nimport * as ExpoNetwork from 'expo-network';\nimport type { ConnectivityData } from '../types';\n\n// No special permission needed for network state\n\n/**\n * Collect current connectivity status.\n * Matches the device.connectivity tool output schema.\n */\nexport async function collectConnectivity(): Promise<ConnectivityData> {\n const networkState = await ExpoNetwork.getNetworkStateAsync();\n\n let type: 'wifi' | 'cellular' | 'none' = 'none';\n let wifiName: string | undefined;\n let cellularType: string | undefined;\n\n if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {\n type = 'wifi';\n // Wi-Fi SSID requires additional permissions on newer OS versions\n // and may not always be available\n try {\n wifiName = await getWifiName();\n } catch {\n wifiName = undefined;\n }\n } else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {\n type = 'cellular';\n cellularType = getCellularGeneration();\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n connectivity: {\n type,\n is_connected: networkState.isConnected ?? false,\n ...(wifiName && { wifi_name: wifiName }),\n ...(cellularType && { cellular_type: cellularType }),\n },\n };\n}\n\n// ─── Helpers ───\n\nasync function getWifiName(): Promise<string | undefined> {\n // expo-network doesn't expose SSID directly on all platforms\n // This would need a native module or expo-wifi in production\n return undefined;\n}\n\nfunction getCellularGeneration(): string {\n // expo-network doesn't expose cellular generation\n // In production, use a native module for this\n return 'LTE';\n}\n","/**\n * Notifications Collector — reads notification state via expo-notifications.\n * Output format matches device-mock-mcp's notifications.list schema.\n *\n * Note: On iOS/Android, expo-notifications only gives access to notifications\n * that YOUR app has posted or received. System-wide notification access\n * requires platform-specific APIs (NotificationListenerService on Android,\n * UNUserNotificationCenter on iOS with app extensions).\n *\n * For the PA, the relay approach means we capture notifications delivered\n * TO our app (from the PA system), not FROM other apps.\n * Other-app notifications would need a native module or accessibility service.\n */\n\nimport * as ExpoNotifications from 'expo-notifications';\nimport type { Notification, NotificationsData } from '../types';\n\n/**\n * Request notification permissions.\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if notification permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect delivered notifications from the notification tray.\n * Only captures notifications delivered to THIS app.\n * Matches the device.notifications.list tool output schema.\n */\nexport async function collectNotifications(): Promise<NotificationsData> {\n const presented = await ExpoNotifications.getPresentedNotificationsAsync();\n\n const notifications: Notification[] = presented.map((n, i) => ({\n id: n.request.identifier || `n${i}`,\n app: (n.request.content.data?.app as string) || 'A-Team',\n title: n.request.content.title || '',\n body: n.request.content.body || '',\n timestamp: n.date ? new Date(n.date).toISOString() : new Date().toISOString(),\n sender: (n.request.content.data?.sender as string) || null,\n priority: mapPriority(n.request.content.data?.priority as string),\n read: false, // Presented notifications are unread by definition\n }));\n\n const unread = notifications.filter(n => !n.read).length;\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n total: notifications.length,\n unread,\n notifications,\n };\n}\n\n// ─── Helpers ───\n\nfunction mapPriority(p: string | undefined): 'high' | 'medium' | 'low' {\n if (p === 'high' || p === 'max') return 'high';\n if (p === 'low' || p === 'min') return 'low';\n return 'medium';\n}\n","/**\n * App Engagement Collector — our OWN usage analytics (no OS permission, fully\n * available on every platform). Feeds the mood summary's app_sessions +\n * app_active_min, which iOS otherwise blocks for third-party apps.\n *\n * app_sessions — count of foreground sessions today (a background→active\n * transition, or a cold start, starts a session).\n * app_active_min — total minutes the app was in the foreground today.\n *\n * Counters are kept per local day and persisted to AsyncStorage so they\n * survive relaunches. start() must be called once at app boot to attach the\n * AppState listener; getToday() returns the current day's tallies.\n */\nimport { AppState, type AppStateStatus } from 'react-native';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\nconst KEY = 'ateam.appEngagement.v1';\n\ninterface DayTally {\n date: string; // local YYYY-MM-DD\n sessions: number;\n activeMs: number;\n}\n\nlet _tally: DayTally | null = null;\nlet _foregroundSince: number | null = null;\nlet _started = false;\nlet _sub: { remove: () => void } | null = null;\n\nfunction todayKey(): string {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n\nfunction fresh(): DayTally {\n return { date: todayKey(), sessions: 0, activeMs: 0 };\n}\n\nasync function load(): Promise<void> {\n try {\n const raw = await AsyncStorage.getItem(KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as DayTally;\n _tally = parsed?.date === todayKey() ? parsed : fresh();\n } else {\n _tally = fresh();\n }\n } catch {\n _tally = fresh();\n }\n}\n\nfunction persist(): void {\n if (_tally) AsyncStorage.setItem(KEY, JSON.stringify(_tally)).catch(() => {});\n}\n\nfunction rollIfNewDay(): void {\n if (!_tally || _tally.date !== todayKey()) {\n _tally = fresh();\n persist();\n }\n}\n\n/** Fold the time spent foregrounded since the last checkpoint into activeMs. */\nfunction flushForeground(): void {\n if (_foregroundSince == null || !_tally) return;\n const now = Date.now();\n const delta = now - _foregroundSince;\n if (delta > 0) _tally.activeMs += delta;\n _foregroundSince = now;\n}\n\nfunction onChange(state: AppStateStatus): void {\n rollIfNewDay();\n if (state === 'active') {\n // New foreground session.\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n } else {\n // Leaving foreground — bank the elapsed active time.\n flushForeground();\n _foregroundSince = null;\n persist();\n }\n}\n\n/** Attach the AppState listener. Idempotent; call once at app boot. */\nexport async function start(): Promise<void> {\n if (_started) return;\n _started = true;\n await load();\n // Count the launch itself as the first active session.\n rollIfNewDay();\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n _sub = AppState.addEventListener('change', onChange);\n}\n\nexport function stop(): void {\n flushForeground();\n persist();\n _sub?.remove();\n _sub = null;\n _started = false;\n}\n\n/** Current day's tallies for the mood summary. */\nexport async function getToday(): Promise<{ app_sessions: number | null; app_active_min: number | null }> {\n if (!_tally) await load();\n rollIfNewDay();\n flushForeground(); // include the in-progress foreground stretch\n persist();\n if (!_tally) return { app_sessions: null, app_active_min: null };\n return {\n app_sessions: _tally.sessions,\n app_active_min: Math.round(_tally.activeMs / 60000),\n };\n}\n","/**\n * Camera capability — permission only, NO collector.\n *\n * Unlike calendar/contacts/health, the camera is a LIVE capability: frames are\n * used in the moment (deviceState.visual, a widget's camera preview) and are\n * analysed on-device — never collected to the relay. So this module carries only\n * the OS-permission check/request that the capability toggle drives; there is no\n * `collect`. The user's grant (this toggle) is what gates access, on the same\n * path as location/contacts.\n */\nimport { Camera } from 'expo-camera';\n\nexport async function hasPermission(): Promise<boolean> {\n try {\n return (await Camera.getCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n\nexport async function requestPermission(): Promise<boolean> {\n try {\n return (await Camera.requestCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n","/**\n * Device-bridge: Biometrics (Face ID / Touch ID / fingerprint)\n *\n * Used by solutions that need a biometric gate before sensitive actions\n * (e.g. \"unlock payment\", \"confirm financial transfer\", \"reveal stored\n * credentials\"). Also useful for protecting session cookies at rest.\n *\n * Wraps expo-local-authentication.\n */\n\nimport * as LocalAuthentication from 'expo-local-authentication';\n\nexport type BiometricType = 'fingerprint' | 'face' | 'iris' | 'none';\n\nexport interface BiometricCapabilities {\n hasHardware: boolean;\n isEnrolled: boolean;\n /**\n * Never contains 'none' — the mapper filters it out, since 'none' means\n * \"the OS reported an auth type we do not recognise\" and is not a\n * biometric a caller can offer. Typed as the guarantee the code makes.\n */\n supportedTypes: Exclude<BiometricType, 'none'>[];\n}\n\n/** What can the device do right now? */\nexport async function capabilities(): Promise<BiometricCapabilities> {\n const [hasHardware, isEnrolled, types] = await Promise.all([\n LocalAuthentication.hasHardwareAsync(),\n LocalAuthentication.isEnrolledAsync(),\n LocalAuthentication.supportedAuthenticationTypesAsync(),\n ]);\n const supportedTypes = types.map((t) => {\n if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return 'fingerprint';\n if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return 'face';\n if (t === LocalAuthentication.AuthenticationType.IRIS) return 'iris';\n return 'none';\n }).filter((t) => t !== 'none') as Exclude<BiometricType, 'none'>[];\n return { hasHardware, isEnrolled, supportedTypes };\n}\n\nexport interface AuthenticateOptions {\n /** User-visible reason shown in the system dialog. */\n reason: string;\n /** Label on the \"cancel\" button (iOS only; Android has system default). */\n cancelLabel?: string;\n /** If biometrics aren't enrolled, fall back to device passcode. Default true. */\n fallbackToPasscode?: boolean;\n}\n\n/** Prompt the user for biometric auth. Resolves { success } or { success:false, error }. */\nexport async function authenticate(opts: AuthenticateOptions): Promise<{ success: boolean; error?: string }> {\n try {\n const res = await LocalAuthentication.authenticateAsync({\n promptMessage: opts.reason,\n cancelLabel: opts.cancelLabel,\n disableDeviceFallback: opts.fallbackToPasscode === false,\n });\n if (res.success) return { success: true };\n return { success: false, error: (res as any).error || 'unknown' };\n } catch (e: any) {\n return { success: false, error: String(e?.message || e) };\n }\n}\n","/**\n * Device-bridge: Sharing (native share sheet)\n *\n * Wraps expo-sharing. Lets solutions hand content off to the OS share sheet\n * — any installed app can receive it (Messages, WhatsApp, Mail, Files, etc.).\n *\n * Typical use: \"Share this article / calendar invite / photo with a contact.\"\n */\n\nimport * as Sharing from 'expo-sharing';\n\nexport interface ShareOptions {\n /** MIME type, e.g. 'application/pdf'. Optional. */\n mimeType?: string;\n /** iOS UTI, e.g. 'public.image'. Optional. */\n UTI?: string;\n /** Dialog title (Android only). */\n dialogTitle?: string;\n}\n\n/** Share a local file URI. */\nexport async function shareFile(fileUri: string, opts: ShareOptions = {}): Promise<{ ok: boolean; error?: string }> {\n try {\n const available = await Sharing.isAvailableAsync();\n if (!available) return { ok: false, error: 'sharing_unavailable' };\n await Sharing.shareAsync(fileUri, opts);\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: String(e?.message || e) };\n }\n}\n\nexport async function isAvailable(): Promise<boolean> {\n return Sharing.isAvailableAsync();\n}\n","/**\n * Device-bridge: Scanner (QR / barcode)\n *\n * Wraps expo-camera (which replaced the deprecated expo-barcode-scanner\n * in Expo SDK 50+). The CameraView component has built-in barcode scanning.\n *\n * Use cases:\n * - Wi-Fi QR handoff\n * - OAuth / pairing codes\n * - Payment QR codes\n * - Product lookup\n *\n * Plugins mount <CameraView onBarcodeScanned={...} /> directly. This module\n * exposes the permission helper + a result normalizer so plugins don't need\n * to import expo-camera directly.\n */\n\nimport { CameraView, useCameraPermissions, type BarcodeScanningResult } from 'expo-camera';\n\nexport interface ScanResult {\n type: string; // e.g. 'qr', 'ean13', 'code128'\n data: string; // decoded content\n}\n\n/** Request camera permission if not already granted. */\nexport async function requestPermissions(): Promise<{ granted: boolean; status: string }> {\n // useCameraPermissions is a hook; for the non-hook flow plugins should use\n // Camera.requestCameraPermissionsAsync. But it's easier to expose the hook\n // helper — plugins typically use the hook form inside their Component.\n // For service-style callers, fall back to the imperative API:\n const { Camera } = await import('expo-camera');\n const { status } = await Camera.requestCameraPermissionsAsync();\n return { granted: status === 'granted', status };\n}\n\n/** Normalize a CameraView barcode result to a stable shape. */\nexport function normalizeResult(r: BarcodeScanningResult): ScanResult {\n return { type: r.type, data: r.data };\n}\n\n/** Re-export the component + hook so plugins consume them via device-bridge. */\nexport { CameraView, useCameraPermissions };\n","/**\n * @ateam-ai-mobile/expo/oauth — the native OAuth provider for the relocated\n * auth screens (LoginScreen / JoinScreen / OpenJoinScreen in @ateam-ai-mobile/ui).\n *\n * This is the ONLY place @react-native-google-signin + expo-apple-authentication\n * are imported. The `ui` package stays native-free: its screens call\n * getOAuthProvider() for the id_token acquisition + the real Apple HIG button.\n *\n * Deliberately a SUBPATH (`@ateam-ai-mobile/expo/oauth`), not part of the main\n * entry — same rule as `/bridge` and `/device`: a host that doesn't install\n * these native modules must still be able to build @ateam-ai-mobile/expo.\n *\n * Behaviour is ported verbatim from the app's old login.tsx so the iPhone\n * parity gate sees no change:\n * - Google: configure({ webClientId, iosClientId }) → hasPlayServices →\n * signIn → normalize the v15-vs-older id_token shape. A missing iosClientId\n * silently falls back to a web flow (unchanged); the caller gates on\n * webClientId, so we never reach here without one for the login path.\n * - a user-cancel (SIGN_IN_CANCELLED / IN_PROGRESS / ERR_REQUEST_CANCELED)\n * resolves to null — the screen treats null as \"no error\", never a red flash.\n */\nimport React from 'react';\nimport { Platform } from 'react-native';\nimport {\n GoogleSignin,\n statusCodes,\n type ConfigureParams,\n} from '@react-native-google-signin/google-signin';\nimport * as AppleAuthentication from 'expo-apple-authentication';\nimport type { OAuthProvider, OAuthGoogleConfig } from '@ateam-ai-mobile/ui';\n\nconst NOOP = () => {};\n\nexport interface ExpoOAuthConfig {\n /**\n * The host's bundled Google client ids, used by the join / open-join screens\n * that carry no tenant config (they pass no cfg to signInWithGoogle). The\n * login screen passes the tenant's configured ids and overrides these.\n */\n googleWebClientId?: string;\n googleIosClientId?: string;\n}\n\n/**\n * Build the native OAuth provider. Wire it once at startup:\n * setOAuthProvider(createExpoOAuthProvider({ googleWebClientId, googleIosClientId }));\n */\nexport function createExpoOAuthProvider(host: ExpoOAuthConfig = {}): OAuthProvider {\n return {\n async signInWithGoogle(cfg?: OAuthGoogleConfig): Promise<string | null> {\n const webClientId = cfg?.webClientId || host.googleWebClientId || '';\n const iosClientId = cfg?.iosClientId || host.googleIosClientId || '';\n // configure is idempotent + cheap; do it at sign-in time so the tenant's\n // ids are always current (replaces login.tsx's configure effect).\n const params: ConfigureParams = { webClientId };\n if (iosClientId) (params as any).iosClientId = iosClientId;\n GoogleSignin.configure(params);\n try {\n // android-only; no-op on iOS. Called defensively, as before.\n await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });\n const result: any = await GoogleSignin.signIn();\n // v15+ returns { data: { idToken } }; older returns { idToken }.\n return result?.data?.idToken || result?.idToken || null;\n } catch (err: any) {\n const code = err?.code;\n if (code === statusCodes.SIGN_IN_CANCELLED || code === statusCodes.IN_PROGRESS) {\n return null;\n }\n throw err;\n }\n },\n\n async signInWithApple(): Promise<string | null> {\n try {\n const credential = await AppleAuthentication.signInAsync({\n requestedScopes: [\n AppleAuthentication.AppleAuthenticationScope.FULL_NAME,\n AppleAuthentication.AppleAuthenticationScope.EMAIL,\n ],\n });\n return credential.identityToken || null;\n } catch (err: any) {\n // ERR_REQUEST_CANCELED = the user dismissed the sheet.\n if (err?.code === 'ERR_REQUEST_CANCELED') return null;\n throw err;\n }\n },\n\n async isAppleAvailable(): Promise<boolean> {\n if (Platform.OS !== 'ios') return false;\n try {\n return await AppleAuthentication.isAvailableAsync();\n } catch {\n return false;\n }\n },\n\n renderAppleButton({ onPress, disabled, style }): React.ReactNode {\n // Apple's HIG requires their own styled button when Apple Sign-In sits\n // alongside other providers. Same chrome the app shipped: CONTINUE /\n // BLACK / fully-rounded. Layout (width/height) comes from the screen.\n return (\n <AppleAuthentication.AppleAuthenticationButton\n buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}\n buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}\n cornerRadius={999}\n style={style as any}\n onPress={disabled ? NOOP : onPress}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["CookieManager","isSupported","NfcManager","import_core","import_plugin_runtime","import_ui","import_async_storage","import_plugin_runtime","AsyncStorage","FileSystem","import_async_storage","AsyncStorage","sub","fixAt","latest","nonNegative","point","latest","err","s","import_react_native","text","AsyncStorage","Haptics","ExpoLocation","import_async_storage","import_expo_modules_core","native","native","import_react_native","import_expo_modules_core","ExpoLocation","verb","refuseArgument","verb","native","isSupported","import_react_native","import_react_native","ExpoCalendar","import_async_storage","ExpoLocation","ExpoBattery","import_react_native","import_async_storage","import_expo_camera","import_expo_camera","Camera","ensurePermission","AsyncStorage","verb","sub","verb","isSupported","point","import_react_native","import_jsx_runtime"]}