@switchlabs/verify-ai-react-native 1.1.0 → 1.1.2

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.
Files changed (48) hide show
  1. package/README.md +17 -4
  2. package/lib/client/index.d.ts +22 -2
  3. package/lib/client/index.js +132 -19
  4. package/lib/components/VerifyAIScanner.d.ts +7 -4
  5. package/lib/components/VerifyAIScanner.js +235 -18
  6. package/lib/hooks/useVerifyAI.d.ts +32 -10
  7. package/lib/hooks/useVerifyAI.js +246 -14
  8. package/lib/index.d.ts +5 -2
  9. package/lib/index.js +3 -0
  10. package/lib/ml/featureExtractor.d.ts +16 -0
  11. package/lib/ml/featureExtractor.js +123 -0
  12. package/lib/ml/imagePreprocessor.d.ts +2 -0
  13. package/lib/ml/imagePreprocessor.js +48 -0
  14. package/lib/ml/index.d.ts +5 -0
  15. package/lib/ml/index.js +4 -0
  16. package/lib/ml/inferenceEngine.d.ts +24 -0
  17. package/lib/ml/inferenceEngine.js +156 -0
  18. package/lib/ml/modelManager.d.ts +26 -0
  19. package/lib/ml/modelManager.js +207 -0
  20. package/lib/ml/policyEngine.d.ts +14 -0
  21. package/lib/ml/policyEngine.js +161 -0
  22. package/lib/ml/types.d.ts +84 -0
  23. package/lib/ml/types.js +4 -0
  24. package/lib/storage/offlineQueue.js +1 -1
  25. package/lib/telemetry/TelemetryContext.d.ts +4 -0
  26. package/lib/telemetry/TelemetryContext.js +5 -0
  27. package/lib/telemetry/TelemetryReporter.d.ts +24 -0
  28. package/lib/telemetry/TelemetryReporter.js +141 -0
  29. package/lib/types/index.d.ts +18 -0
  30. package/lib/version.d.ts +1 -0
  31. package/lib/version.js +1 -0
  32. package/package.json +23 -2
  33. package/src/client/index.ts +176 -25
  34. package/src/components/VerifyAIScanner.tsx +282 -21
  35. package/src/hooks/useVerifyAI.ts +332 -18
  36. package/src/index.ts +20 -1
  37. package/src/ml/featureExtractor.ts +160 -0
  38. package/src/ml/imagePreprocessor.ts +72 -0
  39. package/src/ml/index.ts +14 -0
  40. package/src/ml/inferenceEngine.ts +200 -0
  41. package/src/ml/modelManager.ts +265 -0
  42. package/src/ml/policyEngine.ts +201 -0
  43. package/src/ml/types.ts +104 -0
  44. package/src/storage/offlineQueue.ts +1 -1
  45. package/src/telemetry/TelemetryContext.tsx +8 -0
  46. package/src/telemetry/TelemetryReporter.ts +184 -0
  47. package/src/types/index.ts +20 -0
  48. package/src/version.ts +1 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Deterministic policy engine for React Native SDK.
3
+ * MUST produce identical output to the server TypeScript and Dart implementations.
4
+ *
5
+ * This file shares the same logic as lib/verify-ai/ml/policy-engine.ts
6
+ * but is self-contained for the SDK package.
7
+ */
8
+
9
+ import type {
10
+ FeatureVector,
11
+ PolicyAST,
12
+ PolicyResult,
13
+ RuleResult,
14
+ Rule,
15
+ Condition,
16
+ CategoryPredicate,
17
+ ComparisonOperator,
18
+ } from './types';
19
+
20
+ // ─── Field Resolution ───
21
+
22
+ function resolveField(features: FeatureVector, field: string): unknown {
23
+ const parts = field.split('.');
24
+ let current: unknown = features;
25
+
26
+ for (const part of parts) {
27
+ if (current === null || current === undefined) return undefined;
28
+ if (typeof current !== 'object') return undefined;
29
+ current = (current as Record<string, unknown>)[part];
30
+ }
31
+
32
+ return current;
33
+ }
34
+
35
+ // ─── Condition Evaluation ───
36
+
37
+ function applyOperator(fieldValue: unknown, operator: ComparisonOperator, value: unknown): boolean {
38
+ switch (operator) {
39
+ case 'exists':
40
+ return fieldValue !== null && fieldValue !== undefined;
41
+ case 'not_exists':
42
+ return fieldValue === null || fieldValue === undefined;
43
+ case 'eq':
44
+ return fieldValue === value;
45
+ case 'neq':
46
+ return fieldValue !== value;
47
+ case 'gt':
48
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue > value;
49
+ case 'gte':
50
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue >= value;
51
+ case 'lt':
52
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue < value;
53
+ case 'lte':
54
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue <= value;
55
+ case 'contains':
56
+ if (Array.isArray(fieldValue)) return fieldValue.includes(value);
57
+ if (typeof fieldValue === 'string' && typeof value === 'string') return fieldValue.includes(value);
58
+ return false;
59
+ case 'not_contains':
60
+ if (Array.isArray(fieldValue)) return !fieldValue.includes(value);
61
+ if (typeof fieldValue === 'string' && typeof value === 'string') return !fieldValue.includes(value);
62
+ return true;
63
+ case 'in':
64
+ if (Array.isArray(value)) return value.includes(fieldValue);
65
+ return false;
66
+ case 'not_in':
67
+ if (Array.isArray(value)) return !value.includes(fieldValue);
68
+ return true;
69
+ case 'overlaps':
70
+ if (Array.isArray(fieldValue) && Array.isArray(value)) {
71
+ return fieldValue.some((v) => value.includes(v));
72
+ }
73
+ return false;
74
+ default:
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function evaluateCondition(features: FeatureVector, condition: Condition): boolean {
80
+ const fieldValue = resolveField(features, condition.field);
81
+ return applyOperator(fieldValue, condition.operator, condition.value);
82
+ }
83
+
84
+ function evaluateRule(features: FeatureVector, rule: Rule): RuleResult {
85
+ const passed = rule.conditions.every((c) => evaluateCondition(features, c));
86
+ return {
87
+ rule_id: rule.id,
88
+ passed,
89
+ severity: rule.severity,
90
+ required: rule.required,
91
+ };
92
+ }
93
+
94
+ function resolveCategory(
95
+ features: FeatureVector,
96
+ categories: CategoryPredicate[],
97
+ ruleResults: RuleResult[],
98
+ ): { category_id: string; category_label: string; is_compliant: boolean } {
99
+ const failedCritical = ruleResults.some(
100
+ (r) => !r.passed && r.required && r.severity === 'critical',
101
+ );
102
+
103
+ for (const cat of categories) {
104
+ const matches = cat.conditions.every((c) => {
105
+ if (c.field === '_has_critical_failure') {
106
+ return applyOperator(failedCritical, c.operator, c.value);
107
+ }
108
+ if (c.field === '_failed_rule_count') {
109
+ const failedCount = ruleResults.filter((r) => !r.passed && r.required).length;
110
+ return applyOperator(failedCount, c.operator, c.value);
111
+ }
112
+ if (c.field === '_warning_count') {
113
+ const warnCount = ruleResults.filter((r) => !r.passed && r.severity === 'warning').length;
114
+ return applyOperator(warnCount, c.operator, c.value);
115
+ }
116
+ return evaluateCondition(features, c);
117
+ });
118
+
119
+ if (matches) {
120
+ return {
121
+ category_id: cat.category_id,
122
+ category_label: cat.label,
123
+ is_compliant: cat.is_compliant,
124
+ };
125
+ }
126
+ }
127
+
128
+ // Fallback
129
+ if (failedCritical) {
130
+ return { category_id: 'unsafe', category_label: 'Unsafe', is_compliant: false };
131
+ }
132
+ const hasFailedRequired = ruleResults.some((r) => !r.passed && r.required);
133
+ if (hasFailedRequired) {
134
+ return { category_id: 'improvable', category_label: 'Improvable', is_compliant: false };
135
+ }
136
+ return { category_id: 'compliant', category_label: 'Compliant', is_compliant: true };
137
+ }
138
+
139
+ function generateFeedback(
140
+ isCompliant: boolean,
141
+ violationReasons: string[],
142
+ ruleResults: RuleResult[],
143
+ ): string {
144
+ if (isCompliant) {
145
+ const warnings = ruleResults.filter((r) => !r.passed && r.severity === 'warning');
146
+ if (warnings.length > 0) {
147
+ return `Parking is compliant with ${warnings.length} minor suggestion(s).`;
148
+ }
149
+ return 'Parking meets all requirements.';
150
+ }
151
+
152
+ if (violationReasons.length === 0) {
153
+ return 'Unable to determine compliance.';
154
+ }
155
+
156
+ if (violationReasons.length === 1) {
157
+ return `Issue detected: ${violationReasons[0]}.`;
158
+ }
159
+
160
+ return `${violationReasons.length} issues detected: ${violationReasons.join(', ')}.`;
161
+ }
162
+
163
+ // ─── Main Evaluator ───
164
+
165
+ /**
166
+ * Evaluate a PolicyAST against a FeatureVector.
167
+ * Pure function — no side effects.
168
+ * Must produce identical output to server TS and Dart implementations.
169
+ */
170
+ export function evaluatePolicy(ast: PolicyAST, features: FeatureVector): PolicyResult {
171
+ const ruleResults = ast.rules.map((rule) => evaluateRule(features, rule));
172
+
173
+ const { category_id, category_label, is_compliant } = resolveCategory(
174
+ features,
175
+ ast.categories,
176
+ ruleResults,
177
+ );
178
+
179
+ const violationReasons: string[] = [];
180
+ for (let i = 0; i < ast.rules.length; i++) {
181
+ if (!ruleResults[i].passed && ast.rules[i].required) {
182
+ violationReasons.push(ast.rules[i].label);
183
+ }
184
+ }
185
+
186
+ const totalRules = ruleResults.length;
187
+ const passedRules = ruleResults.filter((r) => r.passed).length;
188
+ const confidence = totalRules > 0 ? passedRules / totalRules : 0;
189
+
190
+ const feedback = generateFeedback(is_compliant, violationReasons, ruleResults);
191
+
192
+ return {
193
+ category_id,
194
+ category_label,
195
+ is_compliant,
196
+ confidence: Math.round(confidence * 100) / 100,
197
+ violation_reasons: violationReasons,
198
+ rule_results: ruleResults,
199
+ feedback,
200
+ };
201
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * On-device ML types for React Native SDK.
3
+ */
4
+
5
+ export interface ModelArtifact {
6
+ role: 'detector' | 'segmenter' | 'classifier';
7
+ architecture: string;
8
+ version: number;
9
+ format: 'coreml' | 'tflite' | 'onnx';
10
+ storagePath: string;
11
+ sizeBytes: number;
12
+ sha256: string;
13
+ downloadUrl?: string;
14
+ localPath?: string;
15
+ }
16
+
17
+ export interface BundleManifest {
18
+ bundleVersion: number;
19
+ ontologyVersion: string;
20
+ schemaVersion: string;
21
+ policyId: string;
22
+ classIds: number[];
23
+ artifacts: ModelArtifact[];
24
+ policyAst: PolicyAST;
25
+ }
26
+
27
+ // ─── Feature Schema Types ───
28
+
29
+ export interface Detection {
30
+ class_id: number;
31
+ class_name: string;
32
+ confidence: number;
33
+ bbox: [number, number, number, number];
34
+ }
35
+
36
+ export interface ImageQuality {
37
+ is_blurry: boolean;
38
+ is_dark: boolean;
39
+ has_vehicle: boolean;
40
+ }
41
+
42
+ export interface FeatureVector {
43
+ schema_version: string;
44
+ detections: Detection[];
45
+ primary_vehicle: Detection | null;
46
+ image_quality: ImageQuality;
47
+ vehicle_on_surface: string | null;
48
+ vehicle_near: string[];
49
+ }
50
+
51
+ // ─── Policy AST Types ───
52
+
53
+ export type ComparisonOperator =
54
+ | 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte'
55
+ | 'contains' | 'not_contains'
56
+ | 'exists' | 'not_exists'
57
+ | 'in' | 'not_in' | 'overlaps';
58
+
59
+ export interface Condition {
60
+ field: string;
61
+ operator: ComparisonOperator;
62
+ value?: unknown;
63
+ }
64
+
65
+ export interface Rule {
66
+ id: string;
67
+ label: string;
68
+ severity: 'critical' | 'warning' | 'info';
69
+ required: boolean;
70
+ conditions: Condition[];
71
+ }
72
+
73
+ export interface CategoryPredicate {
74
+ category_id: string;
75
+ label: string;
76
+ is_compliant: boolean;
77
+ conditions: Condition[];
78
+ }
79
+
80
+ export interface PolicyAST {
81
+ ast_version: string;
82
+ policy_id: string;
83
+ ontology_version: string;
84
+ schema_version: string;
85
+ rules: Rule[];
86
+ categories: CategoryPredicate[];
87
+ }
88
+
89
+ export interface RuleResult {
90
+ rule_id: string;
91
+ passed: boolean;
92
+ severity: 'critical' | 'warning' | 'info';
93
+ required: boolean;
94
+ }
95
+
96
+ export interface PolicyResult {
97
+ category_id: string;
98
+ category_label: string;
99
+ is_compliant: boolean;
100
+ confidence: number;
101
+ violation_reasons: string[];
102
+ rule_results: RuleResult[];
103
+ feedback: string;
104
+ }
@@ -175,7 +175,7 @@ export class OfflineQueue {
175
175
  }
176
176
 
177
177
  try {
178
- const result = await this.client.verify(item.request);
178
+ const result = await this.client.verify(item.request, { idempotencyKey: item.id });
179
179
  processed++;
180
180
  await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
181
181
  onResult?.(item.id, result);
@@ -0,0 +1,8 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import type { TelemetryReporter } from './TelemetryReporter';
3
+
4
+ export const TelemetryContext = createContext<TelemetryReporter | null>(null);
5
+
6
+ export function useTelemetry(): TelemetryReporter | null {
7
+ return useContext(TelemetryContext);
8
+ }
@@ -0,0 +1,184 @@
1
+ import { Platform } from 'react-native';
2
+ import { SDK_VERSION } from '../version';
3
+
4
+ interface TelemetryEvent {
5
+ event_type: string;
6
+ component?: string;
7
+ error_message?: string;
8
+ error_stack?: string;
9
+ error_code?: string;
10
+ metadata?: Record<string, string | number>;
11
+ sdk_platform: string;
12
+ sdk_version: string;
13
+ os_name: string;
14
+ os_version: string;
15
+ session_id: string;
16
+ event_count: number;
17
+ first_occurred_at: string;
18
+ last_occurred_at: string;
19
+ }
20
+
21
+ interface BufferedEvent {
22
+ event: TelemetryEvent;
23
+ dedupKey: string;
24
+ }
25
+
26
+ /** Event types that flush immediately (critical init failures). */
27
+ const CRITICAL_EVENTS = new Set([
28
+ 'camera_init_failure',
29
+ 'camera_preview_timeout',
30
+ 'camera_permission_denied',
31
+ ]);
32
+
33
+ export class TelemetryReporter {
34
+ private buffer: Map<string, TelemetryEvent> = new Map();
35
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
36
+ private sessionId: string;
37
+ private baseUrl: string;
38
+ private apiKey: string;
39
+ private disposed = false;
40
+ private flushing = false;
41
+
42
+ constructor(apiKey: string, baseUrl: string) {
43
+ this.apiKey = apiKey;
44
+ this.baseUrl = baseUrl.replace(/\/$/, '');
45
+ this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
46
+ }
47
+
48
+ /** Track an error event. Fire-and-forget — never throws. */
49
+ track(
50
+ eventType: string,
51
+ opts: {
52
+ component?: string;
53
+ error?: unknown;
54
+ errorCode?: string;
55
+ metadata?: Record<string, string | number>;
56
+ } = {},
57
+ ): void {
58
+ if (this.disposed) return;
59
+
60
+ try {
61
+ const now = new Date().toISOString();
62
+ const errorObj = opts.error instanceof Error ? opts.error : null;
63
+ const errorMessage = errorObj?.message
64
+ ?? (typeof opts.error === 'string' ? opts.error : undefined);
65
+
66
+ const dedupKey = `${eventType}|${errorMessage ?? ''}|${opts.component ?? ''}`;
67
+
68
+ const existing = this.buffer.get(dedupKey);
69
+ if (existing) {
70
+ existing.event_count++;
71
+ existing.last_occurred_at = now;
72
+ // If the new occurrence is critical, flush immediately
73
+ if (CRITICAL_EVENTS.has(eventType)) {
74
+ this.flushNow();
75
+ }
76
+ return;
77
+ }
78
+
79
+ const event: TelemetryEvent = {
80
+ event_type: eventType,
81
+ component: opts.component,
82
+ error_message: errorMessage?.slice(0, 1000),
83
+ error_stack: errorObj?.stack?.slice(0, 2000),
84
+ error_code: opts.errorCode,
85
+ metadata: opts.metadata,
86
+ sdk_platform: Platform.OS,
87
+ sdk_version: SDK_VERSION,
88
+ os_name: Platform.OS,
89
+ os_version: String(Platform.Version),
90
+ session_id: this.sessionId,
91
+ event_count: 1,
92
+ first_occurred_at: now,
93
+ last_occurred_at: now,
94
+ };
95
+
96
+ this.buffer.set(dedupKey, event);
97
+
98
+ if (CRITICAL_EVENTS.has(eventType)) {
99
+ this.flushNow();
100
+ } else {
101
+ this.scheduleFlush();
102
+ }
103
+ } catch {
104
+ // Never throw from telemetry
105
+ }
106
+ }
107
+
108
+ /** Flush all buffered events immediately. Returns a promise but never rejects. */
109
+ async flush(): Promise<void> {
110
+ if (this.buffer.size === 0 || this.flushing) return;
111
+
112
+ this.flushing = true;
113
+ const bufferedEntries = Array.from(this.buffer.entries());
114
+ const events = bufferedEntries.map(([, event]) => event);
115
+ this.buffer.clear();
116
+ this.clearFlushTimer();
117
+ const controller = new AbortController();
118
+ const timeout = setTimeout(() => controller.abort(), 10000);
119
+
120
+ try {
121
+ const response = await fetch(`${this.baseUrl}/telemetry`, {
122
+ method: 'POST',
123
+ headers: {
124
+ 'Content-Type': 'application/json',
125
+ 'X-API-Key': this.apiKey,
126
+ },
127
+ body: JSON.stringify({ events }),
128
+ signal: controller.signal,
129
+ });
130
+
131
+ if (!response.ok) {
132
+ throw new Error(`Telemetry request failed with status ${response.status}`);
133
+ }
134
+ } catch {
135
+ for (const [dedupKey, event] of bufferedEntries) {
136
+ const existing = this.buffer.get(dedupKey);
137
+ if (existing) {
138
+ existing.event_count += event.event_count;
139
+ existing.first_occurred_at = existing.first_occurred_at < event.first_occurred_at
140
+ ? existing.first_occurred_at
141
+ : event.first_occurred_at;
142
+ existing.last_occurred_at = existing.last_occurred_at > event.last_occurred_at
143
+ ? existing.last_occurred_at
144
+ : event.last_occurred_at;
145
+ } else {
146
+ this.buffer.set(dedupKey, event);
147
+ }
148
+ }
149
+ } finally {
150
+ clearTimeout(timeout);
151
+ this.flushing = false;
152
+ if (this.buffer.size > 0 && !this.disposed) {
153
+ this.scheduleFlush();
154
+ }
155
+ }
156
+ }
157
+
158
+ /** Dispose — flush remaining events and stop timers. */
159
+ dispose(): void {
160
+ this.disposed = true;
161
+ this.clearFlushTimer();
162
+ this.flush();
163
+ }
164
+
165
+ private scheduleFlush(): void {
166
+ if (this.flushTimer) return;
167
+ this.flushTimer = setTimeout(() => {
168
+ this.flushTimer = null;
169
+ this.flush();
170
+ }, 5000);
171
+ }
172
+
173
+ private flushNow(): void {
174
+ this.clearFlushTimer();
175
+ this.flush();
176
+ }
177
+
178
+ private clearFlushTimer(): void {
179
+ if (this.flushTimer) {
180
+ clearTimeout(this.flushTimer);
181
+ this.flushTimer = null;
182
+ }
183
+ }
184
+ }
@@ -5,6 +5,8 @@ export interface VerifyAIConfig {
5
5
  baseUrl?: string;
6
6
  timeout?: number;
7
7
  offlineMode?: boolean;
8
+ /** Enable SDK telemetry reporting for client-side errors. Default: true. */
9
+ telemetry?: boolean;
8
10
  }
9
11
 
10
12
  export interface VerificationRequest {
@@ -14,6 +16,13 @@ export interface VerificationRequest {
14
16
  provider?: 'openai' | 'anthropic' | 'gemini';
15
17
  }
16
18
 
19
+ export interface MultipartVerificationRequest {
20
+ imageUri: string; // file URI (e.g. file:///path/to/photo.jpg)
21
+ policy: string;
22
+ metadata?: Record<string, unknown>;
23
+ provider?: 'openai' | 'anthropic' | 'gemini';
24
+ }
25
+
17
26
  export interface VerificationResult {
18
27
  id: string;
19
28
  created_at: string;
@@ -43,6 +52,11 @@ export interface VerificationListParams {
43
52
  end_date?: string;
44
53
  }
45
54
 
55
+ export interface VerifyOptions {
56
+ /** Idempotency key to prevent duplicate verifications on retry. */
57
+ idempotencyKey?: string;
58
+ }
59
+
46
60
  export interface QueueItem {
47
61
  id: string;
48
62
  request: VerificationRequest;
@@ -56,6 +70,11 @@ export interface VerifyAIError {
56
70
  current_usage?: number;
57
71
  limit?: number;
58
72
  upgrade_url?: string;
73
+ request_id?: string;
74
+ code?: string;
75
+ path?: string;
76
+ url?: string;
77
+ method?: string;
59
78
  }
60
79
 
61
80
  export type ScannerStatus = 'idle' | 'capturing' | 'processing' | 'success' | 'error';
@@ -86,6 +105,7 @@ export interface ScannerOverlayConfig {
86
105
  exhaustedMessage?: string;
87
106
  maxAttempts?: number;
88
107
  autoApproveOnExhaust?: boolean;
108
+ showTechnicalErrorDetails?: boolean;
89
109
  }
90
110
 
91
111
  export interface PolicyConfigResponse {
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const SDK_VERSION = '1.1.1';