@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,156 @@
1
+ /**
2
+ * On-device inference engine for React Native.
3
+ * Uses react-native-fast-tflite for TFLite inference (uses CoreML delegate on iOS).
4
+ */
5
+ import { Platform } from 'react-native';
6
+ import { MODEL_OUTPUT_CLASS_IDS } from './featureExtractor';
7
+ export class InferenceEngine {
8
+ constructor() {
9
+ this.model = null;
10
+ this._isLoaded = false;
11
+ this.classIds = MODEL_OUTPUT_CLASS_IDS;
12
+ }
13
+ get isLoaded() {
14
+ return this._isLoaded;
15
+ }
16
+ /** Load the model from a cached bundle artifact. */
17
+ async loadModel(bundle) {
18
+ const artifact = bundle.artifacts.find((a) => a.format === 'tflite');
19
+ if (!artifact?.localPath) {
20
+ throw new Error('No TFLite artifact with local path in bundle');
21
+ }
22
+ try {
23
+ const tfliteModule = await import('react-native-fast-tflite');
24
+ const { loadTensorflowModel } = tfliteModule;
25
+ const preferredDelegate = Platform.OS === 'ios' ? 'core-ml' : undefined;
26
+ try {
27
+ this.model = await loadTensorflowModel({ url: artifact.localPath }, preferredDelegate);
28
+ }
29
+ catch {
30
+ this.model = await loadTensorflowModel({ url: artifact.localPath });
31
+ }
32
+ this.classIds = bundle.classIds.length > 0 ? bundle.classIds : MODEL_OUTPUT_CLASS_IDS;
33
+ this._isLoaded = true;
34
+ }
35
+ catch (err) {
36
+ this._isLoaded = false;
37
+ throw new Error(`Failed to load TFLite model: ${err}`);
38
+ }
39
+ }
40
+ /** Run inference on preprocessed image data. */
41
+ runInference(input, threshold = 0.3) {
42
+ if (!this._isLoaded || !this.model) {
43
+ throw new Error('Model not loaded');
44
+ }
45
+ // Run inference
46
+ const outputs = this.model.runSync([input]);
47
+ return this.parseDetections(outputs, threshold);
48
+ }
49
+ parseDetections(outputs, threshold) {
50
+ const detections = [];
51
+ if (!outputs || outputs.length === 0)
52
+ return detections;
53
+ if (outputs.length === 1) {
54
+ // Single combined output: [1, N, 4+num_classes]
55
+ const output = extractMatrix(outputs[0]);
56
+ const numDetections = output.length;
57
+ for (let i = 0; i < numDetections; i++) {
58
+ const det = output[i];
59
+ if (det.length < 5)
60
+ continue;
61
+ const bbox = det.slice(0, 4);
62
+ const scores = det.slice(4);
63
+ let maxScore = 0;
64
+ let classIndex = 0;
65
+ for (let j = 0; j < scores.length; j++) {
66
+ if (scores[j] > maxScore) {
67
+ maxScore = scores[j];
68
+ classIndex = j;
69
+ }
70
+ }
71
+ if (maxScore >= threshold) {
72
+ detections.push({
73
+ classId: this.resolveClassId(classIndex),
74
+ confidence: maxScore,
75
+ bbox,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ else if (outputs.length >= 2) {
81
+ // Separate boxes and scores
82
+ const boxes = extractMatrix(outputs[0]);
83
+ const scoreMatrix = extractMatrix(outputs[1]);
84
+ const scoreVector = extractVector(outputs[1]);
85
+ for (let i = 0; i < boxes.length; i++) {
86
+ const box = boxes[i];
87
+ let maxScore = 0;
88
+ let classIndex = 0;
89
+ const scoreRow = scoreMatrix[i];
90
+ if (scoreRow) {
91
+ for (let j = 0; j < scoreRow.length; j++) {
92
+ if (scoreRow[j] > maxScore) {
93
+ maxScore = scoreRow[j];
94
+ classIndex = j;
95
+ }
96
+ }
97
+ }
98
+ else {
99
+ const scalarScore = scoreVector[i] ?? null;
100
+ if (scalarScore === null)
101
+ continue;
102
+ maxScore = scalarScore;
103
+ }
104
+ if (maxScore >= threshold) {
105
+ detections.push({
106
+ classId: this.resolveClassId(classIndex),
107
+ confidence: maxScore,
108
+ bbox: box,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ return detections;
114
+ }
115
+ resolveClassId(classIndex) {
116
+ return this.classIds[classIndex] ?? classIndex;
117
+ }
118
+ dispose() {
119
+ this.model = null;
120
+ this._isLoaded = false;
121
+ this.classIds = MODEL_OUTPUT_CLASS_IDS;
122
+ }
123
+ }
124
+ function extractMatrix(value) {
125
+ if (!Array.isArray(value)) {
126
+ return [];
127
+ }
128
+ if (value.length === 0) {
129
+ return [];
130
+ }
131
+ const first = value[0];
132
+ if (Array.isArray(first) && (first.length === 0 || typeof first[0] === 'number')) {
133
+ return value
134
+ .filter(Array.isArray)
135
+ .map((row) => row.map((entry) => Number(entry)));
136
+ }
137
+ if (Array.isArray(first)) {
138
+ return extractMatrix(first);
139
+ }
140
+ return [];
141
+ }
142
+ function extractVector(value) {
143
+ if (!Array.isArray(value)) {
144
+ return [];
145
+ }
146
+ if (value.length === 0) {
147
+ return [];
148
+ }
149
+ if (typeof value[0] === 'number') {
150
+ return value.map((entry) => Number(entry));
151
+ }
152
+ if (Array.isArray(value[0])) {
153
+ return extractVector(value[0]);
154
+ }
155
+ return [];
156
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Model Manager for React Native SDK.
3
+ * Downloads, caches, and version-checks model bundles.
4
+ */
5
+ import type { BundleManifest, ModelArtifact } from './types';
6
+ export type { BundleManifest, ModelArtifact };
7
+ interface ModelManagerConfig {
8
+ apiKey: string;
9
+ baseUrl?: string;
10
+ }
11
+ export declare class ModelManager {
12
+ private apiKey;
13
+ private baseUrl;
14
+ private cachedETag;
15
+ private _currentBundle;
16
+ constructor(config: ModelManagerConfig);
17
+ get currentBundle(): BundleManifest | null;
18
+ /** Check for bundle updates from the server. */
19
+ checkForUpdates(policyId: string, platform: 'ios' | 'android'): Promise<BundleManifest | null>;
20
+ /** Download model artifacts and verify integrity. */
21
+ private downloadArtifacts;
22
+ private readFileSha256;
23
+ private saveBundleManifest;
24
+ /** Load a cached bundle from disk on startup. */
25
+ loadCachedBundle(policyId: string): Promise<BundleManifest | null>;
26
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Model Manager for React Native SDK.
3
+ * Downloads, caches, and version-checks model bundles.
4
+ */
5
+ import { Buffer } from 'buffer';
6
+ import { sha256 } from '@noble/hashes/sha2.js';
7
+ async function loadFileSystem() {
8
+ try {
9
+ return await import('expo-file-system');
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ function bytesToSha256Hex(bytes) {
16
+ return Buffer.from(sha256(bytes)).toString('hex');
17
+ }
18
+ export class ModelManager {
19
+ constructor(config) {
20
+ this.cachedETag = null;
21
+ this._currentBundle = null;
22
+ this.apiKey = config.apiKey;
23
+ this.baseUrl = config.baseUrl || 'https://verify.switchlabs.dev/api/v1';
24
+ }
25
+ get currentBundle() {
26
+ return this._currentBundle;
27
+ }
28
+ /** Check for bundle updates from the server. */
29
+ async checkForUpdates(policyId, platform) {
30
+ try {
31
+ const headers = {
32
+ 'X-API-Key': this.apiKey,
33
+ 'Accept': 'application/json',
34
+ };
35
+ if (this.cachedETag) {
36
+ headers['If-None-Match'] = this.cachedETag;
37
+ }
38
+ const url = `${this.baseUrl}/models/latest?policy=${policyId}&platform=${platform}`;
39
+ const response = await fetch(url, { headers });
40
+ if (response.status === 304) {
41
+ return this._currentBundle;
42
+ }
43
+ if (!response.ok) {
44
+ return this._currentBundle;
45
+ }
46
+ this.cachedETag = response.headers.get('etag');
47
+ const body = (await response.json());
48
+ const bundles = body.bundles;
49
+ if (!bundles || bundles.length === 0) {
50
+ return null;
51
+ }
52
+ const bundleJson = bundles.find((bundle) => bundle.policy_id === policyId) || bundles[0];
53
+ const manifest = parseBundleManifest(bundleJson);
54
+ if (this._currentBundle?.bundleVersion === manifest.bundleVersion) {
55
+ return this._currentBundle;
56
+ }
57
+ // Download artifacts
58
+ await this.downloadArtifacts(manifest, policyId);
59
+ await this.saveBundleManifest(policyId, manifest);
60
+ this._currentBundle = manifest;
61
+ return manifest;
62
+ }
63
+ catch {
64
+ return this._currentBundle;
65
+ }
66
+ }
67
+ /** Download model artifacts and verify integrity. */
68
+ async downloadArtifacts(manifest, policyId) {
69
+ const fileSystem = await loadFileSystem();
70
+ if (!fileSystem || !fileSystem.documentDirectory) {
71
+ console.warn('[VerifyAI] expo-file-system not available, skipping download');
72
+ return;
73
+ }
74
+ const cacheDir = `${fileSystem.documentDirectory}verify-ai/bundles/${policyId}/`;
75
+ await fileSystem.makeDirectoryAsync(cacheDir, { intermediates: true }).catch(() => { });
76
+ for (const artifact of manifest.artifacts) {
77
+ if (!artifact.downloadUrl)
78
+ continue;
79
+ const ext = artifact.format === 'coreml' ? 'mlpackage.zip' : artifact.format;
80
+ const localPath = `${cacheDir}${artifact.format}_v${artifact.version}.${ext}`;
81
+ // Check if already downloaded and intact
82
+ const info = await fileSystem.getInfoAsync(localPath).catch(() => ({ exists: false }));
83
+ if (info.exists) {
84
+ const existingHash = await this.readFileSha256(fileSystem, localPath);
85
+ if (existingHash === artifact.sha256) {
86
+ artifact.localPath = localPath;
87
+ continue;
88
+ }
89
+ await fileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => { });
90
+ }
91
+ // Download
92
+ try {
93
+ const download = await fileSystem.downloadAsync(artifact.downloadUrl, localPath);
94
+ if (download.status === 200) {
95
+ const downloadedHash = await this.readFileSha256(fileSystem, localPath);
96
+ if (downloadedHash === artifact.sha256) {
97
+ artifact.localPath = localPath;
98
+ }
99
+ else {
100
+ await fileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => { });
101
+ }
102
+ }
103
+ else {
104
+ await fileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => { });
105
+ }
106
+ }
107
+ catch {
108
+ await fileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => { });
109
+ }
110
+ }
111
+ }
112
+ async readFileSha256(fileSystem, localPath) {
113
+ try {
114
+ const base64 = await fileSystem.readAsStringAsync(localPath, {
115
+ encoding: fileSystem.EncodingType.Base64,
116
+ });
117
+ return bytesToSha256Hex(Uint8Array.from(Buffer.from(base64, 'base64')));
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ async saveBundleManifest(policyId, manifest) {
124
+ const fileSystem = await loadFileSystem();
125
+ if (!fileSystem || !fileSystem.documentDirectory) {
126
+ return;
127
+ }
128
+ const cacheDir = `${fileSystem.documentDirectory}verify-ai/bundles/${policyId}/`;
129
+ const manifestPath = `${cacheDir}manifest.json`;
130
+ await fileSystem.makeDirectoryAsync(cacheDir, { intermediates: true }).catch(() => { });
131
+ await fileSystem.writeAsStringAsync(manifestPath, JSON.stringify(serializeBundleManifest(manifest)));
132
+ }
133
+ /** Load a cached bundle from disk on startup. */
134
+ async loadCachedBundle(policyId) {
135
+ const fileSystem = await loadFileSystem();
136
+ if (!fileSystem || !fileSystem.documentDirectory) {
137
+ return null;
138
+ }
139
+ try {
140
+ const cacheDir = `${fileSystem.documentDirectory}verify-ai/bundles/${policyId}/`;
141
+ const manifestPath = `${cacheDir}manifest.json`;
142
+ const info = await fileSystem.getInfoAsync(manifestPath);
143
+ if (!info.exists)
144
+ return null;
145
+ const content = await fileSystem.readAsStringAsync(manifestPath);
146
+ const manifest = parseBundleManifest(JSON.parse(content));
147
+ // Verify local paths exist
148
+ for (const artifact of manifest.artifacts) {
149
+ const ext = artifact.format === 'coreml' ? 'mlpackage.zip' : artifact.format;
150
+ const localPath = `${cacheDir}${artifact.format}_v${artifact.version}.${ext}`;
151
+ const fileInfo = await fileSystem.getInfoAsync(localPath).catch(() => ({ exists: false }));
152
+ if (fileInfo.exists) {
153
+ artifact.localPath = localPath;
154
+ }
155
+ }
156
+ if (manifest.artifacts.some((a) => a.localPath)) {
157
+ this._currentBundle = manifest;
158
+ return manifest;
159
+ }
160
+ return null;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ }
167
+ function parseBundleManifest(json) {
168
+ return {
169
+ bundleVersion: json.bundle_version,
170
+ ontologyVersion: json.ontology_version,
171
+ schemaVersion: json.schema_version,
172
+ policyId: json.policy_id,
173
+ classIds: json.class_ids || [],
174
+ artifacts: (json.artifacts || []).map((artifact) => ({
175
+ role: artifact.role,
176
+ architecture: artifact.architecture,
177
+ version: artifact.version,
178
+ format: artifact.format,
179
+ storagePath: artifact.storage_path,
180
+ sizeBytes: artifact.size_bytes,
181
+ sha256: artifact.sha256,
182
+ downloadUrl: artifact.download_url,
183
+ localPath: undefined,
184
+ })),
185
+ policyAst: json.policy_ast,
186
+ };
187
+ }
188
+ function serializeBundleManifest(manifest) {
189
+ return {
190
+ bundle_version: manifest.bundleVersion,
191
+ ontology_version: manifest.ontologyVersion,
192
+ schema_version: manifest.schemaVersion,
193
+ policy_id: manifest.policyId,
194
+ class_ids: manifest.classIds,
195
+ artifacts: manifest.artifacts.map((artifact) => ({
196
+ role: artifact.role,
197
+ architecture: artifact.architecture,
198
+ version: artifact.version,
199
+ format: artifact.format,
200
+ storage_path: artifact.storagePath,
201
+ size_bytes: artifact.sizeBytes,
202
+ sha256: artifact.sha256,
203
+ download_url: artifact.downloadUrl,
204
+ })),
205
+ policy_ast: manifest.policyAst,
206
+ };
207
+ }
@@ -0,0 +1,14 @@
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
+ import type { FeatureVector, PolicyAST, PolicyResult } from './types';
9
+ /**
10
+ * Evaluate a PolicyAST against a FeatureVector.
11
+ * Pure function — no side effects.
12
+ * Must produce identical output to server TS and Dart implementations.
13
+ */
14
+ export declare function evaluatePolicy(ast: PolicyAST, features: FeatureVector): PolicyResult;
@@ -0,0 +1,161 @@
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
+ // ─── Field Resolution ───
9
+ function resolveField(features, field) {
10
+ const parts = field.split('.');
11
+ let current = features;
12
+ for (const part of parts) {
13
+ if (current === null || current === undefined)
14
+ return undefined;
15
+ if (typeof current !== 'object')
16
+ return undefined;
17
+ current = current[part];
18
+ }
19
+ return current;
20
+ }
21
+ // ─── Condition Evaluation ───
22
+ function applyOperator(fieldValue, operator, value) {
23
+ switch (operator) {
24
+ case 'exists':
25
+ return fieldValue !== null && fieldValue !== undefined;
26
+ case 'not_exists':
27
+ return fieldValue === null || fieldValue === undefined;
28
+ case 'eq':
29
+ return fieldValue === value;
30
+ case 'neq':
31
+ return fieldValue !== value;
32
+ case 'gt':
33
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue > value;
34
+ case 'gte':
35
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue >= value;
36
+ case 'lt':
37
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue < value;
38
+ case 'lte':
39
+ return typeof fieldValue === 'number' && typeof value === 'number' && fieldValue <= value;
40
+ case 'contains':
41
+ if (Array.isArray(fieldValue))
42
+ return fieldValue.includes(value);
43
+ if (typeof fieldValue === 'string' && typeof value === 'string')
44
+ return fieldValue.includes(value);
45
+ return false;
46
+ case 'not_contains':
47
+ if (Array.isArray(fieldValue))
48
+ return !fieldValue.includes(value);
49
+ if (typeof fieldValue === 'string' && typeof value === 'string')
50
+ return !fieldValue.includes(value);
51
+ return true;
52
+ case 'in':
53
+ if (Array.isArray(value))
54
+ return value.includes(fieldValue);
55
+ return false;
56
+ case 'not_in':
57
+ if (Array.isArray(value))
58
+ return !value.includes(fieldValue);
59
+ return true;
60
+ case 'overlaps':
61
+ if (Array.isArray(fieldValue) && Array.isArray(value)) {
62
+ return fieldValue.some((v) => value.includes(v));
63
+ }
64
+ return false;
65
+ default:
66
+ return false;
67
+ }
68
+ }
69
+ function evaluateCondition(features, condition) {
70
+ const fieldValue = resolveField(features, condition.field);
71
+ return applyOperator(fieldValue, condition.operator, condition.value);
72
+ }
73
+ function evaluateRule(features, rule) {
74
+ const passed = rule.conditions.every((c) => evaluateCondition(features, c));
75
+ return {
76
+ rule_id: rule.id,
77
+ passed,
78
+ severity: rule.severity,
79
+ required: rule.required,
80
+ };
81
+ }
82
+ function resolveCategory(features, categories, ruleResults) {
83
+ const failedCritical = ruleResults.some((r) => !r.passed && r.required && r.severity === 'critical');
84
+ for (const cat of categories) {
85
+ const matches = cat.conditions.every((c) => {
86
+ if (c.field === '_has_critical_failure') {
87
+ return applyOperator(failedCritical, c.operator, c.value);
88
+ }
89
+ if (c.field === '_failed_rule_count') {
90
+ const failedCount = ruleResults.filter((r) => !r.passed && r.required).length;
91
+ return applyOperator(failedCount, c.operator, c.value);
92
+ }
93
+ if (c.field === '_warning_count') {
94
+ const warnCount = ruleResults.filter((r) => !r.passed && r.severity === 'warning').length;
95
+ return applyOperator(warnCount, c.operator, c.value);
96
+ }
97
+ return evaluateCondition(features, c);
98
+ });
99
+ if (matches) {
100
+ return {
101
+ category_id: cat.category_id,
102
+ category_label: cat.label,
103
+ is_compliant: cat.is_compliant,
104
+ };
105
+ }
106
+ }
107
+ // Fallback
108
+ if (failedCritical) {
109
+ return { category_id: 'unsafe', category_label: 'Unsafe', is_compliant: false };
110
+ }
111
+ const hasFailedRequired = ruleResults.some((r) => !r.passed && r.required);
112
+ if (hasFailedRequired) {
113
+ return { category_id: 'improvable', category_label: 'Improvable', is_compliant: false };
114
+ }
115
+ return { category_id: 'compliant', category_label: 'Compliant', is_compliant: true };
116
+ }
117
+ function generateFeedback(isCompliant, violationReasons, ruleResults) {
118
+ if (isCompliant) {
119
+ const warnings = ruleResults.filter((r) => !r.passed && r.severity === 'warning');
120
+ if (warnings.length > 0) {
121
+ return `Parking is compliant with ${warnings.length} minor suggestion(s).`;
122
+ }
123
+ return 'Parking meets all requirements.';
124
+ }
125
+ if (violationReasons.length === 0) {
126
+ return 'Unable to determine compliance.';
127
+ }
128
+ if (violationReasons.length === 1) {
129
+ return `Issue detected: ${violationReasons[0]}.`;
130
+ }
131
+ return `${violationReasons.length} issues detected: ${violationReasons.join(', ')}.`;
132
+ }
133
+ // ─── Main Evaluator ───
134
+ /**
135
+ * Evaluate a PolicyAST against a FeatureVector.
136
+ * Pure function — no side effects.
137
+ * Must produce identical output to server TS and Dart implementations.
138
+ */
139
+ export function evaluatePolicy(ast, features) {
140
+ const ruleResults = ast.rules.map((rule) => evaluateRule(features, rule));
141
+ const { category_id, category_label, is_compliant } = resolveCategory(features, ast.categories, ruleResults);
142
+ const violationReasons = [];
143
+ for (let i = 0; i < ast.rules.length; i++) {
144
+ if (!ruleResults[i].passed && ast.rules[i].required) {
145
+ violationReasons.push(ast.rules[i].label);
146
+ }
147
+ }
148
+ const totalRules = ruleResults.length;
149
+ const passedRules = ruleResults.filter((r) => r.passed).length;
150
+ const confidence = totalRules > 0 ? passedRules / totalRules : 0;
151
+ const feedback = generateFeedback(is_compliant, violationReasons, ruleResults);
152
+ return {
153
+ category_id,
154
+ category_label,
155
+ is_compliant,
156
+ confidence: Math.round(confidence * 100) / 100,
157
+ violation_reasons: violationReasons,
158
+ rule_results: ruleResults,
159
+ feedback,
160
+ };
161
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * On-device ML types for React Native SDK.
3
+ */
4
+ export interface ModelArtifact {
5
+ role: 'detector' | 'segmenter' | 'classifier';
6
+ architecture: string;
7
+ version: number;
8
+ format: 'coreml' | 'tflite' | 'onnx';
9
+ storagePath: string;
10
+ sizeBytes: number;
11
+ sha256: string;
12
+ downloadUrl?: string;
13
+ localPath?: string;
14
+ }
15
+ export interface BundleManifest {
16
+ bundleVersion: number;
17
+ ontologyVersion: string;
18
+ schemaVersion: string;
19
+ policyId: string;
20
+ classIds: number[];
21
+ artifacts: ModelArtifact[];
22
+ policyAst: PolicyAST;
23
+ }
24
+ export interface Detection {
25
+ class_id: number;
26
+ class_name: string;
27
+ confidence: number;
28
+ bbox: [number, number, number, number];
29
+ }
30
+ export interface ImageQuality {
31
+ is_blurry: boolean;
32
+ is_dark: boolean;
33
+ has_vehicle: boolean;
34
+ }
35
+ export interface FeatureVector {
36
+ schema_version: string;
37
+ detections: Detection[];
38
+ primary_vehicle: Detection | null;
39
+ image_quality: ImageQuality;
40
+ vehicle_on_surface: string | null;
41
+ vehicle_near: string[];
42
+ }
43
+ export type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'exists' | 'not_exists' | 'in' | 'not_in' | 'overlaps';
44
+ export interface Condition {
45
+ field: string;
46
+ operator: ComparisonOperator;
47
+ value?: unknown;
48
+ }
49
+ export interface Rule {
50
+ id: string;
51
+ label: string;
52
+ severity: 'critical' | 'warning' | 'info';
53
+ required: boolean;
54
+ conditions: Condition[];
55
+ }
56
+ export interface CategoryPredicate {
57
+ category_id: string;
58
+ label: string;
59
+ is_compliant: boolean;
60
+ conditions: Condition[];
61
+ }
62
+ export interface PolicyAST {
63
+ ast_version: string;
64
+ policy_id: string;
65
+ ontology_version: string;
66
+ schema_version: string;
67
+ rules: Rule[];
68
+ categories: CategoryPredicate[];
69
+ }
70
+ export interface RuleResult {
71
+ rule_id: string;
72
+ passed: boolean;
73
+ severity: 'critical' | 'warning' | 'info';
74
+ required: boolean;
75
+ }
76
+ export interface PolicyResult {
77
+ category_id: string;
78
+ category_label: string;
79
+ is_compliant: boolean;
80
+ confidence: number;
81
+ violation_reasons: string[];
82
+ rule_results: RuleResult[];
83
+ feedback: string;
84
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * On-device ML types for React Native SDK.
3
+ */
4
+ export {};
@@ -153,7 +153,7 @@ export class OfflineQueue {
153
153
  continue;
154
154
  }
155
155
  try {
156
- const result = await this.client.verify(item.request);
156
+ const result = await this.client.verify(item.request, { idempotencyKey: item.id });
157
157
  processed++;
158
158
  await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
159
159
  onResult?.(item.id, result);
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { TelemetryReporter } from './TelemetryReporter';
3
+ export declare const TelemetryContext: React.Context<TelemetryReporter | null>;
4
+ export declare function useTelemetry(): TelemetryReporter | null;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const TelemetryContext = createContext(null);
3
+ export function useTelemetry() {
4
+ return useContext(TelemetryContext);
5
+ }