diff --git a/src/__tests__/vehicleKnowledgeBase.test.ts b/src/__tests__/vehicleKnowledgeBase.test.ts new file mode 100644 index 00000000..5f283b80 --- /dev/null +++ b/src/__tests__/vehicleKnowledgeBase.test.ts @@ -0,0 +1,217 @@ +/** + * vehicleKnowledgeBase.test.ts — Araç Bilgi Tabanı TEMELİ (PR-28). + * + * Kilitlenen davranışlar: yeni araç kaydı · duplicate yok · PID/DID istatistikleri · + * firstSeen korunur · lastSeen güncellenir · confidence↑ · toplam discovery/PID/DID · + * fail-soft · bounded(8) LRU · safeStorage kalıcılığı. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { + buildKnowledgeRecord, + upsertKnowledge, + vehicleStats, + VehicleKnowledgeBaseStore, + VehicleKnowledgeBase, + MAX_KNOWLEDGE_RECORDS, +} from '../platform/vehicleKnowledgeBase'; +import { buildFingerprint, VehicleFingerprintStore } from '../platform/vehicleFingerprintService'; +import { type VehicleKnowledge, type DiscoveredSignal } from '../platform/autoLearningEngine'; +import { useVidStore } from '../store/useVidStore'; + +let _k = 0; +function kbStore() { return new VehicleKnowledgeBaseStore(`vkb-test-${_k++}`); } + +function sig(firstSeen: number, lastSeen: number, seenCount: number, confidence = 0.5): DiscoveredSignal { + return { firstSeen, lastSeen, seenCount, confidence }; +} + +function knowledge(over: { + vin?: string; protocol?: string; ecu?: string[]; fw?: string; + sourceCount?: number; confidence?: number; now?: number; + pids?: Record; dids?: Record; +} = {}): VehicleKnowledge { + const base = buildFingerprint({ + vin: over.vin ?? '', + protocol: over.protocol ?? '6', + ecuAddresses: over.ecu ?? ['7E8'], + metadata: over.fw ? { firmwareVersion: over.fw } : undefined, + }, over.now ?? 1000); + return { + ...base, + createdAt: over.now ?? 1000, + sourceCount: over.sourceCount ?? 1, + confidence: over.confidence ?? 0.5, + profileHint: 'Renault', + discoveredPids: over.pids ?? {}, + discoveredDids: over.dids ?? {}, + }; +} + +beforeEach(() => { + try { localStorage.clear(); } catch { /* jsdom */ } + useVidStore.getState().resetStore(); +}); + +/* ── buildKnowledgeRecord (SAF) ───────────────────────────────────────────── */ +describe('buildKnowledgeRecord', () => { + it('yeni araç → tam kayıt (fingerprintHash/vin/protocol/istatistik/mod)', () => { + const k = knowledge({ vin: 'VF1BM0A0H12345678', ecu: ['7E8', '7E9'], fw: 'CAL-1', + pids: { A5: sig(1000, 1000, 1) }, dids: { F190: sig(1000, 1000, 1) } }); + const r = buildKnowledgeRecord(null, k, 1000); + expect(r.fingerprintHash).toBe(k.hash); + expect(r.vin).toBe('VF1BM0A0H12345678'); + expect(r.protocol).toBe('6'); + expect(r.vehicleSignature).toBe('6::7E8,7E9'); + expect(r.discoveredEcus).toEqual(['7E8', '7E9']); + expect(r.firmwareVersions).toEqual(['CAL-1']); + expect(r.supportedModes).toEqual(['01', '22']); + expect(r.totalDiscoveries).toBe(2); // 1 PID + 1 DID seenCount + }); + + it('aynı araç güncelleme: duplicate yok, firstSeen korunur, lastSeen güncellenir', () => { + const k1 = knowledge({ vin: 'X', pids: { A5: sig(1000, 1000, 1) }, now: 1000 }); + const r1 = buildKnowledgeRecord(null, k1, 1000); + const k2 = knowledge({ vin: 'X', pids: { A5: sig(1000, 5000, 2) }, now: 5000 }); + const r2 = buildKnowledgeRecord(r1, k2, 5000); + expect(Object.keys(r2.discoveredPids)).toHaveLength(1); // duplicate sinyal yok + expect(r2.discoveredPids['A5'].firstSeen).toBe(1000); // korundu + expect(r2.discoveredPids['A5'].lastSeen).toBe(5000); // güncellendi + expect(r2.firstSeen).toBe(1000); + expect(r2.lastSeen).toBe(5000); + }); + + it('confidence artıyor (seenCount arttıkça)', () => { + const r1 = buildKnowledgeRecord(null, knowledge({ pids: { A5: sig(1000, 1000, 1) } }), 1000); + const r2 = buildKnowledgeRecord(r1, knowledge({ pids: { A5: sig(1000, 5000, 3) } }), 5000); + expect(r2.discoveredPids['A5'].confidence).toBeGreaterThan(r1.discoveredPids['A5'].confidence); + expect(r2.confidence).toBeGreaterThanOrEqual(r1.confidence); + }); + + it('totalConnections fingerprint sourceCount ile beslenir', () => { + const r = buildKnowledgeRecord(null, knowledge({ sourceCount: 4 }), 1000); + expect(r.totalConnections).toBe(4); + }); +}); + +/* ── vehicleStats ─────────────────────────────────────────────────────────── */ +describe('vehicleStats', () => { + it('toplam PID/DID/ECU/Discovery doğru', () => { + const k = knowledge({ + ecu: ['7E8', '7E9', '7EA'], + pids: { A5: sig(1, 1, 2), A6: sig(1, 1, 3) }, + dids: { F190: sig(1, 1, 1) }, + }); + const r = buildKnowledgeRecord(null, k, 1000); + const s = vehicleStats(r); + expect(s.totalPids).toBe(2); + expect(s.totalDids).toBe(1); + expect(s.totalEcus).toBe(3); + expect(s.totalDiscoveries).toBe(6); // 2 + 3 + 1 + }); +}); + +/* ── upsertKnowledge + depo ───────────────────────────────────────────────── */ +describe('upsertKnowledge + VehicleKnowledgeBaseStore', () => { + it('yeni araç knowledge oluşturuyor', () => { + const s = kbStore(); + upsertKnowledge(knowledge({ vin: 'X', pids: { A5: sig(1000, 1000, 1) } }), s, 1000); + expect(s.size).toBe(1); + }); + + it('aynı araç DUPLICATE oluşturmuyor (upsert)', () => { + const s = kbStore(); + const k = knowledge({ vin: 'X', pids: { A5: sig(1000, 1000, 1) } }); + upsertKnowledge(k, s, 1000); + upsertKnowledge(knowledge({ vin: 'X', pids: { A5: sig(1000, 5000, 2) }, now: 5000 }), s, 5000); + expect(s.size).toBe(1); + const rec = s.get(k.hash)!; + expect(rec.discoveredPids['A5'].seenCount).toBe(2); + expect(rec.firstSeen).toBe(1000); + expect(rec.lastSeen).toBe(5000); + }); + + it('PID ve DID istatistikleri doğru saklanır', () => { + const s = kbStore(); + const k = knowledge({ pids: { A5: sig(1000, 2000, 3) }, dids: { '242E': sig(1500, 2500, 2) } }); + upsertKnowledge(k, s, 2500); + const rec = s.get(k.hash)!; + expect(rec.discoveredPids['A5']).toMatchObject({ firstSeen: 1000, lastSeen: 2000, seenCount: 3 }); + expect(rec.discoveredDids['242E']).toMatchObject({ firstSeen: 1500, lastSeen: 2500, seenCount: 2 }); + }); + + it('bounded cache (max 8) — taşınca en eski düşer', () => { + const s = kbStore(); + const records: VehicleKnowledge[] = Array.from({ length: 10 }, (_, i) => + knowledge({ vin: `VIN-${i}`, pids: { A5: sig(1000, 1000, 1) } })); + records.forEach((k) => upsertKnowledge(k, s, 1000)); + expect(s.size).toBe(MAX_KNOWLEDGE_RECORDS); // 8 + expect(s.get(records[0].hash)).toBeNull(); // en eski düştü + expect(s.get(records[9].hash)).not.toBeNull(); + }); + + it('safeStorage kalıcılığı — aynı anahtarla yeni örnek diskten yükler', () => { + const key = `vkb-persist-${_k++}`; + const a = new VehicleKnowledgeBaseStore(key); + const k = knowledge({ vin: 'PERSIST', pids: { A5: sig(1000, 1000, 1) } }); + upsertKnowledge(k, a, 1000); + const b = new VehicleKnowledgeBaseStore(key); + expect(b.size).toBe(1); + expect(b.get(k.hash)?.vin).toBe('PERSIST'); + }); + + it('bozuk disk verisi → fail-soft boş liste', () => { + const key = `vkb-corrupt-${_k++}`; + try { localStorage.setItem(key, '{bozuk'); } catch { /* jsdom */ } + expect(new VehicleKnowledgeBaseStore(key).size).toBe(0); + }); +}); + +/* ── VehicleKnowledgeBase (canlı, SALT-OKUNUR projeksiyon) ────────────────── */ +describe('VehicleKnowledgeBase — canlı', () => { + it('bağlı araç fingerprint\'i öğrenilmişse KB kaydı oluşur', () => { + const fp = new VehicleFingerprintStore(`vkb-fp2-${_k++}`); + const kb = kbStore(); + const observations = [] as never[]; + const engine = new VehicleKnowledgeBase(kb, fp, () => useVidStore.getState(), () => observations, () => 5000); + const stop = engine.start(); + // VID durumu (ECU discovery'den gelmiyor → assemble ecu=[]) — fp'yi TAM bu çözülen kimliğe koy: + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + // Şimdi çözülen input: vin + proto '6' + ecu [] → o hash'e uygun bir knowledge yerleştir. + const resolved = knowledge({ vin: 'VF1BM0A0H12345678', ecu: [], pids: { A5: sig(1000, 1000, 1) } }); + fp.save(resolved); + // Bir tick daha tetikle → KB'ye projekte olsun. + useVidStore.getState().updateTelemetryInfo({ trustScore: 0.9 }); + expect(kb.size).toBe(1); + expect(kb.get(resolved.hash)?.discoveredPids['A5'].seenCount).toBe(1); + stop(); + }); + + it('FAIL-SOFT: KB deposu hata fırlatsa da akış çökmez', () => { + const fp = new VehicleFingerprintStore(`vkb-fp3-${_k++}`); + fp.save(knowledge({ vin: 'VF1BM0A0H12345678', ecu: [], pids: { A5: sig(1000, 1000, 1) } })); + const throwing = { + get: () => { throw new Error('disk'); }, + save: () => { throw new Error('disk'); }, + } as unknown as VehicleKnowledgeBaseStore; + const engine = new VehicleKnowledgeBase(throwing, fp, () => useVidStore.getState(), () => [], () => 5000); + const stop = engine.start(); + expect(() => { + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + }).not.toThrow(); + stop(); + }); + + it('stop() sonrası projeksiyon durur (zero-leak)', () => { + const fp = new VehicleFingerprintStore(`vkb-fp4-${_k++}`); + fp.save(knowledge({ vin: 'VF1BM0A0H12345678', ecu: [], pids: { A5: sig(1000, 1000, 1) } })); + const kb = kbStore(); + const engine = new VehicleKnowledgeBase(kb, fp, () => useVidStore.getState(), () => [], () => 5000); + const stop = engine.start(); + stop(); + useVidStore.getState().updateVehicleInfo({ vin: 'VF1BM0A0H12345678' }); + useVidStore.getState().updateObdAdapterInfo({ isTransportVerified: true, lastProtocolNum: '6' }); + expect(kb.size).toBe(0); + }); +}); diff --git a/src/platform/system/SystemBoot.ts b/src/platform/system/SystemBoot.ts index 558b0c17..6d823796 100644 --- a/src/platform/system/SystemBoot.ts +++ b/src/platform/system/SystemBoot.ts @@ -61,6 +61,7 @@ import { startBatteryProtection } from '../power/BatteryProtectionService'; import { startVehicleIntelligenceService } from '../vehicleIntelligenceService'; import { startAutomaticVehicleFingerprint } from '../vehicleFingerprintBuilder'; import { startAutoLearningEngine } from '../autoLearningEngine'; +import { startVehicleKnowledgeBase } from '../vehicleKnowledgeBase'; import { logError } from '../crashLogger'; import { showToast, dismissToast } from '../errorBus'; import { healthMonitor } from './SystemHealthMonitor'; @@ -536,6 +537,11 @@ class SystemBoot { _log(' › AutoLearningEngine'); this._reg(startAutoLearningEngine()); + // VehicleKnowledgeBase (PR-28): öğrenilen bilgiyi araç bazlı yerel bilgi tabanına + // (istatistik + kalıcı) organize et. SALT-OKUNUR projeksiyon; additive + fail-soft. + _log(' › VehicleKnowledgeBase'); + this._reg(startVehicleKnowledgeBase()); + // GeofenceService: async (Supabase zona sorgusu) _log(' › GeofenceService (async)'); const geofenceCleanup = await startGeofenceService().catch((e: unknown) => { diff --git a/src/platform/vehicleKnowledgeBase.ts b/src/platform/vehicleKnowledgeBase.ts new file mode 100644 index 00000000..8f799914 --- /dev/null +++ b/src/platform/vehicleKnowledgeBase.ts @@ -0,0 +1,354 @@ +/** + * vehicleKnowledgeBase — Araç Bilgi Tabanı TEMELİ (PR-28, foundation-only). + * + * AMAÇ: PR-25/26/27 ile öğrenilen bilgileri (VehicleKnowledge) araç bazlı, ORGANİZE, + * istatistikli bir YEREL bilgi tabanına dönüştürür. Her fingerprint için tek bir zengin + * kayıt tutar; aynı bilgiyi tekrar yazmaz; yerel istatistik üretir; offline-first çalışır. + * + * KATMAN (Clean Architecture): bu KB bir PROJEKSİYON/ORGANİZASYON katmanıdır. Auto Learning'in + * yazdığı VehicleKnowledge'ı (fingerprint deposundan) SALT-OKUNUR okur ve kendi ayrı deposuna + * (car-vehicle-knowledge-base) işler. Öğrenme/fingerprint/discovery mantığını DEĞİŞTİRMEZ. + * + * KESİN SINIRLAR (CLAUDE.md): Native OBD / poll / Discovery Pipeline-Queue / Auto Learning / + * Fingerprint algoritması / PID-DID Registry / SQL-Supabase DEĞİŞMEZ. Cloud/AI/SQL YOK. + * TAMAMEN ADDITIVE + FAIL-SOFT + zero-leak + bounded: KB çökse bile OBD/Discovery/Fingerprint/ + * Auto Learning akışları aynen sürer. + */ + +import { safeGetRaw, safeSetRaw, safeRemoveRaw } from '../utils/safeStorage'; +import { useVidStore, type VidStore } from '../store/useVidStore'; +import { + discoveryCaptureService, + type DiscoveryObservation, +} from './obd/discovery'; +import { + buildFingerprint, + vehicleFingerprintStore, + VehicleFingerprintStore, +} from './vehicleFingerprintService'; +import { + isConnectionComplete, + assembleFingerprintInput, +} from './vehicleFingerprintBuilder'; +import { + deriveConfidence, + type VehicleKnowledge, + type DiscoveredSignal, +} from './autoLearningEngine'; + +/* ══════════════════════════════════════════════════════════════════════════ + * Bilgi kaydı (knowledge record) — araç başına organize bilgi tabanı + * ════════════════════════════════════════════════════════════════════════ */ + +export interface VehicleKnowledgeRecord { + /** Parmak izi kimliği (bilgi tabanı anahtarı). */ + fingerprintHash: string; + /** VIN'den bağımsız araç imzası (protocol + ECU seti). */ + vehicleSignature: string; + vin: string; + profileHint: string; + protocol: string; + + /** Öğrenilen PID istatistikleri (id → firstSeen/lastSeen/seenCount/confidence). */ + discoveredPids: Record; + /** Öğrenilen DID istatistikleri. */ + discoveredDids: Record; + /** Gözlemlenen ECU adresleri (normalize + sıralı). */ + discoveredEcus: string[]; + + firstSeen: number; + lastSeen: number; + + /** Toplam bağlantı (oturum) sayısı — fingerprint sourceCount ile beslenir. */ + totalConnections: number; + /** Toplam keşif gözlemi (tüm PID+DID seenCount toplamı). */ + totalDiscoveries: number; + + confidence: number; + + /** Görülen firmware/kalibrasyon sürümleri (tekilleştirilmiş). */ + firmwareVersions: string[]; + /** Desteklenen OBD modları (PID→'01', DID→'22'; türetilmiş). */ + supportedModes: string[]; +} + +/** Araç bazlı özet istatistik. */ +export interface VehicleKnowledgeStats { + totalPids: number; + totalDids: number; + totalEcus: number; + totalDiscoveries: number; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * SAF birleştirme / türetme + * ════════════════════════════════════════════════════════════════════════ */ + +function _uniqSorted(...lists: readonly string[][]): string[] { + const set = new Set(); + for (const l of lists) for (const s of l) if (s) set.add(s); + return [...set].sort(); +} + +function _sumSeen(map: Record): number { + let n = 0; + for (const k in map) n += map[k]?.seenCount ?? 0; + return n; +} + +/** + * İki sinyal haritasını birleştirir (bilgi tabanı için): sinyal başına firstSeen KORUNUR + * (min), lastSeen GÜNCELLENİR (max), seenCount = max (Auto Learning kümülatif sayacı + * yetkili), confidence = seenCount'tan yeniden türetilir (monotonik). Duplicate oluşmaz. + */ +function _mergeSignals( + existing: Record, + incoming: Record, +): Record { + const out: Record = { ...existing }; + for (const id in incoming) { + const inc = incoming[id]; + if (!inc) continue; + const cur = out[id]; + const seenCount = Math.max(cur?.seenCount ?? 0, inc.seenCount ?? 0); + out[id] = { + firstSeen: cur ? Math.min(cur.firstSeen, inc.firstSeen) : inc.firstSeen, // İLK görülme KORUNUR + lastSeen: Math.max(cur?.lastSeen ?? 0, inc.lastSeen ?? 0), + seenCount, + confidence: deriveConfidence(seenCount), + }; + } + return out; +} + +function _maxSignalConfidence(...maps: Record[]): number { + let c = 0; + for (const m of maps) for (const k in m) c = Math.max(c, m[k]?.confidence ?? 0); + return c; +} + +/** + * Mevcut kayıt (veya null) + yeni öğrenilmiş bilgi → güncel bilgi kaydı. İLK görülme + * korunur, son görülme/istatistik/confidence güncellenir; aynı bilgi tekrar yazılmaz + * (idempotent). SAF — depoya dokunmaz. + */ +export function buildKnowledgeRecord( + existing: VehicleKnowledgeRecord | null, + k: VehicleKnowledge, + now: number = Date.now(), +): VehicleKnowledgeRecord { + const incPids = k.discoveredPids ?? {}; + const incDids = k.discoveredDids ?? {}; + const pids = _mergeSignals(existing?.discoveredPids ?? {}, incPids); + const dids = _mergeSignals(existing?.discoveredDids ?? {}, incDids); + const ecus = _uniqSorted(existing?.discoveredEcus ?? [], k.ecuAddresses ?? []); + const fw = _uniqSorted( + existing?.firmwareVersions ?? [], + k.metadata?.firmwareVersion ? [k.metadata.firmwareVersion] : [], + ); + + const modes: string[] = []; + if (Object.keys(pids).length > 0) modes.push('01'); + if (Object.keys(dids).length > 0) modes.push('22'); + + const kFirst = k.firstSeen ?? now; + const kLast = k.lastSeen ?? now; + + return { + fingerprintHash: k.hash, + vehicleSignature: `${(k.protocol ?? '').toUpperCase()}::${ecus.join(',')}`, + vin: k.vin ?? '', + profileHint: k.profileHint ?? '', + protocol: k.protocol ?? '', + discoveredPids: pids, + discoveredDids: dids, + discoveredEcus: ecus, + firstSeen: existing ? Math.min(existing.firstSeen, kFirst) : kFirst, // KORUNUR + lastSeen: Math.max(existing?.lastSeen ?? 0, kLast), + totalConnections: k.sourceCount ?? existing?.totalConnections ?? 1, + totalDiscoveries: _sumSeen(pids) + _sumSeen(dids), + confidence: Math.max(existing?.confidence ?? 0, k.confidence ?? 0, _maxSignalConfidence(pids, dids)), + firmwareVersions: fw, + supportedModes: modes, + }; +} + +/** Bir bilgi kaydından araç bazlı özet istatistik hesaplar (SAF). */ +export function vehicleStats(record: VehicleKnowledgeRecord): VehicleKnowledgeStats { + return { + totalPids: Object.keys(record.discoveredPids).length, + totalDids: Object.keys(record.discoveredDids).length, + totalEcus: record.discoveredEcus.length, + totalDiscoveries: record.totalDiscoveries, + }; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Bounded (max 8) LRU kalıcı depo — safeStorage (offline-first, ağ YOK) + * ════════════════════════════════════════════════════════════════════════ */ + +export const VEHICLE_KNOWLEDGE_STORAGE_KEY = 'car-vehicle-knowledge-base'; +export const MAX_KNOWLEDGE_RECORDS = 8; + +/** + * Araç bilgi tabanı deposu — bounded(8) LRU, safeStorage ile kalıcı, ağ YOK. En yeni-görülen + * BAŞTA; upsert kaydı öne taşır; taşınca en eski düşer. Bozuk veri → fail-soft boş liste. + */ +export class VehicleKnowledgeBaseStore { + private _items: VehicleKnowledgeRecord[] = []; + private _loaded = false; + private readonly storageKey: string; + private readonly maxItems: number; + + constructor(storageKey = VEHICLE_KNOWLEDGE_STORAGE_KEY, maxItems = MAX_KNOWLEDGE_RECORDS) { + this.storageKey = storageKey; + this.maxItems = maxItems; + } + + private _ensureLoaded(): void { + if (this._loaded) return; + this._loaded = true; + try { + const raw = safeGetRaw(this.storageKey); + if (raw) { + const parsed: unknown = JSON.parse(raw); + if (Array.isArray(parsed)) this._items = parsed as VehicleKnowledgeRecord[]; + } + } catch { + this._items = []; + } + } + + private _persist(): void { + try { safeSetRaw(this.storageKey, JSON.stringify(this._items)); } catch { /* kota — fail-soft */ } + } + + /** Kaydı ekler/günceller (fingerprintHash ile upsert) ve LRU önüne taşır. */ + save(record: VehicleKnowledgeRecord): VehicleKnowledgeRecord { + this._ensureLoaded(); + const idx = this._items.findIndex((x) => x.fingerprintHash === record.fingerprintHash); + if (idx >= 0) this._items.splice(idx, 1); + this._items.unshift(record); + if (this._items.length > this.maxItems) this._items.length = this.maxItems; // en eski düşer + this._persist(); + return record; + } + + /** hash ile kaydı getirir (kopya) veya null. */ + get(hash: string): VehicleKnowledgeRecord | null { + this._ensureLoaded(); + const found = this._items.find((x) => x.fingerprintHash === hash); + return found ? { ...found } : null; + } + + /** Tüm kayıtların kopyası (en yeni-görülen başta). */ + list(): VehicleKnowledgeRecord[] { + this._ensureLoaded(); + return this._items.map((x) => ({ ...x })); + } + + remove(hash: string): boolean { + this._ensureLoaded(); + const idx = this._items.findIndex((x) => x.fingerprintHash === hash); + if (idx < 0) return false; + this._items.splice(idx, 1); + this._persist(); + return true; + } + + clear(): void { + this._items = []; + this._loaded = true; + try { safeRemoveRaw(this.storageKey); } catch { /* yoksay */ } + } + + get size(): number { + this._ensureLoaded(); + return this._items.length; + } +} + +/** Uygulama geneli tekil bilgi tabanı deposu. Testler kendi örneğini kurar. */ +export const vehicleKnowledgeBaseStore = new VehicleKnowledgeBaseStore(); + +/** + * Bir VehicleKnowledge'ı bilgi tabanına işler (upsert): mevcut kayıt güncellenir, duplicate + * oluşmaz. SAF birleştirme + depo yazımı. @returns güncel kayıt. + */ +export function upsertKnowledge( + k: VehicleKnowledge, + store: VehicleKnowledgeBaseStore = vehicleKnowledgeBaseStore, + now: number = Date.now(), +): VehicleKnowledgeRecord { + const existing = store.get(k.hash); + const record = buildKnowledgeRecord(existing, k, now); + return store.save(record); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * VehicleKnowledgeBase — Discovery + VID aboneliği (ince sarmalayıcı) + * ════════════════════════════════════════════════════════════════════════ */ + +export class VehicleKnowledgeBase { + private _unsubDisc: (() => void) | null = null; + private _unsubVid: (() => void) | null = null; + private readonly _kbStore: VehicleKnowledgeBaseStore; + private readonly _fpStore: VehicleFingerprintStore; + private readonly _readVid: () => VidStore; + private readonly _readObs: () => DiscoveryObservation[]; + private readonly _now: () => number; + + constructor( + kbStore: VehicleKnowledgeBaseStore = vehicleKnowledgeBaseStore, + fpStore: VehicleFingerprintStore = vehicleFingerprintStore, + readVid: () => VidStore = () => useVidStore.getState(), + readObs: () => DiscoveryObservation[] = () => discoveryCaptureService.getObservations(), + now: () => number = () => Date.now(), + ) { + this._kbStore = kbStore; + this._fpStore = fpStore; + this._readVid = readVid; + this._readObs = readObs; + this._now = now; + } + + /** Discovery + VID aboneliklerini başlatır (idempotent). Döndürülen fonksiyon durdurur. */ + start(): () => void { + if (this._unsubDisc || this._unsubVid) return () => this.stop(); + this._unsubDisc = discoveryCaptureService.subscribe(() => this._tick()); + this._unsubVid = useVidStore.subscribe(() => this._tick()); + this._tick(); + return () => this.stop(); + } + + stop(): void { + this._unsubDisc?.(); this._unsubDisc = null; + this._unsubVid?.(); this._unsubVid = null; + } + + /** + * Tek projeksiyon adımı: bağlı aracın öğrenilmiş bilgisini (VehicleKnowledge) fingerprint + * deposundan SALT-OKUNUR okur → bilgi tabanına upsert eder. FAIL-SOFT. + */ + private _tick(): void { + try { + const vid = this._readVid(); + if (!isConnectionComplete(vid)) return; + const input = assembleFingerprintInput(vid, this._readObs()); + const base = buildFingerprint(input); + const k = this._fpStore.load(base.hash) as VehicleKnowledge | null; + if (!k) return; // henüz öğrenilmemiş → atla (bir sonraki tick'te işlenir) + upsertKnowledge(k, this._kbStore, this._now()); + } catch { + /* FAIL-SOFT: KB hatası mevcut akışları ASLA bozmaz */ + } + } +} + +/** Uygulama geneli tekil bilgi tabanı motoru (SystemBoot Wave-3'ten başlatılır). */ +export const vehicleKnowledgeBase = new VehicleKnowledgeBase(); + +/** SystemBoot wiring yardımcı — başlatır, cleanup fonksiyonu döndürür. */ +export function startVehicleKnowledgeBase(): () => void { + return vehicleKnowledgeBase.start(); +} diff --git a/src/utils/safeStorage.ts b/src/utils/safeStorage.ts index 28c96f57..747d9e44 100644 --- a/src/utils/safeStorage.ts +++ b/src/utils/safeStorage.ts @@ -86,6 +86,7 @@ const LRU_PROTECTED = new Set([ 'car-expert-trust-hmac-seed', // Expert Trust HMAC tohumu 'car-safety-brain-v1', // Safety Brain VIN profilleri 'car-vehicle-fingerprints', // Araç parmak izi bounded LRU cache (max 8 araç kimliği) + 'car-vehicle-knowledge-base', // Araç bilgi tabanı (öğrenilen PID/DID istatistikleri, bounded LRU) ]); /**