diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index b0193f4d..6a17c40f 100644 --- a/docs/DEVICE_VALIDATION_LEDGER.md +++ b/docs/DEVICE_VALIDATION_LEDGER.md @@ -17,6 +17,7 @@ | # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi | |---|---------|--------|------------------------------------------|---------| +| 26 | **🔴 Device Validation Pending — Evidence Write Bridge (P2-6)** — P2-1→P2-5 öğrenme katmanları kuruldu ama Evidence Store'a runtime YAZICI yoktu (Integration Service yalnız `list()` okuyordu) → learning rozetleri / Expert özeti / diagnostic learning context BOŞ kalıyordu. Bu köprü eksik write-path'i bağlar: VKB güncellenince (discovery gözlemi + VID kimlik değişimi abonelikleri, debounce'lu cold-path) `vehicleLearningEngine.computeEvidence()` çağrılır → üretilen kanıtlar Evidence Store'a `save()` (idempotent reproject — createdAt korunur, observationCount ŞİŞMEZ, vehicleCount distinct) ile yazılır → Pattern/Integration Service `list()` ile okuyabilir. 3Hz OBD hot-path'e GİRMEZ; yalnız anlamlı kimlik/discovery değişiminde tetiklenir; BALANCED/HIGH 8sn, BASIC_JS(low) 20sn debounce; kapanışta flush + dispose; fail-soft (köprü hatası OBD/Discovery'ye SIZMAZ); bounded(store 512 LRU) + throttle'lı yazma + zero-leak. Confidence/decay/pattern-promotion FORMÜLLERİ · Discovery capture/queue · Fingerprint · Auto Learning · VKB · Manufacturer Intelligence · Diagnostic safety/severity/driveSafe · PID/DID registry DEĞİŞMEZ. Cloud/SQL/LLM/Native YOK. | `vehicleLearningEvidenceBridge.ts` (yeni), `system/SystemBoot.ts` (VKB'den SONRA cold-path wiring, `_reg` cleanup), `vehicleLearningEvidenceBridge.test.ts` (yeni, 12 test) | Gerçek araçta: (1) discovery sonrası Evidence Store DOLUYOR (list() boş değil); (2) uygulama restart sonrası evidence geri geliyor (kalıcı); (3) Dashboard learning rozeti görünüyor; (4) Expert "Araç Öğrenme" sayıları >0; (5) Diagnostic learning context doluyor; (6) duplicate gözlem vehicleCount'i ŞİŞİRMİYOR (distinct fingerprint); (7) BASIC_JS tier'da FPS/CPU regresyonu YOK (seyrek debounce + cold-path). ⚠️ Test edilmeden 🟢'a çekme. | 2026-07-10 | | 25 | **🔴 Device Validation Pending — Learning Integration Layer (P2-5)** — P2-1→P2-4 öğrenme katmanları (Evidence Store · Decay · Pattern Engine) uygulamanın mevcut ekran/tanı akışlarına SALT-OKUNUR bağlandı. `vehicleLearningIntegrationService` Evidence Store'dan okur → decay uygular → pattern üretir → (a) Discovery Dashboard'a WEAK/CANDIDATE/STRONG/MANUAL_REVIEW/STALE/CONFLICT rozetleri + confidence/decay/araç/gözlem/ECU alanları + Strong/Candidate/Manual/Conflict filtreleri ekler, (b) Expert Mode'a "Araç Öğrenme" özet bölümü (toplam/weak/candidate/strong/manual/stale/prune + güçlü PID/DID adayları + marka cluster + son öğrenme) ekler, (c) Diagnostic Insight için `enrichInsight` ile öğrenme bağlamı üretir. Hiçbir registry/profile/store'a OTOMATİK YAZMAZ; safety/severity/driveSafe DEĞİŞMEZ; memoized + bounded(512) + fail-soft + zero-leak. BASIC_JS(low) tier yalnız özet istatistik (ağır pattern/conflict detayı kapalı). | `vehicleLearningIntegrationService.ts` (yeni), `discoveryDashboardModel.ts` (öğrenme filtreleri + AnnotationResolver), `DiscoveryDashboard.tsx` (rozet/alan/filtre), `settings/expert/VehicleLearningInspector.tsx` (yeni), `ExpertModePanel.tsx` (bölüm wiring) | Gerçek araçta: (1) yeni PID/DID discovery oluşur → Evidence Store kaydı oluşur → Dashboard'da doğru WEAK/CANDIDATE/STRONG rozeti görünür; (2) duplicate gözlem vehicleCount'i şişirmez (distinct fingerprint); (3) Expert "Araç Öğrenme" özeti doğru sayıları gösterir; (4) Diagnostic Insight öğrenme bağlamını (learnedEvidenceCount/relatedStrongPids/requiresManualReview) gösterir; (5) düşük cihaz tier'ında (BASIC_JS) FPS/CPU regresyonu oluşmaz (ağır pattern detayı kapalı). ⚠️ Salt-okunur/additive → alt katman davranışı değişmez (tsc+test+build yeşil). | 2026-07-10 | | 24 | **🔴 Device Validation Pending — P0-Güvenlik #4: production log hijyeni** (commit 049e290). terser `drop_console:true` (error+warn dahil TÜM console'u siliyor → üretimde hata gözlemlenebilirliği yoktu + logGate ölü kod) yerine dev-aware kesim: release'te yalnız `log/info/debug/trace` çıkarılır, `error`+`warn` KORUNUR (logGate runtime'da adaptif susturur). `NODE_ENV=development` build hiçbir console'ı çıkarmaz. Build-analiz kanıtı: release bundle'da `console.error(`/`warn(` var, `console.log(`/`info(`/`debug(` **çağrı = 0**; dev bundle'da log/info çağrıları korunur. | `vite.config.ts` (`_isDevBuild` + `drop_console: _isDevBuild ? false : ['log','info','debug','trace']`) | **Release APK** cihaz-üstü (debug build ile Chrome inspector): bir hata durumunda `console.error` GÖRÜNÜR (üretim gözlemlenebilirliği geri geldi); debug seviye log GÖRÜNMEZ. Debug APK'da tüm console akar (on-device debug korundu). Hot-path'te debug log kaynaklı ek CPU/IO gözlenmez. | 2026-07-08 | | 23 | **🔴 Device Validation Pending — P0-Güvenlik #2: cleartext trafik kısıtlama** (commit 77e5292). Global `android:usesCleartextTraffic="true"` kaldırıldı; `network_security_config` base-config ile tüm cleartext reddedilir, yalnız loopback (`localhost`/`127.0.0.1`) whitelist'te. Ölü dev IP `192.168.1.12` çıkarıldı. Ham `ServerSocket`'ler (passenger 8765 / diag 8899) bu politikadan etkilenmez. | `AndroidManifest.xml`, `res/xml/network_security_config.xml` | **Release APK**'da dış **https** API'ler (hava/Nominatim/Supabase/AI) çalışmaya devam eder; kasıtlı bir `http://` dış çağrı NSC ile REDDEDİLİR; cihaz-içi OSRM (`http://localhost:5000`) offline routing + yolcu paneli (ayrı telefonun tarayıcısı) çalışır. | 2026-07-08 | diff --git a/src/__tests__/cleanup.remote.test.ts b/src/__tests__/cleanup.remote.test.ts index 258efb18..7091e930 100644 --- a/src/__tests__/cleanup.remote.test.ts +++ b/src/__tests__/cleanup.remote.test.ts @@ -35,6 +35,7 @@ vi.mock('../utils/safeStorage', () => ({ safeFlushKey: () => {}, safeGetRaw: () => null, safeSetRaw: () => {}, + safeRemoveRaw: () => {}, })); /* ── Imports (mock'lardan sonra) ── */ diff --git a/src/__tests__/cleanup.services.test.ts b/src/__tests__/cleanup.services.test.ts index f8639ff9..257cfebe 100644 --- a/src/__tests__/cleanup.services.test.ts +++ b/src/__tests__/cleanup.services.test.ts @@ -32,6 +32,7 @@ vi.mock('../utils/safeStorage', () => ({ safeFlushKey: () => {}, safeGetRaw: () => null, safeSetRaw: () => {}, + safeRemoveRaw: () => {}, })); /* ── Imports (mock'lardan sonra) ── */ diff --git a/src/__tests__/vehicleLearningEvidenceBridge.test.ts b/src/__tests__/vehicleLearningEvidenceBridge.test.ts new file mode 100644 index 00000000..15700563 --- /dev/null +++ b/src/__tests__/vehicleLearningEvidenceBridge.test.ts @@ -0,0 +1,294 @@ +/** + * vehicleLearningEvidenceBridge.test.ts — Evidence Write Bridge (P2-6). + * + * Kilitler: değişimde computeEvidence çağrılıyor · store'a yazılıyor · duplicate kayıt yok · + * aynı araç vehicleCount şişmiyor · observationCount doğru · debounce disk yazımını sınırlıyor · + * BASIC_JS seyrek / BALANCED normal · boş+bozuk fail-soft · hata upstream'e sızmıyor · + * stop/dispose cleanup · flush kapanışta · start idempotent (duplicate subscription yok). + */ +import { describe, it, expect } from 'vitest'; +import { + VehicleLearningEvidenceBridge, + BRIDGE_DEBOUNCE_MS_NORMAL, + BRIDGE_DEBOUNCE_MS_LOW, + type BridgeTimer, +} from '../platform/vehicleLearningEvidenceBridge'; +import { + VehicleLearningEvidenceStore, + type EvidenceStoreIO, +} from '../platform/vehicleLearningEvidenceStore'; +import { type LearningEvidence } from '../platform/vehicleLearningEngine'; + +/* ── Yardımcılar ──────────────────────────────────────────────────────────── */ + +/** Enjekte edilebilir manuel zamanlayıcı — test fire()'ı çağırınca callback çalışır. */ +function manualTimer() { + let scheduled: { cb: () => void; ms: number } | null = null; + let setCount = 0, clearCount = 0; + const timer: BridgeTimer = { + set: (cb, ms) => { scheduled = { cb, ms }; setCount++; return { id: setCount }; }, + clear: () => { scheduled = null; clearCount++; }, + }; + return { + timer, + fire: () => { const s = scheduled; scheduled = null; s?.cb(); }, + pending: () => scheduled, + get setCount() { return setCount; }, + get clearCount() { return clearCount; }, + }; +} + +/** Kaynak yakalayıcı — abone callback'lerini toplar, unsub sayar. */ +function captureSource() { + const cbs: Array<() => void> = []; + let subCount = 0, unsubCount = 0; + const source = (cb: () => void) => { cbs.push(cb); subCount++; return () => { unsubCount++; }; }; + return { + source, + trigger: () => { for (const cb of cbs) cb(); }, + get subCount() { return subCount; }, + get unsubCount() { return unsubCount; }, + }; +} + +function ev(over: Partial = {}): LearningEvidence { + return { + evidenceId: over.evidenceId ?? 'Renault|6|PID|A5|01', + manufacturer: 'Renault', profileHint: 'Renault', protocol: '6', + discoverySource: 'PID', pidOrDid: 'A5', mode: '01', + ecuAddresses: over.ecuAddresses ?? ['7E8'], + supportingVehicleHashes: over.supportingVehicleHashes ?? ['v1'], + vehicleCount: over.vehicleCount ?? 1, + observationCount: over.observationCount ?? 5, + firstSeen: over.firstSeen ?? 1000, + lastSeen: over.lastSeen ?? 2000, + confidence: over.confidence ?? 0.4, + status: over.status ?? 'weak', + createdAt: over.createdAt ?? 1000, + updatedAt: over.updatedAt ?? 2000, + }; +} + +/** Bellek-içi IO ile gerçek Evidence Store (debounce 0 → anında yazım testte kolay). */ +function memStore(): VehicleLearningEvidenceStore { + const mem = new Map(); + const io: EvidenceStoreIO = { + read: (k) => mem.get(k) ?? null, + write: (k, v) => { mem.set(k, v); }, + remove: (k) => { mem.delete(k); }, + }; + return new VehicleLearningEvidenceStore('test-evidence', 512, 0, io, () => 5000); +} + +/* ── Tetikleme & yazım ────────────────────────────────────────────────────── */ +describe('trigger + write', () => { + it('değişim → computeEvidence çağrılıyor + store’a yazılıyor', () => { + const t = manualTimer(); + const src = captureSource(); + let computeCalls = 0; + const written: LearningEvidence[] = []; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => { computeCalls++; return [ev()]; }, + writeEvidence: (e) => { written.push(e); }, + subscribeSources: [src.source], + timer: t.timer, + }); + b.start(); + expect(computeCalls).toBe(0); // boot'ta debounce'lu (henüz çalışmadı) + src.trigger(); // VKB/discovery değişimi + t.fire(); // debounce doldu + expect(computeCalls).toBe(1); + expect(written).toHaveLength(1); + expect(written[0].evidenceId).toBe('Renault|6|PID|A5|01'); + b.stop(); + }); + + it('gerçek store: duplicate kayıt YOK + aynı araç vehicleCount ŞİŞMİYOR + observationCount doğru', () => { + const t = manualTimer(); + const store = memStore(); + let obs = 5; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [ev({ observationCount: obs, supportingVehicleHashes: ['v1'] })], + writeEvidence: (e) => { store.save(e); }, + subscribeSources: [captureSource().source], + timer: t.timer, + }); + b.start(); + b.flush(); // 1. projeksiyon (obs=5) + b.flush(); // 2. projeksiyon AYNI veri (idempotent — şişmemeli) + let list = store.list(); + expect(list).toHaveLength(1); // duplicate kayıt yok + expect(list[0].vehicleCount).toBe(1); // aynı araç → şişmedi + expect(list[0].observationCount).toBe(5); // tekrar yazımda ŞİŞMEDİ (save reproject) + + obs = 9; // VKB büyüdü (computeEvidence daha yüksek obs verir) + b.flush(); + list = store.list(); + expect(list).toHaveLength(1); + expect(list[0].observationCount).toBe(9); // doğru arttı + b.dispose(); + }); + + it('farklı araç → vehicleCount computeEvidence’ten yansıyor', () => { + const t = manualTimer(); + const store = memStore(); + let hashes = ['v1']; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [ev({ supportingVehicleHashes: hashes, vehicleCount: hashes.length })], + writeEvidence: (e) => { store.save(e); }, + subscribeSources: [captureSource().source], + timer: t.timer, + }); + b.start(); b.flush(); + expect(store.list()[0].vehicleCount).toBe(1); + hashes = ['v1', 'v2']; // 2. farklı araç öğrenildi + b.flush(); + expect(store.list()[0].vehicleCount).toBe(2); // farklı araçta arttı + b.dispose(); + }); +}); + +/* ── Throttle / debounce ──────────────────────────────────────────────────── */ +describe('throttle / debounce', () => { + it('art arda sinyaller TEK projeksiyona indirgeniyor (disk yazımı sınırlı)', () => { + const t = manualTimer(); + const src = captureSource(); + let computeCalls = 0; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => { computeCalls++; return [ev()]; }, + writeEvidence: () => {}, + subscribeSources: [src.source], + timer: t.timer, + }); + b.start(); // 1 schedule + src.trigger(); // +1 + src.trigger(); // +1 + src.trigger(); // +1 + expect(t.setCount).toBe(4); // her sinyal yeniden planladı + expect(t.clearCount).toBe(3); // öncekiler iptal (coalesce) + expect(computeCalls).toBe(0); // henüz çalışmadı + t.fire(); + expect(computeCalls).toBe(1); // 4 sinyal → 1 projeksiyon + b.stop(); + }); + + it('BASIC_JS(low) DAHA SEYREK, BALANCED/HIGH normal', () => { + const low = manualTimer(); + const high = manualTimer(); + const bLow = new VehicleLearningEvidenceBridge({ computeEvidence: () => [], timer: low.timer, tier: () => 'low', subscribeSources: [] }); + const bHigh = new VehicleLearningEvidenceBridge({ computeEvidence: () => [], timer: high.timer, tier: () => 'high', subscribeSources: [] }); + bLow.start(); + bHigh.start(); + expect(low.pending()!.ms).toBe(BRIDGE_DEBOUNCE_MS_LOW); + expect(high.pending()!.ms).toBe(BRIDGE_DEBOUNCE_MS_NORMAL); + expect(BRIDGE_DEBOUNCE_MS_LOW).toBeGreaterThan(BRIDGE_DEBOUNCE_MS_NORMAL); + bLow.stop(); bHigh.stop(); + }); +}); + +/* ── Fail-soft ────────────────────────────────────────────────────────────── */ +describe('fail-soft', () => { + it('boş knowledge → fail-soft (yazım yok, çökme yok)', () => { + const t = manualTimer(); + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [], writeEvidence: () => { throw new Error('yazılmamalı'); }, + subscribeSources: [captureSource().source], timer: t.timer, + }); + b.start(); + expect(() => b.flush()).not.toThrow(); + expect(b.lastWriteCount).toBe(0); + b.stop(); + }); + + it('bozuk evidence → yalnız geçerli yazılır (fail-soft)', () => { + const t = manualTimer(); + const written: LearningEvidence[] = []; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => ([null, { evidenceId: '' }, ev({ evidenceId: 'ok' })] as unknown as LearningEvidence[]), + writeEvidence: (e) => { written.push(e); }, + subscribeSources: [captureSource().source], timer: t.timer, + }); + b.start(); b.flush(); + expect(written).toHaveLength(1); + expect(written[0].evidenceId).toBe('ok'); + b.stop(); + }); + + it('köprü hatası OBD/Discovery akışına SIZMIYOR (computeEvidence throw)', () => { + const t = manualTimer(); + const src = captureSource(); + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => { throw new Error('patladı'); }, + writeEvidence: () => {}, + subscribeSources: [src.source], timer: t.timer, + }); + b.start(); + expect(() => src.trigger()).not.toThrow(); // sinyal callback'i çökmez + expect(() => t.fire()).not.toThrow(); // projeksiyon fail-soft + b.stop(); + }); + + it('bir kaynak subscribe atarsa diğerleri çalışır', () => { + const t = manualTimer(); + const good = captureSource(); + const bad = () => { throw new Error('subscribe hata'); }; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [], writeEvidence: () => {}, + subscribeSources: [bad, good.source], timer: t.timer, + }); + expect(() => b.start()).not.toThrow(); + expect(good.subCount).toBe(1); // iyi kaynak yine abone oldu + b.stop(); + }); +}); + +/* ── Lifecycle ────────────────────────────────────────────────────────────── */ +describe('lifecycle', () => { + it('start İDEMPOTENT — ikinci start duplicate subscription oluşturmuyor', () => { + const t = manualTimer(); + const src = captureSource(); + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [], writeEvidence: () => {}, + subscribeSources: [src.source], timer: t.timer, + }); + b.start(); + b.start(); // ikinci — abone açmamalı + expect(src.subCount).toBe(1); + expect(b.isRunning).toBe(true); + b.stop(); + }); + + it('stop cleanup: unsubscribe + timer temizler', () => { + const t = manualTimer(); + const src = captureSource(); + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => [], writeEvidence: () => {}, + subscribeSources: [src.source], timer: t.timer, + }); + b.start(); + b.stop(); + expect(src.unsubCount).toBe(1); + expect(b.isRunning).toBe(false); + expect(t.pending()).toBeNull(); + }); + + it('dispose kapanışta FLUSH ediyor + unsubscribe', () => { + const t = manualTimer(); + const src = captureSource(); + let computeCalls = 0; + const written: LearningEvidence[] = []; + const b = new VehicleLearningEvidenceBridge({ + computeEvidence: () => { computeCalls++; return [ev()]; }, + writeEvidence: (e) => { written.push(e); }, + subscribeSources: [src.source], timer: t.timer, + }); + b.start(); + b.dispose(); + expect(computeCalls).toBe(1); // kapanışta final flush projeksiyonu + expect(written).toHaveLength(1); + expect(src.unsubCount).toBe(1); // abonelik bırakıldı + // dispose sonrası schedule no-op (yeni timer planlanmaz) + src.trigger(); + expect(t.pending()).toBeNull(); + }); +}); diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index 6d823796..3ec6f6ba 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -62,6 +62,7 @@ import { startVehicleIntelligenceService } from '../vehicleIntelligenceService'; import { startAutomaticVehicleFingerprint } from '../vehicleFingerprintBuilder'; import { startAutoLearningEngine } from '../autoLearningEngine'; import { startVehicleKnowledgeBase } from '../vehicleKnowledgeBase'; +import { startVehicleLearningEvidenceBridge } from '../vehicleLearningEvidenceBridge'; import { logError } from '../crashLogger'; import { showToast, dismissToast } from '../errorBus'; import { healthMonitor } from './SystemHealthMonitor'; @@ -542,6 +543,12 @@ class SystemBoot { _log(' › VehicleKnowledgeBase'); this._reg(startVehicleKnowledgeBase()); + // VehicleLearningEvidenceBridge (P2-6): VKB güncellenince computeEvidence() → Evidence + // Store'a idempotent yazar (debounce'lu cold-path). VKB'DEN SONRA başlar (ona bağlı). + // 3Hz hot-path'e girmez; fail-soft + zero-leak (cleanup _reg'le). + _log(' › VehicleLearningEvidenceBridge'); + this._reg(startVehicleLearningEvidenceBridge()); + // GeofenceService: async (Supabase zona sorgusu) _log(' › GeofenceService (async)'); const geofenceCleanup = await startGeofenceService().catch((e: unknown) => { diff --git a/src/platform/vehicleLearningEvidenceBridge.ts b/src/platform/vehicleLearningEvidenceBridge.ts new file mode 100644 index 00000000..e518b9df --- /dev/null +++ b/src/platform/vehicleLearningEvidenceBridge.ts @@ -0,0 +1,211 @@ +/** + * vehicleLearningEvidenceBridge — Öğrenme Kanıtı Yazma Köprüsü (P2-6). + * + * AMAÇ: Vehicle Learning Evidence Store'u runtime öğrenme akışına GÜVENLİ bağlar. P2-1→P2-5 + * katmanları kuruldu ama Evidence Store'a runtime yazıcı YOKtu → learning rozetleri / Expert + * özeti / diagnostic learning context BOŞ kalıyordu. Bu köprü eksik write-path'i bağlar: + * + * Vehicle Knowledge Base (autoLearningEngine ile beslenir — SALT-OKUNUR gözlenir) + * ↓ discovery/VID değişimi (debounce'lu tetik) + * vehicleLearningEngine.computeEvidence() (VKB → LearningEvidence, formül DEĞİŞMEZ) + * ↓ + * vehicleLearningEvidenceStore.save(ev) (idempotent reproject — aşağıya bkz.) + * ↓ + * Pattern Engine / Integration Service list() ile okur → Dashboard/Expert/Diagnostic dolar. + * + * NEDEN save() (upsert DEĞİL): computeEvidence() VKB'nin TAM KÜMÜLATİF projeksiyonudur + * (her çağrı güncel bütün gerçeği verir). Store'un `upsert`i delta-toplayıcıdır + * (observationCount'u EKLER) → aynı projeksiyonu tekrar yazınca sayacı ŞİŞİRİR. `save` + * (overwrite, createdAt korunur, confidence/status P1 formülüyle yeniden hesap) idempotenttir: + * VKB büyüdükçe observationCount doğru artar, tekrar yazımda şişmez, vehicleCount distinct + * kalır. Store'un merge davranışı/formülü DEĞİŞTİRİLMEZ — yalnız uygun mevcut metod seçilir. + * + * KESİN SINIRLAR (CLAUDE.md): confidence/decay/pattern-promotion FORMÜLLERİ DEĞİŞMEZ · + * Discovery capture/queue · Fingerprint · Auto Learning · VKB · Manufacturer Intelligence · + * Diagnostic safety/severity/driveSafe · PID/DID registry DEĞİŞMEZ. Cloud/SQL/LLM/Native YOK. + * 3Hz OBD HOT-PATH'e GİRMEZ (yalnız discovery/knowledge değişiminde, debounce'lu, cold-path). + * bounded (store 512 LRU) · throttle'lı yazma · fail-soft (hata OBD/Discovery'ye SIZMAZ) · + * zero-leak dispose (flush + unsubscribe). + */ + +import { vehicleLearningEngine } from './vehicleLearningEngine'; +import { vehicleLearningEvidenceStore } from './vehicleLearningEvidenceStore'; +import { discoveryCaptureService } from './obd/discovery'; +import { useVidStore } from '../store/useVidStore'; +import { getDeviceTier, type DeviceTier } from './deviceCapabilities'; +import { type LearningEvidence } from './vehicleLearningEngine'; + +/* ══════════════════════════════════════════════════════════════════════════ + * Sabitler + * ════════════════════════════════════════════════════════════════════════ */ + +/** Debounce (ms) — BALANCED/HIGH: normal cold-path yazımı. */ +export const BRIDGE_DEBOUNCE_MS_NORMAL = 8_000; +/** Debounce (ms) — BASIC_JS(low): daha SEYREK (düşük-uçta CPU/IO tasarrufu). */ +export const BRIDGE_DEBOUNCE_MS_LOW = 20_000; + +/** Bir değişim kaynağına abone olan fonksiyon (unsub döndürür). */ +export type BridgeSubscribeSource = (cb: () => void) => (() => void) | void; + +/** Enjekte edilebilir zamanlayıcı (test determinizmi + Node/DOM uyumu). */ +export interface BridgeTimer { + set: (cb: () => void, ms: number) => unknown; + clear: (handle: unknown) => void; +} + +const DEFAULT_TIMER: BridgeTimer = { + set: (cb, ms) => setTimeout(cb, ms), + clear: (h) => clearTimeout(h as ReturnType), +}; + +/** Varsayılan değişim kaynakları — discovery gözlemi + VID (kimlik) değişimi. */ +const DEFAULT_SOURCES: BridgeSubscribeSource[] = [ + (cb) => discoveryCaptureService.subscribe(cb), + (cb) => useVidStore.subscribe(cb), +]; + +export interface BridgeDeps { + /** VKB'den LearningEvidence üretir (varsayılan P2-1 motoru). */ + computeEvidence?: () => LearningEvidence[]; + /** Bir kanıtı depoya YAZAR (varsayılan store.save — idempotent reproject). */ + writeEvidence?: (ev: LearningEvidence) => void; + /** Değişim kaynakları (varsayılan discovery + VID). */ + subscribeSources?: BridgeSubscribeSource[]; + /** Cihaz tier okuyucu (debounce seçimi). */ + tier?: () => DeviceTier; + /** Zamanlayıcı (test için enjekte edilebilir). */ + timer?: BridgeTimer; + /** Debounce süreleri (override). */ + debounceNormalMs?: number; + debounceLowMs?: number; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Köprü + * ════════════════════════════════════════════════════════════════════════ */ + +export class VehicleLearningEvidenceBridge { + private readonly _computeEvidence: () => LearningEvidence[]; + private readonly _writeEvidence: (ev: LearningEvidence) => void; + private readonly _sources: BridgeSubscribeSource[]; + private readonly _tier: () => DeviceTier; + private readonly _timer: BridgeTimer; + private readonly _debounceNormalMs: number; + private readonly _debounceLowMs: number; + + private _unsubs: Array<() => void> = []; + private _handle: unknown = null; + private _started = false; + private _disposed = false; + private _projecting = false; + private _lastWriteCount = 0; + + constructor(deps: BridgeDeps = {}) { + this._computeEvidence = deps.computeEvidence ?? (() => vehicleLearningEngine.computeEvidence()); + this._writeEvidence = deps.writeEvidence ?? ((ev) => { vehicleLearningEvidenceStore.save(ev); }); + this._sources = deps.subscribeSources ?? DEFAULT_SOURCES; + this._tier = deps.tier ?? (() => getDeviceTier()); + this._timer = deps.timer ?? DEFAULT_TIMER; + this._debounceNormalMs = deps.debounceNormalMs ?? BRIDGE_DEBOUNCE_MS_NORMAL; + this._debounceLowMs = deps.debounceLowMs ?? BRIDGE_DEBOUNCE_MS_LOW; + } + + /** + * Değişim kaynaklarına abone olur (İDEMPOTENT — ikinci start duplicate abonelik açmaz). + * Boot'ta ağır iş yapmamak için ilk projeksiyon da debounce'lu planlanır. Döndürülen + * fonksiyon köprüyü durdurur. + */ + start(): () => void { + if (this._started) return () => this.stop(); // idempotent + this._started = true; + this._disposed = false; + for (const sub of this._sources) { + try { + const u = sub(() => this._schedule()); + if (typeof u === 'function') this._unsubs.push(u); + } catch { /* tek kaynak hatası diğerlerini etkilemez (fail-soft) */ } + } + this._schedule(); // ilk projeksiyonu (debounce'lu) planla + return () => this.stop(); + } + + private _debounceMs(): number { + let t: DeviceTier = 'high'; + try { t = this._tier(); } catch { t = 'high'; } + return t === 'low' ? this._debounceLowMs : this._debounceNormalMs; + } + + /** Debounce'lu tetik — art arda sinyaller TEK projeksiyona indirgenir (disk yazımı sınırlı). */ + private _schedule(): void { + if (this._disposed) return; + if (this._handle !== null) this._timer.clear(this._handle); + this._handle = this._timer.set(() => { this._handle = null; this._project(); }, this._debounceMs()); + } + + /** Bekleyen projeksiyonu HEMEN çalıştırır (debounce beklemeden). */ + flush(): void { + if (this._handle !== null) { this._timer.clear(this._handle); this._handle = null; } + this._project(); + } + + /** VKB → evidence projeksiyonu + depoya yazım. Reentry-safe · FAIL-SOFT (upstream'e sızmaz). */ + private _project(): void { + if (this._projecting) return; // reentry guard + this._projecting = true; + try { + const list = this._computeEvidence() ?? []; + let written = 0; + for (const ev of list) { + try { + if (ev && typeof ev === 'object' && typeof ev.evidenceId === 'string' && ev.evidenceId) { + this._writeEvidence(ev); + written++; + } + } catch { /* tek kayıt hatası diğerlerini etkilemez */ } + } + this._lastWriteCount = written; + } catch { + /* köprü hatası OBD/Discovery akışına SIZMAZ (fail-soft) */ + } finally { + this._projecting = false; + } + } + + /** Abonelikleri bırakır + zamanlayıcıyı temizler (yazım YAPMAZ). */ + stop(): void { + for (const u of this._unsubs) { try { u(); } catch { /* yoksay */ } } + this._unsubs = []; + if (this._handle !== null) { this._timer.clear(this._handle); this._handle = null; } + this._started = false; + } + + /** Kapanışta: bekleyen projeksiyonu FLUSH et, sonra abonelikleri bırak (zero-leak). */ + dispose(): void { + if (this._disposed) return; + this._disposed = true; + if (this._handle !== null) { this._timer.clear(this._handle); this._handle = null; } + this._project(); // final flush + for (const u of this._unsubs) { try { u(); } catch { /* yoksay */ } } + this._unsubs = []; + this._started = false; + } + + /** Son projeksiyonda yazılan kanıt sayısı (gözlemlenebilirlik). */ + get lastWriteCount(): number { return this._lastWriteCount; } + /** Köprü aktif mi. */ + get isRunning(): boolean { return this._started; } +} + +/* ── Uygulama geneli tekil köprü + boot wiring ────────────────────────────── */ + +/** Uygulama geneli tekil köprü. */ +export const vehicleLearningEvidenceBridge = new VehicleLearningEvidenceBridge(); + +/** + * SystemBoot cold-path wiring — köprüyü başlatır (idempotent). Döndürülen fonksiyon durdurur + * (SystemBoot `_reg` ile kaydeder → kapanışta stop). Kapanışta kalıcı flush için `dispose()` + * çağrılabilir; stop() abonelik/timer sızıntısını önler. + */ +export function startVehicleLearningEvidenceBridge(): () => void { + return vehicleLearningEvidenceBridge.start(); +}